[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\n# github: [lisonge]\ncustom: ['https://github.com/lisonge/sponsor']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 错误反馈 / Bug report\ntitle: \"[BUG] \"\ndescription: 反馈你遇到的错误 / Report the bug you encountered\nlabels: [\"pending triage\"]\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## 感谢您花时间填写，在提交问题之前，请确保您完成以下操作\n        1. 请 **确保** 您已经查阅了 [GKD 官方文档](https://gkd.li) 以及 [常见问题](https://gkd.li/guide/faq)\n        2. 请 **判断** 是不是第三方规则订阅的问题，如果是，你应该向规则提供者反馈，而不是在这里提交，**此处只接受 GKD 应用本体的问题**\n        3. 请 **确保** 已有的 [问题](https://github.com/gkd-kit/gkd/issues?q=is%3Aissue) 或 [讨论](https://github.com/orgs/gkd-kit/discussions?discussions_q=) 中没有人提交过相似问题，否则请在该问题下进行讨论\n        4. 请 **不要** 开启重复相关的 issue，这将导致别人搜索 issue 时出现无关的低质量信息，否则你的问题将会被直接关闭甚至删除\n        5. 请 **确保** 你的问题能在 [releases](https://github.com/gkd-kit/gkd/releases/latest) 发布的最新版本(包含测试版本)上复现 (如果不是请先更新到最新版本复现后再提交问题)\n        6. 请 **务必** 给 issue 填写一个简洁明了的标题，以便他人快速检索\n  - type: textarea\n    id: log-file\n    attributes:\n      label: |\n        日志文件\n      description: |\n        首页-设置-关于-日志，上传日志文件或生成链接并粘贴到下面的输入框\\\n        任何问题都需要提供日志文件. 否则将直接关闭，请不要纯发文字/截图/视频\n    validations:\n      required: true\n  - type: textarea\n    id: bug-1\n    attributes:\n      label: |\n        BUG描述(文字/截图/视频)\n      description: |\n        请使用尽量准确的描述，否则你的问题将会被直接关闭\\\n        另外如果你的问题是关于快照/选择器的，请必须提供快照链接或者快照文件，否则你的问题将会被直接关闭\n    validations:\n      required: true\n  - type: textarea\n    id: bug-2\n    attributes:\n      label: |\n        期望行为(文字/截图/视频)\n      description: |\n        请使用尽量准确的描述，否则你的问题将会被直接关闭\n    validations:\n      required: true\n  - type: textarea\n    id: bug-3\n    attributes:\n      label: |\n        实际行为(文字/截图/视频)\n      description: |\n        请使用尽量准确的描述，否则你的问题将会被直接关闭\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 讨论交流 / Discussions\n    url: https://github.com/gkd-kit/gkd/discussions\n    about: '其他提问或请求: 选择器/规则/快照/无障碍/设备/相关问题. 在 issues 搜索不到可前往搜索讨论'\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 功能请求 / Feature request\ntitle: \"[Feature] \"\ndescription: 提出你的功能请求 / Propose your feature request\nlabels: [\"pending triage\"]\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## 感谢您花时间填写，在提交问题之前，请确保您完成以下操作\n        1. GKD 默认不提供任何规则，你可以查看 [GKD 官方文档](https://gkd.li) 后自行编写规则或者导入远程订阅，请不要再提出类似想要XXX规则这种问题\n        2. 请 **判断** 是不是第三方规则订阅的功能请求，如果是，你应该向规则提供者反馈，而不是在这里提交，**此处只接受 GKD 应用本体的功能请求**\n        3. 请 **确保** 已有的 [问题](https://github.com/gkd-kit/gkd/issues?q=is%3Aissue) 或 [讨论](https://github.com/orgs/gkd-kit/discussions?discussions_q=) 中没有人提交过相似问题，否则请在该问题下进行讨论\n        4. 请 **不要** 开启重复相关的 issue，这将导致别人搜索 issue 时出现无关的低质量信息，否则你的问题将会被直接关闭甚至删除\n        5. 请 **确保** 你想要的功能在 [releases](https://github.com/gkd-kit/gkd/releases/latest) 发布的最新版本(包含测试版本)上没有找到 (如果不是请先更新到最新版本验证后再提交问题)\n        6. 请 **务必** 给 issue 填写一个简洁明了的标题，以便他人快速检索\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: |\n        新功能描述\n      description: |\n        例如: 我希望在 GKD 中的什么页面添加什么功能，以及这个功能的作用是什么\\\n        或者在规则定义中添加某个字段，以及这个字段的作用是什么\\\n        请使用准确的描述，否则你的问题将会被直接关闭\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/workflows/Build-Apk.yml",
    "content": "name: Build-Apk\n\non:\n  workflow_dispatch:\n\n  push:\n    branches:\n      - '**'\n    paths-ignore:\n      - 'LICENSE'\n      - '*.md'\n      - '.github/**'\n\njobs:\n  build:\n    if: ${{ !startsWith(github.event.head_commit.message, 'chore:') && !startsWith(github.event.head_commit.message, 'chore(') }}\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n\n      - uses: actions/setup-java@v5\n        with:\n          distribution: 'zulu'\n          java-version: '21'\n\n      - uses: gradle/actions/setup-gradle@v5\n        with:\n          cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}\n\n      - name: write gkd secrets info\n        run: |\n          echo ${{ secrets.GKD_STORE_FILE_BASE64 }} | base64 --decode > ${{ github.workspace }}/gkd.jks\n          echo GKD_STORE_FILE='${{ github.workspace }}/gkd.jks' >> gradle.properties\n          echo GKD_STORE_PASSWORD='${{ secrets.GKD_STORE_PASSWORD }}' >> gradle.properties\n          echo GKD_KEY_ALIAS='${{ secrets.GKD_KEY_ALIAS }}' >> gradle.properties\n          echo GKD_KEY_PASSWORD='${{ secrets.GKD_KEY_PASSWORD }}' >> gradle.properties\n\n      - run: chmod 777 ./gradlew\n      - run: ./gradlew app:assembleGkdRelease\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: release\n          path: app/build/outputs/apk/gkd/release\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: outputs\n          path: app/build/outputs\n"
  },
  {
    "path": ".github/workflows/Build-Release.yml",
    "content": "name: Build-Release\n\non:\n  push:\n    tags:\n      - v*\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n\n      - uses: actions/setup-java@v5\n        with:\n          distribution: 'zulu'\n          java-version: '21'\n\n      - uses: gradle/actions/setup-gradle@v5\n        with:\n          cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}\n\n      - name: write gkd secrets info\n        run: |\n          echo ${{ secrets.GKD_STORE_FILE_BASE64 }} | base64 --decode > ${{ github.workspace }}/gkd.jks\n          echo GKD_STORE_FILE='${{ github.workspace }}/gkd.jks' >> gradle.properties\n          echo GKD_STORE_PASSWORD='${{ secrets.GKD_STORE_PASSWORD }}' >> gradle.properties\n          echo GKD_KEY_ALIAS='${{ secrets.GKD_KEY_ALIAS }}' >> gradle.properties\n          echo GKD_KEY_PASSWORD='${{ secrets.GKD_KEY_PASSWORD }}' >> gradle.properties\n\n      - name: write play secrets info\n        run: |\n          echo ${{ secrets.PLAY_STORE_FILE_BASE64 }} | base64 --decode > ${{ github.workspace }}/play.jks\n          echo PLAY_STORE_FILE='${{ github.workspace }}/play.jks' >> gradle.properties\n          echo PLAY_STORE_PASSWORD='${{ secrets.PLAY_STORE_PASSWORD }}' >> gradle.properties\n          echo PLAY_KEY_ALIAS='${{ secrets.PLAY_KEY_ALIAS }}' >> gradle.properties\n          echo PLAY_KEY_PASSWORD='${{ secrets.PLAY_KEY_PASSWORD }}' >> gradle.properties\n\n      - run: chmod 777 ./gradlew\n      - run: ./gradlew app:assembleGkdRelease app:bundlePlayRelease\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: release\n          path: app/build/outputs/apk/gkd/release\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: playRelease\n          path: app/build/outputs/bundle/playRelease\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: outputs\n          path: app/build/outputs\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: CHANGELOG.md\n          path: CHANGELOG.md\n\n  release:\n    needs: build\n    permissions: write-all\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/download-artifact@v4\n        with:\n          name: outputs\n          path: outputs\n\n      - uses: actions/download-artifact@v4\n        with:\n          name: release\n          path: release\n\n      - uses: actions/download-artifact@v4\n        with:\n          name: CHANGELOG.md\n\n      - run: ls -R\n\n      - id: create_release\n        uses: actions/create-release@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          tag_name: ${{ github.ref_name }}\n          release_name: Release ${{ github.ref_name }}\n          body_path: ./CHANGELOG.md\n          prerelease: ${{ contains(github.ref_name, 'beta') }}\n\n      - uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: release/app-gkd-release.apk\n          asset_name: gkd-${{ github.ref_name }}.apk\n          asset_content_type: application/vnd.android.package-archive\n\n      - run: zip -r outputs.zip outputs\n      - uses: actions/upload-release-asset@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          upload_url: ${{ steps.create_release.outputs.upload_url }}\n          asset_path: outputs.zip\n          asset_name: outputs-${{ github.ref_name }}.zip\n          asset_content_type: application/zip\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n.cxx\nlocal.properties\n.idea\n/kotlin-js-store\n.vscode\n\n*.jks\n*.keystore\n\n/_assets\n/.kotlin\n\n/gradle/libs.versions.updates.toml\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# v1.11.6\n\n以下是本次更新的主要内容\n\n## 优化和修复\n\n- 新增权限受限提示\n- 新增应用列表分组筛选\n- 新增图标按钮长按提示\n- 优化触发记录日期显示\n- 修复某些情况下界面背景颜色异常\n\n## 更新方式\n\n- GKD - 设置 - 关于 - 检测更新\n- 下列方式之一\n\n<a href=\"https://gkd.li/guide/\"><img src=\"https://e.gkd.li/f23b704d-d781-494b-9719-393f95683b89\" alt=\"Download from GKD.LI\" width=\"32%\" /></a><a href=\"https://play.google.com/store/apps/details?id=li.songe.gkd\"><img src=\"https://e.gkd.li/f63fabeb-0342-4961-a46d-cac61b0f8856\" alt=\"Download from Google Play\" width=\"32%\" /></a><a href=\"https://github.com/gkd-kit/gkd/releases\"><img src=\"https://e.gkd.li/c1ef2bb9-7472-46d5-9806-81b4c37e5b4d\" alt=\"Download from GitHub releases\" width=\"32%\" /></a>\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>."
  },
  {
    "path": "README.md",
    "content": "# gkd\n\n<p align=\"center\">\n<a href=\"https://gkd.li/\"><img src=\"https://e.gkd.li/2a0a7787-f2dd-4529-a885-93f3b8c857c3\" alt=\"GKD.LI\" width=\"40%\" /></a>\n</p>\n\n基于 [高级选择器](https://gkd.li/guide/selector) + [订阅规则](https://gkd.li/guide/subscription) + [快照审查](https://github.com/gkd-kit/inspect) 的自定义屏幕点击 Android 应用\n\n通过自定义规则，在指定界面，满足指定条件(如屏幕上存在特定文字)时，点击特定的节点或位置或执行其他操作\n\n- **快捷操作**\n\n  帮助你简化一些重复的流程, 如某些软件自动确认电脑登录\n\n- **跳过流程**\n\n  某些软件可能在启动时存在一些烦人的流程, 这个软件可以帮助你点击跳过这个流程\n\n## 免责声明\n\n**本项目遵循 [GPL-3.0](/LICENSE) 开源，项目仅供学习交流，禁止用于商业或非法用途**\n\n## 安装\n\n<a href=\"https://gkd.li/guide/\"><img src=\"https://e.gkd.li/f23b704d-d781-494b-9719-393f95683b89\" alt=\"Download from GKD.LI\" width=\"32%\" /></a><a href=\"https://play.google.com/store/apps/details?id=li.songe.gkd\"><img src=\"https://e.gkd.li/f63fabeb-0342-4961-a46d-cac61b0f8856\" alt=\"Download from Google Play\" width=\"32%\" /></a><a href=\"https://github.com/gkd-kit/gkd/releases\"><img src=\"https://e.gkd.li/c1ef2bb9-7472-46d5-9806-81b4c37e5b4d\" alt=\"Download from GitHub releases\" width=\"32%\" /></a>\n\n如遇问题请先查看 [疑难解答](https://gkd.li/guide/faq)\n\n## 截图\n\n|                                                               |                                                               |                                                               |                                                               |\n| ------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------- |\n| ![img](https://e.gkd.li/1e8934c1-2303-4182-9ef2-ad4c46882570) | ![img](https://e.gkd.li/01f230d7-9b89-4314-b573-38bd233d22f9) | ![img](https://e.gkd.li/dfa0a782-b21e-473a-96e4-eef27773b71b) | ![img](https://e.gkd.li/641decd1-2e60-4e95-b78c-df38d1d98a4d) |\n| ![img](https://e.gkd.li/b216b703-d3de-4798-81ba-29e0ae63264f) | ![img](https://e.gkd.li/76c25ac9-4189-47cd-b40b-b9e72c79b584) | ![img](https://e.gkd.li/7288502e-808b-4d9a-88b5-1085abaa0d46) | ![img](https://e.gkd.li/aa974940-7773-409a-ae84-3c02fee9c770) |\n\n## 订阅\n\nGKD **默认不提供规则**，需自行添加本地规则，或者通过订阅链接的方式获取远程规则\n\n也可通过 [subscription-template](https://github.com/gkd-kit/subscription-template) 快速构建自己的远程订阅\n\n第三方订阅列表可在 <https://github.com/topics/gkd-subscription> 查看\n\n要加入此列表, 需点击仓库主页右上角设置图标后在 Topics 中添加 `gkd-subscription`\n\n<details>\n<summary>示例图片 - 添加至 Topics (点击展开)</summary>\n\n![image](https://e.gkd.li/9e340459-254f-4ca0-8a44-cc823069e5a7)\n\n</details>\n\n## 选择器\n\n一个类似 CSS 选择器的选择器, 能联系节点上下文信息, 更容易也更精确找到目标节点\n\n<https://gkd.li/guide/selector>\n\n[@[vid=\\\"menu\\\"] < [vid=\\\"menu_container\\\"] - [vid=\\\"dot_text_layout\\\"] > [text^=\\\"广告\\\"]](https://i.gkd.li/i/14881985?gkd=QFt2aWQ9Im1lbnUiXSA8IFt2aWQ9Im1lbnVfY29udGFpbmVyIl0gLSBbdmlkPSJkb3RfdGV4dF9sYXlvdXQiXSA-IFt0ZXh0Xj0i5bm_5ZGKIl0)\n\n<details>\n<summary>示例图片 - 选择器路径视图 (点击展开)</summary>\n\n[![image](https://e.gkd.li/a2ae667b-b8c5-4556-a816-37743347b972)](https://i.gkd.li/i/14881985?gkd=QFt2aWQ9Im1lbnUiXSA8IFt2aWQ9Im1lbnVfY29udGFpbmVyIl0gLSBbdmlkPSJkb3RfdGV4dF9sYXlvdXQiXSA-IFt0ZXh0Xj0i5bm_5ZGKIl0)\n\n</details>\n\n## 捐赠\n\n如果 GKD 对你有用, 可以通过以下链接支持该项目\n\n<https://github.com/lisonge/sponsor>\n\n或前往 [Google Play](https://play.google.com/store/apps/details?id=li.songe.gkd) 给个好评\n\n## Star History\n\n[![Stargazers over time](https://starchart.cc/gkd-kit/gkd.svg?variant=adaptive)](https://starchart.cc/gkd-kit/gkd)\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n/release\n"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport kotlin.reflect.full.declaredMemberProperties\n\nfun String.runCommand(): String {\n    val process = ProcessBuilder(split(\" \"))\n        .redirectErrorStream(true)\n        .start()\n    val output = process.inputStream.bufferedReader().readText().trim()\n    val exitCode = process.waitFor()\n    if (exitCode != 0) {\n        error(\"Command failed with exit code $exitCode: $output\")\n    }\n    return output\n}\n\ndata class GitInfo(\n    val commitId: String,\n    val commitTime: String,\n    val tagName: String?,\n)\n\nval gitInfo = GitInfo(\n    commitId = \"git rev-parse HEAD\".runCommand(),\n    commitTime = \"git log -1 --format=%ct\".runCommand() + \"000\",\n    tagName = runCatching { \"git describe --tags --exact-match\".runCommand() }.getOrNull(),\n)\n\nval debugSuffixPairList by lazy {\n    javax.xml.parsers.DocumentBuilderFactory\n        .newInstance()\n        .newDocumentBuilder()\n        .parse(file(\"$projectDir/src/main/res/values/strings.xml\"))\n        .documentElement.getElementsByTagName(\"string\").run {\n            (0 until length).mapNotNull { i ->\n                val node = item(i)\n                if (node.attributes.getNamedItem(\"debug_suffix\") != null) {\n                    val key = node.attributes.getNamedItem(\"name\").nodeValue\n                    val value = node.textContent\n                    key to value\n                } else {\n                    null\n                }\n            }\n        }\n}\n\nplugins {\n    alias(libs.plugins.android.application)\n    alias(libs.plugins.androidx.room)\n    alias(libs.plugins.kotlin.parcelize)\n    alias(libs.plugins.kotlin.serialization)\n    alias(libs.plugins.kotlin.compose)\n    alias(libs.plugins.kotlinx.atomicfu)\n    alias(libs.plugins.google.ksp)\n    alias(libs.plugins.rikka.refine)\n    alias(libs.plugins.loc)\n}\n\nandroid {\n    namespace = rootProject.ext[\"android.namespace\"].toString()\n    compileSdk = rootProject.ext[\"android.compileSdk\"] as Int\n    buildToolsVersion = rootProject.ext[\"android.buildToolsVersion\"].toString()\n\n    defaultConfig {\n        minSdk = rootProject.ext[\"android.minSdk\"] as Int\n        targetSdk = rootProject.ext[\"android.targetSdk\"] as Int\n\n        applicationId = \"li.songe.gkd\"\n        versionCode = 81\n        versionName = \"1.11.6\"\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        vectorDrawables {\n            useSupportLibrary = true\n        }\n        androidResources {\n            localeFilters += listOf(\"zh\", \"en\")\n        }\n        ndk {\n            // noinspection ChromeOsAbiSupport\n            abiFilters += listOf(\"arm64-v8a\", \"x86_64\")\n        }\n\n        GitInfo::class.declaredMemberProperties.onEach {\n            manifestPlaceholders[it.name] = it.get(gitInfo) ?: \"\"\n        }\n    }\n\n    buildFeatures {\n        compose = true\n        aidl = true\n        resValues = true\n    }\n\n    val gkdSigningConfig = signingConfigs.create(\"gkd\") {\n        storeFile = file(project.properties[\"GKD_STORE_FILE\"] as String)\n        storePassword = project.properties[\"GKD_STORE_PASSWORD\"].toString()\n        keyAlias = project.properties[\"GKD_KEY_ALIAS\"].toString()\n        keyPassword = project.properties[\"GKD_KEY_PASSWORD\"].toString()\n    }\n\n    val playSigningConfig = if (project.hasProperty(\"PLAY_STORE_FILE\")) {\n        signingConfigs.create(\"play\") {\n            storeFile = file(project.properties[\"PLAY_STORE_FILE\"].toString())\n            storePassword = project.properties[\"PLAY_STORE_PASSWORD\"].toString()\n            keyAlias = project.properties[\"PLAY_KEY_ALIAS\"].toString()\n            keyPassword = project.properties[\"PLAY_KEY_PASSWORD\"].toString()\n        }\n    } else {\n        gkdSigningConfig\n    }\n\n    buildTypes {\n        all {\n            if (gitInfo.tagName == null) {\n                versionNameSuffix = \"-${gitInfo.commitId.take(7)}\"\n            }\n        }\n        release {\n            isMinifyEnabled = true\n            isShrinkResources = true\n            isDebuggable = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\",\n            )\n        }\n        debug {\n            signingConfig = gkdSigningConfig\n            applicationIdSuffix = \".debug\"\n            resValue(\"color\", \"better_black\", \"#FF5D92\")\n            debugSuffixPairList.onEach { (key, value) ->\n                resValue(\"string\", key, \"$value-debug\")\n            }\n        }\n    }\n    productFlavors {\n        flavorDimensions += \"channel\"\n        create(\"gkd\") {\n            isDefault = true\n            signingConfig = gkdSigningConfig\n            resValue(\"bool\", \"is_accessibility_tool\", \"true\")\n        }\n        create(\"play\") {\n            signingConfig = playSigningConfig\n            resValue(\"bool\", \"is_accessibility_tool\", \"false\")\n        }\n        all {\n            dimension = flavorDimensions.first()\n            manifestPlaceholders[\"channel\"] = name\n        }\n    }\n    compileOptions {\n        sourceCompatibility = rootProject.ext[\"android.javaVersion\"] as JavaVersion\n        targetCompatibility = rootProject.ext[\"android.javaVersion\"] as JavaVersion\n    }\n    dependenciesInfo.includeInApk = false\n    packaging.resources.excludes += setOf(\n        // https://github.com/Kotlin/kotlinx.coroutines/issues/2023\n        \"META-INF/**\", \"**/attach_hotspot_windows.dll\",\n\n        \"**.properties\", \"**.bin\", \"**/*.proto\",\n        \"**/kotlin-tooling-metadata.json\",\n\n        // ktor\n        \"**/custom.config.conf\",\n        \"**/custom.config.yaml\",\n    )\n}\n\nkotlin {\n    compilerOptions {\n        jvmTarget.set(rootProject.ext[\"kotlin.jvmTarget\"] as JvmTarget)\n        freeCompilerArgs.addAll(\n            \"-opt-in=kotlin.RequiresOptIn\",\n            \"-opt-in=kotlin.contracts.ExperimentalContracts\",\n            \"-opt-in=kotlinx.coroutines.FlowPreview\",\n            \"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi\",\n            \"-opt-in=kotlinx.serialization.ExperimentalSerializationApi\",\n            \"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api\",\n            \"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi\",\n            \"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi\",\n            \"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi\",\n            \"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi\",\n            \"-Xcontext-parameters\",\n            \"-XXLanguage:+MultiDollarInterpolation\",\n        )\n    }\n}\n\n// https://developer.android.com/jetpack/androidx/releases/room?hl=zh-cn#compiler-options\nroom {\n    schemaDirectory(\"$projectDir/schemas\")\n}\n\ncomposeCompiler {\n    reportsDestination = layout.buildDirectory.dir(\"compose_compiler\")\n    stabilityConfigurationFiles.addAll(\n        rootProject.layout.projectDirectory.file(\"stability_config.conf\"),\n    )\n}\n\nloc {\n    template = \"{packageName}.{methodName}({fileName}:{lineNumber})\"\n}\n\ndependencies {\n    implementation(libs.kotlin.stdlib)\n\n    implementation(project(\":selector\"))\n\n    implementation(libs.androidx.appcompat)\n    implementation(libs.androidx.core.ktx)\n    implementation(libs.androidx.lifecycle.runtime.ktx)\n    implementation(libs.androidx.lifecycle.service)\n\n    implementation(libs.compose.ui)\n    implementation(libs.compose.ui.graphics)\n    implementation(libs.compose.animation)\n    implementation(libs.compose.animation.graphics)\n    implementation(libs.compose.icons)\n    implementation(libs.compose.preview)\n    debugImplementation(libs.compose.tooling)\n    androidTestImplementation(libs.compose.junit4)\n\n    implementation(libs.compose.activity)\n    implementation(libs.compose.material3)\n\n    implementation(libs.androidx.navigation3.ui)\n    implementation(libs.androidx.navigation3.runtime)\n    implementation(libs.androidx.lifecycle.viewmodel.navigation3)\n    implementation(libs.androidx.material3.adaptive.navigation3)\n\n    testImplementation(libs.junit)\n    androidTestImplementation(libs.androidx.junit)\n    androidTestImplementation(libs.androidx.espresso)\n\n    compileOnly(project(\":hidden_api\"))\n    implementation(libs.rikka.shizuku.api)\n    implementation(libs.rikka.shizuku.provider)\n    implementation(libs.lsposed.hiddenapibypass)\n\n    implementation(libs.androidx.room.runtime)\n    implementation(libs.androidx.room.ktx)\n    implementation(libs.androidx.room.paging)\n    ksp(libs.androidx.room.compiler)\n\n    implementation(libs.androidx.paging.runtime)\n    implementation(libs.androidx.paging.compose)\n\n    implementation(libs.ktor.server.core)\n    implementation(libs.ktor.server.cio)\n    implementation(libs.ktor.server.content.negotiation)\n\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.okhttp)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.serialization.kotlinx.json)\n\n    implementation(libs.google.accompanist.drawablepainter)\n\n    implementation(libs.kotlinx.serialization.core)\n    implementation(libs.kotlinx.serialization.json)\n    // https://github.com/Kotlin/kotlinx-atomicfu/issues/145\n    implementation(libs.kotlinx.atomicfu)\n\n    implementation(libs.activityResultLauncher)\n\n    implementation(libs.reorderable)\n\n    implementation(libs.androidx.splashscreen)\n\n    implementation(libs.coil.compose)\n    implementation(libs.coil.network)\n    implementation(libs.coil.gif)\n\n    implementation(libs.exp4j)\n\n    implementation(libs.toaster)\n    implementation(libs.permissions)\n    implementation(libs.device)\n\n    implementation(libs.json5)\n    compileOnly(libs.loc.annotation)\n\n    implementation(libs.kevinnzouWebview)\n}"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# http://developer.android.com/guide/developing/tools/proguard.html\n\n-dontwarn **\n"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/1.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 1,\n    \"identityHash\": \"e755c87d937b753ba02d02e8fa9afcec\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"app_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionCode\",\n            \"columnName\": \"app_version_code\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionName\",\n            \"columnName\": \"app_version_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"click_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `group_key` INTEGER NOT NULL, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e755c87d937b753ba02d02e8fa9afcec')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/10.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 10,\n    \"identityHash\": \"f3e8d3fb1a6de3876a6fceb921a456a2\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"app_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionCode\",\n            \"columnName\": \"app_version_code\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionName\",\n            \"columnName\": \"app_version_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"exclude\",\n            \"columnName\": \"exclude\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"action_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"activity_log_v2\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"app_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f3e8d3fb1a6de3876a6fceb921a456a2')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/11.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 11,\n    \"identityHash\": \"8cc58f95916481333213b24587c5d4d3\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"exclude\",\n            \"columnName\": \"exclude\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"action_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"activity_log_v2\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"app_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8cc58f95916481333213b24587c5d4d3')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/12.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 12,\n    \"identityHash\": \"58d6b0ebb55bc58ac6016a2b675e3ac4\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"exclude\",\n            \"columnName\": \"exclude\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"action_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"activity_log_v2\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"app_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '58d6b0ebb55bc58ac6016a2b675e3ac4')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/13.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 13,\n    \"identityHash\": \"f57629976fb6ff444f59487622f93814\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"exclude\",\n            \"columnName\": \"exclude\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"action_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"activity_log_v2\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"app_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"app_visit_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `mtime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f57629976fb6ff444f59487622f93814')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/14.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 14,\n    \"identityHash\": \"8c34795e4b3ae52bf0188358d7bd3037\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"exclude\",\n            \"columnName\": \"exclude\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"action_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"activity_log_v2\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"app_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"app_visit_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `mtime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"a11y_event_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `type` INTEGER NOT NULL, `appId` TEXT NOT NULL, `name` TEXT NOT NULL, `desc` TEXT, `text` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"appId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"desc\",\n            \"columnName\": \"desc\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"text\",\n            \"columnName\": \"text\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8c34795e4b3ae52bf0188358d7bd3037')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/2.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 2,\n    \"identityHash\": \"3e6e28a11589fe6c2d8aff5b9467a489\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"app_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionCode\",\n            \"columnName\": \"app_version_code\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionName\",\n            \"columnName\": \"app_version_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"click_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `group_key` INTEGER NOT NULL, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e6e28a11589fe6c2d8aff5b9467a489')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/3.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 3,\n    \"identityHash\": \"b52c1f25e2052865818be5151b6ac6a0\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"app_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionCode\",\n            \"columnName\": \"app_version_code\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionName\",\n            \"columnName\": \"app_version_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"click_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `group_key` INTEGER NOT NULL, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b52c1f25e2052865818be5151b6ac6a0')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/4.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 4,\n    \"identityHash\": \"d1a618bf8475b588793fb1d201815a08\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"app_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionCode\",\n            \"columnName\": \"app_version_code\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionName\",\n            \"columnName\": \"app_version_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"click_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd1a618bf8475b588793fb1d201815a08')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/5.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 5,\n    \"identityHash\": \"4219672d163fce6e91926d9e15fc0e64\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"app_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionCode\",\n            \"columnName\": \"app_version_code\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionName\",\n            \"columnName\": \"app_version_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"exclude\",\n            \"columnName\": \"exclude\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"click_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4219672d163fce6e91926d9e15fc0e64')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/6.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 6,\n    \"identityHash\": \"6cbd7772c779598a5448b9d5dc36c524\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"app_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionCode\",\n            \"columnName\": \"app_version_code\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionName\",\n            \"columnName\": \"app_version_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"exclude\",\n            \"columnName\": \"exclude\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"click_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6cbd7772c779598a5448b9d5dc36c524')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/7.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 7,\n    \"identityHash\": \"9d5e657834ed630ac5cf00753cf24a55\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"app_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionCode\",\n            \"columnName\": \"app_version_code\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionName\",\n            \"columnName\": \"app_version_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"exclude\",\n            \"columnName\": \"exclude\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"click_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"activity_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9d5e657834ed630ac5cf00753cf24a55')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/8.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 8,\n    \"identityHash\": \"409bb51310bcdb55ea721a8e88b6cef6\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"app_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionCode\",\n            \"columnName\": \"app_version_code\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionName\",\n            \"columnName\": \"app_version_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"exclude\",\n            \"columnName\": \"exclude\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"click_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"activity_log_v2\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '409bb51310bcdb55ea721a8e88b6cef6')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/li.songe.gkd.db.AppDb/9.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 9,\n    \"identityHash\": \"343aa23eaf071a84fab19c5979d95f13\",\n    \"entities\": [\n      {\n        \"tableName\": \"subs_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mtime\",\n            \"columnName\": \"mtime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enableUpdate\",\n            \"columnName\": \"enable_update\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"updateUrl\",\n            \"columnName\": \"update_url\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"snapshot\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appName\",\n            \"columnName\": \"app_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionCode\",\n            \"columnName\": \"app_version_code\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"appVersionName\",\n            \"columnName\": \"app_version_name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"screenHeight\",\n            \"columnName\": \"screen_height\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"screenWidth\",\n            \"columnName\": \"screen_width\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isLandscape\",\n            \"columnName\": \"is_landscape\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"githubAssetId\",\n            \"columnName\": \"github_asset_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"subs_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"exclude\",\n            \"columnName\": \"exclude\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"category_config\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"enable\",\n            \"columnName\": \"enable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsItemId\",\n            \"columnName\": \"subs_item_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"categoryKey\",\n            \"columnName\": \"category_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"action_log\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"subsId\",\n            \"columnName\": \"subs_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subsVersion\",\n            \"columnName\": \"subs_version\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"groupKey\",\n            \"columnName\": \"group_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"groupType\",\n            \"columnName\": \"group_type\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"2\"\n          },\n          {\n            \"fieldPath\": \"ruleIndex\",\n            \"columnName\": \"rule_index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ruleKey\",\n            \"columnName\": \"rule_key\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"activity_log_v2\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"ctime\",\n            \"columnName\": \"ctime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"appId\",\n            \"columnName\": \"app_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"activityId\",\n            \"columnName\": \"activity_id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '343aa23eaf071a84fab19c5979d95f13')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/src/androidTest/kotlin/li/songe/gkd/ExampleInstrumentedTest.kt",
    "content": "package li.songe.gkd\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"li.songe.gkd\", appContext.packageName)\n    }\n}"
  },
  {
    "path": "app/src/gkd/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission\n        android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\"\n        tools:ignore=\"RequestInstallPackagesPolicy\" />\n</manifest>"
  },
  {
    "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.POST_NOTIFICATIONS\" />\n    <uses-permission\n        android:name=\"android.permission.FOREGROUND_SERVICE\"\n        tools:ignore=\"ForegroundServicesPolicy\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_SPECIAL_USE\" />\n    <uses-permission\n        android:name=\"android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION\"\n        android:maxSdkVersion=\"29\" />\n    <uses-permission\n        android:name=\"android.permission.QUERY_ALL_PACKAGES\"\n        tools:ignore=\"PackageVisibilityPolicy,QueryAllPackagesPermission\" />\n    <uses-permission android:name=\"com.android.permission.GET_INSTALLED_APPS\" />\n    <uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\" />\n    <uses-permission\n        android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n        android:maxSdkVersion=\"28\" />\n    <uses-permission\n        android:name=\"android.permission.WRITE_SECURE_SETTINGS\"\n        tools:ignore=\"ProtectedPermissions\" />\n    <uses-permission\n        android:name=\"android.permission.GET_APP_OPS_STATS\"\n        tools:ignore=\"ProtectedPermissions\" />\n    <uses-permission android:name=\"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS\" />\n\n    <application\n        android:name=\".App\"\n        android:allowBackup=\"true\"\n        android:icon=\"@drawable/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:roundIcon=\"@drawable/ic_launcher\"\n        android:supportsRtl=\"false\"\n        android:theme=\"@style/AppTheme\">\n\n        <meta-data\n            android:name=\"channel\"\n            android:value=\"${channel}\" />\n        <meta-data\n            android:name=\"commitId\"\n            android:value=\"${commitId}\" />\n        <meta-data\n            android:name=\"commitTime\"\n            android:value=\"${commitTime}\" />\n        <meta-data\n            android:name=\"tagName\"\n            android:value=\"${tagName}\" />\n\n        <activity\n            android:name=\".MainActivity\"\n            android:enableOnBackInvokedCallback=\"false\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTop\"\n            android:theme=\"@style/SplashScreenTheme\"\n            tools:ignore=\"UnusedAttribute\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n        <activity\n            android:name=\".OpenSchemeActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/TransparentTheme\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data android:scheme=\"gkd\" />\n            </intent-filter>\n        </activity>\n        <activity\n            android:name=\".OpenFileActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/TransparentTheme\">\n            <intent-filter android:label=\"@string/import_data\">\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data android:scheme=\"content\" />\n                <data android:mimeType=\"application/zip\" />\n                <data android:mimeType=\"application/x-zip-compressed\" />\n            </intent-filter>\n        </activity>\n        <activity\n            android:name=\".OpenTileActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/TransparentTheme\">\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE_PREFERENCES\" />\n            </intent-filter>\n        </activity>\n\n        <provider\n            android:name=\"rikka.shizuku.ShizukuProvider\"\n            android:authorities=\"${applicationId}.shizuku\"\n            android:enabled=\"true\"\n            android:exported=\"true\"\n            android:multiprocess=\"false\"\n            android:permission=\"android.permission.INTERACT_ACROSS_USERS_FULL\" />\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.provider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/file_paths\" />\n        </provider>\n\n        <service\n            android:name=\"com.google.android.accessibility.selecttospeak.SelectToSpeakService\"\n            android:exported=\"false\"\n            android:label=\"@string/app_name\"\n            android:permission=\"android.permission.BIND_ACCESSIBILITY_SERVICE\"\n            tools:ignore=\"AccessibilityPolicy\">\n            <intent-filter>\n                <action android:name=\"android.accessibilityservice.AccessibilityService\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.accessibilityservice\"\n                android:resource=\"@xml/ab_desc\" />\n        </service>\n\n        <service\n            android:name=\".service.ScreenshotService\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"mediaProjection\" />\n        <service\n            android:name=\".service.StatusService\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"specialUse\">\n            <property\n                android:name=\"android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE\"\n                android:value=\"Display the running state of the application\" />\n        </service>\n        <service\n            android:name=\".service.HttpService\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"specialUse\">\n            <property\n                android:name=\"android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE\"\n                android:value=\"Enable the HTTP server to provide external browser connections and debugging\" />\n        </service>\n        <service\n            android:name=\".service.ButtonService\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"specialUse\">\n            <property\n                android:name=\"android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE\"\n                android:value=\"Display a screenshot button for users to actively save screen information.\" />\n        </service>\n        <service\n            android:name=\".service.ActivityService\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"specialUse\">\n            <property\n                android:name=\"android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE\"\n                android:value=\"Display a text card for users to show current activity info\" />\n        </service>\n        <service\n            android:name=\".service.EventService\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"specialUse\">\n            <property\n                android:name=\"android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE\"\n                android:value=\"Display a text card for users to show a11y event log\" />\n        </service>\n        <service\n            android:name=\".service.ExposeService\"\n            android:exported=\"true\"\n            android:foregroundServiceType=\"specialUse\"\n            tools:ignore=\"ExportedService\">\n            <property\n                android:name=\"android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE\"\n                android:value=\"Provide an interface for third-party applications to actively invoke snapshot capturing.\" />\n        </service>\n\n        <service\n            android:name=\".service.GkdTileService\"\n            android:exported=\"true\"\n            android:icon=\"@drawable/ic_status\"\n            android:label=\"@string/app_name\"\n            android:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\">\n            <meta-data\n                android:name=\"android.service.quicksettings.TOGGLEABLE_TILE\"\n                android:value=\"true\" />\n            <meta-data\n                android:name=\"QS_TILE_URI\"\n                android:value=\"gkd://page\" />\n\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE\" />\n            </intent-filter>\n        </service>\n        <service\n            android:name=\".service.SnapshotTileService\"\n            android:exported=\"true\"\n            android:icon=\"@drawable/ic_capture\"\n            android:label=\"@string/capture_snapshot\"\n            android:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\">\n            <meta-data\n                android:name=\"QS_TILE_URI\"\n                android:value=\"gkd://page/2\" />\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE\" />\n            </intent-filter>\n        </service>\n        <service\n            android:name=\".service.HttpTileService\"\n            android:exported=\"true\"\n            android:icon=\"@drawable/ic_http\"\n            android:label=\"@string/http_server\"\n            android:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\">\n            <meta-data\n                android:name=\"android.service.quicksettings.TOGGLEABLE_TILE\"\n                android:value=\"true\" />\n            <meta-data\n                android:name=\"QS_TILE_URI\"\n                android:value=\"gkd://page/1\" />\n\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE\" />\n            </intent-filter>\n        </service>\n        <service\n            android:name=\".service.ButtonTileService\"\n            android:exported=\"true\"\n            android:icon=\"@drawable/ic_radio_button\"\n            android:label=\"@string/snapshot_button\"\n            android:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\">\n            <meta-data\n                android:name=\"android.service.quicksettings.TOGGLEABLE_TILE\"\n                android:value=\"true\" />\n            <meta-data\n                android:name=\"QS_TILE_URI\"\n                android:value=\"gkd://page/1\" />\n\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE\" />\n            </intent-filter>\n        </service>\n        <service\n            android:name=\".service.MatchTileService\"\n            android:exported=\"true\"\n            android:icon=\"@drawable/ic_flash_on\"\n            android:label=\"@string/rule_match\"\n            android:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\">\n            <meta-data\n                android:name=\"android.service.quicksettings.TOGGLEABLE_TILE\"\n                android:value=\"true\" />\n            <meta-data\n                android:name=\"QS_TILE_URI\"\n                android:value=\"gkd://page?tab=1\" />\n\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE\" />\n            </intent-filter>\n        </service>\n        <service\n            android:name=\".service.ActivityTileService\"\n            android:exported=\"true\"\n            android:icon=\"@drawable/ic_layers\"\n            android:label=\"@string/record_activity\"\n            android:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\">\n            <meta-data\n                android:name=\"android.service.quicksettings.TOGGLEABLE_TILE\"\n                android:value=\"true\" />\n            <meta-data\n                android:name=\"QS_TILE_URI\"\n                android:value=\"gkd://page/1\" />\n\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE\" />\n            </intent-filter>\n        </service>\n        <service\n            android:name=\".service.EventTileService\"\n            android:exported=\"true\"\n            android:icon=\"@drawable/ic_event_list\"\n            android:label=\"@string/record_a11y_event\"\n            android:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\">\n            <meta-data\n                android:name=\"android.service.quicksettings.TOGGLEABLE_TILE\"\n                android:value=\"true\" />\n            <meta-data\n                android:name=\"QS_TILE_URI\"\n                android:value=\"gkd://page/1\" />\n\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE\" />\n            </intent-filter>\n        </service>\n\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/aidl/li/songe/gkd/shizuku/CommandResult.aidl",
    "content": "package li.songe.gkd.shizuku;\n\nparcelable CommandResult;\n"
  },
  {
    "path": "app/src/main/aidl/li/songe/gkd/shizuku/IUserService.aidl",
    "content": "package li.songe.gkd.shizuku;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Rect;\nimport li.songe.gkd.shizuku.CommandResult;\n\ninterface IUserService {\n    void destroy() = 16777114; // Destroy method defined by Shizuku server\n    void exit() = 1;\n    CommandResult execCommand(String command) = 2;\n    Bitmap takeScreenshot1(int width, int height) = 3;\n    Bitmap takeScreenshot2(in Rect crop, int rotation) = 4;\n    Bitmap takeScreenshot3(in Rect crop) = 5;\n    int killLegacyService() = 6;\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/google/android/accessibility/selecttospeak/SelectToSpeakService.kt",
    "content": "package com.google.android.accessibility.selecttospeak\n\nimport li.songe.gkd.service.A11yService\n\n// https://github.com/ven-coder/Assists/issues/12#issuecomment-2684469065\nclass SelectToSpeakService : A11yService()\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/App.kt",
    "content": "package li.songe.gkd\n\nimport android.app.ActivityManager\nimport android.app.AppOpsManager\nimport android.app.Application\nimport android.app.KeyguardManager\nimport android.content.ClipboardManager\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.ApplicationInfo\nimport android.content.pm.PackageInfo\nimport android.content.pm.PackageManager\nimport android.database.ContentObserver\nimport android.net.Uri\nimport android.os.PowerManager\nimport android.provider.Settings\nimport android.view.WindowManager\nimport android.view.accessibility.AccessibilityManager\nimport android.view.inputmethod.InputMethodManager\nimport androidx.core.content.ContextCompat\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.a11y.initA11yFeat\nimport li.songe.gkd.data.selfAppInfo\nimport li.songe.gkd.notif.initChannel\nimport li.songe.gkd.service.clearHttpSubs\nimport li.songe.gkd.service.initA11yWhiteAppList\nimport li.songe.gkd.shizuku.initShizuku\nimport li.songe.gkd.store.initStore\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.PKG_FLAGS\nimport li.songe.gkd.util.initAppState\nimport li.songe.gkd.util.initSubsState\nimport li.songe.gkd.util.initToast\nimport li.songe.gkd.util.toast\nimport org.lsposed.hiddenapibypass.HiddenApiBypass\nimport kotlin.system.exitProcess\n\n\nval appScope by lazy { MainScope() }\n\nprivate lateinit var innerApp: App\nval app: App\n    get() = innerApp\n\nprivate val applicationInfo by lazy {\n    app.packageManager.getApplicationInfo(\n        app.packageName,\n        PackageManager.GET_META_DATA\n    )\n}\n\nprivate fun getMetaString(key: String): String {\n    return applicationInfo.metaData.getString(key) ?: error(\"Missing meta-data: $key\")\n}\n\n// https://github.com/android-cs/16/blob/main/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java#L41\nprivate const val ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ':'\n\n@Serializable\ndata class AppMeta(\n    val channel: String = getMetaString(\"channel\"),\n    val commitId: String = getMetaString(\"commitId\"),\n    val commitTime: Long = getMetaString(\"commitTime\").toLong(),\n    val tagName: String? = getMetaString(\"tagName\").takeIf { it.isNotEmpty() },\n    val debuggable: Boolean = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0,\n    val versionCode: Int = selfAppInfo.versionCode,\n    val versionName: String = selfAppInfo.versionName!!,\n    val appId: String = app.packageName!!,\n    val appName: String = app.getString(R.string.app_name)\n) {\n    val commitUrl = \"https://github.com/gkd-kit/gkd/\".run {\n        plus(if (tagName != null) \"tree/$tagName\" else \"commit/$commitId\")\n    }\n    val isGkdChannel get() = channel == \"gkd\"\n    val updateEnabled get() = isGkdChannel\n    val isBeta get() = versionName.contains(\"beta\")\n}\n\nval META by lazy { AppMeta() }\n\nfun contentObserver(listener: () -> Unit) = object : ContentObserver(null) {\n    override fun onChange(selfChange: Boolean) = listener()\n}\n\nclass App : Application() {\n    companion object {\n        const val START_WAIT_TIME = 3000L\n    }\n\n    init {\n        innerApp = this\n    }\n\n    override fun attachBaseContext(base: Context?) {\n        super.attachBaseContext(base)\n        if (AndroidTarget.P) {\n            HiddenApiBypass.addHiddenApiExemptions(\"L\")\n        }\n    }\n\n    fun registerObserver(\n        uri: Uri,\n        observer: ContentObserver\n    ) {\n        contentResolver.registerContentObserver(uri, false, observer)\n    }\n\n    fun unregisterObserver(observer: ContentObserver) {\n        contentResolver.unregisterContentObserver(observer)\n    }\n\n    fun getSecureString(name: String): String? = Settings.Secure.getString(contentResolver, name)\n    fun putSecureString(name: String, value: String?): Boolean {\n        return Settings.Secure.putString(contentResolver, name, value)\n    }\n\n    fun putSecureInt(name: String, value: Int): Boolean {\n        return Settings.Secure.putInt(contentResolver, name, value)\n    }\n\n    fun getSecureA11yServices(): MutableSet<ComponentName> {\n        val value = getSecureString(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)\n        if (value.isNullOrEmpty()) return mutableSetOf()\n        return value.split(\n            ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR\n        ).mapNotNull { ComponentName.unflattenFromString(it) }.toHashSet()\n    }\n\n    fun putSecureA11yServices(services: Set<ComponentName>) {\n        putSecureString(\n            Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,\n            services.joinToString(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR.toString()) { it.flattenToShortString() }\n        )\n    }\n\n    fun resolveAppId(intent: Intent): String? {\n        return intent.resolveActivity(packageManager)?.packageName\n    }\n\n    fun getPkgInfo(appId: String): PackageInfo? = try {\n        packageManager.getPackageInfo(appId, PKG_FLAGS)\n    } catch (_: PackageManager.NameNotFoundException) {\n        null\n    }\n\n    fun resolveAppId(action: String, category: String? = null): String? {\n        val intent = Intent(action)\n        if (category != null) {\n            intent.addCategory(category)\n        }\n        return resolveAppId(intent)\n    }\n\n    fun startLaunchActivity() {\n        val intent = packageManager.getLaunchIntentForPackage(META.appId)!!\n        intent.addFlags(\n            Intent.FLAG_ACTIVITY_NEW_TASK\n                    or Intent.FLAG_ACTIVITY_CLEAR_TOP\n                    or Intent.FLAG_ACTIVITY_CLEAR_TASK\n        )\n        startActivity(intent)\n    }\n\n    fun checkGrantedPermission(permission: String) = ContextCompat.checkSelfPermission(\n        this,\n        permission,\n    ) == PackageManager.PERMISSION_GRANTED\n\n    val startTime = System.currentTimeMillis()\n    var justStarted: Boolean = true\n        get() {\n            if (field) {\n                field = System.currentTimeMillis() - startTime < START_WAIT_TIME\n            }\n            return field\n        }\n\n    val activityManager by lazy { app.getSystemService(ACTIVITY_SERVICE) as ActivityManager }\n    val appOpsManager by lazy { app.getSystemService(APP_OPS_SERVICE) as AppOpsManager }\n    val inputMethodManager by lazy { app.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager }\n    val windowManager by lazy { app.getSystemService(WINDOW_SERVICE) as WindowManager }\n    val keyguardManager by lazy { app.getSystemService(KEYGUARD_SERVICE) as KeyguardManager }\n    val clipboardManager by lazy { app.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager }\n    val powerManager by lazy { getSystemService(POWER_SERVICE) as PowerManager }\n    val a11yManager by lazy { getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager }\n\n    override fun onCreate() {\n        super.onCreate()\n        LogUtils.d()\n        Thread.setDefaultUncaughtExceptionHandler { t, e ->\n            toast(e.message ?: e.toString())\n            LogUtils.d(\"UncaughtExceptionHandler\", t, e)\n            appScope.launch(Dispatchers.IO) {\n                delay(1500)\n                if (isActivityVisible) {\n                    startLaunchActivity()\n                }\n                android.os.Process.killProcess(android.os.Process.myPid())\n                exitProcess(0)\n            }\n        }\n        initToast()\n        initStore()\n        initChannel()\n        initAppState()\n        initA11yFeat()\n        initShizuku()\n        initSubsState()\n        initA11yWhiteAppList()\n        clearHttpSubs()\n        syncFixState()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/MainActivity.kt",
    "content": "package li.songe.gkd\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.activity.viewModels\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.WindowInsetsAnimationCompat\nimport androidx.core.view.WindowInsetsCompat\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator\nimport androidx.navigation3.runtime.entryProvider\nimport androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator\nimport androidx.navigation3.ui.NavDisplay\nimport com.dylanc.activityresult.launcher.PickContentLauncher\nimport com.dylanc.activityresult.launcher.StartActivityLauncher\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.flow.updateAndGet\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport li.songe.gkd.a11y.topActivityFlow\nimport li.songe.gkd.a11y.updateSystemDefaultAppId\nimport li.songe.gkd.a11y.updateTopActivity\nimport li.songe.gkd.permission.AuthDialog\nimport li.songe.gkd.permission.updatePermissionState\nimport li.songe.gkd.service.A11yService\nimport li.songe.gkd.service.StatusService\nimport li.songe.gkd.service.fixRestartAutomatorService\nimport li.songe.gkd.service.updateTopTaskAppId\nimport li.songe.gkd.shizuku.automationRegisteredExceptionFlow\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.A11YScopeAppListRoute\nimport li.songe.gkd.ui.A11yEventLogPage\nimport li.songe.gkd.ui.A11yEventLogRoute\nimport li.songe.gkd.ui.A11yScopeAppListPage\nimport li.songe.gkd.ui.AboutPage\nimport li.songe.gkd.ui.AboutRoute\nimport li.songe.gkd.ui.ActionLogPage\nimport li.songe.gkd.ui.ActionLogRoute\nimport li.songe.gkd.ui.ActivityLogPage\nimport li.songe.gkd.ui.ActivityLogRoute\nimport li.songe.gkd.ui.AdvancedPage\nimport li.songe.gkd.ui.AdvancedPageRoute\nimport li.songe.gkd.ui.AppConfigPage\nimport li.songe.gkd.ui.AppConfigRoute\nimport li.songe.gkd.ui.AppOpsAllowPage\nimport li.songe.gkd.ui.AppOpsAllowRoute\nimport li.songe.gkd.ui.AuthA11yPage\nimport li.songe.gkd.ui.AuthA11yRoute\nimport li.songe.gkd.ui.BlockA11yAppListPage\nimport li.songe.gkd.ui.BlockA11yAppListRoute\nimport li.songe.gkd.ui.EditBlockAppListPage\nimport li.songe.gkd.ui.EditBlockAppListRoute\nimport li.songe.gkd.ui.ImagePreviewPage\nimport li.songe.gkd.ui.ImagePreviewRoute\nimport li.songe.gkd.ui.SlowGroupPage\nimport li.songe.gkd.ui.SlowGroupRoute\nimport li.songe.gkd.ui.SnapshotPage\nimport li.songe.gkd.ui.SnapshotPageRoute\nimport li.songe.gkd.ui.SubsAppGroupListPage\nimport li.songe.gkd.ui.SubsAppGroupListRoute\nimport li.songe.gkd.ui.SubsAppListPage\nimport li.songe.gkd.ui.SubsAppListRoute\nimport li.songe.gkd.ui.SubsCategoryPage\nimport li.songe.gkd.ui.SubsCategoryRoute\nimport li.songe.gkd.ui.SubsGlobalGroupExcludePage\nimport li.songe.gkd.ui.SubsGlobalGroupExcludeRoute\nimport li.songe.gkd.ui.SubsGlobalGroupListPage\nimport li.songe.gkd.ui.SubsGlobalGroupListRoute\nimport li.songe.gkd.ui.UpsertRuleGroupPage\nimport li.songe.gkd.ui.UpsertRuleGroupRoute\nimport li.songe.gkd.ui.WebViewPage\nimport li.songe.gkd.ui.WebViewRoute\nimport li.songe.gkd.ui.component.BuildDialog\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.ShareDataDialog\nimport li.songe.gkd.ui.component.SubsSheet\nimport li.songe.gkd.ui.component.TermsAcceptDialog\nimport li.songe.gkd.ui.component.TextDialog\nimport li.songe.gkd.ui.home.HomePage\nimport li.songe.gkd.ui.home.HomeRoute\nimport li.songe.gkd.ui.share.FixedWindowInsets\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.AppTheme\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.BarUtils\nimport li.songe.gkd.util.EditGithubCookieDlg\nimport li.songe.gkd.util.KeyboardUtils\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.ShortUrlSet\nimport li.songe.gkd.util.appInfoMapFlow\nimport li.songe.gkd.util.componentName\nimport li.songe.gkd.util.copyText\nimport li.songe.gkd.util.fixSomeProblems\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.mapState\nimport li.songe.gkd.util.openApp\nimport li.songe.gkd.util.openUri\nimport li.songe.gkd.util.shizukuAppId\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\nimport kotlin.concurrent.Volatile\nimport kotlin.reflect.jvm.jvmName\n\nclass MainActivity : ComponentActivity() {\n    val startTime = System.currentTimeMillis()\n    val mainVm by viewModels<MainViewModel>()\n    val launcher by lazy { StartActivityLauncher(this) }\n    val pickContentLauncher by lazy { PickContentLauncher(this) }\n\n    val imeFullHiddenFlow = MutableStateFlow(true)\n    val imePlayingFlow = MutableStateFlow(false)\n\n    private val imeVisible: Boolean\n        get() = ViewCompat.getRootWindowInsets(window.decorView)!!\n            .isVisible(WindowInsetsCompat.Type.ime())\n\n    var topBarWindowInsets by mutableStateOf(WindowInsets(top = BarUtils.getStatusBarHeight()))\n\n    private fun watchKeyboardVisible() {\n        if (AndroidTarget.R) {\n            ViewCompat.setWindowInsetsAnimationCallback(\n                window.decorView,\n                object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {\n                    override fun onStart(\n                        animation: WindowInsetsAnimationCompat,\n                        bounds: WindowInsetsAnimationCompat.BoundsCompat\n                    ): WindowInsetsAnimationCompat.BoundsCompat {\n                        imePlayingFlow.update { imeVisible }\n                        return super.onStart(animation, bounds)\n                    }\n\n                    override fun onProgress(\n                        insets: WindowInsetsCompat,\n                        runningAnimations: List<WindowInsetsAnimationCompat>\n                    ): WindowInsetsCompat {\n                        return insets\n                    }\n\n                    override fun onEnd(animation: WindowInsetsAnimationCompat) {\n                        imeFullHiddenFlow.update { !imeVisible }\n                        imePlayingFlow.update { false }\n                        super.onEnd(animation)\n                    }\n                })\n        } else {\n            KeyboardUtils.registerSoftInputChangedListener(window) { height ->\n                // onEnd\n                imeFullHiddenFlow.update { height == 0 }\n            }\n        }\n    }\n\n    suspend fun hideSoftInput(): Boolean {\n        if (!imeFullHiddenFlow.updateAndGet { !imeVisible }) {\n            KeyboardUtils.hideSoftInput(this@MainActivity)\n            imeFullHiddenFlow.drop(1).first()\n            return true\n        }\n        return false\n    }\n\n    fun justHideSoftInput(): Boolean {\n        if (!imeFullHiddenFlow.updateAndGet { !imeVisible }) {\n            KeyboardUtils.hideSoftInput(this@MainActivity)\n            return true\n        }\n        return false\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        installSplashScreen()\n        enableEdgeToEdge()\n        fixSomeProblems()\n        super.onCreate(savedInstanceState)\n        LogUtils.d()\n        mainVm\n        launcher\n        pickContentLauncher\n        lifecycleScope.launch {\n            storeFlow.mapState(lifecycleScope) { s -> s.excludeFromRecents }.collect {\n                app.activityManager.appTasks.forEach { task ->\n                    task.setExcludeFromRecents(it)\n                }\n            }\n        }\n        addOnNewIntentListener {\n            mainVm.handleIntent(it)\n            intent = null\n        }\n        watchKeyboardVisible()\n        StatusService.autoStart()\n        if (storeFlow.value.enableBlockA11yAppList) {\n            updateTopTaskAppId(META.appId)\n        }\n        setContent {\n            val latestInsets = TopAppBarDefaults.windowInsets\n            val density = LocalDensity.current\n            if (latestInsets.getTop(density) > topBarWindowInsets.getTop(density)) {\n                topBarWindowInsets = FixedWindowInsets(latestInsets)\n            }\n            CompositionLocalProvider(\n                LocalMainViewModel provides mainVm\n            ) {\n                AppTheme {\n                    NavDisplay(\n                        entryDecorators = listOf(\n                            rememberSaveableStateHolderNavEntryDecorator(),\n                            rememberViewModelStoreNavEntryDecorator(),\n                        ),\n                        backStack = mainVm.backStack,\n                        onBack = mainVm::popPage,\n                        entryProvider = entryProvider {\n                            entry<HomeRoute> { HomePage() }\n                            entry<AuthA11yRoute> { AuthA11yPage() }\n                            entry<AboutRoute> { AboutPage() }\n                            entry<BlockA11yAppListRoute> { BlockA11yAppListPage() }\n                            entry<AdvancedPageRoute> { AdvancedPage() }\n                            entry<SnapshotPageRoute> { SnapshotPage() }\n                            entry<AppOpsAllowRoute> { AppOpsAllowPage() }\n                            entry<A11YScopeAppListRoute> { A11yScopeAppListPage() }\n                            entry<ActivityLogRoute> { ActivityLogPage() }\n                            entry<A11yEventLogRoute> { A11yEventLogPage() }\n                            entry<EditBlockAppListRoute> { EditBlockAppListPage() }\n                            entry<SlowGroupRoute> { SlowGroupPage() }\n                            entry<SubsAppListRoute> { SubsAppListPage(it) }\n                            entry<WebViewRoute> { WebViewPage(it) }\n                            entry<SubsCategoryRoute> { SubsCategoryPage(it) }\n                            entry<SubsGlobalGroupListRoute> { SubsGlobalGroupListPage(it) }\n                            entry<SubsGlobalGroupExcludeRoute> { SubsGlobalGroupExcludePage(it) }\n                            entry<ActionLogRoute> { ActionLogPage(it) }\n                            entry<ImagePreviewRoute> { ImagePreviewPage(it) }\n                            entry<UpsertRuleGroupRoute> { UpsertRuleGroupPage(it) }\n                            entry<SubsAppGroupListRoute> { SubsAppGroupListPage(it) }\n                            entry<AppConfigRoute> { AppConfigPage(it) }\n                        },\n                        transitionSpec = {\n                            slideInHorizontally(initialOffsetX = { it }) togetherWith\n                                    slideOutHorizontally(targetOffsetX = { -it })\n                        },\n                        popTransitionSpec = {\n                            slideInHorizontally(initialOffsetX = { -it }) togetherWith\n                                    slideOutHorizontally(targetOffsetX = { it })\n                        },\n                        predictivePopTransitionSpec = {\n                            slideInHorizontally(initialOffsetX = { -it }) togetherWith\n                                    slideOutHorizontally(targetOffsetX = { it })\n                        },\n                    )\n                    if (!mainVm.termsAcceptedFlow.collectAsState().value) {\n                        TermsAcceptDialog()\n                    } else {\n                        UiAutomationAlreadyRegisteredDlg()\n                        AccessRestrictedSettingsDlg()\n                        ShizukuErrorDialog(mainVm.shizukuErrorFlow)\n                        AuthDialog(mainVm.authReasonFlow)\n                        BuildDialog(mainVm.dialogFlow)\n                        mainVm.uploadOptions.ShowDialog()\n                        EditGithubCookieDlg()\n                        mainVm.updateStatus?.UpgradeDialog()\n                        SubsSheet(mainVm, mainVm.sheetSubsIdFlow)\n                        ShareDataDialog(mainVm, mainVm.showShareDataIdsFlow)\n                        mainVm.inputSubsLinkOption.ContentDialog()\n                        mainVm.ruleGroupState.Render()\n                        TextDialog(mainVm.textFlow)\n                    }\n                }\n            }\n            LaunchedEffect(null) {\n                intent?.let {\n                    mainVm.handleIntent(it)\n                    intent = null\n                }\n            }\n        }\n    }\n\n    override fun onStart() {\n        super.onStart()\n        LogUtils.d()\n        activityVisibleState++\n        if (topActivityFlow.value.appId != META.appId) {\n            updateTopActivity(META.appId, MainActivity::class.jvmName)\n        }\n    }\n\n    var isFirstResume = true\n    override fun onResume() {\n        super.onResume()\n        LogUtils.d()\n        if (isFirstResume && startTime - app.startTime < 2000) {\n            isFirstResume = false\n        } else {\n            syncFixState()\n        }\n    }\n\n    override fun onStop() {\n        super.onStop()\n        LogUtils.d()\n        activityVisibleState--\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        LogUtils.d()\n    }\n}\n\n@Volatile\nprivate var activityVisibleState = 0\nval isActivityVisible get() = activityVisibleState > 0\n\nval activityNavSourceName by lazy { META.appId + \".activity.nav.source\" }\n\nfun Activity.navToMainActivity() {\n    if (intent != null) {\n        val navIntent = Intent(intent)\n        navIntent.component = MainActivity::class.componentName\n        navIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK\n        navIntent.putExtra(activityNavSourceName, this::class.jvmName)\n        startActivity(navIntent)\n    }\n    finish()\n}\n\nprivate val syncStateMutex = Mutex()\nfun syncFixState() {\n    appScope.launchTry(Dispatchers.IO) {\n        if (syncStateMutex.isLocked) {\n            LogUtils.d(\"syncFixState isLocked\")\n        }\n        syncStateMutex.withLock {\n            updateSystemDefaultAppId()\n            shizukuContextFlow.value.grantSelf()\n            updatePermissionState()\n            fixRestartAutomatorService()\n        }\n    }\n}\n\n@Composable\nprivate fun ShizukuErrorDialog(stateFlow: MutableStateFlow<Throwable?>) {\n    val state = stateFlow.collectAsState().value\n    if (state != null) {\n        val errorText = remember { state.stackTraceToString() }\n        val appInfoCache = appInfoMapFlow.collectAsState().value\n        val installed = appInfoCache.contains(shizukuAppId)\n        AlertDialog(\n            onDismissRequest = { stateFlow.value = null },\n            title = { Text(text = \"授权错误\") },\n            text = {\n                Column {\n                    Text(\n                        text = if (installed) {\n                            \"Shizuku 授权失败，请检查是否运行\"\n                        } else {\n                            \"Shizuku 授权失败，检测到 Shizuku 未安装，请先下载后安装，如果你是通过其它方式授权，请忽略此提示自行查找原因\"\n                        }\n                    )\n                    Spacer(modifier = Modifier.height(8.dp))\n                    Box(\n                        modifier = Modifier.fillMaxWidth()\n                    ) {\n                        SelectionContainer(\n                            modifier = Modifier\n                                .align(Alignment.TopStart)\n                                .fillMaxWidth()\n                        ) {\n                            Text(\n                                text = errorText,\n                                modifier = Modifier\n                                    .clip(MaterialTheme.shapes.extraSmall)\n                                    .background(MaterialTheme.colorScheme.secondaryContainer)\n                                    .padding(8.dp)\n                                    .heightIn(max = 400.dp)\n                                    .verticalScroll(rememberScrollState()),\n                                style = MaterialTheme.typography.bodySmall,\n                            )\n                        }\n                        PerfIcon(\n                            modifier = Modifier\n                                .align(Alignment.TopEnd)\n                                .clickable(onClick = throttle {\n                                    copyText(errorText)\n                                })\n                                .padding(4.dp)\n                                .size(20.dp),\n                            imageVector = PerfIcon.ContentCopy,\n                            tint = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.75f),\n                        )\n                    }\n                }\n            },\n            confirmButton = {\n                if (installed) {\n                    TextButton(onClick = {\n                        stateFlow.value = null\n                        openApp(shizukuAppId)\n                    }) {\n                        Text(text = \"打开 Shizuku\")\n                    }\n                } else {\n                    TextButton(onClick = {\n                        stateFlow.value = null\n                        openUri(ShortUrlSet.URL4)\n                    }) {\n                        Text(text = \"去下载\")\n                    }\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = { stateFlow.value = null }) {\n                    Text(text = \"我知道了\")\n                }\n            }\n        )\n    }\n}\n\n\nval accessRestrictedSettingsShowFlow = MutableStateFlow(false)\n\n@Composable\nfun AccessRestrictedSettingsDlg() {\n    val a11yRunning by A11yService.isRunning.collectAsState()\n    LaunchedEffect(a11yRunning) {\n        if (a11yRunning) {\n            accessRestrictedSettingsShowFlow.value = false\n        }\n    }\n    val accessRestrictedSettingsShow by accessRestrictedSettingsShowFlow.collectAsState()\n    val mainVm = LocalMainViewModel.current\n    val isA11yPage = mainVm.topRoute is AuthA11yRoute\n    LaunchedEffect(isA11yPage, accessRestrictedSettingsShow) {\n        if (isA11yPage && accessRestrictedSettingsShow && !a11yRunning) {\n            toast(\"请重新授权以解除限制\")\n            accessRestrictedSettingsShowFlow.value = false\n        }\n    }\n    if (accessRestrictedSettingsShow && !isA11yPage && !a11yRunning) {\n        AlertDialog(\n            title = {\n                Text(text = \"权限受限\")\n            },\n            text = {\n                Text(text = \"当前操作权限「访问受限设置」已被限制, 请先解除限制\")\n            },\n            onDismissRequest = {\n                accessRestrictedSettingsShowFlow.value = false\n            },\n            confirmButton = {\n                TextButton({\n                    accessRestrictedSettingsShowFlow.value = false\n                    mainVm.navigateWebPage(ShortUrlSet.URL2)\n                }) {\n                    Text(text = \"解除\")\n                }\n            },\n            dismissButton = {\n                TextButton({\n                    accessRestrictedSettingsShowFlow.value = false\n                }) {\n                    Text(text = \"关闭\")\n                }\n            },\n        )\n    }\n}\n\n@Composable\nfun UiAutomationAlreadyRegisteredDlg() {\n    if (automationRegisteredExceptionFlow.collectAsState().value != null) {\n        AlertDialog(\n            onDismissRequest = {\n                automationRegisteredExceptionFlow.value = null\n            },\n            title = { Text(text = \"启动失败\") },\n            text = {\n                Text(text = \"自动化服务启动失败，检测到自动化服务已被其他应用占用，请先关闭已有服务后重试\\n\\n注：自动化服务只能同时运行一个，请确保没有其他应用或测试框架占用后再启动\")\n            },\n            confirmButton = {\n                TextButton(onClick = {\n                    automationRegisteredExceptionFlow.value = null\n                }) {\n                    Text(text = \"我知道了\")\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/MainViewModel.kt",
    "content": "package li.songe.gkd\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.net.Uri\nimport android.webkit.URLUtil\nimport androidx.lifecycle.viewModelScope\nimport androidx.navigation3.runtime.NavBackStack\nimport androidx.navigation3.runtime.NavKey\nimport io.ktor.client.request.get\nimport io.ktor.client.statement.bodyAsText\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport li.songe.gkd.a11y.useA11yServiceEnabledFlow\nimport li.songe.gkd.a11y.useEnabledA11yServicesFlow\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.data.SubsItem\nimport li.songe.gkd.data.importData\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.permission.AuthReason\nimport li.songe.gkd.permission.shizukuGrantedState\nimport li.songe.gkd.service.A11yService\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.shizuku.uiAutomationFlow\nimport li.songe.gkd.shizuku.updateBinderMutex\nimport li.songe.gkd.store.createTextFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.AdvancedPageRoute\nimport li.songe.gkd.ui.AppOpsAllowRoute\nimport li.songe.gkd.ui.SnapshotPageRoute\nimport li.songe.gkd.ui.WebViewRoute\nimport li.songe.gkd.ui.component.AlertDialogOptions\nimport li.songe.gkd.ui.component.InputSubsLinkOption\nimport li.songe.gkd.ui.component.RuleGroupState\nimport li.songe.gkd.ui.component.UploadOptions\nimport li.songe.gkd.ui.home.BottomNavItem\nimport li.songe.gkd.ui.home.HomeRoute\nimport li.songe.gkd.ui.share.BaseViewModel\nimport li.songe.gkd.util.AutomatorModeOption\nimport li.songe.gkd.util.LOCAL_SUBS_ID\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.OnSimpleLife\nimport li.songe.gkd.util.ThrottleTimer\nimport li.songe.gkd.util.UpdateStatus\nimport li.songe.gkd.util.appIconMapFlow\nimport li.songe.gkd.util.clearCache\nimport li.songe.gkd.util.client\nimport li.songe.gkd.util.findOption\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.openUri\nimport li.songe.gkd.util.openWeChatScaner\nimport li.songe.gkd.util.runMainPost\nimport li.songe.gkd.util.stopCoroutine\nimport li.songe.gkd.util.subsFolder\nimport li.songe.gkd.util.subsItemsFlow\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateSubsMutex\nimport li.songe.gkd.util.updateSubscription\nimport rikka.shizuku.Shizuku\nimport kotlin.reflect.jvm.jvmName\n\nclass MainViewModel : BaseViewModel(), OnSimpleLife {\n    companion object {\n        private var _instance: MainViewModel? = null\n        val instance get() = _instance!!\n        private var tempTermsAccepted = false\n    }\n\n    init {\n        _instance = this\n        addCloseable {\n            if (_instance == this) { // 可能同时存在 2 个 MainViewModel 实例\n                _instance = null\n            }\n        }\n    }\n\n    override val scope get() = viewModelScope\n\n    val backStack: NavBackStack<NavKey> = NavBackStack(HomeRoute)\n    val topRoute get() = backStack.last()\n\n    private val backThrottleTimer = ThrottleTimer()\n    fun popPage() = runMainPost {\n        if (backThrottleTimer.expired() && backStack.size > 1) {\n            backStack.removeAt(backStack.lastIndex)\n        }\n    }\n\n    fun navigatePage(navKey: NavKey, replaced: Boolean = false) = runMainPost {\n        if (navKey != backStack.last()) {\n            if (replaced) {\n                backStack[backStack.lastIndex] = navKey\n            } else {\n                backStack.add(navKey)\n            }\n        }\n    }\n\n    fun navigateWebPage(url: String) = navigatePage(WebViewRoute(url))\n\n    val dialogFlow = MutableStateFlow<AlertDialogOptions?>(null)\n    val authReasonFlow = MutableStateFlow<AuthReason?>(null)\n\n    val updateStatus = if (META.updateEnabled) UpdateStatus(viewModelScope) else null\n\n    val shizukuErrorFlow = MutableStateFlow<Throwable?>(null)\n\n    val uploadOptions = UploadOptions(this)\n\n    val showEditCookieDlgFlow = MutableStateFlow(false)\n\n    val inputSubsLinkOption = InputSubsLinkOption()\n\n    val sheetSubsIdFlow = MutableStateFlow<Long?>(null)\n\n    val showShareDataIdsFlow = MutableStateFlow<Set<Long>?>(null)\n\n    val appOrderListFlow = DbSet.actionLogDao.queryLatestUniqueAppIds().stateInit(emptyList())\n    val appVisitOrderMapFlow = DbSet.appVisitLogDao.query().map {\n        it.mapIndexed { i, appId -> appId to i }.toMap()\n    }.debounce(500).stateInit(emptyMap())\n\n    fun addOrModifySubs(\n        url: String,\n        oldItem: SubsItem? = null,\n    ) = viewModelScope.launchTry(Dispatchers.IO) {\n        if (updateSubsMutex.mutex.isLocked) return@launchTry\n        updateSubsMutex.withStateLock {\n            val subItems = subsItemsFlow.value\n            val text = try {\n                client.get(url).bodyAsText()\n            } catch (e: Exception) {\n                e.printStackTrace()\n                LogUtils.d(e)\n                toast(\"下载订阅文件失败\\n${e.message}\".trimEnd())\n                return@launchTry\n            }\n            val newSubsRaw = try {\n                RawSubscription.parse(text)\n            } catch (e: Exception) {\n                e.printStackTrace()\n                LogUtils.d(e)\n                toast(\"解析订阅文件失败\\n${e.message}\".trimEnd())\n                return@launchTry\n            }\n            if (oldItem == null) {\n                if (subItems.any { it.id == newSubsRaw.id }) {\n                    toast(\"订阅已存在\")\n                    return@launchTry\n                }\n            } else {\n                if (oldItem.id != newSubsRaw.id) {\n                    toast(\"订阅id不对应\")\n                    return@launchTry\n                }\n            }\n            if (newSubsRaw.id < 0) {\n                toast(\"订阅id不可为${newSubsRaw.id}\\n负数id为内部使用\")\n                return@launchTry\n            }\n            val newItem = oldItem?.copy(updateUrl = url) ?: SubsItem(\n                id = newSubsRaw.id,\n                updateUrl = url,\n                order = if (subItems.isEmpty()) 1 else (subItems.maxBy { it.order }.order + 1)\n            )\n            updateSubscription(newSubsRaw)\n            if (oldItem == null) {\n                DbSet.subsItemDao.insert(newItem)\n                toast(\"成功添加订阅\")\n            } else {\n                DbSet.subsItemDao.update(newItem)\n                toast(\"成功修改订阅\")\n            }\n        }\n    }\n\n    val ruleGroupState = RuleGroupState(this)\n\n    val textFlow = MutableStateFlow<String?>(null)\n    fun openUrl(url: String) {\n        if (URLUtil.isNetworkUrl(url)) {\n            textFlow.value = url\n        } else {\n            openUri(url)\n        }\n    }\n\n    val tabFlow = MutableStateFlow(BottomNavItem.Control.key)\n    val resetPageScrollEvent = MutableSharedFlow<BottomNavItem>()\n    private var lastClickTabTime = 0L\n    fun handleClickTab(navItem: BottomNavItem) {\n        val t = System.currentTimeMillis()\n        // double click\n        if (navItem.key == tabFlow.value && t - lastClickTabTime < 500) {\n            viewModelScope.launch { resetPageScrollEvent.emit(navItem) }\n        }\n        tabFlow.value = navItem.key\n        lastClickTabTime = t\n    }\n\n    fun handleGkdUri(uri: Uri) {\n        val notFoundToast = { toast(\"未知URI\\n${uri}\") }\n        when (uri.host) {\n            \"page\" -> when (uri.path) {\n                \"\" -> {\n                    val tab = uri.getQueryParameter(\"tab\")?.toIntOrNull()\n                    if (tab != null && BottomNavItem.allSubObjects.any { it.key == tab }) {\n                        tabFlow.value = tab\n                    }\n                }\n\n                \"/1\" -> navigatePage(AdvancedPageRoute)\n                \"/2\" -> navigatePage(SnapshotPageRoute)\n                \"/3\" -> navigatePage(AppOpsAllowRoute)\n                else -> notFoundToast()\n            }\n\n            \"invoke\" -> when (uri.path) {\n                \"/1\" -> openWeChatScaner()\n                else -> notFoundToast()\n            }\n\n            else -> notFoundToast()\n        }\n    }\n\n    fun handleIntent(intent: Intent) = viewModelScope.launchTry {\n        LogUtils.d(intent)\n        val uri = intent.data?.normalizeScheme()\n        val source = intent.getStringExtra(activityNavSourceName)\n        if (uri?.scheme == \"gkd\") {\n            handleGkdUri(uri)\n        } else if (source == OpenFileActivity::class.jvmName && uri != null) {\n            toast(\"加载导入中...\")\n            tabFlow.value = BottomNavItem.SubsManage.key\n            withContext(Dispatchers.IO) { importData(uri) }\n        }\n    }\n\n    val termsAcceptedFlow by lazy {\n        if (tempTermsAccepted) {\n            MutableStateFlow(true)\n        } else {\n            createTextFlow(\n                key = \"terms_accepted\",\n                decode = { it == \"true\" },\n                encode = {\n                    tempTermsAccepted = it\n                    it.toString()\n                },\n                scope = viewModelScope,\n            ).apply {\n                tempTermsAccepted = value\n            }\n        }\n    }\n\n    val githubCookieFlow by lazy {\n        createTextFlow(\n            key = \"github_cookie\",\n            decode = { it ?: \"\" },\n            encode = { it },\n            private = true,\n            scope = viewModelScope,\n        )\n    }\n\n    fun switchEnableShizuku(value: Boolean) {\n        if (updateBinderMutex.mutex.isLocked) {\n            toast(\"正在连接中，请稍后\")\n            return\n        }\n        storeFlow.update { s -> s.copy(enableShizuku = value) }\n    }\n\n    fun requestShizuku() {\n        if (shizukuContextFlow.value.ok) return\n        if (updateBinderMutex.mutex.isLocked) {\n            toast(\"正在连接中，请稍后\")\n            return\n        }\n        try {\n            Shizuku.requestPermission(Activity.RESULT_OK)\n        } catch (e: Throwable) {\n            shizukuErrorFlow.value = e\n        }\n    }\n\n    suspend fun guardShizukuContext() {\n        if (shizukuContextFlow.value.ok) return\n        if (!storeFlow.value.enableShizuku) {\n            storeFlow.update { it.copy(enableShizuku = true) }\n        }\n        if (!shizukuGrantedState.updateAndGet()) {\n            requestShizuku()\n            stopCoroutine()\n        }\n        if (shizukuContextFlow.value.ok) return\n        stopCoroutine()\n    }\n\n    private val a11yServicesFlow = useEnabledA11yServicesFlow()\n    val a11yServiceEnabledFlow = useA11yServiceEnabledFlow(a11yServicesFlow)\n    val hasOtherA11yFlow = a11yServicesFlow.mapNew { list ->\n        list.any { it != A11yService.a11yCn }\n    }\n\n    val automatorModeFlow = storeFlow.mapNew {\n        AutomatorModeOption.objects.findOption(it.automatorMode)\n    }\n\n    fun updateAutomatorMode(option: AutomatorModeOption) {\n        if (automatorModeFlow.value == option) return\n        storeFlow.update { it.copy(automatorMode = option.value, enableAutomator = false) }\n        A11yService.instance?.shutdown()\n        uiAutomationFlow.value?.shutdown()\n    }\n\n    init {\n        // preload\n        appIconMapFlow.value\n        viewModelScope.launchTry(Dispatchers.IO) {\n            val subsItems = DbSet.subsItemDao.queryAll()\n            if (!subsItems.any { s -> s.id == LOCAL_SUBS_ID }) {\n                if (!subsFolder.resolve(\"${LOCAL_SUBS_ID}.json\").exists()) {\n                    updateSubscription(\n                        RawSubscription(\n                            id = LOCAL_SUBS_ID,\n                            name = \"本地订阅\",\n                            version = 0\n                        )\n                    )\n                }\n                DbSet.subsItemDao.insert(\n                    SubsItem(\n                        id = LOCAL_SUBS_ID,\n                        order = subsItems.minByOrNull { it.order }?.order ?: 0,\n                    )\n                )\n            }\n        }\n\n        viewModelScope.launchTry(Dispatchers.IO) {\n            // 每次进入删除缓存\n            clearCache()\n        }\n\n        if (updateStatus != null && termsAcceptedFlow.value) {\n            updateStatus.checkUpdate()\n        }\n\n        viewModelScope.launch(Dispatchers.IO) {\n            // preload\n            githubCookieFlow.value\n        }\n\n        // for OnSimpleLife\n        onCreated()\n        addCloseable { onDestroyed() }\n        toast(\"MainViewModel:init\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/OpenFileActivity.kt",
    "content": "package li.songe.gkd\n\nimport android.app.Activity\nimport android.os.Bundle\n\nclass OpenFileActivity : Activity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        navToMainActivity()\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/OpenSchemeActivity.kt",
    "content": "package li.songe.gkd\n\nimport android.app.Activity\nimport android.os.Bundle\n\nclass OpenSchemeActivity : Activity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        navToMainActivity()\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/OpenTileActivity.kt",
    "content": "package li.songe.gkd\n\nimport android.app.Activity\nimport android.content.pm.PackageManager\nimport android.os.Bundle\nimport androidx.core.net.toUri\nimport li.songe.gkd.util.extraCptName\n\nclass OpenTileActivity : Activity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val qsTileCpt = intent?.extraCptName\n        if (qsTileCpt != null && intent.data == null) {\n            val serviceInfo =\n                app.packageManager.getServiceInfo(qsTileCpt, PackageManager.GET_META_DATA)\n            val uriValue = serviceInfo.metaData.getString(\"QS_TILE_URI\")\n            if (uriValue != null) {\n                intent.data = uriValue.toUri()\n            }\n        }\n        navToMainActivity()\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/a11y/A11yCommonImpl.kt",
    "content": "package li.songe.gkd.a11y\n\nimport android.graphics.Bitmap\nimport android.view.accessibility.AccessibilityNodeInfo\nimport android.view.accessibility.AccessibilityWindowInfo\nimport kotlinx.coroutines.CoroutineScope\nimport li.songe.gkd.util.AutomatorModeOption\n\ninterface A11yCommonImpl {\n    suspend fun screenshot(): Bitmap?\n    val windowNodeInfo: AccessibilityNodeInfo?\n    val windowInfos: List<AccessibilityWindowInfo>\n    val scope: CoroutineScope\n    var justStarted: Boolean\n    val mode: AutomatorModeOption\n    val ruleEngine: A11yRuleEngine\n    fun shutdown(temp: Boolean = false)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/a11y/A11yContext.kt",
    "content": "package li.songe.gkd.a11y\n\nimport android.util.Log\nimport android.util.LruCache\nimport android.view.accessibility.AccessibilityNodeInfo\nimport kotlinx.atomicfu.atomic\nimport li.songe.gkd.META\nimport li.songe.gkd.data.ResolvedRule\nimport li.songe.gkd.shizuku.casted\nimport li.songe.gkd.util.InterruptRuleMatchException\nimport li.songe.selector.FastQuery\nimport li.songe.selector.MatchOption\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Selector\nimport li.songe.selector.Transform\nimport li.songe.selector.getBooleanInvoke\nimport li.songe.selector.getCharSequenceAttr\nimport li.songe.selector.getCharSequenceInvoke\nimport li.songe.selector.getIntInvoke\n\n\nprivate operator fun <K, V> LruCache<K, V>.set(child: K, value: V): V {\n    return put(child, value)\n}\n\nprivate fun List<Any>.getInt(i: Int = 0) = get(i) as Int\n\nprivate const val MAX_CACHE_SIZE = MAX_DESCENDANTS_SIZE\n\nprivate val AccessibilityNodeInfo?.notExpiredNode: AccessibilityNodeInfo?\n    get() {\n        if (this != null) {\n            val expiryMillis = if (text == null) 2000L else 1000L\n            if (isExpired(expiryMillis)) {\n                return null\n            }\n        }\n        return this\n    }\n\nclass A11yContext(\n    private val a11yEngine: A11yRuleEngine,\n    private val interruptable: Boolean = true,\n) {\n    private var childCache =\n        LruCache<Pair<AccessibilityNodeInfo, Int>, AccessibilityNodeInfo>(MAX_CACHE_SIZE)\n    private var indexCache = LruCache<AccessibilityNodeInfo, Int>(MAX_CACHE_SIZE)\n    private var parentCache = LruCache<AccessibilityNodeInfo, AccessibilityNodeInfo>(MAX_CACHE_SIZE)\n    val rootCache = atomic<AccessibilityNodeInfo?>(null)\n\n    private fun clearChildCache(node: AccessibilityNodeInfo) {\n        repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { i ->\n            childCache.remove(node to i)?.let {\n                clearChildCache(it)\n            }\n        }\n    }\n\n    fun clearNodeCache(eventNode: AccessibilityNodeInfo? = null) {\n        if (rootCache.value?.packageName != topActivityFlow.value.appId) {\n            rootCache.value = null\n        }\n        if (eventNode != null) {\n            clearChildCache(eventNode)\n            parentCache[eventNode]?.let { p ->\n                getPureIndex(eventNode)?.let { i ->\n                    childCache[p to i] = eventNode\n                }\n            }\n            if (rootCache.value == eventNode) {\n                rootCache.value = eventNode\n            } else {\n                if (META.debuggable) {\n                    Log.d(\n                        \"cache\",\n                        \"clear node cache ${eventNode.packageName}/${eventNode.className}\"\n                    )\n                }\n                return\n            }\n        }\n        if (META.debuggable) {\n            val sizeList = listOf(childCache.size(), parentCache.size(), indexCache.size())\n            if (sizeList.any { it > 0 }) {\n                Log.d(\"cache\", \"clear cache -> $sizeList\")\n            }\n        }\n        try {\n            childCache.evictAll()\n            parentCache.evictAll()\n            indexCache.evictAll()\n        } catch (_: Exception) {\n            // https://github.com/gkd-kit/gkd/issues/664\n            // 在某些机型上 未知原因 缓存不一致 导致删除失败\n            childCache = LruCache(MAX_CACHE_SIZE)\n            indexCache = LruCache(MAX_CACHE_SIZE)\n            parentCache = LruCache(MAX_CACHE_SIZE)\n        }\n    }\n\n    private var lastAppChangeTime = appChangeTime\n    fun clearOldAppNodeCache(): Boolean {\n        if (appChangeTime != lastAppChangeTime) {\n            lastAppChangeTime = appChangeTime\n            clearNodeCache()\n            return true\n        }\n        return false\n    }\n\n    var currentRule: ResolvedRule? = null\n\n    @Volatile\n    var interruptKey = 0\n    private var interruptInnerKey = 0\n\n    private fun guardInterrupt() {\n        if (!interruptable) return\n        if (interruptInnerKey == interruptKey) return\n        interruptInnerKey = interruptKey\n        val rule = currentRule ?: return\n        if (!activityRuleFlow.value.activePriority) return\n        if (!activityRuleFlow.value.currentRules.any { it === rule }) return\n        if (rule.isPriority()) return\n        if (META.debuggable) {\n            Log.d(\"guardInterrupt\", \"中断 rule=${rule.statusText()}\")\n        }\n        throw InterruptRuleMatchException()\n    }\n\n    private fun getA11Root(): AccessibilityNodeInfo? {\n        guardInterrupt()\n        return a11yEngine.safeActiveWindow\n    }\n\n    private fun getA11Child(node: AccessibilityNodeInfo, index: Int): AccessibilityNodeInfo? {\n        guardInterrupt()\n        return node.getChild(index)?.setGeneratedTime()\n    }\n\n    private fun getA11Parent(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {\n        guardInterrupt()\n        return node.parent?.setGeneratedTime()\n    }\n\n    private fun getA11ByText(\n        node: AccessibilityNodeInfo,\n        value: String\n    ): List<AccessibilityNodeInfo> {\n        guardInterrupt()\n        return node.findAccessibilityNodeInfosByText(value).apply {\n            forEach { it.setGeneratedTime() }\n        }\n    }\n\n    private fun getA11ById(\n        node: AccessibilityNodeInfo,\n        value: String\n    ): List<AccessibilityNodeInfo> {\n        guardInterrupt()\n        return node.findAccessibilityNodeInfosByViewId(value).apply {\n            forEach { it.setGeneratedTime() }\n        }\n    }\n\n    private fun getFastQueryNodes(\n        node: AccessibilityNodeInfo,\n        fastQuery: FastQuery\n    ): List<AccessibilityNodeInfo> {\n        return when (fastQuery) {\n            is FastQuery.Id -> getA11ById(node, fastQuery.value)\n            is FastQuery.Text -> getA11ByText(node, fastQuery.value)\n            is FastQuery.Vid -> getA11ById(node, \"${node.packageName}:id/${fastQuery.value}\")\n        }\n    }\n\n    private fun getCacheRoot(node: AccessibilityNodeInfo? = null): AccessibilityNodeInfo? {\n        if (rootCache.value.notExpiredNode == null) {\n            rootCache.value = getA11Root()\n        }\n        if (node == rootCache.value) return null\n        return rootCache.value\n    }\n\n    private fun getCacheParent(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {\n        if (getCacheRoot() == node) {\n            return null\n        }\n        parentCache[node].notExpiredNode?.let { return it }\n        return getA11Parent(node).apply {\n            if (this != null) {\n                parentCache[node] = this\n            } else {\n                rootCache.value = node\n            }\n        }\n    }\n\n    private fun getCacheChild(node: AccessibilityNodeInfo, index: Int): AccessibilityNodeInfo? {\n        if (index !in 0 until node.childCount) {\n            return null\n        }\n        return childCache[node to index].notExpiredNode ?: getA11Child(node, index)?.also { child ->\n            indexCache[child] = index\n            parentCache[child] = node\n            childCache[node to index] = child\n        }\n    }\n\n    private fun getPureIndex(node: AccessibilityNodeInfo): Int? {\n        return indexCache[node]\n    }\n\n    private fun getCacheIndex(node: AccessibilityNodeInfo): Int {\n        indexCache[node]?.let { return it }\n        getCacheChildren(getCacheParent(node)).forEachIndexed { index, child ->\n            if (child == node) {\n                indexCache[node] = index\n                return index\n            }\n        }\n        return 0\n    }\n\n    /**\n     * 在无缓存时, 此方法小概率造成无限节点片段,底层原因未知\n     *\n     * https://github.com/gkd-kit/gkd/issues/28\n     */\n    private fun getCacheDepth(node: AccessibilityNodeInfo): Int {\n        var p: AccessibilityNodeInfo = node\n        var depth = 0\n        while (true) {\n            val p2 = getCacheParent(p)\n            if (p2 != null) {\n                p = p2\n                depth++\n            } else {\n                break\n            }\n        }\n        return depth\n    }\n\n    private fun getCacheChildren(node: AccessibilityNodeInfo?): Sequence<AccessibilityNodeInfo> {\n        if (node == null) return emptySequence()\n        return sequence {\n            repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { index ->\n                val child = getCacheChild(node, index) ?: return@sequence\n                yield(child)\n            }\n        }\n    }\n\n    private var tempVid: CharSequence? = null\n    private var tempVidNode: AccessibilityNodeInfo? = null\n    private fun getTempVid(n: AccessibilityNodeInfo): CharSequence? {\n        if (n !== tempVidNode) {\n            tempVid = n.getVid()\n            tempVidNode = n\n        }\n        return tempVid\n    }\n\n    private fun getCacheAttr(node: AccessibilityNodeInfo, name: String): Any? = when (name) {\n        \"id\" -> node.viewIdResourceName\n        \"vid\" -> getTempVid(node)\n\n        \"name\" -> node.className\n        \"text\" -> node.text\n        \"desc\" -> node.contentDescription\n\n        \"clickable\" -> node.isClickable\n        \"focusable\" -> node.isFocusable\n        \"checkable\" -> node.isCheckable\n        \"checked\" -> node.compatChecked\n\n        \"editable\" -> node.isEditable\n        \"longClickable\" -> node.isLongClickable\n        \"visibleToUser\" -> node.isVisibleToUser\n\n        \"left\" -> node.casted.boundsInScreen.left\n        \"top\" -> node.casted.boundsInScreen.top\n        \"right\" -> node.casted.boundsInScreen.right\n        \"bottom\" -> node.casted.boundsInScreen.bottom\n\n        \"width\" -> node.casted.boundsInScreen.width()\n        \"height\" -> node.casted.boundsInScreen.height()\n\n        \"index\" -> getCacheIndex(node)\n        \"depth\" -> getCacheDepth(node)\n        \"childCount\" -> node.childCount\n\n        \"parent\" -> getCacheParent(node)\n\n        else -> null\n    }\n\n    private val transform = Transform(\n        getAttr = { target, name ->\n            when (target) {\n                is QueryContext<*> -> when (name) {\n                    \"prev\" -> target.prev\n                    \"current\" -> target.current\n                    else -> getCacheAttr(target.current as AccessibilityNodeInfo, name)\n                }\n\n                is AccessibilityNodeInfo -> getCacheAttr(target, name)\n                is CharSequence -> getCharSequenceAttr(target, name)\n                else -> null\n            }\n        },\n        getInvoke = { target, name, args ->\n            when (target) {\n                is AccessibilityNodeInfo -> when (name) {\n                    \"getChild\" -> {\n                        getCacheChild(target, args.getInt())\n                    }\n\n                    else -> null\n                }\n\n                is QueryContext<*> -> when (name) {\n                    \"getPrev\" -> {\n                        args.getInt().let { target.getPrev(it) }\n                    }\n\n                    \"getChild\" -> {\n                        getCacheChild(target.current as AccessibilityNodeInfo, args.getInt())\n                    }\n\n                    else -> null\n                }\n\n                is CharSequence -> getCharSequenceInvoke(target, name, args)\n                is Int -> getIntInvoke(target, name, args)\n                is Boolean -> getBooleanInvoke(target, name, args)\n\n                else -> null\n            }\n        },\n        getName = { node -> node.className },\n        getChildren = ::getCacheChildren,\n        getParent = ::getCacheParent,\n        getRoot = ::getCacheRoot,\n        getDescendants = { node ->\n            sequence {\n                val stack = getCacheChildren(node).toMutableList()\n                if (stack.isEmpty()) return@sequence\n                stack.reverse()\n                val tempNodes = mutableListOf<AccessibilityNodeInfo>()\n                do {\n                    val top = stack.removeAt(stack.lastIndex)\n                    yield(top)\n                    for (childNode in getCacheChildren(top)) {\n                        tempNodes.add(childNode)\n                    }\n                    if (tempNodes.isNotEmpty()) {\n                        for (i in tempNodes.size - 1 downTo 0) {\n                            stack.add(tempNodes[i])\n                        }\n                        tempNodes.clear()\n                    }\n                } while (stack.isNotEmpty())\n            }.take(MAX_DESCENDANTS_SIZE)\n        },\n        traverseChildren = { node, connectExpression ->\n            sequence {\n                repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { offset ->\n                    connectExpression.maxOffset?.let { maxOffset ->\n                        if (offset > maxOffset) return@sequence\n                    }\n                    if (connectExpression.checkOffset(offset)) {\n                        val child = getCacheChild(node, offset) ?: return@sequence\n                        yield(child)\n                    }\n                }\n            }\n        },\n        traverseBeforeBrothers = { node, connectExpression ->\n            sequence {\n                val parentVal = getCacheParent(node) ?: return@sequence\n                // 如果 node 由 fastQuery 得到, 则第一次调用此方法可能得到 cache.index 是空\n                val index = getPureIndex(node)\n                if (index != null) {\n                    var i = index - 1\n                    var offset = 0\n                    while (0 <= i && i < parentVal.childCount) {\n                        connectExpression.maxOffset?.let { maxOffset ->\n                            if (offset > maxOffset) return@sequence\n                        }\n                        if (connectExpression.checkOffset(offset)) {\n                            val child = getCacheChild(parentVal, i) ?: return@sequence\n                            yield(child)\n                        }\n                        i--\n                        offset++\n                    }\n                } else {\n                    val list = getCacheChildren(parentVal).takeWhile { it != node }.toMutableList()\n                    list.reverse()\n                    yieldAll(list.filterIndexed { i, _ ->\n                        connectExpression.checkOffset(\n                            i\n                        )\n                    })\n                }\n            }\n        },\n        traverseAfterBrothers = { node, connectExpression ->\n            val parentVal = getCacheParent(node)\n            if (parentVal != null) {\n                val index = getPureIndex(node)\n                if (index != null) {\n                    sequence {\n                        var i = index + 1\n                        var offset = 0\n                        while (0 <= i && i < parentVal.childCount) {\n                            connectExpression.maxOffset?.let { maxOffset ->\n                                if (offset > maxOffset) return@sequence\n                            }\n                            if (connectExpression.checkOffset(offset)) {\n                                val child = getCacheChild(parentVal, i) ?: return@sequence\n                                yield(child)\n                            }\n                            i++\n                            offset++\n                        }\n                    }\n                } else {\n                    getCacheChildren(parentVal).dropWhile { it != node }\n                        .drop(1)\n                        .let {\n                            if (connectExpression.maxOffset != null) {\n                                it.take(connectExpression.maxOffset!! + 1)\n                            } else {\n                                it\n                            }\n                        }\n                        .filterIndexed { i, _ ->\n                            connectExpression.checkOffset(\n                                i\n                            )\n                        }\n                }\n            } else {\n                emptySequence()\n            }\n        },\n        traverseDescendants = { node, connectExpression ->\n            sequence {\n                val stack = getCacheChildren(node).toMutableList()\n                if (stack.isEmpty()) return@sequence\n                stack.reverse()\n                val tempNodes = mutableListOf<AccessibilityNodeInfo>()\n                var offset = 0\n                do {\n                    val top = stack.removeAt(stack.lastIndex)\n                    if (connectExpression.checkOffset(offset)) {\n                        yield(top)\n                    }\n                    offset++\n                    if (offset > MAX_DESCENDANTS_SIZE) {\n                        return@sequence\n                    }\n                    connectExpression.maxOffset?.let { maxOffset ->\n                        if (offset > maxOffset) return@sequence\n                    }\n                    for (childNode in getCacheChildren(top)) {\n                        tempNodes.add(childNode)\n                    }\n                    if (tempNodes.isNotEmpty()) {\n                        for (i in tempNodes.size - 1 downTo 0) {\n                            stack.add(tempNodes[i])\n                        }\n                        tempNodes.clear()\n                    }\n                } while (stack.isNotEmpty())\n            }\n        },\n        traverseFastQueryDescendants = { node, list ->\n            sequence {\n                for (fastQuery in list) {\n                    val nodes = getFastQueryNodes(node, fastQuery)\n                    nodes.forEach { childNode ->\n                        yield(childNode)\n                    }\n                }\n            }\n        }\n    )\n\n    fun querySelfOrSelector(\n        node: AccessibilityNodeInfo,\n        selector: Selector,\n        option: MatchOption,\n    ): AccessibilityNodeInfo? {\n        if (selector.isMatchRoot) {\n            return selector.match(\n                getCacheRoot() ?: return null,\n                transform,\n                option\n            )\n        }\n        selector.match(node, transform, option)?.let {\n            return it\n        }\n        return transform.querySelector(node, selector, option)\n    }\n\n    fun queryRule(\n        rule: ResolvedRule,\n        node: AccessibilityNodeInfo,\n    ): AccessibilityNodeInfo? {\n        currentRule = rule\n        try {\n            val queryNode = if (rule.matchRoot) {\n                getCacheRoot()\n            } else {\n                node\n            } ?: return null\n            var resultNode: AccessibilityNodeInfo? = null\n            if (rule.anyMatches.isNotEmpty()) {\n                for (selector in rule.anyMatches) {\n                    resultNode = querySelfOrSelector(\n                        queryNode,\n                        selector,\n                        rule.matchOption,\n                    )\n                    if (resultNode != null) break\n                }\n                if (resultNode == null) return null\n            }\n            for (selector in rule.matches) {\n                resultNode = querySelfOrSelector(\n                    queryNode,\n                    selector,\n                    rule.matchOption,\n                ) ?: return null\n            }\n            for (selector in rule.excludeMatches) {\n                querySelfOrSelector(\n                    queryNode,\n                    selector,\n                    rule.matchOption,\n                )?.let { return null }\n            }\n            if (rule.excludeAllMatches.isNotEmpty()) {\n                val allExclude = rule.excludeAllMatches.all {\n                    querySelfOrSelector(\n                        queryNode,\n                        it,\n                        rule.matchOption,\n                    ) == null\n                }\n                if (!allExclude) {\n                    return null\n                }\n            }\n            return resultNode\n        } finally {\n            currentRule = null\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/a11y/A11yExt.kt",
    "content": "package li.songe.gkd.a11y\n\nimport android.content.ComponentName\nimport android.provider.Settings\nimport android.view.accessibility.AccessibilityEvent\nimport android.view.accessibility.AccessibilityNodeInfo\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport li.songe.gkd.app\nimport li.songe.gkd.contentObserver\nimport li.songe.gkd.service.A11yService\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.OnSimpleLife\nimport li.songe.gkd.util.mapState\nimport li.songe.selector.initDefaultTypeInfo\nimport kotlin.contracts.contract\n\ncontext(context: OnSimpleLife)\nfun useEnabledA11yServicesFlow(): StateFlow<Set<ComponentName>> {\n    val stateFlow = MutableStateFlow(app.getSecureA11yServices())\n    val contextObserver = contentObserver {\n        stateFlow.value = app.getSecureA11yServices()\n    }\n    app.registerObserver(\n        Settings.Secure.getUriFor(Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES),\n        contextObserver\n    )\n    context.onDestroyed {\n        app.unregisterObserver(contextObserver)\n    }\n    return stateFlow\n}\n\ncontext(context: OnSimpleLife)\nfun useA11yServiceEnabledFlow(servicesFlow: StateFlow<Set<ComponentName>> = useEnabledA11yServicesFlow()): StateFlow<Boolean> {\n    return servicesFlow.mapState(context.scope) {\n        it.contains(A11yService.a11yCn)\n    }\n}\n\nconst val STATE_CHANGED = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED\nconst val CONTENT_CHANGED = AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED\n\n// 某些应用耗时 300ms\nprivate val AccessibilityEvent.safeSource: AccessibilityNodeInfo?\n    get() = if (className == null) {\n        null // https://github.com/gkd-kit/gkd/issues/426 event.clear 已被系统调用\n    } else {\n        try {\n            source?.setGeneratedTime()\n        } catch (_: Exception) {\n            // 原因未知, 仍然报错 Cannot perform this action on a not sealed instance.\n            null\n        }\n    }\n\nfun AccessibilityNodeInfo.getVid(): CharSequence? {\n    val id = viewIdResourceName ?: return null\n    val appId = packageName ?: return null\n    if (id.startsWith(appId) && id.startsWith(\":id/\", appId.length)) {\n        return id.subSequence(\n            appId.length + \":id/\".length,\n            id.length\n        )\n    }\n    return null\n}\n\n// https://github.com/gkd-kit/gkd/issues/115\n// https://github.com/gkd-kit/gkd/issues/650\n// 限制节点遍历的数量避免内存溢出\nconst val MAX_CHILD_SIZE = 512\nconst val MAX_DESCENDANTS_SIZE = 4096\n\nprivate const val A11Y_NODE_TIME_KEY = \"generatedTime\"\nfun AccessibilityNodeInfo.setGeneratedTime(): AccessibilityNodeInfo {\n    extras.putLong(A11Y_NODE_TIME_KEY, System.currentTimeMillis())\n    return this\n}\n\nfun AccessibilityNodeInfo.isExpired(expiryMillis: Long): Boolean {\n    val generatedTime = extras.getLong(A11Y_NODE_TIME_KEY, -1)\n    if (generatedTime == -1L) {\n        // https://github.com/gkd-kit/gkd/issues/759\n        return true\n    }\n    return (System.currentTimeMillis() - generatedTime) > expiryMillis\n}\n\nval typeInfo by lazy { initDefaultTypeInfo().globalType }\n\nval AccessibilityNodeInfo.compatChecked: Boolean?\n    get() = if (AndroidTarget.BAKLAVA) {\n        when (checked) {\n            AccessibilityNodeInfo.CHECKED_STATE_TRUE -> true\n            AccessibilityNodeInfo.CHECKED_STATE_FALSE -> false\n            AccessibilityNodeInfo.CHECKED_STATE_PARTIAL -> null\n            else -> null\n        }\n    } else {\n        @Suppress(\"DEPRECATION\")\n        isChecked\n    }\n\n\nprivate const val interestedEvents = STATE_CHANGED or CONTENT_CHANGED\nfun AccessibilityEvent?.isUseful(): Boolean {\n    contract {\n        returns(true) implies (this@isUseful != null)\n    }\n    return (this != null && packageName != null && className != null && eventType and interestedEvents != 0)\n}\n\ndata class A11yEvent(\n    val type: Int,\n    val time: Long,\n    val appId: String,\n    val name: String,\n    val event: AccessibilityEvent,\n) {\n    val safeSource: AccessibilityNodeInfo?\n        get() = event.safeSource\n\n    fun sameAs(other: A11yEvent): Boolean {\n        if (other === this) return true\n        return type == other.type && appId == other.appId && name == other.name\n    }\n}\n\n// AccessibilityEvent 的 clear 方法会在后续时间被 某些系统 调用导致内部数据丢失, 导致异步子线程获取到的数据不一致\nfun AccessibilityEvent.toA11yEvent(): A11yEvent? {\n    val appId = packageName ?: return null\n    val b = className ?: return null\n    return A11yEvent(\n        type = eventType,\n        time = System.currentTimeMillis(),\n        appId = appId.toString(),\n        name = b.toString(),\n        event = this,\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/a11y/A11yFeat.kt",
    "content": "package li.songe.gkd.a11y\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.util.Log\nimport android.view.accessibility.AccessibilityEvent\nimport androidx.core.content.ContextCompat\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.app\nimport li.songe.gkd.appScope\nimport li.songe.gkd.permission.shizukuGrantedState\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.ScreenUtils\nimport li.songe.gkd.util.SnapshotExt\nimport li.songe.gkd.util.UpdateTimeOption\nimport li.songe.gkd.util.checkSubsUpdate\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.mapState\nimport li.songe.selector.MatchOption\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Selector\nimport li.songe.selector.Transform\nimport li.songe.selector.getBooleanInvoke\nimport li.songe.selector.getCharSequenceAttr\nimport li.songe.selector.getCharSequenceInvoke\nimport li.songe.selector.getIntInvoke\n\n\nfun onA11yFeatEvent(event: AccessibilityEvent) = event.run {\n    if (event.eventType == STATE_CHANGED) {\n        watchCaptureScreenshot()\n        if (event.packageName == launcherAppId) {\n            watchCheckShizukuState()\n            watchAutoUpdateSubs()\n        }\n    }\n}\n\nprivate var lastCheckShizukuTime = 0L\nprivate fun watchCheckShizukuState() {\n    // 借助无障碍轮询校验 shizuku 权限, 因为 shizuku 可能无故被关闭\n    if (storeFlow.value.enableShizuku) {\n        val t = System.currentTimeMillis()\n        if (t - lastCheckShizukuTime > 60 * 60_000L) {\n            lastCheckShizukuTime = t\n            appScope.launchTry(Dispatchers.IO) {\n                shizukuGrantedState.updateAndGet()\n            }\n        }\n    }\n}\n\nprivate var tempEventSelector = \"\" to (null as Selector?)\nprivate fun AccessibilityEvent.getEventAttr(name: String): Any? = when (name) {\n    \"name\" -> className\n    \"desc\" -> contentDescription\n    \"text\" -> text\n    else -> null\n}\n\nprivate val a11yEventTransform by lazy {\n    Transform<AccessibilityEvent>(\n        getAttr = { target, name ->\n            when (target) {\n                is QueryContext<*> -> when (name) {\n                    \"prev\" -> target.prev\n                    \"current\" -> target.current\n                    else -> (target.current as AccessibilityEvent).getEventAttr(name)\n                }\n\n                is CharSequence -> getCharSequenceAttr(target, name)\n                is AccessibilityEvent -> target.getEventAttr(name)\n                is List<*> -> when (name) {\n                    \"size\" -> target.size\n                    else -> null\n                }\n\n                else -> null\n            }\n        },\n        getInvoke = { target, name, args ->\n            Log.d(\"A11yEventTransform\", \"getInvoke: $name(${args.joinToString()}) on $target\")\n            when (target) {\n                is Int -> getIntInvoke(target, name, args)\n                is Boolean -> getBooleanInvoke(target, name, args)\n                is CharSequence -> getCharSequenceInvoke(target, name, args)\n                is List<*> -> when (name) {\n                    \"get\" -> {\n                        (args.singleOrNull() as? Int)?.let { index ->\n                            target.getOrNull(index)\n                        }\n                    }\n\n                    else -> null\n                }\n\n                else -> null\n            }\n        },\n        getName = { it.className },\n        getChildren = { emptySequence() },\n        getParent = { null }\n    )\n}\n\ncontext(event: AccessibilityEvent)\nprivate fun watchCaptureScreenshot() {\n    if (!storeFlow.value.captureScreenshot) return\n    if (event.packageName != storeFlow.value.screenshotTargetAppId) return\n    if (tempEventSelector.first != storeFlow.value.screenshotEventSelector) {\n        tempEventSelector =\n            storeFlow.value.screenshotEventSelector to Selector.parseOrNull(storeFlow.value.screenshotEventSelector)\n    }\n    val selector = tempEventSelector.second ?: return\n    selector.match(event, a11yEventTransform, MatchOption(fastQuery = false)).let {\n        if (it == null) return\n    }\n    appScope.launchTry {\n        SnapshotExt.captureSnapshot(skipScreenshot = true)\n    }\n}\n\nprivate var lastUpdateSubsTime = 0L\nprivate fun watchAutoUpdateSubs() {\n    val i = storeFlow.value.updateSubsInterval\n    if (i <= 0) return\n    val t = System.currentTimeMillis()\n    if (t - lastUpdateSubsTime > i.coerceAtLeast(UpdateTimeOption.Everyday.value)) {\n        lastUpdateSubsTime = t\n        checkSubsUpdate()\n    }\n}\n\nprivate fun initRuleChangedLog() {\n    appScope.launch(Dispatchers.Default) {\n        activityRuleFlow.debounce(300).drop(1).collect {\n            if (storeFlow.value.enableMatch && it.currentRules.isNotEmpty()) {\n                LogUtils.d(it.topActivity, *it.currentRules.map { r ->\n                    r.statusText()\n                }.toTypedArray())\n            }\n        }\n    }\n}\n\nprivate const val volumeChangedAction = \"android.media.VOLUME_CHANGED_ACTION\"\nprivate fun createVolumeReceiver() = object : BroadcastReceiver() {\n    var lastVolumeTriggerTime = -1L\n    override fun onReceive(context: Context?, intent: Intent?) {\n        if (intent?.action == volumeChangedAction) {\n            val t = System.currentTimeMillis()\n            if (t - lastVolumeTriggerTime > 3000 && !ScreenUtils.isScreenLock()) {\n                lastVolumeTriggerTime = t\n                appScope.launchTry {\n                    SnapshotExt.captureSnapshot()\n                }\n            }\n        }\n    }\n}\n\nprivate fun initCaptureVolume() {\n    var captureVolumeReceiver: BroadcastReceiver? = null\n    val changeRegister: (Boolean) -> Unit = {\n        captureVolumeReceiver?.let(app::unregisterReceiver)\n        captureVolumeReceiver = if (it) {\n            createVolumeReceiver().apply {\n                ContextCompat.registerReceiver(\n                    app,\n                    this,\n                    IntentFilter(volumeChangedAction),\n                    ContextCompat.RECEIVER_EXPORTED\n                )\n            }\n        } else {\n            null\n        }\n    }\n    appScope.launch(Dispatchers.IO) {\n        storeFlow.mapState(appScope) { s -> s.captureVolumeChange }.collect(changeRegister)\n    }\n}\n\nvar isInteractive = true\n    private set\nprivate val screenStateReceiver = object : BroadcastReceiver() {\n    override fun onReceive(\n        context: Context?,\n        intent: Intent?\n    ) {\n        val action = intent?.action ?: return\n        LogUtils.d(\"screenStateReceiver->${action}\")\n        isInteractive = when (action) {\n            Intent.ACTION_SCREEN_ON -> true\n            Intent.ACTION_SCREEN_OFF -> false\n            Intent.ACTION_USER_PRESENT -> true\n            else -> isInteractive\n        }\n        if (isInteractive) {\n            val t = System.currentTimeMillis()\n            if (t - appChangeTime > 500) { // 37.872(a11y) -> 38.228(onReceive)\n                A11yRuleEngine.onScreenForcedActive()\n            }\n        }\n    }\n}\n\nprivate fun initScreenStateReceiver() {\n    isInteractive = app.powerManager.isInteractive\n    ContextCompat.registerReceiver(\n        app,\n        screenStateReceiver,\n        IntentFilter().apply {\n            addAction(Intent.ACTION_SCREEN_ON)\n            addAction(Intent.ACTION_SCREEN_OFF)\n            addAction(Intent.ACTION_USER_PRESENT)\n        },\n        ContextCompat.RECEIVER_EXPORTED\n    )\n}\n\nfun initA11yFeat() {\n    initRuleChangedLog()\n    initCaptureVolume()\n    initScreenStateReceiver()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/a11y/A11yRuleEngine.kt",
    "content": "package li.songe.gkd.a11y\n\nimport android.accessibilityservice.AccessibilityService\nimport android.graphics.Bitmap\nimport android.util.Log\nimport android.view.KeyEvent\nimport android.view.accessibility.AccessibilityEvent\nimport android.view.accessibility.AccessibilityNodeInfo\nimport android.view.accessibility.AccessibilityWindowInfo\nimport kotlinx.atomicfu.atomic\nimport kotlinx.atomicfu.getAndUpdate\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.asCoroutineDispatcher\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runInterruptible\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.withTimeoutOrNull\nimport li.songe.gkd.META\nimport li.songe.gkd.data.ActionPerformer\nimport li.songe.gkd.data.ActionResult\nimport li.songe.gkd.data.AppRule\nimport li.songe.gkd.data.GkdAction\nimport li.songe.gkd.data.ResolvedRule\nimport li.songe.gkd.data.RpcError\nimport li.songe.gkd.data.RuleStatus\nimport li.songe.gkd.isActivityVisible\nimport li.songe.gkd.service.A11yService\nimport li.songe.gkd.service.EventService\nimport li.songe.gkd.service.topAppIdFlow\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.shizuku.uiAutomationFlow\nimport li.songe.gkd.store.actualBlockA11yAppList\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.util.AutomatorModeOption\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.runMainPost\nimport li.songe.gkd.util.showActionToast\nimport li.songe.gkd.util.systemUiAppId\nimport li.songe.selector.MatchOption\nimport li.songe.selector.Selector\nimport java.util.concurrent.Executors\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\n\n\nprivate val eventDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()\nprivate val queryDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()\nprivate val actionDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()\n\nprivate val latestServiceMode = atomic(0)\nprivate val latestServiceTime = atomic(0L)\n\nclass A11yRuleEngine(val service: A11yCommonImpl) {\n    private val a11yContext = A11yContext(this)\n    private val effective get() = latestServiceMode.value == service.mode.value\n    private val hasOthersService = when (service.mode) {\n        AutomatorModeOption.A11yMode -> uiAutomationFlow.value != null\n        AutomatorModeOption.AutomationMode -> A11yService.instance != null\n    }\n\n    fun onA11yConnected() {\n        val serviceTime = System.currentTimeMillis()\n        latestServiceMode.value = service.mode.value\n        latestServiceTime.value = serviceTime\n        if (storeFlow.value.enableBlockA11yAppList && !actualBlockA11yAppList.contains(topAppIdFlow.value)) {\n            startQueryJob(byForced = true)\n        }\n        runMainPost(1000L) {// 共存 1000ms, 等待另一个服务稳定\n            if (latestServiceTime.value == serviceTime) {\n                when (service.mode) {\n                    AutomatorModeOption.A11yMode -> uiAutomationFlow.value?.shutdown(true)\n                    AutomatorModeOption.AutomationMode -> A11yService.instance?.shutdown(true)\n                }\n            }\n        }\n    }\n\n    fun onScreenForcedActive() {\n        // 关闭屏幕 -> Activity::onStop -> 点亮屏幕 -> Activity::onStart -> Activity::onResume\n        val a = topActivityFlow.value\n        updateTopActivity(a.appId, a.activityId, scene = ActivityScene.ScreenOn)\n        startQueryJob()\n    }\n\n    val safeActiveWindow: AccessibilityNodeInfo?\n        get() = try {\n            // 某些应用耗时 554ms\n            // java.lang.SecurityException: Call from user 0 as user -2 without permission INTERACT_ACROSS_USERS or INTERACT_ACROSS_USERS_FULL not allowed.\n            service.windowNodeInfo?.setGeneratedTime()\n        } catch (_: Throwable) {\n            null\n        }.apply {\n            a11yContext.rootCache.value = this\n        }\n\n    val safeActiveWindowAppId: String?\n        get() = safeActiveWindow?.packageName?.toString()\n\n    private val scope get() = service.scope\n\n    private var lastContentEventTime = 0L\n    private var lastEventTime = 0L\n    private val eventDeque = ArrayDeque<A11yEvent>()\n    fun onA11yEvent(event: AccessibilityEvent?) {\n        if (!effective) return\n        if (!event.isUseful()) return\n        onA11yFeatEvent(event)\n        if (event.eventType == CONTENT_CHANGED) {\n            if (!isInteractive) return // 屏幕关闭后仍然有无障碍事件 type:2048, time:8094, app:com.miui.aod, cls:android.widget.TextView\n            if (event.packageName == systemUiAppId && event.packageName != topActivityFlow.value.appId) return\n        }\n        // 过滤部分输入法事件\n        if (event.packageName == imeAppId && topActivityFlow.value.appId != imeAppId) {\n            if (event.recordCount == 0 && event.action == 0 && !event.isFullScreen) return\n        }\n        // 直接丢弃自身事件，自行更新 topActivity\n        if ((event.eventType == CONTENT_CHANGED || !isActivityVisible) && event.packageName == META.appId) return\n\n        val a11yEvent = event.toA11yEvent() ?: return\n        if (a11yEvent.type == CONTENT_CHANGED) {\n            // 防止 content 类型事件过快\n            if (a11yEvent.time - lastContentEventTime < 100 && a11yEvent.time - appChangeTime > 5000 && a11yEvent.time - lastTriggerTime > 3000) {\n                return\n            }\n            lastContentEventTime = a11yEvent.time\n        }\n        EventService.logEvent(event)\n        if (META.debuggable) {\n            Log.d(\n                \"onNewA11yEvent\",\n                \"type:${event.eventType}, time:${event.eventTime - lastEventTime}, app:${event.packageName}, cls:${event.className}\"\n            )\n        }\n        if (event.eventTime < lastEventTime) {\n            // 某些应用会发送负时间事件, 直接丢弃\n            // type:32, time:-104, app:com.miui.home, cls:com.miui.home.launcher.Launcher\n            return\n        }\n        lastEventTime = event.eventTime\n        synchronized(eventDeque) { eventDeque.addLast(a11yEvent) }\n        scope.launch(eventDispatcher) { consumeEvent(a11yEvent) }\n    }\n\n    private val queryEvents = mutableListOf<A11yEvent>()\n    private suspend fun consumeEvent(headEvent: A11yEvent) {\n        val consumedEvents = synchronized(eventDeque) {\n            if (eventDeque.firstOrNull() !== headEvent) return\n            eventDeque.filter { it.sameAs(headEvent) }.apply {\n                repeat(size) { eventDeque.removeFirst() }\n            }\n        }\n        val latestEvent = consumedEvents.last()\n        val evAppId = latestEvent.appId\n        val evActivityId = latestEvent.name\n        val oldAppId = topActivityFlow.value.appId\n        val rightAppId = if (oldAppId == evAppId) {\n            evAppId\n        } else {\n            getTimeoutAppId() ?: return\n        }\n        if (rightAppId == evAppId) {\n            if (latestEvent.type == STATE_CHANGED) {\n                // tv.danmaku.bili, com.miui.home, com.miui.home.launcher.Launcher\n                if (isActivity(evAppId, evActivityId)) {\n                    updateTopActivity(evAppId, evActivityId)\n                }\n            }\n        }\n        if (rightAppId != topActivityFlow.value.appId) {\n            // 从 锁屏，下拉通知栏 返回等情况, 应用不会发送事件, 但是系统组件会发送事件\n            val topCpn = shizukuContextFlow.value.topCpn()\n            if (topCpn?.packageName == rightAppId) {\n                updateTopActivity(topCpn.packageName, topCpn.className)\n            } else {\n                updateTopActivity(rightAppId, null)\n            }\n        }\n        val activityRule = activityRuleFlow.value\n        if (evAppId != rightAppId || activityRule.skipConsumeEvent || !storeFlow.value.enableMatch) {\n            return\n        }\n        synchronized(queryEvents) { queryEvents.addAll(consumedEvents) }\n        a11yContext.interruptKey++\n        startQueryJob(byEvent = latestEvent)\n    }\n\n    private var lastGetAppIdTime = 0L\n    private var lastAppId: String? = null\n    private suspend fun getTimeoutAppId(): String? {\n        if (lastAppId != null && System.currentTimeMillis() - lastGetAppIdTime <= 100) return lastAppId\n        // 某些应用通过无障碍获取 safeActiveWindow 耗时长，导致多个事件连续堆积堵塞，无法检测到 appId 切换导致状态异常\n        // https://github.com/gkd-kit/gkd/issues/622\n        lastAppId = withTimeoutOrNull(100) {\n            runInterruptible(Dispatchers.IO) { safeActiveWindowAppId }\n        } ?: shizukuContextFlow.value.topCpn()?.packageName\n        lastGetAppIdTime = System.currentTimeMillis()\n        return lastAppId\n    }\n\n    // 某些场景耗时 5000 ms\n    private suspend fun getTimeoutActiveWindow(): AccessibilityNodeInfo? = suspendCoroutine { s ->\n        val temp = atomic<Continuation<AccessibilityNodeInfo?>?>(s)\n        scope.launch(Dispatchers.IO) {\n            delay(500L)\n            temp.getAndUpdate { null }?.resume(null)\n        }\n        scope.launch(Dispatchers.IO) {\n            val a = safeActiveWindow\n            temp.getAndUpdate { null }?.resume(a)\n        }\n    }\n\n    @Volatile\n    private var querying = false\n\n    @Synchronized\n    private fun startQueryJob(\n        byEvent: A11yEvent? = null,\n        byForced: Boolean = false,\n        byDelayRule: ResolvedRule? = null,\n    ) {\n        if (!effective) return\n        if (!storeFlow.value.enableMatch) return\n        if (activityRuleFlow.value.currentRules.isEmpty()) return\n        if (querying) return\n        // 无障碍从零启动时获取 safeActiveWindow 非常耗时\n        if (byEvent == null && service.justStarted && !hasOthersService) return checkFutureStartJob()\n        scope.launchTry(queryDispatcher) {\n            querying = true\n            val st = System.currentTimeMillis()\n            try {\n                Log.d(\n                    \"A11yRuleEngine\",\n                    \"startQueryJob start byEvent=${byEvent != null}, byForced=$byForced, byDelayRule=${byDelayRule != null}\"\n                )\n                queryAction(byEvent, byForced, byDelayRule)\n            } finally {\n                checkFutureStartJob()\n                val et = System.currentTimeMillis() - st\n                Log.d(\"A11yRuleEngine\", \"startQueryJob end $et ms\")\n                querying = false\n            }\n        }\n    }\n\n    private fun checkFutureStartJob() {\n        val t = System.currentTimeMillis()\n        if (t - lastTriggerTime < 3000L || t - appChangeTime < 3000L) {\n            scope.launch(actionDispatcher) {\n                delay(300)\n                startQueryJob()\n            }\n        } else if (activityRuleFlow.value.hasFeatureAction) {\n            scope.launch(actionDispatcher) {\n                delay(300)\n                startQueryJob(byForced = true)\n            }\n        }\n    }\n\n    private fun fixAppId(rightAppId: String) {\n        if (topActivityFlow.value.appId == rightAppId) return\n        val topCpn = shizukuContextFlow.value.topCpn()\n        if (topCpn?.packageName == rightAppId) {\n            updateTopActivity(topCpn.packageName, topCpn.className)\n        } else {\n            updateTopActivity(rightAppId, null)\n        }\n        scope.launch(actionDispatcher) {\n            delay(300)\n            startQueryJob()\n        }\n    }\n\n    private suspend fun queryAction(\n        byEvent: A11yEvent? = null,\n        byForced: Boolean = false,\n        delayRule: ResolvedRule? = null,\n    ) {\n        val newEvents = if (delayRule != null) {// 延迟规则不消耗事件\n            null\n        } else {\n            synchronized(queryEvents) {\n                if (byEvent != null && queryEvents.isEmpty()) {\n                    return\n                }\n                (if (queryEvents.size > 1) {\n                    val hasDiffItem = queryEvents.any { e ->\n                        queryEvents.any { e2 -> !e.sameAs(e2) }\n                    }\n                    if (hasDiffItem) {\n                        // 存在不同的事件节点, 全部丢弃使用 root 查询\n                        null\n                    } else {\n                        // type,appId,className 一致, 需要在 synchronized 外验证是否是同一节点\n                        arrayOf(\n                            queryEvents[queryEvents.size - 2],\n                            queryEvents.last(),\n                        )\n                    }\n                } else if (queryEvents.size == 1) {\n                    arrayOf(queryEvents.last())\n                } else {\n                    null\n                }).apply {\n                    queryEvents.clear()\n                }\n            }\n        }\n        val activityRule = activityRuleFlow.value\n        activityRule.currentRules.forEach { rule ->\n            if (rule.status == RuleStatus.Status3 && rule.matchDelayJob.value == null) {\n                rule.matchDelayJob.value = scope.launch(actionDispatcher) {\n                    delay(rule.matchDelay)\n                    rule.matchDelayJob.value = null\n                    startQueryJob(byDelayRule = rule)\n                }\n            }\n        }\n        if (activityRule.skipMatch) {\n            // 如果当前应用没有规则/暂停匹配, 则不去调用获取事件节点避免阻塞\n            return\n        }\n        var lastNode = if (newEvents == null || newEvents.size <= 1) {\n            newEvents?.firstOrNull()?.safeSource\n        } else {\n            // 获取最后两个事件, 如果最后两个事件的节点不一致, 则丢弃\n            // 相等则是同一个节点发出的连续事件, 常见于倒计时界面\n            val lastNode = newEvents.last().safeSource\n            if (lastNode == null || lastNode == newEvents[0].safeSource) {\n                lastNode\n            } else {\n                null\n            }\n        }\n        var lastNodeUsed = false\n        if (!a11yContext.clearOldAppNodeCache()) {\n            if (byEvent != null) { // 此为多数情况\n                // 新事件到来时, 若缓存清理不及时会导致无法查询到节点\n                a11yContext.clearNodeCache(lastNode)\n            }\n        }\n        for (rule in activityRule.priorityRules) { // 规则数量有可能过多导致耗时过长\n            if (!effective) return\n            if (activityRule !== activityRuleFlow.value) break\n            if (delayRule != null && delayRule !== rule) continue\n            if (rule.status != RuleStatus.StatusOk) continue\n            if (byForced && !rule.checkForced()) continue\n            lastNode?.let { n ->\n                val refreshOk = (!lastNodeUsed) || (try {\n                    val e = n.refresh()\n                    if (e) {\n                        n.setGeneratedTime()\n                    }\n                    e\n                } catch (_: Throwable) {\n                    false\n                })\n                lastNodeUsed = true\n                if (!refreshOk) {\n                    lastNode = null\n                }\n            }\n            val nodeVal = (lastNode ?: getTimeoutActiveWindow()) ?: continue\n            val rightAppId = nodeVal.packageName?.toString() ?: break\n            val matchApp = rule.matchActivity(rightAppId)\n            if (topActivityFlow.value.appId != rightAppId || (!matchApp && rule is AppRule)) {\n                scope.launch(eventDispatcher) { fixAppId(rightAppId) }\n                return\n            }\n            if (!matchApp) continue\n            val target = a11yContext.queryRule(rule, nodeVal) ?: continue\n            if (activityRule !== activityRuleFlow.value) break\n            if (rule.checkDelay() && rule.actionDelayJob.value == null) {\n                rule.actionDelayJob.value = scope.launch(actionDispatcher) {\n                    delay(rule.actionDelay)\n                    rule.actionDelayJob.value = null\n                    startQueryJob(byDelayRule = rule)\n                }\n                continue\n            }\n            if (rule.status != RuleStatus.StatusOk) break\n            val actionResult = rule.performAction(target)\n            if (actionResult.result) {\n                val topActivity = topActivityFlow.value\n                rule.trigger()\n                scope.launch(actionDispatcher) {\n                    delay(300)\n                    startQueryJob()\n                }\n                if (actionResult.action != ActionPerformer.None.action) {\n                    showActionToast(rule)\n                }\n                addActionLog(rule, topActivity, target, actionResult)\n            }\n        }\n    }\n\n    companion object {\n        val service: A11yCommonImpl?\n            get() = uiAutomationFlow.value?.takeIf {\n                it.mode.value == latestServiceMode.value\n            } ?: A11yService.instance\n        val instance: A11yRuleEngine? get() = service?.ruleEngine\n\n        fun compatWindows(): List<AccessibilityWindowInfo> {\n            return try {\n                service?.windowInfos\n            } catch (_: Throwable) {\n                null\n            } ?: emptyList()\n        }\n\n        fun onScreenForcedActive() {\n            instance?.onScreenForcedActive()\n        }\n\n        fun performActionBack(): Boolean {\n            return (shizukuContextFlow.value.inputManager?.key(KeyEvent.KEYCODE_BACK)\n                ?: A11yService.instance?.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)) == true\n        }\n\n        suspend fun screenshot(): Bitmap? = service?.screenshot()\n\n        suspend fun execAction(gkdAction: GkdAction): ActionResult {\n            val selector = Selector.parseOrNull(gkdAction.selector) ?: throw RpcError(\"非法选择器\")\n            runCatching { selector.checkType(typeInfo) }.exceptionOrNull()?.let {\n                throw RpcError(\"选择器类型错误:${it.message}\")\n            }\n            val s = instance ?: throw RpcError(\"服务未连接\")\n            val a = s.safeActiveWindow ?: throw RpcError(\"界面没有节点信息\")\n            val targetNode = A11yContext(s, interruptable = false).querySelfOrSelector(\n                a, selector, MatchOption(fastQuery = gkdAction.fastQuery)\n            ) ?: throw RpcError(\"没有查询到节点\")\n            return withContext(Dispatchers.IO) {\n                ActionPerformer.getAction(gkdAction.action ?: ActionPerformer.None.action)\n                    .perform(targetNode, gkdAction.position)\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/a11y/A11yState.kt",
    "content": "package li.songe.gkd.a11y\n\nimport android.content.ComponentName\nimport android.content.Intent\nimport android.provider.Settings\nimport android.util.LruCache\nimport android.view.accessibility.AccessibilityNodeInfo\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport li.songe.gkd.META\nimport li.songe.gkd.app\nimport li.songe.gkd.appScope\nimport li.songe.gkd.data.ActionLog\nimport li.songe.gkd.data.ActionResult\nimport li.songe.gkd.data.ActivityLog\nimport li.songe.gkd.data.AttrInfo\nimport li.songe.gkd.data.ResetMatchType\nimport li.songe.gkd.data.ResolvedRule\nimport li.songe.gkd.data.RuleStatus\nimport li.songe.gkd.data.isSystem\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.service.updateTopTaskAppId\nimport li.songe.gkd.shizuku.safeInvokeShizuku\nimport li.songe.gkd.store.actionCountFlow\nimport li.songe.gkd.store.checkAppBlockMatch\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.PKG_FLAGS\nimport li.songe.gkd.util.RuleSummary\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.ruleSummaryFlow\nimport li.songe.gkd.util.systemUiAppId\nimport li.songe.loc.Loc\n\ndata class TopActivity(\n    val appId: String = \"\",\n    val activityId: String? = null,\n    val number: Int = 0\n) {\n    val shortActivityId: String?\n        get() {\n            val a = if (activityId != null && activityId.startsWith(appId)) {\n                activityId.substring(appId.length)\n            } else {\n                activityId\n            }\n            return a\n        }\n\n    fun format(): String {\n        return \"${appId}/${shortActivityId}/${number}\"\n    }\n\n    fun sameAs(a: String, b: String?): Boolean {\n        return appId == a && activityId == b\n    }\n\n    fun sameAs(cn: ComponentName): Boolean {\n        return appId == cn.packageName && activityId == cn.className\n    }\n}\n\nval topActivityFlow = MutableStateFlow(TopActivity())\nprivate var lastValidActivity: TopActivity = topActivityFlow.value\n    set(value) {\n        if (value.activityId != null) {\n            field = value\n        }\n    }\n\nprivate var activityLogCount = 0\nprivate var lastActivityUpdateTime = 0L\nprivate var lastActivityForceUpdateTime = 0L\nprivate val tempActivityLogList = mutableListOf<ActivityLog>()\n\nprivate object ActivityCache : LruCache<Pair<String, String>, Boolean>(256) {\n    override fun create(key: Pair<String, String>): Boolean = try {\n        app.packageManager.getActivityInfo(\n            ComponentName(key.first, key.second),\n            PKG_FLAGS\n        )\n        true\n    } catch (_: Exception) {\n        false\n    }\n}\n\nfun isActivity(\n    appId: String,\n    activityId: String,\n): Boolean {\n    return topActivityFlow.value.sameAs(appId, activityId) || ActivityCache.get(appId to activityId)\n}\n\nclass ActivityRule(\n    val topActivity: TopActivity = TopActivity(),\n    val ruleSummary: RuleSummary = RuleSummary(),\n) {\n    val blockMatch = checkAppBlockMatch(topActivity.appId)\n    val appRules = ruleSummary.appIdToRules[topActivity.appId] ?: emptyList()\n    val activityRules = if (blockMatch) emptyList() else appRules.filter { rule ->\n        rule.matchActivity(topActivity.appId, topActivity.activityId)\n    }\n    val globalRules = if (blockMatch) emptyList() else ruleSummary.globalRules.filter { r ->\n        r.matchActivity(topActivity.appId, topActivity.activityId)\n    }\n\n    val currentRules = (activityRules + globalRules).sortedBy { it.order }\n    val hasPriorityRule = currentRules.size > 1 && currentRules.any { it.priorityEnabled }\n    val activePriority: Boolean\n        get() = hasPriorityRule && currentRules.any { it.isPriority() }\n    val priorityRules: List<ResolvedRule>\n        get() = if (hasPriorityRule) {\n            currentRules.sortedBy { if (it.isPriority()) 0 else 1 }\n        } else {\n            currentRules\n        }\n    val skipMatch: Boolean\n        get() {\n            return currentRules.all { r -> !r.status.ok }\n        }\n    val skipConsumeEvent: Boolean\n        get() {\n            return currentRules.all { r -> !r.status.alive }\n        }\n    val hasFeatureAction: Boolean\n        get() = currentRules.any { r -> r.checkForced() && (r.status == RuleStatus.StatusOk || r.status == RuleStatus.Status5) }\n}\n\nval activityRuleFlow = MutableStateFlow(ActivityRule())\n\nprivate var lastAppId = \"\"\n\nsealed class ActivityScene {\n    data object ScreenOn : ActivityScene()\n    data object A11y : ActivityScene()\n    data object TaskStack : ActivityScene()\n}\n\n@Loc\n@Synchronized\nfun updateTopActivity(\n    appId: String,\n    activityId: String?,\n    scene: ActivityScene = ActivityScene.A11y,\n    @Loc loc: String = \"\",\n) {\n    val t = System.currentTimeMillis()\n    if (scene == ActivityScene.TaskStack) {\n        updateTopTaskAppId(appId)\n    }\n    val oldActivity = topActivityFlow.value\n    val isSame = scene != ActivityScene.ScreenOn && oldActivity.sameAs(appId, activityId)\n    if (scene == ActivityScene.TaskStack) {\n        lastActivityForceUpdateTime = t\n    } else if (scene == ActivityScene.A11y) {\n        if (lastActivityForceUpdateTime > 0) {\n            // ITaskStackListener 的变速快于无障碍\n            if (t - lastActivityForceUpdateTime < 1000) return\n            if (activityId != null && t - lastActivityForceUpdateTime < 3000) return\n        }\n        if (isSame && t - lastActivityUpdateTime < 1000) return\n    }\n    val number = if (isSame) {\n        oldActivity.number + 1\n    } else {\n        0\n    }\n    topActivityFlow.value = TopActivity(\n        appId = appId,\n        activityId = activityId ?: lastValidActivity.takeIf { it.appId == appId }?.activityId,\n        number = number,\n    )\n    lastValidActivity = oldActivity\n    lastActivityUpdateTime = t\n    tempActivityLogList.add(\n        ActivityLog(\n            appId = appId,\n            activityId = activityId,\n            ctime = t,\n        )\n    )\n    if (tempActivityLogList.size >= 16 || appId == META.appId) {\n        val logs = tempActivityLogList.toTypedArray()\n        tempActivityLogList.clear()\n        appScope.launchTry {\n            DbSet.activityLogDao.insert(*logs)\n        }\n    }\n    if (activityLogCount++ % 100 == 0) {\n        appScope.launchTry { DbSet.activityLogDao.deleteKeepLatest() }\n    }\n    val topActivity = topActivityFlow.value\n    val oldActivityRule = activityRuleFlow.value\n    val ruleSummary = ruleSummaryFlow.value\n    val idChanged = (scene == ActivityScene.ScreenOn ||\n            topActivity.appId != oldActivityRule.topActivity.appId)\n    val topChanged = idChanged || oldActivityRule.topActivity != topActivity\n    val ruleChanged = oldActivityRule.ruleSummary !== ruleSummary\n    if (topChanged || ruleChanged) {\n        val newActivityRule = ActivityRule(\n            ruleSummary = ruleSummary,\n            topActivity = topActivity,\n        )\n        if (idChanged) {\n            val oldAppId = lastAppId\n            lastAppId = appId\n            appScope.launchTry {\n                DbSet.appVisitLogDao.insert(oldAppId, appId, t)\n            }\n            appChangeTime = t\n            ruleSummary.globalRules.forEach { it.resetState(t) }\n            ruleSummary.appIdToRules[oldActivityRule.topActivity.appId]?.forEach { it.resetState(t) }\n            newActivityRule.appRules.forEach { it.resetState(t) }\n        } else {\n            newActivityRule.currentRules.forEach { r ->\n                when (r.resetMatchType) {\n                    ResetMatchType.App -> {\n                        if (r.isFirstMatchApp) {\n                            r.resetState(t)\n                        }\n                    }\n\n                    ResetMatchType.Activity -> r.resetState(t)\n                    ResetMatchType.Match -> {\n                        // is new rule\n                        if (!oldActivityRule.currentRules.contains(r)) {\n                            r.resetState(t)\n                        }\n                    }\n                }\n            }\n        }\n        activityRuleFlow.value = newActivityRule\n        LogUtils.d(\n            \"${oldActivity.format()} -> ${topActivityFlow.value.format()} (scene=$scene)\",\n            loc = loc,\n            tag = \"updateTopActivity\",\n        )\n    }\n}\n\n@Volatile\nvar lastTriggerRule: ResolvedRule? = null\n\n@Volatile\nvar lastTriggerTime = 0L\n\n@Volatile\nvar appChangeTime = 0L\n\nvar imeAppId = \"\"\nvar launcherAppId = \"\"\nvar systemRecentCn = ComponentName(\"\", \"\")\n\nfun updateSystemDefaultAppId() {\n    imeAppId = app.getSecureString(Settings.Secure.DEFAULT_INPUT_METHOD)\n        ?.let(ComponentName::unflattenFromString)?.packageName ?: \"\"\n    val launcherCn = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)\n        .resolveActivity(app.packageManager)\n    launcherAppId = launcherCn.packageName\n    if (app.getPkgInfo(launcherAppId)?.applicationInfo?.isSystem == true) {\n        systemRecentCn = launcherCn\n    } else {\n        safeInvokeShizuku {\n            if (AndroidTarget.P) {\n                systemRecentCn = ComponentName.unflattenFromString(\n                    app.getString(com.android.internal.R.string.config_recentsComponentName)\n                ) ?: systemRecentCn\n            }\n        }\n        if (systemRecentCn.packageName.isEmpty()) {\n            // https://github.com/android-cs/8/blob/main/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java\n            systemRecentCn = ComponentName(\n                systemUiAppId,\n                \"$systemUiAppId.recents.RecentsActivity\",\n            )\n        }\n    }\n}\n\nprivate val actionLogMutex = Mutex()\nfun addActionLog(\n    rule: ResolvedRule,\n    topActivity: TopActivity,\n    target: AccessibilityNodeInfo,\n    actionResult: ActionResult,\n) = appScope.launchTry(Dispatchers.IO) {\n    val ctime = System.currentTimeMillis()\n    actionLogMutex.withLock {\n        val actionLog = ActionLog(\n            appId = topActivity.appId,\n            activityId = topActivity.activityId,\n            subsId = rule.subsItem.id,\n            subsVersion = rule.rawSubs.version,\n            groupKey = rule.g.group.key,\n            groupType = rule.g.group.groupType,\n            ruleIndex = rule.index,\n            ruleKey = rule.key,\n            ctime = ctime,\n        )\n        DbSet.actionLogDao.insert(actionLog)\n        if (actionCountFlow.value % 100 == 0L) {\n            DbSet.actionLogDao.deleteKeepLatest()\n        }\n    }\n    LogUtils.d(\n        rule.statusText(),\n        AttrInfo.info2data(target, 0, 0),\n        actionResult\n    )\n}.let {}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/A11yEventLog.kt",
    "content": "package li.songe.gkd.data\n\nimport android.view.accessibility.AccessibilityEvent\nimport androidx.paging.PagingSource\nimport androidx.room.ColumnInfo\nimport androidx.room.Dao\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.a11y.STATE_CHANGED\n\n@Serializable\n@Entity(tableName = \"a11y_event_log\")\nclass A11yEventLog(\n    @PrimaryKey @ColumnInfo(name = \"id\") val id: Int,\n    @ColumnInfo(name = \"ctime\") val ctime: Long,\n    @ColumnInfo(name = \"type\") val type: Int,\n    @ColumnInfo(name = \"appId\") val appId: String,\n    @ColumnInfo(name = \"name\") val name: String,\n    @ColumnInfo(name = \"desc\") val desc: String?,\n    @ColumnInfo(name = \"text\") val text: List<String>,\n) {\n    override fun equals(other: Any?): Boolean {\n        if (other !is A11yEventLog) return false\n        return id == other.id\n    }\n\n    override fun hashCode(): Int {\n        return id\n    }\n\n    val isStateChanged: Boolean\n        get() = type == STATE_CHANGED\n\n    val fixedName: String\n        get() {\n            if (isStateChanged && name.startsWith(appId)) {\n                return name.substring(appId.length)\n            }\n            if (name.contains(\"View\") || name.contains(\"Layout\") || viewSuffixes.any {\n                    name.startsWith(\n                        it\n                    )\n                }) {\n                return name.substring(name.lastIndexOf('.') + 1)\n            }\n            return name\n        }\n\n    @Dao\n    interface A11yEventLogDao {\n        @Insert\n        suspend fun insert(objects: List<A11yEventLog>): List<Long>\n\n        @Query(\"DELETE FROM a11y_event_log\")\n        suspend fun deleteAll()\n\n        @Query(\"SELECT COUNT(*) FROM a11y_event_log\")\n        fun count(): Flow<Int>\n\n        @Query(\"SELECT * FROM a11y_event_log ORDER BY ctime DESC \")\n        fun pagingSource(): PagingSource<Int, A11yEventLog>\n\n        @Query(\"SELECT MAX(id) FROM a11y_event_log\")\n        suspend fun maxId(): Int?\n\n        @Query(\n            \"\"\"\n            DELETE FROM a11y_event_log\n            WHERE (\n                    SELECT COUNT(*)\n                    FROM a11y_event_log\n                ) > 1000\n                AND id <= (\n                    SELECT id\n                    FROM a11y_event_log\n                    ORDER BY id DESC\n                    LIMIT 1 OFFSET 1000\n                )\n        \"\"\"\n        )\n        suspend fun deleteKeepLatest(): Int\n\n\n    }\n\n}\n\nprivate val viewSuffixes = listOf(\n    \"android.widget.\",\n    \"android.view.\",\n    \"android.support.\",\n)\n\nfun AccessibilityEvent.toA11yEventLog(id: Int) = A11yEventLog(\n    id = id,\n    ctime = System.currentTimeMillis(),\n    type = eventType,\n    appId = packageName.toString(),\n    name = className.toString(),\n    desc = contentDescription?.toString(),\n    text = text.map { it.toString() }\n)\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/ActionLog.kt",
    "content": "package li.songe.gkd.data\n\nimport androidx.paging.PagingSource\nimport androidx.room.ColumnInfo\nimport androidx.room.Dao\nimport androidx.room.DeleteTable\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.migration.AutoMigrationSpec\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.util.format\nimport li.songe.gkd.util.getShowActivityId\n\n@Serializable\n@Entity(\n    tableName = \"action_log\",\n)\ndata class ActionLog(\n    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = \"id\") val id: Int = 0,\n    @ColumnInfo(name = \"ctime\") val ctime: Long,\n    @ColumnInfo(name = \"app_id\") val appId: String,\n    @ColumnInfo(name = \"activity_id\") val activityId: String? = null,\n    @ColumnInfo(name = \"subs_id\") val subsId: Long,\n    @ColumnInfo(name = \"subs_version\", defaultValue = \"0\") val subsVersion: Int,\n    @ColumnInfo(name = \"group_key\") val groupKey: Int,\n    @ColumnInfo(name = \"group_type\", defaultValue = \"2\") val groupType: Int,\n    @ColumnInfo(name = \"rule_index\") val ruleIndex: Int,\n    @ColumnInfo(name = \"rule_key\") val ruleKey: Int? = null,\n) {\n\n    val showActivityId by lazy { getShowActivityId(appId, activityId) }\n\n    val date by lazy { ctime.format(\"MM-dd HH:mm:ss SSS\") }\n\n    @DeleteTable.Entries(\n        DeleteTable(tableName = \"click_log\")\n    )\n    class ActionLogSpec : AutoMigrationSpec\n\n\n    @Dao\n    interface ActionLogDao {\n\n\n        @Insert\n        suspend fun insert(vararg objects: ActionLog): List<Long>\n\n\n        @Query(\"DELETE FROM action_log WHERE subs_id IN (:subsIds)\")\n        suspend fun deleteBySubsId(vararg subsIds: Long): Int\n\n        @Query(\"DELETE FROM action_log\")\n        suspend fun deleteAll()\n\n        @Query(\"DELETE FROM action_log WHERE subs_id=:subsId\")\n        suspend fun deleteSubsAll(subsId: Long)\n\n        @Query(\"DELETE FROM action_log WHERE app_id=:appId\")\n        suspend fun deleteAppAll(appId: String)\n\n        @Query(\"SELECT * FROM action_log ORDER BY id DESC LIMIT 1000\")\n        fun query(): Flow<List<ActionLog>>\n\n        @Query(\"SELECT * FROM action_log ORDER BY id DESC \")\n        fun pagingSource(): PagingSource<Int, ActionLog>\n\n        @Query(\"SELECT * FROM action_log WHERE subs_id=:subsId ORDER BY id DESC \")\n        fun pagingSubsSource(subsId: Long): PagingSource<Int, ActionLog>\n\n        @Query(\"SELECT * FROM action_log WHERE app_id=:appId ORDER BY id DESC \")\n        fun pagingAppSource(appId: String): PagingSource<Int, ActionLog>\n\n        @Query(\"SELECT COUNT(*) FROM action_log\")\n        fun count(): Flow<Int>\n\n\n        @Query(\"SELECT * FROM action_log ORDER BY id DESC LIMIT 1\")\n        fun queryLatest(): Flow<ActionLog?>\n\n        @Query(\n            \"\"\"\n            SELECT cl.* FROM action_log AS cl\n            INNER JOIN (\n                SELECT subs_id, group_type, group_key, MAX(id) AS max_id FROM action_log\n                WHERE app_id = :appId AND subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)\n                GROUP BY subs_id, group_type, group_key\n            ) AS latest_log ON cl.subs_id = latest_log.subs_id \n            AND cl.group_type = latest_log.group_type\n            AND cl.group_key = latest_log.group_key\n            AND cl.id = latest_log.max_id\n        \"\"\"\n        )\n        fun queryLatestByAppId(appId: String): Flow<List<ActionLog>>\n\n\n        @Query(\n            \"\"\"\n            DELETE FROM action_log\n            WHERE (\n                    SELECT COUNT(*)\n                    FROM action_log\n                ) > 500\n                AND id <= (\n                    SELECT id\n                    FROM action_log\n                    ORDER BY id DESC\n                    LIMIT 1 OFFSET 500\n                )\n        \"\"\"\n        )\n        suspend fun deleteKeepLatest(): Int\n\n        @Query(\"SELECT DISTINCT app_id FROM action_log ORDER BY id DESC\")\n        fun queryLatestUniqueAppIds(): Flow<List<String>>\n\n        @Query(\"SELECT DISTINCT app_id FROM action_log WHERE subs_id=:subsItemId AND group_type=${SubsConfig.AppGroupType} ORDER BY id DESC\")\n        fun queryLatestUniqueAppIds(subsItemId: Long): Flow<List<String>>\n\n        @Query(\"SELECT DISTINCT app_id FROM action_log WHERE subs_id=:subsItemId AND group_key=:globalGroupKey AND group_type=${SubsConfig.GlobalGroupType} ORDER BY id DESC\")\n        fun queryLatestUniqueAppIds(subsItemId: Long, globalGroupKey: Int): Flow<List<String>>\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/ActivityLog.kt",
    "content": "package li.songe.gkd.data\n\nimport androidx.paging.PagingSource\nimport androidx.room.ColumnInfo\nimport androidx.room.Dao\nimport androidx.room.DeleteTable\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.migration.AutoMigrationSpec\nimport kotlinx.coroutines.flow.Flow\nimport li.songe.gkd.util.format\nimport li.songe.gkd.util.getShowActivityId\n\n@Entity(\n    tableName = \"activity_log_v2\",\n)\ndata class ActivityLog(\n    // 不使用时间戳作为主键的原因\n    // https://github.com/gkd-kit/gkd/issues/704\n    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = \"id\") val id: Int = 0,\n    @ColumnInfo(name = \"ctime\") val ctime: Long,\n    @ColumnInfo(name = \"app_id\") val appId: String,\n    @ColumnInfo(name = \"activity_id\") val activityId: String? = null,\n) {\n    val showActivityId by lazy { getShowActivityId(appId, activityId) }\n    val date by lazy { ctime.format(\"HH:mm:ss SSS\") }\n\n    @Dao\n    interface ActivityLogDao {\n        @Insert\n        suspend fun insert(vararg objects: ActivityLog): List<Long>\n\n        @Query(\"DELETE FROM activity_log_v2\")\n        suspend fun deleteAll()\n\n        @Query(\"SELECT * FROM activity_log_v2 ORDER BY ctime DESC \")\n        fun pagingSource(): PagingSource<Int, ActivityLog>\n\n        @Query(\"SELECT COUNT(*) FROM activity_log_v2\")\n        fun count(): Flow<Int>\n\n        @Query(\n            \"\"\"\n            DELETE FROM activity_log_v2\n            WHERE (\n                    SELECT COUNT(*)\n                    FROM activity_log_v2\n                ) > 500\n                AND ctime <= (\n                    SELECT ctime\n                    FROM activity_log_v2\n                    ORDER BY ctime DESC\n                    LIMIT 1 OFFSET 500\n                )\n        \"\"\"\n        )\n        suspend fun deleteKeepLatest(): Int\n    }\n\n\n    @DeleteTable.Entries(\n        DeleteTable(tableName = \"activity_log\")\n    )\n    class ActivityLogV2Spec : AutoMigrationSpec\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/AppConfig.kt",
    "content": "package li.songe.gkd.data\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Dao\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.Update\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@Entity(\n    tableName = \"app_config\",\n)\ndata class AppConfig(\n    @PrimaryKey @ColumnInfo(name = \"id\") val id: Long = System.currentTimeMillis(),\n    @ColumnInfo(name = \"enable\") val enable: Boolean,\n    @ColumnInfo(name = \"subs_id\") val subsId: Long,\n    @ColumnInfo(name = \"app_id\") val appId: String,\n) {\n    @Dao\n    interface AppConfigDao {\n        @Update\n        suspend fun update(vararg objects: AppConfig): Int\n\n        @Insert(onConflict = OnConflictStrategy.REPLACE)\n        suspend fun insert(vararg users: AppConfig): List<Long>\n\n        @Query(\"SELECT * FROM app_config WHERE subs_id=:subsId\")\n        fun queryAppTypeConfig(subsId: Long): Flow<List<AppConfig>>\n\n        @Query(\"SELECT * FROM app_config WHERE app_id=:appId AND subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)\")\n        fun queryAppUsedList(appId: String): Flow<List<AppConfig>>\n\n        @Query(\"SELECT * FROM app_config WHERE subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)\")\n        fun queryUsedList(): Flow<List<AppConfig>>\n\n        @Insert(onConflict = OnConflictStrategy.IGNORE)\n        suspend fun insertOrIgnore(vararg objects: AppConfig): List<Long>\n\n        @Query(\"SELECT * FROM app_config WHERE subs_id IN (:subsItemIds)\")\n        suspend fun querySubsItemConfig(subsItemIds: List<Long>): List<AppConfig>\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/AppInfo.kt",
    "content": "package li.songe.gkd.data\n\nimport android.content.Intent\nimport android.content.pm.ApplicationInfo\nimport android.content.pm.PackageInfo\nimport android.content.pm.PackageManager\nimport android.graphics.drawable.Drawable\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.app\nimport li.songe.gkd.shizuku.casted\nimport li.songe.gkd.shizuku.currentUserId\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.pkgIcon\n\n@Serializable\ndata class AppInfo(\n    val id: String,\n    val name: String,\n    val versionCode: Int,\n    val versionName: String?,\n    val isSystem: Boolean,\n    val mtime: Long,\n    val hidden: Boolean,\n    val enabled: Boolean,\n    val userId: Int,\n) {\n    override fun equals(other: Any?): Boolean {\n        if (other !is AppInfo) return false\n        return id == other.id && mtime == other.mtime && userId == other.userId\n    }\n\n    override fun hashCode(): Int {\n        var result = id.hashCode()\n        result = 31 * result + mtime.hashCode()\n        result = 31 * result + userId\n        return result\n    }\n}\n\nval selfAppInfo by lazy {\n    app.packageManager.getPackageInfo(app.packageName, 0).toAppInfo()\n}\n\nprivate val PackageInfo.compatVersionCode: Int\n    get() = if (AndroidTarget.P) {\n        longVersionCode.toInt()\n    } else {\n        @Suppress(\"DEPRECATION\")\n        versionCode\n    }\n\nval ApplicationInfo.isSystem: Boolean\n    get() = flags and ApplicationInfo.FLAG_SYSTEM != 0 || flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP != 0\n\nprivate fun checkHasActivity(packageName: String): Boolean {\n    return app.packageManager.getLaunchIntentForPackage(packageName) != null || app.packageManager.queryIntentActivities(\n        Intent().setPackage(packageName),\n        PackageManager.MATCH_DISABLED_COMPONENTS\n    ).isNotEmpty() || try {\n        app.packageManager.getPackageInfo(\n            packageName,\n            PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.GET_ACTIVITIES\n        ).activities?.isNotEmpty() == true\n    } catch (_: Throwable) {\n        // #1195 packageManager.getPackageInfo android.os.DeadSystemRuntimeException\n        true\n    }\n}\n\nprivate fun PackageInfo.getEnabled(userId: Int): Boolean {\n    val enabled = applicationInfo?.enabled ?: true\n    if (enabled) return true\n    val state = try {\n        // https://github.com/gkd-kit/gkd/issues/1169#issuecomment-3489260246\n        if (userId == currentUserId) {\n            app.packageManager.getApplicationEnabledSetting(packageName)\n        } else {\n            shizukuContextFlow.value.packageManager?.getApplicationEnabledSetting(\n                packageName,\n                currentUserId\n            )\n        }\n    } catch (_: IllegalArgumentException) {\n        null\n    }\n    return when (state) {\n        null,\n        PackageManager.COMPONENT_ENABLED_STATE_DISABLED,\n        PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER,\n        PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED -> false\n\n        else -> true\n    }\n}\n\n// all->433 isOverlay->354 checkAppHasActivity->271\nfun PackageInfo.toAppInfo(\n    userId: Int = currentUserId,\n    hidden: Boolean? = null,\n): AppInfo {\n    val isSystem = applicationInfo?.isSystem ?: false\n    return AppInfo(\n        userId = userId,\n        id = packageName,\n        versionCode = compatVersionCode,\n        versionName = versionName,\n        mtime = lastUpdateTime,\n        isSystem = isSystem,\n        name = applicationInfo?.run { loadLabel(app.packageManager).toString() } ?: packageName,\n        hidden = hidden ?: (isSystem && (casted.overlayTarget != null || !checkHasActivity(\n            packageName\n        ))),\n        enabled = getEnabled(userId),\n    )\n}\n\nfun PackageInfo.toAppInfoAndIcon(\n    userId: Int = currentUserId,\n    hidden: Boolean? = null,\n): Pair<AppInfo, Drawable?> {\n    val appInfo = toAppInfo(userId, hidden)\n    return if (appInfo.hidden) {\n        appInfo to null\n    } else {\n        appInfo to pkgIcon\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/AppRule.kt",
    "content": "package li.songe.gkd.data\n\nclass AppRule(\n    rule: RawSubscription.RawAppRule,\n    g: ResolvedAppGroup,\n    appInfo: AppInfo?,\n) : ResolvedRule(\n    rule = rule,\n    g = g,\n) {\n    val group = g.group\n    val app = g.app\n    val enable = appInfo?.let {\n        if (rule.versionCode?.match(it.versionCode) == false) {\n            return@let false\n        }\n        if (rule.versionName?.match(it.versionName) == false) {\n            return@let false\n        }\n        null\n    } ?: true\n    val appId = app.id\n    private val activityIds = getFixActivityIds(app.id, rule.activityIds ?: group.activityIds)\n    private val excludeActivityIds =\n        (getFixActivityIds(\n            app.id,\n            rule.excludeActivityIds ?: group.excludeActivityIds\n        ) + (excludeData.activityIds.filter { e -> e.first == appId }\n            .map { e -> e.second })).distinct()\n\n    override val type = \"app\"\n    override fun matchActivity(appId: String, activityId: String?): Boolean {\n        if (!enable) return false\n        if (appId != app.id) return false\n        activityId ?: return true\n        if (excludeActivityIds.any { activityId.startsWith(it) }) return false\n        return activityIds.isEmpty() || activityIds.any { activityId.startsWith(it) }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/AppVisitLog.kt",
    "content": "package li.songe.gkd.data\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Dao\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.Transaction\nimport kotlinx.coroutines.flow.Flow\nimport li.songe.gkd.META\nimport li.songe.gkd.a11y.launcherAppId\nimport li.songe.gkd.util.systemUiAppId\n\n@Entity(\n    tableName = \"app_visit_log\",\n)\ndata class AppVisitLog(\n    @PrimaryKey() @ColumnInfo(name = \"id\") val id: String,\n    @ColumnInfo(name = \"mtime\") val mtime: Long,\n) {\n    @Dao\n    interface AppLogDao {\n        @Insert(onConflict = OnConflictStrategy.REPLACE)\n        suspend fun insert(vararg objects: AppVisitLog): List<Long>\n\n        @Transaction\n        suspend fun insert(oldAppId: String, newAppId: String, mtime: Long) {\n            insert(\n                AppVisitLog(oldAppId, fixAppVisitTime(oldAppId, mtime - 1)),\n                AppVisitLog(newAppId, fixAppVisitTime(newAppId, mtime)),\n            )\n            if (appLogCount++ % 100 == 0) {\n                deleteKeepLatest()\n            }\n        }\n\n        @Query(\"SELECT DISTINCT id FROM app_visit_log ORDER BY mtime DESC\")\n        fun query(): Flow<List<String>>\n\n        @Query(\n            \"\"\"\n            DELETE FROM app_visit_log\n            WHERE (\n                    SELECT COUNT(*)\n                    FROM app_visit_log\n                ) > 500\n                AND mtime <= (\n                    SELECT mtime\n                    FROM app_visit_log\n                    ORDER BY mtime DESC\n                    LIMIT 1 OFFSET 500\n                )\n        \"\"\"\n        )\n        suspend fun deleteKeepLatest(): Int\n    }\n}\n\nprivate fun fixAppVisitTime(appId: String, t: Long): Long = when (appId) {\n    META.appId, launcherAppId, systemUiAppId -> t - 60_000\n    else -> t\n}\n\nprivate var appLogCount = 0\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/AttrInfo.kt",
    "content": "package li.songe.gkd.data\n\nimport android.view.accessibility.AccessibilityNodeInfo\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.a11y.compatChecked\nimport li.songe.gkd.shizuku.casted\n\n@Serializable\ndata class AttrInfo(\n    val id: String?,\n    val vid: String?,\n    val name: String?,\n    val text: String?,\n    val desc: String?,\n\n    val clickable: Boolean,\n    val focusable: Boolean,\n    val checkable: Boolean,\n    val checked: Boolean?,\n    val editable: Boolean,\n    val longClickable: Boolean,\n    val visibleToUser: Boolean,\n\n    val left: Int,\n    val top: Int,\n    val right: Int,\n    val bottom: Int,\n\n    val width: Int,\n    val height: Int,\n\n    val childCount: Int,\n\n    val index: Int,\n    val depth: Int,\n) {\n    companion object {\n        fun info2data(\n            node: AccessibilityNodeInfo,\n            index: Int,\n            depth: Int,\n        ): AttrInfo {\n            val rect = node.casted.boundsInScreen\n            val appId = node.packageName?.toString() ?: \"\"\n            val id: String? = node.viewIdResourceName\n            val idPrefix = \"$appId:id/\"\n            val vid = if (id != null && id.startsWith(idPrefix)) {\n                id.substring(idPrefix.length)\n            } else {\n                // 此处不使用 id 是因为某些节点的 id 没有 appId:id/ 前缀\n                null\n            }\n            return AttrInfo(\n                id = id,\n                vid = vid,\n                name = node.className?.toString(),\n                text = node.text?.toString(),\n                desc = node.contentDescription?.toString(),\n\n                clickable = node.isClickable,\n                focusable = node.isFocusable,\n                checkable = node.isCheckable,\n                checked = node.compatChecked,\n                editable = node.isEditable,\n                longClickable = node.isLongClickable,\n                visibleToUser = node.isVisibleToUser,\n\n                left = rect.left,\n                top = rect.top,\n                right = rect.right,\n                bottom = rect.bottom,\n\n                width = rect.width(),\n                height = rect.height(),\n\n                childCount = node.childCount,\n\n                index = index,\n                depth = depth,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/BaseSnapshot.kt",
    "content": "package li.songe.gkd.data\n\ninterface BaseSnapshot {\n    val id: Long\n\n    val appId: String\n    val activityId: String?\n\n    val screenHeight: Int\n    val screenWidth: Int\n    val isLandscape: Boolean\n\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/CategoryConfig.kt",
    "content": "package li.songe.gkd.data\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.Update\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@Entity(\n    tableName = \"category_config\",\n)\ndata class CategoryConfig(\n    @PrimaryKey @ColumnInfo(name = \"id\") val id: Long = System.currentTimeMillis(),\n    @ColumnInfo(name = \"enable\") val enable: Boolean? = null,\n    @ColumnInfo(name = \"subs_id\") val subsId: Long,\n    @ColumnInfo(name = \"category_key\") val categoryKey: Int,\n) {\n    @Dao\n    interface CategoryConfigDao {\n\n        @Update\n        suspend fun update(vararg objects: CategoryConfig): Int\n\n        @Insert(onConflict = OnConflictStrategy.REPLACE)\n        suspend fun insert(vararg objects: CategoryConfig): List<Long>\n\n        @Insert(onConflict = OnConflictStrategy.IGNORE)\n        suspend fun insertOrIgnore(vararg objects: CategoryConfig): List<Long>\n\n        @Delete\n        suspend fun delete(vararg objects: CategoryConfig): Int\n\n        @Query(\"DELETE FROM category_config WHERE subs_id=:subsItemId\")\n        suspend fun deleteBySubsItemId(subsItemId: Long): Int\n\n        @Query(\"DELETE FROM category_config WHERE subs_id IN (:subsIds)\")\n        suspend fun deleteBySubsId(vararg subsIds: Long): Int\n\n        @Query(\"DELETE FROM category_config WHERE subs_id=:subsItemId AND category_key=:categoryKey\")\n        suspend fun deleteByCategoryKey(subsItemId: Long, categoryKey: Int): Int\n\n        @Query(\"SELECT * FROM category_config WHERE subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)\")\n        fun queryUsedList(): Flow<List<CategoryConfig>>\n\n        @Query(\"SELECT * FROM category_config WHERE subs_id=:subsItemId\")\n        fun queryConfig(subsItemId: Long): Flow<List<CategoryConfig>>\n\n        @Query(\"SELECT * FROM category_config WHERE subs_id=:subsId AND category_key=:categoryKey\")\n        suspend fun queryCategoryConfig(subsId: Long, categoryKey: Int): CategoryConfig?\n\n        @Query(\"SELECT * FROM category_config WHERE subs_id IN (:subsItemIds)\")\n        suspend fun querySubsItemConfig(subsItemIds: List<Long>): List<CategoryConfig>\n\n        @Query(\"SELECT * FROM category_config WHERE subs_id IN (:subsItemIds)\")\n        fun queryBySubsIds(subsItemIds: List<Long>): Flow<List<CategoryConfig>>\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/ComplexSnapshot.kt",
    "content": "package li.songe.gkd.data\n\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.util.appInfoMapFlow\n\n@Serializable\ndata class ComplexSnapshot(\n    override val id: Long,\n    override val appId: String,\n    override val activityId: String?,\n    override val screenHeight: Int,\n    override val screenWidth: Int,\n    override val isLandscape: Boolean,\n    val appInfo: AppInfo? = appInfoMapFlow.value[appId],\n    val gkdAppInfo: AppInfo? = selfAppInfo,\n    val device: DeviceInfo = DeviceInfo(),\n    val nodes: List<NodeInfo>,\n) : BaseSnapshot {\n    fun toSnapshot(): Snapshot {\n        return Snapshot(\n            id = id,\n            appId = appId,\n            activityId = activityId,\n            screenHeight = screenHeight,\n            screenWidth = screenWidth,\n            isLandscape = isLandscape,\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/DeviceInfo.kt",
    "content": "package li.songe.gkd.data\n\nimport android.os.Build\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class DeviceInfo(\n    val device: String = Build.DEVICE,\n    val model: String = Build.MODEL,\n    val manufacturer: String = Build.MANUFACTURER,\n    val brand: String = Build.BRAND,\n    val sdkInt: Int = Build.VERSION.SDK_INT,\n    val release: String = Build.VERSION.RELEASE,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/GithubPoliciesAsset.kt",
    "content": "package li.songe.gkd.data\n\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.util.FILE_SHORT_URL\n\n@Serializable\ndata class GithubPoliciesAsset(\n    val id: Int,\n    val href: String,\n) {\n    val shortHref: String\n        get() = FILE_SHORT_URL + id\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/GkdAction.kt",
    "content": "package li.songe.gkd.data\n\nimport android.accessibilityservice.GestureDescription\nimport android.graphics.Path\nimport android.view.ViewConfiguration\nimport android.view.accessibility.AccessibilityNodeInfo\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.a11y.A11yRuleEngine\nimport li.songe.gkd.service.A11yService\nimport li.songe.gkd.shizuku.casted\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.util.ScreenUtils\n\n@Serializable\ndata class GkdAction(\n    val selector: String,\n    val fastQuery: Boolean = false,\n    val action: String? = null,\n    val position: RawSubscription.Position? = null,\n)\n\n@Serializable\ndata class ActionResult(\n    val action: String,\n    val result: Boolean,\n    val shizuku: Boolean = false,\n    val position: Pair<Float, Float>? = null,\n)\n\nsealed class ActionPerformer(val action: String) {\n    abstract fun perform(\n        node: AccessibilityNodeInfo,\n        position: RawSubscription.Position?,\n    ): ActionResult\n\n    data object ClickNode : ActionPerformer(\"clickNode\") {\n        override fun perform(\n            node: AccessibilityNodeInfo,\n            position: RawSubscription.Position?,\n        ): ActionResult {\n            return ActionResult(\n                action = action,\n                result = node.performAction(AccessibilityNodeInfo.ACTION_CLICK)\n            )\n        }\n    }\n\n    data object ClickCenter : ActionPerformer(\"clickCenter\") {\n        override fun perform(\n            node: AccessibilityNodeInfo,\n            position: RawSubscription.Position?,\n        ): ActionResult {\n            val rect = node.casted.boundsInScreen\n            val p = position?.calc(rect)\n            val x = p?.first ?: ((rect.right + rect.left) / 2f)\n            val y = p?.second ?: ((rect.bottom + rect.top) / 2f)\n            return ActionResult(\n                action = action,\n                result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {\n                    if (shizukuContextFlow.value.tap(x, y)) {\n                        return ActionResult(\n                            action = action, result = true, shizuku = true, position = x to y\n                        )\n                    }\n                    val gestureDescription = GestureDescription.Builder()\n                    val path = Path()\n                    path.moveTo(x, y)\n                    gestureDescription.addStroke(\n                        GestureDescription.StrokeDescription(\n                            path, 0, ViewConfiguration.getTapTimeout().toLong()\n                        )\n                    )\n                    A11yService.instance?.dispatchGesture(\n                        gestureDescription.build(), null, null\n                    ) != null\n                } else {\n                    false\n                },\n                position = x to y\n            )\n        }\n    }\n\n    data object Click : ActionPerformer(\"click\") {\n        override fun perform(\n            node: AccessibilityNodeInfo,\n            position: RawSubscription.Position?,\n        ): ActionResult {\n            if (node.isClickable) {\n                val result = ClickNode.perform(node, position)\n                if (result.result) {\n                    return result\n                }\n            }\n            return ClickCenter.perform(node, position)\n        }\n    }\n\n    data object LongClickNode : ActionPerformer(\"longClickNode\") {\n        override fun perform(\n            node: AccessibilityNodeInfo,\n            position: RawSubscription.Position?,\n        ): ActionResult {\n            return ActionResult(\n                action = action,\n                result = node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)\n            )\n        }\n    }\n\n    data object LongClickCenter : ActionPerformer(\"longClickCenter\") {\n        override fun perform(\n            node: AccessibilityNodeInfo,\n            position: RawSubscription.Position?,\n        ): ActionResult {\n            val rect = node.casted.boundsInScreen\n            val p = position?.calc(rect)\n            val x = p?.first ?: ((rect.right + rect.left) / 2f)\n            val y = p?.second ?: ((rect.bottom + rect.top) / 2f)\n            // 某些系统的 ViewConfiguration.getLongPressTimeout() 返回 300 , 这将导致触发普通的 click 事件\n            val longClickDuration = 500L\n            return ActionResult(\n                action = action,\n                result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {\n                    if (shizukuContextFlow.value.tap(\n                            x, y, longClickDuration\n                        )\n                    ) {\n                        return ActionResult(\n                            action = action, result = true, shizuku = true, position = x to y\n                        )\n                    }\n                    val gestureDescription = GestureDescription.Builder()\n                    val path = Path()\n                    path.moveTo(x, y)\n                    gestureDescription.addStroke(\n                        GestureDescription.StrokeDescription(\n                            path, 0, longClickDuration\n                        )\n                    )\n                    A11yService.instance?.dispatchGesture(\n                        gestureDescription.build(), null, null\n                    ) != null\n                } else {\n                    false\n                },\n                position = x to y\n            )\n        }\n    }\n\n    data object LongClick : ActionPerformer(\"longClick\") {\n        override fun perform(\n            node: AccessibilityNodeInfo,\n            position: RawSubscription.Position?,\n        ): ActionResult {\n            if (node.isLongClickable) {\n                val result = LongClickNode.perform(node, position)\n                if (result.result) {\n                    return result\n                }\n            }\n            return LongClickCenter.perform(node, position)\n        }\n    }\n\n    data object Back : ActionPerformer(\"back\") {\n        override fun perform(\n            node: AccessibilityNodeInfo,\n            position: RawSubscription.Position?,\n        ): ActionResult {\n            return ActionResult(\n                action = action,\n                result = A11yRuleEngine.performActionBack()\n            )\n        }\n    }\n\n    data object None : ActionPerformer(\"none\") {\n        override fun perform(\n            node: AccessibilityNodeInfo,\n            position: RawSubscription.Position?,\n        ): ActionResult {\n            return ActionResult(\n                action = action, result = true\n            )\n        }\n    }\n\n    companion object {\n        private val allSubObjects by lazy {\n            arrayOf(\n                ClickNode, ClickCenter, Click, LongClickNode, LongClickCenter, LongClick, Back, None\n            )\n        }\n\n        fun getAction(action: String?): ActionPerformer {\n            return allSubObjects.find { it.action == action } ?: Click\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/GlobalRule.kt",
    "content": "package li.songe.gkd.data\n\nimport li.songe.gkd.a11y.launcherAppId\nimport li.songe.gkd.util.systemAppsFlow\n\ndata class GlobalApp(\n    val id: String,\n    val enable: Boolean,\n    val activityIds: List<String>,\n    val excludeActivityIds: List<String>,\n)\n\nclass GlobalRule(\n    rule: RawSubscription.RawGlobalRule,\n    g: ResolvedGlobalGroup,\n    appInfoCache: Map<String, AppInfo>,\n) : ResolvedRule(\n    rule = rule,\n    g = g,\n) {\n    val groupExcludeAppIds = g.groupExcludeAppIds\n    val group = g.group\n    private val matchAnyApp = rule.matchAnyApp ?: group.matchAnyApp ?: true\n    private val matchLauncher = rule.matchLauncher ?: group.matchLauncher ?: false\n    private val matchSystemApp = rule.matchSystemApp ?: group.matchSystemApp ?: false\n    val apps = mutableMapOf<String, GlobalApp>().apply {\n        (rule.apps ?: group.apps ?: emptyList()).filter { a ->\n            // https://github.com/gkd-kit/gkd/issues/619\n            appInfoCache.isEmpty() || appInfoCache.containsKey(a.id) // 过滤掉未安装应用\n        }.forEach { a ->\n            val enable = a.enable ?: appInfoCache[a.id]?.let { appInfo ->\n                if (a.versionCode?.match(appInfo.versionCode) == false) {\n                    return@let false\n                }\n                if (a.versionName?.match(appInfo.versionName) == false) {\n                    return@let false\n                }\n                null\n            } ?: true\n            this[a.id] = GlobalApp(\n                id = a.id,\n                enable = enable,\n                activityIds = getFixActivityIds(a.id, a.activityIds),\n                excludeActivityIds = getFixActivityIds(a.id, a.excludeActivityIds),\n            )\n        }\n    }\n\n    override val type = \"global\"\n\n    private val excludeAppIds = apps.filter { e ->\n        !e.value.enable\n    }.keys\n\n    private val enableApps = apps.filter { e -> e.value.enable }\n\n    /**\n     * 内置禁用>用户配置>规则自带\n     * 范围越精确优先级越高\n     */\n    override fun matchActivity(appId: String, activityId: String?): Boolean {\n        // 规则自带禁用\n        if (excludeAppIds.contains(appId) || groupExcludeAppIds.contains(appId)) {\n            return false\n        }\n\n        // 用户自定义禁用\n        if (excludeData.excludeAppIds.contains(appId)) {\n            return false\n        }\n        if (activityId != null && excludeData.activityIds.contains(appId to activityId)) {\n            return false\n        }\n        if (excludeData.includeAppIds.contains(appId)) {\n            activityId ?: return true\n            val app = enableApps[appId] ?: return true\n            // 规则自带页面的禁用\n            return !app.excludeActivityIds.any { e -> e.startsWith(activityId) }\n        }\n\n        // 范围比较\n        val app = enableApps[appId]\n        if (app != null) { // 规则自定义启用\n            activityId ?: return true\n            return app.activityIds.isEmpty() || app.activityIds.any { e -> e.startsWith(activityId) }\n        } else {\n            if (!matchLauncher && appId == launcherAppId) {\n                return false\n            }\n            if (!matchSystemApp && systemAppsFlow.value.contains(appId)) {\n                return false\n            }\n            return matchAnyApp\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/NodeInfo.kt",
    "content": "package li.songe.gkd.data\n\nimport android.view.accessibility.AccessibilityNodeInfo\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.a11y.MAX_CHILD_SIZE\nimport li.songe.gkd.a11y.topActivityFlow\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.toast\nimport kotlin.system.measureTimeMillis\n\n@Serializable\ndata class NodeInfo(\n    val id: Int,\n    val pid: Int,\n    val idQf: Boolean?,\n    val textQf: Boolean?,\n    val attr: AttrInfo,\n)\n\nprivate data class TempNodeData(\n    val node: AccessibilityNodeInfo,\n    val parent: TempNodeData?,\n    val index: Int,\n    val depth: Int,\n) {\n    var id = 0\n    val attr = AttrInfo.info2data(node, index, depth)\n    var children: List<TempNodeData> = emptyList()\n\n    var idQfInit = false\n    var idQf: Boolean? = null\n        set(value) {\n            field = value\n            idQfInit = true\n        }\n    var textQfInit = false\n    var textQf: Boolean? = null\n        set(value) {\n            field = value\n            textQfInit = true\n        }\n}\n\nprivate fun getChildren(node: AccessibilityNodeInfo) = sequence {\n    repeat(node.childCount.coerceAtMost(MAX_CHILD_SIZE)) { i ->\n        val child = node.getChild(i) ?: return@sequence\n        yield(child)\n    }\n}\n\nprivate const val MAX_KEEP_SIZE = 5000\n\n// 先获取所有节点构建树结构, 然后再判断 idQf/textQf 如果存在一个能同时 idQf 和 textQf 的节点, 则认为 idQf 和 textQf 等价\nfun info2nodeList(root: AccessibilityNodeInfo?): List<NodeInfo> {\n    if (root == null) {\n        return emptyList()\n    }\n    val nodes = mutableListOf<TempNodeData>()\n    val collectTime = measureTimeMillis {\n        val stack = mutableListOf<TempNodeData>()\n        var times = 0\n        stack.add(TempNodeData(root, null, 0, 0))\n        while (stack.isNotEmpty()) {\n            times++\n            val node = stack.removeAt(stack.lastIndex)\n            node.id = times - 1\n            val children = getChildren(node.node).mapIndexed { i, child ->\n                TempNodeData(\n                    child, node, i, node.depth + 1\n                )\n            }.toList()\n            node.children = children\n            nodes.add(node)\n            repeat(children.size) { i ->\n                stack.add(children[children.size - i - 1])\n            }\n            if (times > MAX_KEEP_SIZE) {\n                // https://github.com/gkd-kit/gkd/issues/28\n                toast(\"节点数量至多保留$MAX_KEEP_SIZE,丢弃后续节点\")\n                LogUtils.d(\n                    \"节点数量过多\",\n                    root.packageName,\n                    topActivityFlow.value.activityId,\n                )\n                break\n            }\n        }\n    }\n    val qfTime = measureTimeMillis {\n        val idQfCache = mutableMapOf<String, List<AccessibilityNodeInfo>>()\n        val textQfCache = mutableMapOf<String, List<AccessibilityNodeInfo>>()\n        var idTextQf = false\n        fun updateQf(n: TempNodeData) {\n            if (!n.idQfInit && !n.attr.id.isNullOrEmpty()) {\n                n.idQf = (idQfCache[n.attr.id]\n                    ?: root.findAccessibilityNodeInfosByViewId(n.attr.id)).apply {\n                    idQfCache[n.attr.id] = this\n                }\n                    .any { t -> t == n.node }\n\n            }\n\n            if (!n.textQfInit && !n.attr.text.isNullOrEmpty()) {\n                n.textQf = (textQfCache[n.attr.text]\n                    ?: root.findAccessibilityNodeInfosByText(n.attr.text)).apply {\n                    textQfCache[n.attr.text] = this\n                }\n                    .any { t -> t == n.node }\n            }\n\n            if (n.idQf == true && n.textQf == true) {\n                idTextQf = true\n            }\n\n            if (!n.idQfInit && n.idQf != null) {\n                n.parent?.children?.forEach { c ->\n                    c.idQf = n.idQf\n                    if (idTextQf) {\n                        c.textQf = n.textQf\n                    }\n                }\n                if (n.idQf == true) {\n                    var p = n.parent\n                    while (p != null && !p.idQfInit) {\n                        p.idQf = n.idQf\n                        if (idTextQf) {\n                            p.textQf = n.textQf\n                        }\n                        p = p.parent\n                        p?.children?.forEach { bro ->\n                            bro.idQf = n.idQf\n                            if (idTextQf) {\n                                bro.textQf = n.textQf\n                            }\n                        }\n                    }\n                } else {\n                    val tempStack = mutableListOf(n)\n                    while (tempStack.isNotEmpty()) {\n                        val top = tempStack.removeAt(tempStack.lastIndex)\n                        top.idQf = n.idQf\n                        if (idTextQf) {\n                            top.textQf = n.textQf\n                        }\n                        repeat(top.children.size) { i ->\n                            tempStack.add(top.children[top.children.size - i - 1])\n                        }\n                    }\n                }\n            }\n\n            if (!n.textQfInit && n.textQf != null) {\n                n.parent?.children?.forEach { c ->\n                    c.textQf = n.textQf\n                    if (idTextQf) {\n                        c.idQf = n.idQf\n                    }\n                }\n                if (n.textQf == true) {\n                    var p = n.parent\n                    while (p != null && !p.textQfInit) {\n                        p.textQf = n.textQf\n                        if (idTextQf) {\n                            p.idQf = n.idQf\n                        }\n                        p = p.parent\n                        p?.children?.forEach { bro ->\n                            bro.textQf = n.textQf\n                            if (idTextQf) {\n                                bro.idQf = bro.idQf\n                            }\n                        }\n                    }\n                } else {\n                    val tempStack = mutableListOf(n)\n                    while (tempStack.isNotEmpty()) {\n                        val top = tempStack.removeAt(tempStack.lastIndex)\n                        top.textQf = n.textQf\n                        if (idTextQf) {\n                            top.idQf = n.idQf\n                        }\n                        repeat(top.children.size) { i ->\n                            tempStack.add(top.children[top.children.size - i - 1])\n                        }\n                    }\n                }\n            }\n\n            n.idQfInit = true\n            n.textQfInit = true\n        }\n        for (i in (nodes.size - 1) downTo 0) {\n            val n = nodes[i]\n            if (n.children.isEmpty()) {\n                updateQf(n)\n            }\n        }\n        for (i in (nodes.size - 1) downTo 0) {\n            val n = nodes[i]\n            if (n.children.isNotEmpty()) {\n                updateQf(n)\n            }\n        }\n    }\n\n    LogUtils.d(\n        topActivityFlow.value,\n        \"快照节点数量:${nodes.size}, 总耗时:${collectTime + qfTime}ms\",\n        \"收集节点耗时:${collectTime}ms, 收集 fastQuery 耗时:${qfTime}ms\",\n    )\n\n    return nodes.map { n ->\n        NodeInfo(\n            id = n.id,\n            pid = n.parent?.id ?: -1,\n            idQf = n.idQf,\n            textQf = n.textQf,\n            attr = n.attr\n        )\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt",
    "content": "package li.songe.gkd.data\n\nimport android.graphics.Rect\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonArray\nimport kotlinx.serialization.json.JsonElement\nimport kotlinx.serialization.json.JsonNull\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.JsonPrimitive\nimport kotlinx.serialization.json.boolean\nimport kotlinx.serialization.json.encodeToJsonElement\nimport kotlinx.serialization.json.int\nimport kotlinx.serialization.json.jsonArray\nimport kotlinx.serialization.json.jsonObject\nimport kotlinx.serialization.json.jsonPrimitive\nimport kotlinx.serialization.json.long\nimport li.songe.gkd.a11y.typeInfo\nimport li.songe.gkd.util.LOCAL_SUBS_IDS\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.appInfoMapFlow\nimport li.songe.gkd.util.distinctByIfAny\nimport li.songe.gkd.util.filterIfNotAll\nimport li.songe.gkd.util.json\nimport li.songe.gkd.util.toJson5String\nimport li.songe.gkd.util.toast\nimport li.songe.json5.Json5\nimport li.songe.selector.Selector\nimport net.objecthunter.exp4j.Expression\nimport net.objecthunter.exp4j.ExpressionBuilder\nimport java.util.Objects\n\n\n@Serializable\ndata class RawSubscription(\n    val id: Long,\n    val name: String,\n    val version: Int,\n    val author: String? = null,\n    val updateUrl: String? = null,\n    val supportUri: String? = null,\n    val checkUpdateUrl: String? = null,\n    val globalGroups: List<RawGlobalGroup> = emptyList(),\n    val categories: List<RawCategory> = emptyList(),\n    val apps: List<RawApp> = emptyList(),\n) {\n    // 重写 equals 和 hashCode 便于 compose 重组比较\n    override fun equals(other: Any?): Boolean {\n        return other === this\n    }\n\n    override fun hashCode(): Int {\n        return Objects.hash(id, name, version)\n    }\n\n    val isEmpty: Boolean\n        get() = globalGroups.isEmpty() && apps.all { it.groups.isEmpty() } && categories.isEmpty()\n\n    val isLocal: Boolean\n        get() = LOCAL_SUBS_IDS.contains(id)\n\n    val hasRule get() = globalGroups.isNotEmpty() || apps.any { it.groups.isNotEmpty() }\n\n    val usedApps by lazy {\n        apps.run {\n            if (any { it.groups.isEmpty() }) {\n                filterNot { it.groups.isEmpty() }\n            } else {\n                this\n            }\n        }\n    }\n\n    fun getSafeCategory(key: Int): RawCategory {\n        return categories.find { it.key == key } ?: RawCategory(\n            key = key,\n            name = key.toString(),\n            enable = false,\n            desc = null\n        )\n    }\n\n    val categoryToGroupsMap by lazy {\n        val allAppGroups = apps.flatMap { a -> a.groups.map { g -> g to a } }\n        allAppGroups.groupBy { g ->\n            categories.find { c -> g.first.name.startsWith(c.name) }\n        }\n    }\n\n    private val categoryToAppMap by lazy {\n        val map = mutableMapOf<Int, MutableList<RawApp>>()\n        categories.forEach { c ->\n            apps.forEach { a ->\n                if (a.groups.any { g -> g.name.startsWith(c.name) }) {\n                    val list = map[c.key]\n                    if (list == null) {\n                        map[c.key] = mutableListOf(a)\n                    } else {\n                        list.add(a)\n                    }\n                }\n            }\n        }\n        map\n    }\n\n    fun getCategoryApps(categoryKey: Int): List<RawApp> {\n        return categoryToAppMap[categoryKey] ?: emptyList()\n    }\n\n    fun getAppGroups(appId: String): List<RawAppGroup> {\n        return apps.find { a -> a.id == appId }?.groups ?: emptyList()\n    }\n\n    fun getApp(appId: String): RawApp {\n        return apps.find { a -> a.id == appId } ?: RawApp(\n            id = appId,\n            name = appInfoMapFlow.value[appId]?.name,\n            groups = emptyList()\n        )\n    }\n\n    val groupToCategoryMap by lazy {\n        val map = mutableMapOf<RawAppGroup, RawCategory>()\n        categoryToGroupsMap.forEach { (key, value) ->\n            value.forEach { (g) ->\n                if (key != null) {\n                    map[g] = key\n                }\n            }\n        }\n        map\n    }\n\n    val appGroups by lazy {\n        apps.flatMap { a -> a.groups }\n    }\n\n    val groupsSize by lazy {\n        appGroups.size + globalGroups.size\n    }\n\n    val globalGroupAppGroupNameDisableMap by lazy {\n        globalGroups.mapNotNull { g ->\n            val n = g.disableIfAppGroupMatch\n            if (n != null) {\n                val gName = n.ifEmpty { g.name }\n                g.key to apps.filter { a ->\n                    a.groups.any { ag ->\n                        ag.ignoreGlobalGroupMatch != true && ag.name.startsWith(gName)\n                    }\n                }.map { it.id }.toHashSet()\n            } else {\n                null\n            }\n        }.toMap()\n    }\n\n    fun getGlobalGroupInnerDisabled(globalGroup: RawGlobalGroup, appId: String): Boolean {\n        globalGroup.appIdEnable[appId]?.let {\n            if (!it) return true\n        }\n        globalGroupAppGroupNameDisableMap[globalGroup.key]?.let {\n            if (it.contains(appId)) {\n                return true\n            }\n        }\n        return false\n    }\n\n    val numText by lazy {\n        val appsSize = apps.size\n        val appGroupsSize = appGroups.size\n        val globalGroupSize = globalGroups.size\n        if (appGroupsSize + globalGroupSize > 0) {\n            if (globalGroupSize > 0) {\n                \"${globalGroupSize}全局\" + if (appGroupsSize > 0) {\n                    \"/\"\n                } else {\n                    \"\"\n                }\n            } else {\n                \"\"\n            } + if (appGroupsSize > 0) {\n                \"${appsSize}应用/${appGroupsSize}规则组\"\n            } else {\n                \"\"\n            }\n        } else {\n            \"暂无规则\"\n        }\n    }\n\n    @Serializable\n    data class StringMatcher(\n        val pattern: String?,\n        val include: List<String>?,\n        val exclude: List<String>?,\n    ) {\n        private val patternRegex by lazy {\n            pattern?.let { p ->\n                runCatching { Regex(p) }.getOrNull()\n            }\n        }\n\n        fun match(value: String?): Boolean {\n            if (value == null) return false\n            if (exclude?.contains(value) == true) return false\n            if (include?.contains(value) == false) return false\n            if (patternRegex?.matches(value) == false) return false\n            return true\n        }\n    }\n\n    @Serializable\n    data class IntegerMatcher(\n        val minimum: Int?,\n        val maximum: Int?,\n        val include: List<Int>?,\n        val exclude: List<Int>?,\n    ) {\n        fun match(value: Int?): Boolean {\n            if (value == null) return false\n            if (exclude?.contains(value) == true) return false\n            if (include?.contains(value) == false) return false\n            if (minimum != null && value < minimum) return false\n            if (maximum != null && value > maximum) return false\n            return true\n        }\n    }\n\n    @Serializable\n    data class RawApp(\n        val id: String,\n        val name: String?,\n        val groups: List<RawAppGroup> = emptyList(),\n    )\n\n\n    @Serializable\n    data class RawCategory(\n        val key: Int,\n        val name: String,\n        val enable: Boolean?,\n        val desc: String?,\n    )\n\n\n    @Serializable\n    data class Position(\n        val left: String?, val top: String?, val right: String?, val bottom: String?\n    ) {\n        private val leftExp by lazy { getExpression(left) }\n        private val topExp by lazy { getExpression(top) }\n        private val rightExp by lazy { getExpression(right) }\n        private val bottomExp by lazy { getExpression(bottom) }\n\n        val isValid by lazy {\n            ((leftExp != null && (topExp != null || bottomExp != null)) || (rightExp != null && (topExp != null || bottomExp != null)))\n        }\n\n        /**\n         * return (x, y)\n         */\n        fun calc(rect: Rect): Pair<Float, Float>? {\n            if (!isValid) return null\n            arrayOf(\n                leftExp, topExp, rightExp, bottomExp\n            ).forEach { exp ->\n                if (exp != null) {\n                    setVariables(exp, rect)\n                }\n            }\n            try {\n                if (leftExp != null) {\n                    if (topExp != null) {\n                        return (rect.left + leftExp!!.evaluate()\n                            .toFloat()) to (rect.top + topExp!!.evaluate().toFloat())\n                    }\n                    if (bottomExp != null) {\n                        return (rect.left + leftExp!!.evaluate()\n                            .toFloat()) to (rect.bottom - bottomExp!!.evaluate().toFloat())\n                    }\n                } else if (rightExp != null) {\n                    if (topExp != null) {\n                        return (rect.right - rightExp!!.evaluate()\n                            .toFloat()) to (rect.top + topExp!!.evaluate().toFloat())\n                    }\n                    if (bottomExp != null) {\n                        return (rect.right - rightExp!!.evaluate()\n                            .toFloat()) to (rect.bottom - bottomExp!!.evaluate().toFloat())\n                    }\n                }\n            } catch (e: Exception) {\n                // 可能存在 1/0 导致错误\n                e.printStackTrace()\n                LogUtils.d(e)\n                toast(e.message ?: e.stackTraceToString())\n            }\n            return null\n        }\n    }\n\n\n    sealed interface RawCommonProps {\n        val actionCd: Long?\n        val actionDelay: Long?\n        val fastQuery: Boolean?\n        val matchRoot: Boolean?\n        val matchDelay: Long?\n        val matchTime: Long?\n        val actionMaximum: Int?\n        val resetMatch: String?\n        val actionCdKey: Int?\n        val actionMaximumKey: Int?\n        val order: Int?\n        val forcedTime: Long?\n        val snapshotUrls: List<String>?\n        val excludeSnapshotUrls: List<String>?\n        val exampleUrls: List<String>?\n        val priorityTime: Long?\n        val priorityActionMaximum: Int?\n    }\n\n    sealed interface RawRuleProps : RawCommonProps {\n        val name: String?\n        val key: Int?\n        val preKeys: List<Int>?\n        val action: String?\n        val position: Position?\n        val matches: List<String>?\n        val anyMatches: List<String>?\n        val excludeMatches: List<String>?\n        val excludeAllMatches: List<String>?\n\n        fun getAllSelectorStrings(): List<String> {\n            return listOfNotNull(matches, excludeMatches, anyMatches, excludeAllMatches).flatten()\n        }\n    }\n\n    sealed interface RawGroupProps : RawCommonProps {\n        val name: String\n        val key: Int\n        val desc: String?\n        val enable: Boolean?\n        val scopeKeys: List<Int>?\n        val rules: List<RawRuleProps>\n\n        val valid: Boolean\n        val errorDesc: String?\n        val allExampleUrls: List<String>\n        val cacheMap: MutableMap<String, Selector?>\n        val cacheStr: String\n        val cacheJsonObject: JsonObject\n\n        val groupType: Int\n            get() = when (this) {\n                is RawAppGroup -> SubsConfig.AppGroupType\n                is RawGlobalGroup -> SubsConfig.GlobalGroupType\n            }\n    }\n\n    sealed interface RawAppRuleProps {\n        val activityIds: List<String>?\n        val excludeActivityIds: List<String>?\n\n        val versionCode: IntegerMatcher?\n        val versionName: StringMatcher?\n    }\n\n    sealed interface RawGlobalRuleProps {\n        val matchAnyApp: Boolean?\n        val matchSystemApp: Boolean?\n        val matchLauncher: Boolean?\n        val apps: List<RawGlobalApp>?\n    }\n\n\n    @Serializable\n    data class RawGlobalApp(\n        val id: String,\n        val enable: Boolean?,\n        override val activityIds: List<String>?,\n        override val excludeActivityIds: List<String>?,\n        override val versionCode: IntegerMatcher?,\n        override val versionName: StringMatcher?,\n    ) : RawAppRuleProps\n\n\n    @Serializable\n    data class RawGlobalGroup(\n        override val key: Int,\n        override val name: String,\n        override val desc: String?,\n        override val enable: Boolean?,\n        override val scopeKeys: List<Int>?,\n        override val actionCd: Long?,\n        override val actionDelay: Long?,\n        override val fastQuery: Boolean?,\n        override val matchRoot: Boolean?,\n        override val matchDelay: Long?,\n        override val matchTime: Long?,\n        override val actionMaximum: Int?,\n        override val resetMatch: String?,\n        override val actionCdKey: Int?,\n        override val actionMaximumKey: Int?,\n        override val priorityTime: Long?,\n        override val priorityActionMaximum: Int?,\n        override val order: Int?,\n        override val forcedTime: Long?,\n        override val snapshotUrls: List<String>?,\n        override val excludeSnapshotUrls: List<String>?,\n        override val exampleUrls: List<String>?,\n        override val matchAnyApp: Boolean?,\n        override val matchSystemApp: Boolean?,\n        override val matchLauncher: Boolean?,\n        val disableIfAppGroupMatch: String?,\n        override val rules: List<RawGlobalRule>,\n        override val apps: List<RawGlobalApp>?,\n    ) : RawGroupProps, RawGlobalRuleProps {\n        val appIdEnable: Map<String, Boolean> by lazy {\n            if (rules.all { r -> r.apps.isNullOrEmpty() }) {\n                apps?.associate { a -> a.id to (a.enable ?: true) } ?: emptyMap()\n            } else {\n                val allIds = mutableSetOf<String>()\n                apps?.forEach { a ->\n                    allIds.add(a.id)\n                }\n                rules.forEach { r ->\n                    r.apps?.forEach { a ->\n                        allIds.add(a.id)\n                    }\n                }\n                val dataMap = mutableMapOf<String, Boolean>()\n                allIds.forEach forEachId@{ id ->\n                    var temp: Boolean? = null\n                    rules.forEach { r ->\n                        val v = (r.apps ?: apps)?.find { it.id == id }?.enable ?: return@forEachId\n                        if (temp == null) {\n                            temp = v\n                        } else if (temp != v) {\n                            return@forEachId\n                        }\n                    }\n                    if (temp != null) {\n                        dataMap[id] = temp\n                    }\n                }\n                dataMap\n            }\n        }\n\n        override val cacheMap by lazy { HashMap<String, Selector?>() }\n        override val errorDesc by lazy { getErrorDesc() }\n        override val valid by lazy { errorDesc == null }\n        override val allExampleUrls by lazy {\n            ((exampleUrls ?: emptyList()) + rules.flatMap { r ->\n                r.exampleUrls ?: emptyList()\n            }).distinct()\n        }\n        override val cacheStr by lazy { toJson5String(this) }\n        override val cacheJsonObject by lazy { json.encodeToJsonElement(this).jsonObject }\n    }\n\n\n    @Serializable\n    data class RawGlobalRule(\n        override val key: Int?,\n        override val name: String?,\n        override val actionCd: Long?,\n        override val actionDelay: Long?,\n        override val fastQuery: Boolean?,\n        override val matchRoot: Boolean?,\n        override val matchDelay: Long?,\n        override val matchTime: Long?,\n        override val actionMaximum: Int?,\n        override val resetMatch: String?,\n        override val actionCdKey: Int?,\n        override val actionMaximumKey: Int?,\n        override val priorityTime: Long?,\n        override val priorityActionMaximum: Int?,\n        override val order: Int?,\n        override val forcedTime: Long?,\n        override val snapshotUrls: List<String>?,\n        override val excludeSnapshotUrls: List<String>?,\n        override val exampleUrls: List<String>?,\n        override val preKeys: List<Int>?,\n        override val action: String?,\n        override val position: Position?,\n        override val matches: List<String>?,\n        override val excludeMatches: List<String>?,\n        override val excludeAllMatches: List<String>?,\n        override val anyMatches: List<String>?,\n        override val matchAnyApp: Boolean?,\n        override val matchSystemApp: Boolean?,\n        override val matchLauncher: Boolean?,\n        override val apps: List<RawGlobalApp>?\n    ) : RawRuleProps, RawGlobalRuleProps\n\n    @Serializable\n    data class RawAppGroup(\n        override val key: Int,\n        override val name: String,\n        override val desc: String?,\n        override val enable: Boolean?,\n        override val scopeKeys: List<Int>?,\n        override val actionCdKey: Int?,\n        override val actionMaximumKey: Int?,\n        override val actionCd: Long?,\n        override val actionDelay: Long?,\n        override val fastQuery: Boolean?,\n        override val matchRoot: Boolean?,\n        override val actionMaximum: Int?,\n        override val priorityTime: Long?,\n        override val priorityActionMaximum: Int?,\n        override val order: Int?,\n        override val forcedTime: Long?,\n        override val matchDelay: Long?,\n        override val matchTime: Long?,\n        override val resetMatch: String?,\n        override val snapshotUrls: List<String>?,\n        override val excludeSnapshotUrls: List<String>?,\n        override val exampleUrls: List<String>?,\n        override val activityIds: List<String>?,\n        override val excludeActivityIds: List<String>?,\n        override val rules: List<RawAppRule>,\n        override val versionCode: IntegerMatcher?,\n        override val versionName: StringMatcher?,\n        val ignoreGlobalGroupMatch: Boolean?,\n    ) : RawGroupProps, RawAppRuleProps {\n        override val cacheMap by lazy { HashMap<String, Selector?>() }\n        override val errorDesc by lazy { getErrorDesc() }\n        override val valid by lazy { errorDesc == null }\n        override val allExampleUrls by lazy {\n            ((exampleUrls ?: emptyList()) + rules.flatMap { r ->\n                r.exampleUrls ?: emptyList()\n            }).distinct()\n        }\n        override val cacheStr by lazy { toJson5String(this) }\n        override val cacheJsonObject by lazy { json.encodeToJsonElement(this).jsonObject }\n    }\n\n    @Serializable\n    data class RawAppRule(\n        override val key: Int?,\n        override val name: String?,\n        override val preKeys: List<Int>?,\n        override val action: String?,\n        override val position: Position?,\n        override val matches: List<String>?,\n        override val excludeMatches: List<String>?,\n        override val excludeAllMatches: List<String>?,\n        override val anyMatches: List<String>?,\n\n        override val actionCdKey: Int?,\n        override val actionMaximumKey: Int?,\n        override val actionCd: Long?,\n        override val actionDelay: Long?,\n        override val fastQuery: Boolean?,\n        override val matchRoot: Boolean?,\n        override val actionMaximum: Int?,\n        override val priorityTime: Long?,\n        override val priorityActionMaximum: Int?,\n        override val order: Int?,\n        override val forcedTime: Long?,\n        override val matchDelay: Long?,\n        override val matchTime: Long?,\n        override val resetMatch: String?,\n        override val snapshotUrls: List<String>?,\n        override val excludeSnapshotUrls: List<String>?,\n        override val exampleUrls: List<String>?,\n\n        override val activityIds: List<String>?,\n        override val excludeActivityIds: List<String>?,\n\n        override val versionCode: IntegerMatcher?,\n        override val versionName: StringMatcher?,\n    ) : RawRuleProps, RawAppRuleProps\n\n    companion object {\n\n        private fun RawGroupProps.getErrorDesc(): String? {\n            val allSelectorStrings = rules.map { r ->\n                r.getAllSelectorStrings()\n            }.flatten()\n            allSelectorStrings.forEach { source ->\n                try {\n                    val selector = Selector.parse(source)\n                    selector.checkType(typeInfo)\n                    cacheMap[source] = selector\n                } catch (e: Exception) {\n                    LogUtils.d(\"非法选择器\", source, e.toString())\n                    return \"非法选择器\\n$source\\n${e.message}\"\n                }\n            }\n            rules.forEach { r ->\n                if (r.position?.isValid == false) {\n                    return \"非法位置:${r.position}\"\n                }\n            }\n            return null\n        }\n\n        private val expVars = arrayOf(\n            \"left\",\n            \"top\",\n            \"right\",\n            \"bottom\",\n            \"width\",\n            \"height\",\n            \"random\"\n        )\n\n        private fun setVariables(exp: Expression, rect: Rect) {\n            exp.setVariable(\"left\", rect.left.toDouble())\n            exp.setVariable(\"top\", rect.top.toDouble())\n            exp.setVariable(\"right\", rect.right.toDouble())\n            exp.setVariable(\"bottom\", rect.bottom.toDouble())\n            exp.setVariable(\"width\", rect.width().toDouble())\n            exp.setVariable(\"height\", rect.height().toDouble())\n            exp.setVariable(\"random\", Math.random())\n        }\n\n        private fun getExpression(value: String?): Expression? {\n            return if (value != null) {\n                try {\n                    ExpressionBuilder(value).variables(*expVars).build().apply {\n                        expVars.forEach { v ->\n                            // 预填充作 validate\n                            setVariable(v, 0.0)\n                        }\n                    }.let { e ->\n                        if (e.validate().isValid) {\n                            e\n                        } else {\n                            null\n                        }\n                    }\n                } catch (e: Exception) {\n                    e.printStackTrace()\n                    null\n                }\n            } else {\n                null\n            }\n        }\n\n        private fun getPosition(jsonObject: JsonObject?): Position? {\n            return when (val element = jsonObject?.get(\"position\")) {\n                JsonNull, null -> null\n                is JsonObject -> {\n                    Position(\n                        left = element[\"left\"]?.jsonPrimitive?.content,\n                        bottom = element[\"bottom\"]?.jsonPrimitive?.content,\n                        top = element[\"top\"]?.jsonPrimitive?.content,\n                        right = element[\"right\"]?.jsonPrimitive?.content,\n                    )\n                }\n\n                else -> null\n            }\n        }\n\n        private fun getStringIArray(jsonObject: JsonObject?, name: String): List<String>? {\n            return when (val element = jsonObject?.get(name)) {\n                JsonNull, null -> null\n                is JsonObject -> error(\"Element ${this::class} can not be object\")\n                is JsonArray -> element.map {\n                    when (it) {\n                        is JsonObject, is JsonArray, JsonNull -> error(\"Element ${this::class} is not a int\")\n                        is JsonPrimitive -> it.content\n                    }\n                }\n\n                is JsonPrimitive -> listOf(element.content)\n            }\n        }\n\n        @Suppress(\"SameParameterValue\")\n        private fun getIntMatcher(jsonObject: JsonObject?, key: String): IntegerMatcher? {\n            return when (val element = jsonObject?.get(key)) {\n                JsonNull, null -> null\n                is JsonObject -> IntegerMatcher(\n                    minimum = getInt(element, \"minimum\"),\n                    maximum = getInt(element, \"maximum\"),\n                    include = getIntIArray(element, \"include\"),\n                    exclude = getIntIArray(element, \"exclude\"),\n                )\n\n                else -> error(\"Element $element is not a IntMatcher\")\n            }\n        }\n\n        @Suppress(\"SameParameterValue\")\n        private fun getStringMatcher(jsonObject: JsonObject?, key: String): StringMatcher? {\n            return when (val element = jsonObject?.get(key)) {\n                JsonNull, null -> null\n                is JsonObject -> StringMatcher(\n                    pattern = getString(element, \"pattern\"),\n                    include = getStringIArray(element, \"include\"),\n                    exclude = getStringIArray(element, \"exclude\"),\n                )\n\n                else -> error(\"Element $element is not a StringMatcher\")\n            }\n        }\n\n        private fun getIntIArray(jsonObject: JsonObject?, key: String): List<Int>? {\n            return when (val element = jsonObject?.get(key)) {\n                JsonNull, null -> null\n                is JsonArray -> element.map {\n                    when (it) {\n                        is JsonObject, is JsonArray, JsonNull -> error(\"Element $it is not a int\")\n                        is JsonPrimitive -> it.int\n                    }\n                }\n\n                is JsonPrimitive -> listOf(element.int)\n                else -> error(\"Element $element is not a Array\")\n            }\n        }\n\n        private fun getString(jsonObject: JsonObject?, key: String): String? =\n            when (val p = jsonObject?.get(key)) {\n                JsonNull, null -> null\n                is JsonPrimitive -> {\n                    if (p.isString) {\n                        p.content\n                    } else {\n                        null\n                    }\n                }\n\n                else -> null\n            }\n\n        private fun getLong(jsonObject: JsonObject?, key: String): Long? =\n            when (val p = jsonObject?.get(key)) {\n                JsonNull, null -> null\n                is JsonPrimitive -> {\n                    p.long\n                }\n\n                else -> null\n            }\n\n        private fun getInt(jsonObject: JsonObject?, key: String): Int? =\n            when (val p = jsonObject?.get(key)) {\n                JsonNull, null -> null\n                is JsonPrimitive -> {\n                    p.int\n                }\n\n                else -> null\n            }\n\n        private fun getBoolean(jsonObject: JsonObject?, key: String): Boolean? =\n            when (val p = jsonObject?.get(key)) {\n                JsonNull, null -> null\n                is JsonPrimitive -> {\n                    p.boolean\n                }\n\n                else -> null\n            }\n\n        private fun getCompatVersionCode(jsonObject: JsonObject): IntegerMatcher? {\n            getIntMatcher(jsonObject, \"versionCode\")?.let { return it }\n            // compat old value\n            val a = getIntIArray(jsonObject, \"versionCodes\")\n            val b = getIntIArray(jsonObject, \"excludeVersionCodes\")\n            if (a != null || b != null) {\n                return IntegerMatcher(\n                    minimum = null,\n                    maximum = null,\n                    include = a,\n                    exclude = b\n                )\n            }\n            return null\n        }\n\n        private fun getCompatVersionName(jsonObject: JsonObject): StringMatcher? {\n            getStringMatcher(jsonObject, \"versionName\")?.let { return it }\n            // compat old value\n            val a = getStringIArray(jsonObject, \"versionNames\")\n            val b = getStringIArray(jsonObject, \"excludeVersionNames\")\n            if (a != null || b != null) {\n                return StringMatcher(\n                    pattern = null,\n                    include = a,\n                    exclude = b\n                )\n            }\n            return null\n        }\n\n        private fun jsonToRuleRaw(rulesRawJson: JsonElement): RawAppRule {\n            val jsonObject = when (rulesRawJson) {\n                JsonNull -> error(\"miss current rule\")\n                is JsonObject -> rulesRawJson\n                is JsonPrimitive, is JsonArray -> JsonObject(mapOf(\"matches\" to rulesRawJson))\n            }\n            return RawAppRule(\n                activityIds = getStringIArray(jsonObject, \"activityIds\"),\n                excludeActivityIds = getStringIArray(jsonObject, \"excludeActivityIds\"),\n                matches = getStringIArray(jsonObject, \"matches\"),\n                excludeMatches = getStringIArray(jsonObject, \"excludeMatches\"),\n                excludeAllMatches = getStringIArray(jsonObject, \"excludeAllMatches\"),\n                anyMatches = getStringIArray(jsonObject, \"anyMatches\"),\n                key = getInt(jsonObject, \"key\"),\n                name = getString(jsonObject, \"name\"),\n                actionCd = getLong(jsonObject, \"actionCd\") ?: getLong(jsonObject, \"cd\"),\n                actionDelay = getLong(jsonObject, \"actionDelay\") ?: getLong(jsonObject, \"delay\"),\n                preKeys = getIntIArray(jsonObject, \"preKeys\"),\n                action = getString(jsonObject, \"action\"),\n                fastQuery = getBoolean(jsonObject, \"fastQuery\"),\n                matchRoot = getBoolean(jsonObject, \"matchRoot\"),\n                actionMaximum = getInt(jsonObject, \"actionMaximum\"),\n                matchDelay = getLong(jsonObject, \"matchDelay\"),\n                matchTime = getLong(jsonObject, \"matchTime\"),\n                resetMatch = getString(jsonObject, \"resetMatch\"),\n                snapshotUrls = getStringIArray(jsonObject, \"snapshotUrls\"),\n                excludeSnapshotUrls = getStringIArray(jsonObject, \"excludeSnapshotUrls\"),\n                exampleUrls = getStringIArray(jsonObject, \"exampleUrls\"),\n                actionMaximumKey = getInt(jsonObject, \"actionMaximumKey\"),\n                actionCdKey = getInt(jsonObject, \"actionCdKey\"),\n                order = getInt(jsonObject, \"order\"),\n                versionCode = getCompatVersionCode(jsonObject),\n                versionName = getCompatVersionName(jsonObject),\n                position = getPosition(jsonObject),\n                forcedTime = getLong(jsonObject, \"forcedTime\"),\n                priorityTime = getLong(jsonObject, \"priorityTime\"),\n                priorityActionMaximum = getInt(jsonObject, \"priorityActionMaximum\"),\n            )\n        }\n\n\n        private fun jsonToGroupRaw(groupRawJson: JsonElement): RawAppGroup {\n            val jsonObject = when (groupRawJson) {\n                JsonNull -> error(\"group must not be null\")\n                is JsonObject -> groupRawJson\n                is JsonPrimitive, is JsonArray -> JsonObject(mapOf(\"rules\" to groupRawJson))\n            }\n            return RawAppGroup(\n                activityIds = getStringIArray(jsonObject, \"activityIds\"),\n                excludeActivityIds = getStringIArray(jsonObject, \"excludeActivityIds\"),\n                actionCd = getLong(jsonObject, \"actionCd\") ?: getLong(jsonObject, \"cd\"),\n                actionDelay = getLong(jsonObject, \"actionDelay\") ?: getLong(jsonObject, \"delay\"),\n                name = getString(jsonObject, \"name\") ?: error(\"miss group name\"),\n                desc = getString(jsonObject, \"desc\"),\n                enable = getBoolean(jsonObject, \"enable\"),\n                key = getInt(jsonObject, \"key\") ?: error(\"miss group key\"),\n                rules = when (val rulesJson = jsonObject[\"rules\"]) {\n                    null, JsonNull -> emptyList()\n                    is JsonPrimitive, is JsonObject -> JsonArray(listOf(rulesJson))\n                    is JsonArray -> rulesJson\n                }.map {\n                    jsonToRuleRaw(it)\n                }.distinctNotNullBy { it.key },\n                fastQuery = getBoolean(jsonObject, \"fastQuery\"),\n                matchRoot = getBoolean(jsonObject, \"matchRoot\"),\n                actionMaximum = getInt(jsonObject, \"actionMaximum\"),\n                matchDelay = getLong(jsonObject, \"matchDelay\"),\n                matchTime = getLong(jsonObject, \"matchTime\"),\n                resetMatch = getString(jsonObject, \"resetMatch\"),\n                snapshotUrls = getStringIArray(jsonObject, \"snapshotUrls\"),\n                excludeSnapshotUrls = getStringIArray(jsonObject, \"excludeSnapshotUrls\"),\n                exampleUrls = getStringIArray(jsonObject, \"exampleUrls\"),\n                actionMaximumKey = getInt(jsonObject, \"actionMaximumKey\"),\n                actionCdKey = getInt(jsonObject, \"actionCdKey\"),\n                order = getInt(jsonObject, \"order\"),\n                forcedTime = getLong(jsonObject, \"forcedTime\"),\n                scopeKeys = getIntIArray(jsonObject, \"scopeKeys\"),\n                versionCode = getCompatVersionCode(jsonObject),\n                versionName = getCompatVersionName(jsonObject),\n                priorityTime = getLong(jsonObject, \"priorityTime\"),\n                priorityActionMaximum = getInt(jsonObject, \"priorityActionMaximum\"),\n                ignoreGlobalGroupMatch = getBoolean(jsonObject, \"ignoreGlobalGroupMatch\"),\n            )\n        }\n\n        private fun jsonToAppRaw(jsonObject: JsonObject, appIndex: Int? = null): RawApp {\n            return RawApp(\n                id = getString(jsonObject, \"id\") ?: error(\n                    if (appIndex != null) {\n                        \"miss subscription.apps[$appIndex].id\"\n                    } else {\n                        \"miss id\"\n                    }\n                ),\n                name = getString(jsonObject, \"name\"),\n                groups = (when (val groupsJson = jsonObject[\"groups\"]) {\n                    null, JsonNull -> emptyList()\n                    is JsonPrimitive, is JsonObject -> JsonArray(listOf(groupsJson))\n                    is JsonArray -> groupsJson\n                }).map { jsonElement ->\n                    jsonToGroupRaw(jsonElement)\n                }.distinctByIfAny { it.key },\n            )\n        }\n\n\n        private fun jsonToGlobalApp(jsonObject: JsonObject, index: Int): RawGlobalApp {\n            return RawGlobalApp(\n                id = getString(jsonObject, \"id\") ?: error(\"miss apps[$index].id\"),\n                enable = getBoolean(jsonObject, \"enable\"),\n                activityIds = getStringIArray(jsonObject, \"activityIds\"),\n                excludeActivityIds = getStringIArray(jsonObject, \"excludeActivityIds\"),\n                versionCode = getCompatVersionCode(jsonObject),\n                versionName = getCompatVersionName(jsonObject),\n            )\n        }\n\n        private fun jsonToGlobalRule(jsonObject: JsonObject): RawGlobalRule {\n            return RawGlobalRule(\n                key = getInt(jsonObject, \"key\"),\n                name = getString(jsonObject, \"name\"),\n                actionCd = getLong(jsonObject, \"actionCd\"),\n                actionDelay = getLong(jsonObject, \"actionDelay\"),\n                fastQuery = getBoolean(jsonObject, \"fastQuery\"),\n                matchRoot = getBoolean(jsonObject, \"matchRoot\"),\n                actionMaximum = getInt(jsonObject, \"actionMaximum\"),\n                matchDelay = getLong(jsonObject, \"matchDelay\"),\n                matchTime = getLong(jsonObject, \"matchTime\"),\n                resetMatch = getString(jsonObject, \"resetMatch\"),\n                snapshotUrls = getStringIArray(jsonObject, \"snapshotUrls\"),\n                excludeSnapshotUrls = getStringIArray(jsonObject, \"excludeSnapshotUrls\"),\n                exampleUrls = getStringIArray(jsonObject, \"exampleUrls\"),\n                actionMaximumKey = getInt(jsonObject, \"actionMaximumKey\"),\n                actionCdKey = getInt(jsonObject, \"actionCdKey\"),\n                matchAnyApp = getBoolean(jsonObject, \"matchAnyApp\"),\n                matchSystemApp = getBoolean(jsonObject, \"matchSystemApp\"),\n                matchLauncher = getBoolean(jsonObject, \"matchLauncher\"),\n                apps = jsonObject[\"apps\"]?.jsonArray?.mapIndexed { index, jsonElement ->\n                    jsonToGlobalApp(\n                        jsonElement.jsonObject, index\n                    )\n                }?.distinctByIfAny { it.id },\n                action = getString(jsonObject, \"action\"),\n                preKeys = getIntIArray(jsonObject, \"preKeys\"),\n                excludeMatches = getStringIArray(jsonObject, \"excludeMatches\"),\n                excludeAllMatches = getStringIArray(jsonObject, \"excludeAllMatches\"),\n                matches = getStringIArray(jsonObject, \"matches\"),\n                anyMatches = getStringIArray(jsonObject, \"anyMatches\"),\n                order = getInt(jsonObject, \"order\"),\n                forcedTime = getLong(jsonObject, \"forcedTime\"),\n                position = getPosition(jsonObject),\n                priorityTime = getLong(jsonObject, \"priorityTime\"),\n                priorityActionMaximum = getInt(jsonObject, \"priorityActionMaximum\"),\n            )\n        }\n\n        private fun jsonToGlobalGroup(jsonObject: JsonObject, groupIndex: Int): RawGlobalGroup {\n            return RawGlobalGroup(\n                key = getInt(jsonObject, \"key\") ?: error(\"miss group[$groupIndex].key\"),\n                name = getString(jsonObject, \"name\") ?: error(\"miss group[$groupIndex].name\"),\n                desc = getString(jsonObject, \"desc\"),\n                enable = getBoolean(jsonObject, \"enable\"),\n                actionCd = getLong(jsonObject, \"actionCd\"),\n                actionDelay = getLong(jsonObject, \"actionDelay\"),\n                fastQuery = getBoolean(jsonObject, \"fastQuery\"),\n                matchRoot = getBoolean(jsonObject, \"matchRoot\"),\n                actionMaximum = getInt(jsonObject, \"actionMaximum\"),\n                matchDelay = getLong(jsonObject, \"matchDelay\"),\n                matchTime = getLong(jsonObject, \"matchTime\"),\n                resetMatch = getString(jsonObject, \"resetMatch\"),\n                snapshotUrls = getStringIArray(jsonObject, \"snapshotUrls\"),\n                excludeSnapshotUrls = getStringIArray(jsonObject, \"excludeSnapshotUrls\"),\n                exampleUrls = getStringIArray(jsonObject, \"exampleUrls\"),\n                actionMaximumKey = getInt(jsonObject, \"actionMaximumKey\"),\n                actionCdKey = getInt(jsonObject, \"actionCdKey\"),\n                matchSystemApp = getBoolean(jsonObject, \"matchSystemApp\"),\n                matchAnyApp = getBoolean(jsonObject, \"matchAnyApp\"),\n                matchLauncher = getBoolean(jsonObject, \"matchLauncher\"),\n                apps = jsonObject[\"apps\"]?.jsonArray?.mapIndexed { index, jsonElement ->\n                    jsonToGlobalApp(\n                        jsonElement.jsonObject, index\n                    )\n                }?.distinctByIfAny { it.id },\n                rules = (jsonObject[\"rules\"]?.jsonArray?.map { jsonElement ->\n                    jsonToGlobalRule(jsonElement.jsonObject)\n                } ?: emptyList()).distinctNotNullBy { it.key },\n                order = getInt(jsonObject, \"order\"),\n                scopeKeys = getIntIArray(jsonObject, \"scopeKeys\"),\n                forcedTime = getLong(jsonObject, \"forcedTime\"),\n                priorityTime = getLong(jsonObject, \"priorityTime\"),\n                priorityActionMaximum = getInt(jsonObject, \"priorityActionMaximum\"),\n                disableIfAppGroupMatch = getString(jsonObject, \"disableIfAppGroupMatch\"),\n            )\n        }\n\n        private fun jsonToSubscriptionRaw(rootJson: JsonObject): RawSubscription {\n            return RawSubscription(\n                id = getLong(rootJson, \"id\") ?: error(\"miss subscription.id\"),\n                name = getString(rootJson, \"name\") ?: error(\"miss subscription.name\"),\n                version = getInt(rootJson, \"version\") ?: error(\"miss subscription.version\"),\n                author = getString(rootJson, \"author\"),\n                updateUrl = getString(rootJson, \"updateUrl\"),\n                supportUri = getString(rootJson, \"supportUri\"),\n                checkUpdateUrl = getString(rootJson, \"checkUpdateUrl\"),\n                apps = (rootJson[\"apps\"]?.jsonArray?.mapIndexed { index, jsonElement ->\n                    jsonToAppRaw(\n                        jsonElement.jsonObject,\n                        index\n                    )\n                } ?: emptyList()).filterIfNotAll { it.groups.isNotEmpty() }\n                    .distinctByIfAny { it.id },\n                categories = (rootJson[\"categories\"]?.jsonArray?.mapIndexed { index, jsonElement ->\n                    RawCategory(\n                        key = getInt(jsonElement.jsonObject, \"key\")\n                            ?: error(\"miss categories[$index].key\"),\n                        name = getString(jsonElement.jsonObject, \"name\")\n                            ?: error(\"miss categories[$index].name\"),\n                        enable = getBoolean(jsonElement.jsonObject, \"enable\"),\n                        desc = getString(jsonElement.jsonObject, \"desc\")\n                    )\n                } ?: emptyList()).filterIfNotAll { it.name.isNotEmpty() }\n                    .distinctByIfAny { it.key },\n                globalGroups = (rootJson[\"globalGroups\"]?.jsonArray?.mapIndexed { index, jsonElement ->\n                    jsonToGlobalGroup(jsonElement.jsonObject, index)\n                } ?: emptyList()).distinctByIfAny { it.key }\n            )\n        }\n\n        private fun <T> List<T>.distinctNotNullBy(selector: (T) -> Any?): List<T> {\n            val set = HashSet<Any>()\n            val list = ArrayList<T>()\n            forEach { e ->\n                val key = selector(e)\n                if (key == null || set.add(key)) {\n                    list.add(e)\n                }\n            }\n            return list\n        }\n\n        fun parse(source: String, json5: Boolean = true): RawSubscription {\n            val element = if (json5) {\n                Json5.parseToJson5Element(source)\n            } else {\n                json.parseToJsonElement(source)\n            }\n            return jsonToSubscriptionRaw(element.jsonObject)\n        }\n\n        fun parseApp(jsonObject: JsonObject): RawApp {\n            return jsonToAppRaw(jsonObject)\n        }\n\n        fun parseAppGroup(jsonObject: JsonObject): RawAppGroup {\n            return jsonToGroupRaw(jsonObject)\n        }\n\n        fun parseGlobalGroup(jsonObject: JsonObject): RawGlobalGroup {\n            return jsonToGlobalGroup(jsonObject, 0)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/ResolvedGroup.kt",
    "content": "package li.songe.gkd.data\n\nsealed class ResolvedGroup(\n    open val group: RawSubscription.RawGroupProps,\n    val subscription: RawSubscription,\n    val subsItem: SubsItem,\n    val config: SubsConfig?,\n) {\n    val excludeData by lazy { ExcludeData.parse(config?.exclude) }\n\n    abstract val appId: String?\n}\n\nclass ResolvedAppGroup(\n    override val group: RawSubscription.RawAppGroup,\n    subscription: RawSubscription,\n    subsItem: SubsItem,\n    config: SubsConfig?,\n    val app: RawSubscription.RawApp,\n    val enable: Boolean,\n) : ResolvedGroup(group, subscription, subsItem, config) {\n    override val appId: String?\n        get() = app.id\n}\n\nclass ResolvedGlobalGroup(\n    override val group: RawSubscription.RawGlobalGroup,\n    subscription: RawSubscription,\n    subsItem: SubsItem,\n    config: SubsConfig?,\n) : ResolvedGroup(group, subscription, subsItem, config) {\n    override val appId: String?\n        get() = null\n\n    val groupExcludeAppIds by lazy {\n        subscription.globalGroupAppGroupNameDisableMap[group.key] ?: emptySet()\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt",
    "content": "package li.songe.gkd.data\n\nimport android.view.accessibility.AccessibilityNodeInfo\nimport kotlinx.atomicfu.atomic\nimport kotlinx.atomicfu.update\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.updateAndGet\nimport li.songe.gkd.a11y.appChangeTime\nimport li.songe.gkd.a11y.lastTriggerRule\nimport li.songe.gkd.a11y.lastTriggerTime\nimport li.songe.gkd.store.actionCountFlow\nimport li.songe.selector.MatchOption\nimport li.songe.selector.Selector\n\nsealed class ResolvedRule(\n    val rule: RawSubscription.RawRuleProps,\n    val g: ResolvedGroup,\n) {\n    private val group = g.group\n    val subsItem = g.subsItem\n    val rawSubs = g.subscription\n    val key = rule.key\n    val index = group.rules.indexOfFirst { r -> r === rule }\n    val excludeData = g.excludeData\n    private val preKeys = (rule.preKeys ?: emptyList()).toSet()\n    val matches =\n        (rule.matches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) }\n    val anyMatches =\n        (rule.anyMatches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) }\n    val excludeMatches =\n        (rule.excludeMatches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) }\n    val excludeAllMatches =\n        (rule.excludeAllMatches ?: emptyList()).map { s -> group.cacheMap[s] ?: Selector.parse(s) }\n\n    private val resetMatch = rule.resetMatch ?: group.resetMatch\n    val matchDelay = rule.matchDelay ?: group.matchDelay ?: 0L\n    val actionDelay = rule.actionDelay ?: group.actionDelay ?: 0L\n    private val matchTime = rule.matchTime ?: group.matchTime\n    private val forcedTime = rule.forcedTime ?: group.forcedTime ?: 0L\n    val matchOption = MatchOption(\n        fastQuery = rule.fastQuery ?: group.fastQuery ?: false\n    )\n    val matchRoot = rule.matchRoot ?: group.matchRoot ?: false\n    val order = rule.order ?: group.order ?: 0\n\n    private val actionCdKey = rule.actionCdKey ?: group.actionCdKey\n    private val actionCd = rule.actionCd ?: if (actionCdKey != null) {\n        group.rules.find { r -> r.key == actionCdKey }?.actionCd\n    } else {\n        null\n    } ?: group.actionCd ?: 1000L\n\n    private val actionMaximumKey = rule.actionMaximumKey ?: group.actionMaximumKey\n    private val actionMaximum = rule.actionMaximum ?: if (actionMaximumKey != null) {\n        group.rules.find { r -> r.key == actionMaximumKey }?.actionMaximum\n    } else {\n        null\n    } ?: group.actionMaximum\n\n    private val hasSlowSelector by lazy {\n        (matches + excludeMatches + anyMatches + excludeAllMatches).any { s -> s.isSlow(matchOption) }\n    }\n    val priorityTime = rule.priorityTime ?: group.priorityTime ?: 0\n    val priorityActionMaximum = rule.priorityActionMaximum ?: group.priorityActionMaximum ?: 1\n    val priorityEnabled: Boolean\n        get() = priorityTime > 0\n\n    fun isPriority(): Boolean {\n        if (!priorityEnabled) return false\n        if (priorityActionMaximum <= actionCount.value) return false\n        if (!status.ok) return false\n        val t = System.currentTimeMillis()\n        return t - matchChangedTime.value < priorityTime + matchDelay\n    }\n\n    val isSlow by lazy { preKeys.isEmpty() && (matchTime == null || matchTime > 10_000L) && hasSlowSelector }\n\n    var groupToRules: Map<out RawSubscription.RawGroupProps, List<ResolvedRule>> = emptyMap()\n        set(value) {\n            field = value\n            val selfGroupRules = field[group] ?: emptyList()\n            val othersGroupRules =\n                (group.scopeKeys ?: emptyList()).distinct().filter { k -> k != group.key }\n                    .flatMap { k ->\n                        field.entries.find { e -> e.key.key == k }?.value ?: emptyList()\n                    }\n            val groupRules = selfGroupRules + othersGroupRules\n\n            // 共享次数\n            if (actionMaximumKey != null) {\n                val otherRule = groupRules.find { r -> r.key == actionMaximumKey }\n                if (otherRule != null) {\n                    actionCount = otherRule.actionCount\n                }\n            }\n            // 共享 cd\n            if (actionCdKey != null) {\n                val otherRule = groupRules.find { r -> r.key == actionCdKey }\n                if (otherRule != null) {\n                    actionTriggerTime = otherRule.actionTriggerTime\n                }\n            }\n            preRules = groupRules.filter { otherRule ->\n                (otherRule.key != null) && preKeys.contains(\n                    otherRule.key\n                )\n            }.toSet()\n        }\n\n    private var preRules = emptySet<ResolvedRule>()\n    val hasNext = group.rules.any { r -> r.preKeys?.any { k -> k == rule.key } == true }\n\n    private var actionDelayTriggerTime = atomic(0L)\n    val actionDelayJob = atomic<Job?>(null)\n    fun checkDelay(): Boolean {\n        if (actionDelay > 0 && actionDelayTriggerTime.value == 0L) {\n            actionDelayTriggerTime.value = System.currentTimeMillis()\n            return true\n        }\n        return false\n    }\n\n    fun checkForced(): Boolean {\n        if (forcedTime <= 0) return false\n        return System.currentTimeMillis() < matchChangedTime.value + matchDelay + forcedTime\n    }\n\n    private var actionTriggerTime = atomic(0L)\n    fun trigger() {\n        val t = System.currentTimeMillis()\n        actionTriggerTime.value = t\n        actionDelayTriggerTime.value = 0L\n        actionCount.incrementAndGet()\n        lastTriggerTime = t\n        lastTriggerRule = this\n        actionCountFlow.updateAndGet { it + 1 }\n    }\n\n    private var actionCount = atomic(0)\n\n    private val matchChangedTime = atomic(0L)\n    val isFirstMatchApp: Boolean\n        get() = matchChangedTime.value < appChangeTime\n\n    private val matchLimitTime = (matchTime ?: 0) + matchDelay\n\n    val resetMatchType = ResetMatchType.allSubObject.find {\n        it.value == resetMatch\n    } ?: ResetMatchType.Activity\n\n    fun resetState(t: Long) {\n        actionCount.value = 0\n        actionDelayTriggerTime.value = 0L\n        actionTriggerTime.value = 0\n        actionDelayJob.update { it?.cancel(); null }\n        matchDelayJob.update { it?.cancel(); null }\n        matchChangedTime.value = t\n    }\n\n    private val performer = ActionPerformer.getAction(rule.action ?: rule.position?.let {\n        ActionPerformer.ClickCenter.action\n    })\n\n    fun performAction(node: AccessibilityNodeInfo): ActionResult {\n        return performer.perform(node, rule.position)\n    }\n\n    val matchDelayJob = atomic<Job?>(null)\n\n    val status: RuleStatus\n        get() {\n            if (actionMaximum != null) {\n                if (actionCount.value >= actionMaximum) {\n                    return RuleStatus.Status1 // 达到最大执行次数\n                }\n            }\n            if (preRules.isNotEmpty() && !preRules.any { it === lastTriggerRule }) {\n                return RuleStatus.Status2 // 需要提前触发某个规则\n            }\n            val t = System.currentTimeMillis()\n            val c = matchChangedTime.value\n            if (matchDelay > 0 && t - c < matchDelay) {\n                return RuleStatus.Status3 // 处于匹配延迟中\n            }\n            if (matchTime != null && t - c > matchLimitTime) {\n                return RuleStatus.Status4 // 超出匹配时间\n            }\n            if (actionTriggerTime.value + actionCd > t) {\n                return RuleStatus.Status5 // 处于冷却时间\n            }\n            val d = actionDelayTriggerTime.value\n            if (d > 0) {\n                if (d + actionDelay > t) {\n                    return RuleStatus.Status6 // 处于触发延迟中\n                }\n            }\n            return RuleStatus.StatusOk\n        }\n\n    fun statusText(): String {\n        return \"id:${subsItem.id}, v:${rawSubs.version}, type:${type}, gKey=${group.key}, gName:${group.name}, index:${index}, key:${key}, status:${status.name}\"\n    }\n\n    abstract val type: String\n\n    // 范围越精确, 优先级越高\n    abstract fun matchActivity(appId: String, activityId: String? = null): Boolean\n}\n\nsealed class ResetMatchType(val value: String) {\n    data object Activity : ResetMatchType(\"activity\")\n    data object Match : ResetMatchType(\"match\")\n    data object App : ResetMatchType(\"app\")\n\n    companion object {\n        val allSubObject by lazy { listOf(Activity, Match, App) }\n    }\n}\n\nsealed class RuleStatus(val name: String) {\n    data object StatusOk : RuleStatus(\"ok\")\n    data object Status1 : RuleStatus(\"达到最大执行次数\")\n    data object Status2 : RuleStatus(\"需要提前触发某个规则\")\n    data object Status3 : RuleStatus(\"处于匹配延迟\")\n    data object Status4 : RuleStatus(\"超出匹配时间\")\n    data object Status5 : RuleStatus(\"处于冷却时间\")\n    data object Status6 : RuleStatus(\"处于触发延迟\")\n\n    val ok: Boolean\n        get() = this === StatusOk\n\n    val alive: Boolean\n        get() = this !== Status1 && this !== Status2 && this !== Status4\n}\n\nfun getFixActivityIds(\n    appId: String,\n    activityIds: List<String>?,\n): List<String> {\n    if (activityIds.isNullOrEmpty()) return emptyList()\n    return activityIds.map { activityId ->\n        if (activityId.startsWith('.')) { // .a.b.c -> com.x.y.x.a.b.c\n            appId + activityId\n        } else {\n            activityId\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/RpcError.kt",
    "content": "package li.songe.gkd.data\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class RpcError(\n    override val message: String,\n    @SerialName(\"__error\") val error: Boolean = true,\n    val unknown: Boolean = false,\n) : Exception(message)\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/Snapshot.kt",
    "content": "package li.songe.gkd.data\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.Update\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.util.SnapshotExt\nimport li.songe.gkd.util.format\n\n@Entity(\n    tableName = \"snapshot\",\n)\n@Serializable\ndata class Snapshot(\n    @PrimaryKey @ColumnInfo(name = \"id\") override val id: Long,\n\n    @ColumnInfo(name = \"app_id\") override val appId: String,\n    @ColumnInfo(name = \"activity_id\") override val activityId: String?,\n\n    @ColumnInfo(name = \"screen_height\") override val screenHeight: Int,\n    @ColumnInfo(name = \"screen_width\") override val screenWidth: Int,\n    @ColumnInfo(name = \"is_landscape\") override val isLandscape: Boolean,\n\n    @ColumnInfo(name = \"github_asset_id\") val githubAssetId: Int? = null,\n\n    ) : BaseSnapshot {\n\n    val date by lazy { id.format(\"MM-dd HH:mm:ss\") }\n\n    val screenshotFile by lazy { SnapshotExt.screenshotFile(id) }\n\n    @Dao\n    interface SnapshotDao {\n        @Update\n        suspend fun update(vararg objects: Snapshot): Int\n\n        @Insert\n        suspend fun insert(vararg users: Snapshot): List<Long>\n\n        @Insert(onConflict = OnConflictStrategy.IGNORE)\n        suspend fun insertOrIgnore(vararg users: Snapshot): List<Long>\n\n        @Query(\"DELETE FROM snapshot\")\n        suspend fun deleteAll()\n\n        @Delete\n        suspend fun delete(vararg users: Snapshot): Int\n\n        @Query(\"SELECT * FROM snapshot ORDER BY id DESC\")\n        fun query(): Flow<List<Snapshot>>\n\n        @Query(\"UPDATE snapshot SET github_asset_id=null WHERE id = :id\")\n        suspend fun deleteGithubAssetId(id: Long)\n\n        @Query(\"SELECT COUNT(*) FROM snapshot\")\n        fun count(): Flow<Int>\n    }\n}\n\n\n\n\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/SubsConfig.kt",
    "content": "package li.songe.gkd.data\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.Transaction\nimport androidx.room.Update\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.util.isValidActivityId\nimport li.songe.gkd.util.isValidAppId\n\n\nprivate var lastId = 0L\n\n@Synchronized\nprivate fun buildUniqueTimeMillisId(): Long {\n    val id = System.currentTimeMillis()\n    if (id > lastId) {\n        lastId = id\n    } else {\n        lastId += 1\n    }\n    return lastId\n}\n\n@Serializable\n@Entity(\n    tableName = \"subs_config\",\n)\ndata class SubsConfig(\n    @PrimaryKey @ColumnInfo(name = \"id\") val id: Long = buildUniqueTimeMillisId(),\n    @ColumnInfo(name = \"type\") val type: Int,\n    @ColumnInfo(name = \"enable\") val enable: Boolean? = null,\n    @ColumnInfo(name = \"subs_id\") val subsId: Long,\n    @ColumnInfo(name = \"app_id\") val appId: String = \"\",\n    @ColumnInfo(name = \"group_key\") val groupKey: Int = -1,\n    @ColumnInfo(name = \"exclude\", defaultValue = \"\") val exclude: String = \"\",\n) {\n\n    @Suppress(\"ConstPropertyName\")\n    companion object {\n        const val AppGroupType = 2\n        const val GlobalGroupType = 3\n    }\n\n    @Dao\n    interface SubsConfigDao {\n\n        @Update\n        suspend fun update(vararg objects: SubsConfig): Int\n\n        @Insert(onConflict = OnConflictStrategy.REPLACE)\n        suspend fun insert(vararg users: SubsConfig): List<Long>\n\n        @Insert(onConflict = OnConflictStrategy.IGNORE)\n        suspend fun insertOrIgnore(vararg users: SubsConfig): List<Long>\n\n        @Delete\n        suspend fun delete(vararg users: SubsConfig): Int\n\n        @Transaction\n        suspend fun insertAndDelete(newList: List<SubsConfig>, deleteList: List<SubsConfig>) {\n            insert(*newList.toTypedArray())\n            delete(*deleteList.toTypedArray())\n        }\n\n        @Query(\"DELETE FROM subs_config WHERE subs_id=:subsItemId\")\n        suspend fun delete(subsItemId: Long): Int\n\n        @Query(\"DELETE FROM subs_config WHERE subs_id IN (:subsIds)\")\n        suspend fun deleteBySubsId(vararg subsIds: Long): Int\n\n        @Query(\"DELETE FROM subs_config WHERE subs_id=:subsItemId AND app_id=:appId\")\n        suspend fun deleteAppConfig(subsItemId: Long, appId: String): Int\n\n        @Query(\"DELETE FROM subs_config WHERE type=${AppGroupType} AND subs_id=:subsItemId AND app_id=:appId AND group_key=:groupKey\")\n        suspend fun deleteAppGroupConfig(subsItemId: Long, appId: String, groupKey: Int): Int\n\n\n        @Query(\"DELETE FROM subs_config WHERE type=${AppGroupType} AND subs_id=:subsItemId AND app_id=:appId AND group_key IN (:keyList)\")\n        suspend fun batchDeleteAppGroupConfig(\n            subsItemId: Long,\n            appId: String,\n            keyList: List<Int>\n        ): Int\n\n        @Query(\"DELETE FROM subs_config WHERE type=${GlobalGroupType} AND subs_id=:subsItemId AND group_key=:groupKey\")\n        suspend fun deleteGlobalGroupConfig(subsItemId: Long, groupKey: Int): Int\n\n        @Query(\"DELETE FROM subs_config WHERE type=${GlobalGroupType} AND subs_id=:subsItemId AND group_key IN (:keyList)\")\n        suspend fun batchDeleteGlobalGroupConfig(subsItemId: Long, keyList: List<Int>): Int\n\n        @Query(\"SELECT * FROM subs_config WHERE subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)\")\n        fun queryUsedList(): Flow<List<SubsConfig>>\n\n        @Query(\"SELECT * FROM subs_config WHERE type=${AppGroupType} AND subs_id=:subsItemId\")\n        fun querySubsGroupTypeConfig(subsItemId: Long): Flow<List<SubsConfig>>\n\n        @Query(\"SELECT * FROM subs_config WHERE type=${AppGroupType} AND subs_id=:subsItemId AND app_id=:appId\")\n        fun queryAppGroupTypeConfig(subsItemId: Long, appId: String): Flow<List<SubsConfig>>\n\n        @Query(\"SELECT * FROM subs_config WHERE type=${AppGroupType} AND subs_id=:subsItemId AND app_id=:appId AND group_key=:groupKey\")\n        fun queryAppGroupTypeConfig(\n            subsItemId: Long, appId: String, groupKey: Int\n        ): Flow<SubsConfig?>\n\n        @Query(\"SELECT * FROM subs_config WHERE type=${GlobalGroupType} AND subs_id=:subsItemId\")\n        fun queryGlobalGroupTypeConfig(subsItemId: Long): Flow<List<SubsConfig>>\n\n        @Query(\"SELECT * FROM subs_config WHERE type=${GlobalGroupType} AND subs_id=:subsItemId AND group_key=:groupKey\")\n        fun queryGlobalGroupTypeConfig(subsItemId: Long, groupKey: Int): Flow<SubsConfig?>\n\n        @Query(\"SELECT * FROM subs_config WHERE type=${AppGroupType} AND app_id=:appId AND subs_id IN (:subsItemIds)\")\n        fun queryAppConfig(subsItemIds: List<Long>, appId: String): Flow<List<SubsConfig>>\n\n        @Query(\"SELECT * FROM subs_config WHERE type=${GlobalGroupType} AND subs_id IN (:subsItemIds)\")\n        fun queryGlobalConfig(subsItemIds: List<Long>): Flow<List<SubsConfig>>\n\n        @Query(\"SELECT * FROM subs_config WHERE type=${GlobalGroupType} AND subs_id IN (SELECT si.id FROM subs_item si WHERE si.enable = 1)\")\n        fun queryUsedGlobalConfig(): Flow<List<SubsConfig>>\n\n        @Query(\"SELECT * FROM subs_config WHERE subs_id IN (:subsItemIds) \")\n        suspend fun querySubsItemConfig(subsItemIds: List<Long>): List<SubsConfig>\n\n        @Query(\"UPDATE subs_config SET enable = null WHERE type=${AppGroupType} AND subs_id=:subsItemId AND app_id=:appId AND group_key=:groupKey AND enable IS NOT NULL\")\n        suspend fun resetAppGroupTypeEnable(subsItemId: Long, appId: String, groupKey: Int): Int\n\n        @Transaction\n        suspend fun batchResetAppGroupEnable(\n            subsItemId: Long,\n            list: List<Pair<RawSubscription.RawAppGroup, RawSubscription.RawApp>>\n        ): List<Pair<RawSubscription.RawAppGroup, RawSubscription.RawApp>> {\n            return list.filter { (g, a) ->\n                resetAppGroupTypeEnable(subsItemId, a.id, g.key) > 0\n            }\n        }\n    }\n\n}\n\ndata class ExcludeData(\n    val appIds: Map<String, Boolean>,\n    val activityIds: Set<Pair<String, String>>,\n) {\n    val excludeAppIds = appIds.entries.filter { e -> e.value }.map { e -> e.key }.toHashSet()\n    val includeAppIds = appIds.entries.filter { e -> !e.value }.map { e -> e.key }.toHashSet()\n\n    fun stringify(appId: String? = null): String {\n        return if (appId != null) {\n            activityIds.filter { e -> e.first == appId }.map { e -> e.second }.sorted()\n                .joinToString(\"\\n\\n\")\n        } else {\n            (appIds.entries.map { e ->\n                if (e.value) {\n                    e.key\n                } else {\n                    \"!${e.key}\"\n                }\n            } + activityIds.map { e -> \"${e.first}/${e.second}\" }).sorted().joinToString(\"\\n\\n\")\n        }\n    }\n\n    fun clear(appId: String): ExcludeData {\n        return copy(\n            appIds = appIds.toMutableMap().apply {\n                remove(appId)\n            },\n        )\n    }\n\n    fun switch(appId: String, activityId: String? = null): ExcludeData {\n        return if (activityId == null) {\n            copy(\n                appIds = appIds.toMutableMap().apply {\n                    if (get(appId) != false) {\n                        set(appId, false)\n                    } else {\n                        set(appId, true)\n                    }\n                },\n            )\n        } else {\n            copy(activityIds = activityIds.toMutableSet().apply {\n                val e = appId to activityId\n                if (contains(e)) {\n                    remove(e)\n                } else {\n                    add(e)\n                }\n            })\n        }\n    }\n\n    companion object {\n        private val empty = ExcludeData(emptyMap(), emptySet())\n\n        fun parse(exclude: String?): ExcludeData {\n            if (exclude.isNullOrBlank()) {\n                return empty\n            }\n            val appIds = HashMap<String, Boolean>()\n            val activityIds = HashSet<Pair<String, String>>()\n            exclude.split('\\n')\n                .filter { it.isNotBlank() }\n                .forEach { s ->\n                    if (s[0] == '!') {\n                        val appId = s.substring(1)\n                        if (appId.isValidAppId()) {\n                            appIds[appId] = false\n                        }\n                    } else {\n                        val a = s.split('/', limit = 2)\n                        val appId = a[0]\n                        if (appId.isValidAppId()) {\n                            val activityId = a.getOrNull(1)\n                            if (activityId != null) {\n                                if (activityId.isValidActivityId()) {\n                                    activityIds.add(appId to activityId)\n                                }\n                            } else {\n                                appIds[appId] = true\n                            }\n                        }\n                    }\n                }\n            return ExcludeData(\n                appIds = appIds,\n                activityIds = activityIds,\n            )\n        }\n\n        fun parse(exclude: String?, appId: String): ExcludeData {\n            if (exclude.isNullOrBlank()) return empty\n            return parse(exclude.split('\\n').joinToString(\"\\n\") { \"$appId/$it\" })\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/SubsItem.kt",
    "content": "package li.songe.gkd.data\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.Transaction\nimport androidx.room.Update\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.util.LOCAL_SUBS_IDS\nimport li.songe.gkd.util.format\n\n@Serializable\n@Entity(\n    tableName = \"subs_item\",\n)\ndata class SubsItem(\n    @PrimaryKey @ColumnInfo(name = \"id\") val id: Long,\n\n    @ColumnInfo(name = \"ctime\") val ctime: Long = System.currentTimeMillis(),\n    @ColumnInfo(name = \"mtime\") val mtime: Long = System.currentTimeMillis(),\n    @ColumnInfo(name = \"enable\") val enable: Boolean = false,\n    @ColumnInfo(name = \"enable_update\") val enableUpdate: Boolean = true,\n    @ColumnInfo(name = \"order\") val order: Int,\n    @ColumnInfo(name = \"update_url\") val updateUrl: String? = null,\n\n    ) {\n\n    val isLocal: Boolean\n        get() = LOCAL_SUBS_IDS.contains(id)\n\n    val mtimeStr by lazy { mtime.format(\"yyyy-MM-dd HH:mm:ss\") }\n\n    @Dao\n    interface SubsItemDao {\n        @Update\n        suspend fun update(vararg objects: SubsItem): Int\n\n        @Query(\"UPDATE subs_item SET enable=:enable WHERE id=:id\")\n        suspend fun updateEnable(id: Long, enable: Boolean): Int\n\n        @Query(\"UPDATE subs_item SET `order`=:order WHERE id=:id\")\n        suspend fun updateOrder(id: Long, order: Int): Int\n\n        @Transaction\n        suspend fun batchUpdateOrder(subsItems: List<SubsItem>) {\n            subsItems.forEach { subsItem ->\n                updateOrder(subsItem.id, subsItem.order)\n            }\n        }\n\n        @Insert(onConflict = OnConflictStrategy.REPLACE)\n        suspend fun insert(vararg users: SubsItem): List<Long>\n\n        @Insert(onConflict = OnConflictStrategy.IGNORE)\n        suspend fun insertOrIgnore(vararg users: SubsItem): List<Long>\n\n        @Delete\n        suspend fun delete(vararg users: SubsItem): Int\n\n        @Query(\"UPDATE subs_item SET mtime=:mtime WHERE id=:id\")\n        suspend fun updateMtime(id: Long, mtime: Long = System.currentTimeMillis()): Int\n\n        @Query(\"SELECT * FROM subs_item ORDER BY `order`\")\n        fun query(): Flow<List<SubsItem>>\n\n        @Query(\"SELECT * FROM subs_item ORDER BY `order`\")\n        fun queryAll(): List<SubsItem>\n\n        @Query(\"DELETE FROM subs_item WHERE id IN (:ids)\")\n        suspend fun deleteById(vararg ids: Long): Int\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/SubsVersion.kt",
    "content": "package li.songe.gkd.data\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SubsVersion(val id: Long, val version: Int)\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/TransferData.kt",
    "content": "package li.songe.gkd.data\n\nimport android.net.Uri\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.util.LOCAL_SUBS_IDS\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.UriUtils\nimport li.songe.gkd.util.ZipUtils\nimport li.songe.gkd.util.checkSubsUpdate\nimport li.songe.gkd.util.createGkdTempDir\nimport li.songe.gkd.util.json\nimport li.songe.gkd.util.sharedDir\nimport li.songe.gkd.util.subsItemsFlow\nimport li.songe.gkd.util.subsMapFlow\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateSubscription\nimport java.io.File\n\n@Serializable\nprivate data class TransferData(\n    val type: String = TYPE,\n    val ctime: Long = System.currentTimeMillis(),\n    val subsItems: List<SubsItem> = emptyList(),\n    val subsConfigs: List<SubsConfig> = emptyList(),\n    val categoryConfigs: List<CategoryConfig> = emptyList(),\n    val appConfigs: List<AppConfig> = emptyList()\n) {\n    companion object {\n        const val TYPE = \"transfer_data\"\n    }\n}\n\nprivate const val subsDirName = \"files\"\n\nprivate suspend fun importTransferData(transferData: TransferData): Boolean {\n    val maxOrder = (subsItemsFlow.value.maxOfOrNull { it.order } ?: -1) + 1\n    val subsItems =\n        transferData.subsItems.filter { s -> s.id >= 0 || LOCAL_SUBS_IDS.contains(s.id) }\n            .mapIndexed { i, s ->\n                s.copy(order = maxOrder + i)\n            }\n    val hasNewSubsItem =\n        subsItems.any { newSubs -> newSubs.id >= 0 && subsItemsFlow.value.all { oldSubs -> oldSubs.id != newSubs.id } }\n    DbSet.subsItemDao.insertOrIgnore(*subsItems.toTypedArray())\n    DbSet.subsConfigDao.insertOrIgnore(*transferData.subsConfigs.toTypedArray())\n    DbSet.categoryConfigDao.insertOrIgnore(*transferData.categoryConfigs.toTypedArray())\n    DbSet.appConfigDao.insertOrIgnore(*transferData.appConfigs.toTypedArray())\n    return hasNewSubsItem\n}\n\nsuspend fun exportData(subsIds: Collection<Long>): File {\n    val tempDir = createGkdTempDir()\n    val dataFile = tempDir.resolve(\"${TransferData.TYPE}.json\")\n    dataFile.writeText(\n        json.encodeToString(\n            TransferData(\n                subsItems = subsItemsFlow.value.filter { subsIds.contains(it.id) },\n                subsConfigs = DbSet.subsConfigDao.querySubsItemConfig(subsIds.toList()),\n                categoryConfigs = DbSet.categoryConfigDao.querySubsItemConfig(subsIds.toList()),\n                appConfigs = DbSet.appConfigDao.querySubsItemConfig(subsIds.toList()),\n            )\n        )\n    )\n    val localSubsList = subsMapFlow.value.values.filter {\n        it.id < 0 && subsIds.contains(it.id) && !it.isEmpty\n    }\n    val files = if (localSubsList.isNotEmpty()) {\n        val f = tempDir.resolve(subsDirName).apply { mkdir() }\n        localSubsList.forEach {\n            val file = f.resolve(\"${it.id}.json\")\n            file.writeText(json.encodeToString(it))\n        }\n        f\n    } else {\n        null\n    }\n    val file = sharedDir.resolve(\"backup-${System.currentTimeMillis()}.zip\")\n    ZipUtils.zipFiles(listOfNotNull(dataFile, files), file)\n    tempDir.deleteRecursively()\n    return file\n}\n\nsuspend fun importData(uri: Uri) {\n    val tempDir = createGkdTempDir()\n    val zipFile = tempDir.resolve(\"file.zip\").apply {\n        writeBytes(UriUtils.uri2Bytes(uri))\n    }\n    val unzipDir = tempDir.resolve(\"unzip\").apply {\n        ZipUtils.unzipFile(zipFile, this)\n    }\n    val transferFile = unzipDir.resolve(\"${TransferData.TYPE}.json\")\n    if (!transferFile.exists() || !transferFile.isFile) {\n        toast(\"导入无数据\")\n        tempDir.deleteRecursively()\n        return\n    }\n    val data = withContext(Dispatchers.Default) {\n        json.decodeFromString<TransferData>(transferFile.readText())\n    }\n    val hasNewSubsItem = importTransferData(data)\n    val files = unzipDir.resolve(subsDirName)\n    if (files.exists()) {\n        val subscriptions = (files.listFiles { f -> f.isFile && f.name.endsWith(\".json\") }\n            ?: emptyArray()).mapNotNull { f ->\n            try {\n                RawSubscription.parse(f.readText())\n            } catch (e: Exception) {\n                LogUtils.d(e)\n                null\n            }\n        }\n        subscriptions.forEach { subscription ->\n            if (LOCAL_SUBS_IDS.contains(subscription.id)) {\n                updateSubscription(subscription)\n            }\n        }\n    }\n    toast(\"导入成功\")\n    tempDir.deleteRecursively()\n    if (hasNewSubsItem) {\n        delay(1000)\n        checkSubsUpdate(true)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/UserInfo.kt",
    "content": "package li.songe.gkd.data\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class UserInfo(\n    val id: Int,\n    val name: String,\n)\n\nval otherUserMapFlow = MutableStateFlow(emptyMap<Int, UserInfo>())\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/data/Value.kt",
    "content": "package li.songe.gkd.data\n\ndata class Value<T>( var value: T)\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/db/AppDb.kt",
    "content": "package li.songe.gkd.db\n\nimport androidx.room.AutoMigration\nimport androidx.room.Database\nimport androidx.room.DeleteColumn\nimport androidx.room.RenameColumn\nimport androidx.room.Room\nimport androidx.room.RoomDatabase\nimport androidx.room.TypeConverter\nimport androidx.room.TypeConverters\nimport androidx.room.migration.AutoMigrationSpec\nimport li.songe.gkd.app\nimport li.songe.gkd.data.A11yEventLog\nimport li.songe.gkd.data.ActionLog\nimport li.songe.gkd.data.ActivityLog\nimport li.songe.gkd.data.AppConfig\nimport li.songe.gkd.data.AppVisitLog\nimport li.songe.gkd.data.CategoryConfig\nimport li.songe.gkd.data.Snapshot\nimport li.songe.gkd.data.SubsConfig\nimport li.songe.gkd.data.SubsItem\nimport li.songe.gkd.util.dbFolder\nimport li.songe.gkd.util.json\n\n@Database(\n    version = 14,\n    entities = [\n        SubsItem::class,\n        Snapshot::class,\n        SubsConfig::class,\n        CategoryConfig::class,\n        ActionLog::class,\n        ActivityLog::class,\n        AppConfig::class,\n        AppVisitLog::class,\n        A11yEventLog::class,\n    ],\n    autoMigrations = [\n        AutoMigration(from = 1, to = 2),\n        AutoMigration(from = 2, to = 3),\n        AutoMigration(from = 3, to = 4),\n        AutoMigration(from = 4, to = 5),\n        AutoMigration(from = 5, to = 6),\n        AutoMigration(from = 6, to = 7),\n        AutoMigration(from = 7, to = 8, spec = ActivityLog.ActivityLogV2Spec::class),\n        AutoMigration(from = 8, to = 9, spec = ActionLog.ActionLogSpec::class),\n        AutoMigration(from = 9, to = 10, spec = Migration9To10Spec::class),\n        AutoMigration(from = 10, to = 11, spec = Migration10To11Spec::class),\n        AutoMigration(from = 11, to = 12),\n        AutoMigration(from = 12, to = 13),\n        AutoMigration(from = 13, to = 14),\n    ]\n)\n@TypeConverters(DbConverters::class)\nabstract class AppDb : RoomDatabase() {\n    abstract fun subsItemDao(): SubsItem.SubsItemDao\n    abstract fun snapshotDao(): Snapshot.SnapshotDao\n    abstract fun subsConfigDao(): SubsConfig.SubsConfigDao\n    abstract fun appConfigDao(): AppConfig.AppConfigDao\n    abstract fun categoryConfigDao(): CategoryConfig.CategoryConfigDao\n    abstract fun actionLogDao(): ActionLog.ActionLogDao\n    abstract fun activityLogDao(): ActivityLog.ActivityLogDao\n    abstract fun appVisitLogDao(): AppVisitLog.AppLogDao\n    abstract fun a11yEventLogDao(): A11yEventLog.A11yEventLogDao\n}\n\n@RenameColumn(\n    tableName = \"subs_config\",\n    fromColumnName = \"subs_item_id\",\n    toColumnName = \"subs_id\"\n)\n@RenameColumn(\n    tableName = \"category_config\",\n    fromColumnName = \"subs_item_id\",\n    toColumnName = \"subs_id\"\n)\nclass Migration9To10Spec : AutoMigrationSpec\n\n@DeleteColumn(\n    tableName = \"snapshot\",\n    columnName = \"app_name\"\n)\n@DeleteColumn(\n    tableName = \"snapshot\",\n    columnName = \"app_version_code\"\n)\n@DeleteColumn(\n    tableName = \"snapshot\",\n    columnName = \"app_version_name\"\n)\nclass Migration10To11Spec : AutoMigrationSpec\n\n@Suppress(\"unused\")\nclass DbConverters {\n    @TypeConverter\n    fun fromListStringToString(list: List<String>): String {\n        return json.encodeToString(list)\n    }\n\n    @TypeConverter\n    fun fromStringToList(value: String): List<String> {\n        if (value.isEmpty()) return emptyList()\n        return try {\n            json.decodeFromString(value)\n        } catch (_: Exception) {\n            emptyList()\n        }\n    }\n}\n\nobject DbSet {\n    private val db by lazy {\n        Room.databaseBuilder(\n            app,\n            AppDb::class.java,\n            dbFolder.resolve(\"gkd.db\").absolutePath\n        ).fallbackToDestructiveMigration(false).build()\n    }\n    val subsItemDao get() = db.subsItemDao()\n    val subsConfigDao get() = db.subsConfigDao()\n    val snapshotDao get() = db.snapshotDao()\n    val actionLogDao get() = db.actionLogDao()\n    val categoryConfigDao get() = db.categoryConfigDao()\n    val activityLogDao get() = db.activityLogDao()\n    val appConfigDao get() = db.appConfigDao()\n    val appVisitLogDao get() = db.appVisitLogDao()\n    val a11yEventLogDao get() = db.a11yEventLogDao()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/notif/Notif.kt",
    "content": "package li.songe.gkd.notif\n\nimport android.annotation.SuppressLint\nimport android.app.Notification\nimport android.app.PendingIntent\nimport android.app.Service\nimport android.content.Intent\nimport android.content.pm.ServiceInfo\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.core.app.ServiceCompat\nimport androidx.core.net.toUri\nimport kotlinx.atomicfu.atomic\nimport li.songe.gkd.META\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.app\nimport li.songe.gkd.permission.foregroundServiceSpecialUseState\nimport li.songe.gkd.permission.notificationState\nimport li.songe.gkd.service.ActivityService\nimport li.songe.gkd.service.ButtonService\nimport li.songe.gkd.service.EventService\nimport li.songe.gkd.service.HttpService\nimport li.songe.gkd.service.ScreenshotService\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.componentName\nimport kotlin.reflect.KClass\n\n// 相同的 request code 会导致后续 PendingIntent 失效\nprivate val pendingIntentReqId = atomic(0)\n\ndata class Notif(\n    val channel: NotifChannel = NotifChannel.Default,\n    val id: Int,\n    val smallIcon: Int = R.drawable.ic_status,\n    val title: String,\n    val text: String? = null,\n    val ongoing: Boolean = true,\n    val autoCancel: Boolean = false,\n    val uri: String? = null,\n    val stopService: KClass<out Service>? = null,\n) {\n    private fun toNotification(): Notification {\n        val contextIntent = PendingIntent.getActivity(\n            app,\n            pendingIntentReqId.incrementAndGet(),\n            Intent().apply {\n                component = MainActivity::class.componentName\n                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK\n                data = uri?.toUri()\n            },\n            PendingIntent.FLAG_IMMUTABLE\n        )\n        val notification = NotificationCompat.Builder(app, channel.id)\n            .setSmallIcon(smallIcon)\n            .setContentTitle(title)\n            .setContentText(text)\n            .setContentIntent(contextIntent)\n            .setOngoing(ongoing)\n            .setAutoCancel(autoCancel)\n        if (stopService != null) {\n            val deleteIntent = PendingIntent.getBroadcast(\n                app,\n                pendingIntentReqId.incrementAndGet(),\n                StopServiceReceiver.getIntent(stopService),\n                PendingIntent.FLAG_IMMUTABLE\n            )\n            notification\n                .setDeleteIntent(deleteIntent)\n                .addAction(0, \"停止\", deleteIntent)\n        }\n        return notification.build()\n    }\n\n    fun notifySelf() {\n        if (!notificationState.updateAndGet()) return\n        if (!foregroundServiceSpecialUseState.updateAndGet()) return\n        @SuppressLint(\"MissingPermission\")\n        NotificationManagerCompat.from(app).notify(id, toNotification())\n    }\n\n    context(service: Service)\n    fun notifyService() {\n        if (!notificationState.updateAndGet()) return\n        if (!foregroundServiceSpecialUseState.updateAndGet()) return\n        ServiceCompat.startForeground(\n            service,\n            id,\n            toNotification(),\n            if (AndroidTarget.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST else -1\n        )\n    }\n}\n\nval abNotif by lazy {\n    Notif(\n        id = 100,\n        title = META.appName,\n        text = \"无障碍正在运行\",\n    )\n}\n\nval screenshotNotif = Notif(\n    id = 101,\n    title = \"截屏服务正在运行\",\n    text = \"保存快照时截取屏幕\",\n    uri = \"gkd://page/1\",\n    stopService = ScreenshotService::class,\n)\n\nval buttonNotif = Notif(\n    id = 102,\n    title = \"快照按钮服务正在运行\",\n    text = \"点击按钮捕获快照\",\n    uri = \"gkd://page/1\",\n    stopService = ButtonService::class,\n)\n\nval httpNotif = Notif(\n    id = 103,\n    title = \"HTTP服务正在运行\",\n    uri = \"gkd://page/1\",\n    stopService = HttpService::class,\n)\n\nval exposeNotif = Notif(\n    id = 104,\n    title = \"运行外部调用任务中\",\n    text = \"任务完成后自动关闭\",\n)\n\nval snapshotNotif = Notif(\n    channel = NotifChannel.Snapshot,\n    id = 105,\n    title = \"快照已保存\",\n    ongoing = false,\n    autoCancel = true,\n    uri = \"gkd://page/2\",\n)\n\nval recordNotif = Notif(\n    id = 106,\n    title = \"记录服务正在运行\",\n    uri = \"gkd://page/1\",\n    stopService = ActivityService::class,\n)\n\nval eventNotif = Notif(\n    id = 107,\n    title = \"事件服务正在运行\",\n    uri = \"gkd://page/1\",\n    stopService = EventService::class,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/notif/NotifChannel.kt",
    "content": "package li.songe.gkd.notif\n\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport androidx.core.app.NotificationManagerCompat\nimport li.songe.gkd.META\nimport li.songe.gkd.app\n\nsealed class NotifChannel(\n    val id: String,\n    val name: String? = null,\n    val desc: String? = null,\n) {\n    data object Default : NotifChannel(\n        id = \"0\",\n    )\n\n    data object Snapshot : NotifChannel(\n        id = \"1\",\n        name = \"保存快照通知\",\n    )\n}\n\nfun initChannel() {\n    val channels = arrayOf(NotifChannel.Default, NotifChannel.Snapshot)\n    val manager = NotificationManagerCompat.from(app)\n    // delete old channels\n    manager.notificationChannels.filter { channels.none { c -> c.id == it.id } }.forEach {\n        manager.deleteNotificationChannel(it.id)\n    }\n    // create/update new channels\n    channels.forEach {\n        val channel = NotificationChannel(\n            it.id,\n            it.name ?: META.appName,\n            NotificationManager.IMPORTANCE_LOW\n        ).apply {\n            description = it.desc\n        }\n        manager.createNotificationChannel(channel)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/notif/StopServiceReceiver.kt",
    "content": "package li.songe.gkd.notif\n\nimport android.app.Service\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport androidx.core.content.ContextCompat\nimport li.songe.gkd.META\nimport li.songe.gkd.util.OnSimpleLife\nimport kotlin.reflect.KClass\nimport kotlin.reflect.jvm.jvmName\n\nclass StopServiceReceiver(private val service: Service) : BroadcastReceiver() {\n    override fun onReceive(context: Context?, intent: Intent?) {\n        context ?: return\n        intent ?: return\n        if (intent.action == STOP_ACTION && intent.getStringExtra(STOP_ACTION) == service::class.jvmName) {\n            service.stopSelf()\n        }\n    }\n\n    companion object {\n        private val STOP_ACTION by lazy { META.appId + \".STOP_SERVICE\" }\n\n        fun getIntent(clazz: KClass<out Service>) = Intent().apply {\n            action = STOP_ACTION\n            putExtra(STOP_ACTION, clazz.jvmName)\n            setPackage(META.appId)\n        }\n\n        context(service: T)\n        fun <T> autoRegister() where T : Service, T : OnSimpleLife {\n            val receiver = StopServiceReceiver(service)\n            service.onCreated {\n                ContextCompat.registerReceiver(\n                    service,\n                    receiver,\n                    IntentFilter(STOP_ACTION),\n                    ContextCompat.RECEIVER_NOT_EXPORTED\n                )\n            }\n            service.onDestroyed {\n                service.unregisterReceiver(receiver)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/permission/PermissionDialog.kt",
    "content": "package li.songe.gkd.permission\n\nimport android.app.Activity\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.util.stopCoroutine\n\ndata class AuthReason(\n    val text: () -> String,\n    val confirm: ((Activity) -> Unit)? = null,\n)\n\n@Composable\nfun AuthDialog(authReasonFlow: MutableStateFlow<AuthReason?>) {\n    val authAction = authReasonFlow.collectAsState().value\n    val context = LocalActivity.current as MainActivity\n    if (authAction != null) {\n        AlertDialog(\n            title = {\n                Text(text = \"权限请求\")\n            },\n            text = {\n                Text(text = authAction.text())\n            },\n            onDismissRequest = { authReasonFlow.value = null },\n            confirmButton = {\n                TextButton(onClick = {\n                    authReasonFlow.value = null\n                    authAction.confirm?.invoke(context)\n                }) {\n                    Text(text = \"确认\")\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = { authReasonFlow.value = null }) {\n                    Text(text = \"取消\")\n                }\n            }\n        )\n    }\n}\n\nsealed class PermissionResult {\n    data object Granted : PermissionResult()\n    data class Denied(val doNotAskAgain: Boolean) : PermissionResult()\n}\n\nsuspend fun requiredPermission(\n    context: MainActivity,\n    permissionState: PermissionState\n) {\n    if (permissionState.updateAndGet()) return\n    val result = permissionState.request?.invoke(context)\n    if (result == null) {\n        context.mainVm.authReasonFlow.value = permissionState.reason\n        stopCoroutine()\n    } else if (result is PermissionResult.Denied) {\n        if (result.doNotAskAgain) {\n            context.mainVm.authReasonFlow.value = permissionState.reason\n        }\n        stopCoroutine()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/permission/PermissionState.kt",
    "content": "package li.songe.gkd.permission\n\nimport android.Manifest\nimport android.app.Activity\nimport android.app.AppOpsManager\nimport android.app.AppOpsManagerHidden\nimport android.content.pm.PackageManager\nimport android.provider.Settings\nimport com.hjq.permissions.XXPermissions\nimport com.hjq.permissions.permission.PermissionLists\nimport com.hjq.permissions.permission.base.IPermission\nimport kotlinx.coroutines.CompletableDeferred\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.updateAndGet\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.MainViewModel\nimport li.songe.gkd.app\nimport li.songe.gkd.appScope\nimport li.songe.gkd.shizuku.SafeAppOpsService\nimport li.songe.gkd.shizuku.SafePackageManager\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.ui.AppOpsAllowRoute\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateAllAppInfo\nimport li.songe.gkd.util.updateAppMutex\nimport rikka.shizuku.Shizuku\n\nclass PermissionState(\n    val name: String,\n    val check: () -> Boolean,\n    val request: (suspend (context: MainActivity) -> PermissionResult)? = null,\n    /**\n     * show it when user doNotAskAgain\n     */\n    val reason: AuthReason? = null,\n) {\n    val stateFlow = MutableStateFlow(false)\n    val value get() = stateFlow.value\n\n    fun updateAndGet(): Boolean {\n        return stateFlow.updateAndGet { check() }\n    }\n\n    fun updateChanged(): Boolean {\n        return value != updateAndGet()\n    }\n\n    fun checkOrToast(): Boolean = if (!updateAndGet()) {\n        val r = updateAndGet()\n        if (!r) {\n            reason?.text?.let { toast(it()) }\n        }\n        r\n    } else {\n        true\n    }\n}\n\nprivate suspend fun asyncRequestPermission(\n    context: Activity,\n    permission: IPermission,\n): PermissionResult {\n    if (XXPermissions.isGrantedPermission(context, permission)) {\n        return PermissionResult.Granted\n    }\n    val deferred = CompletableDeferred<PermissionResult>()\n    XXPermissions.with(context)\n        .unchecked()\n        .permission(permission)\n        .request { grantedList, _ ->\n            if (grantedList.contains(permission)) {\n                PermissionResult.Granted\n            } else {\n                PermissionResult.Denied(\n                    XXPermissions.isDoNotAskAgainPermissions(\n                        context,\n                        arrayOf(permission)\n                    )\n                )\n            }.let { deferred.complete(it) }\n        }\n    return deferred.await()\n}\n\nprivate fun checkAllowedOp(op: String): Boolean = app.appOpsManager.checkOpNoThrow(\n    op,\n    android.os.Process.myUid(),\n    app.packageName\n).let {\n    it != AppOpsManager.MODE_IGNORED && it != AppOpsManager.MODE_ERRORED\n}\n\n// https://github.com/gkd-kit/gkd/issues/954\n// https://github.com/gkd-kit/gkd/issues/887\nval foregroundServiceSpecialUseState by lazy {\n    PermissionState(\n        name = \"特殊用途的前台服务\",\n        check = {\n            if (AndroidTarget.UPSIDE_DOWN_CAKE) {\n                checkAllowedOp(AppOpsManagerHidden.OPSTR_FOREGROUND_SERVICE_SPECIAL_USE)\n            } else {\n                true\n            }\n        },\n        reason = AuthReason(\n            text = { \"当前操作权限「特殊用途的前台服务」已被限制, 请先解除限制\" },\n            confirm = {\n                MainViewModel.instance.navigatePage(AppOpsAllowRoute)\n            },\n        ),\n    )\n}\n\n// https://github.com/orgs/gkd-kit/discussions/1234\nval accessA11yState by lazy {\n    PermissionState(\n        name = \"访问无障碍\",\n        check = {\n            if (AndroidTarget.Q) {\n                checkAllowedOp(AppOpsManagerHidden.OPSTR_ACCESS_ACCESSIBILITY)\n            } else {\n                true\n            }\n        },\n    )\n}\n\nval createA11yOverlayState by lazy {\n    PermissionState(\n        name = \"创建无障碍悬浮窗\",\n        check = {\n            if (SafeAppOpsService.supportCreateA11yOverlay) {\n                checkAllowedOp(AppOpsManagerHidden.OPSTR_CREATE_ACCESSIBILITY_OVERLAY)\n            } else {\n                true\n            }\n        },\n    )\n}\n\nconst val Manifest_permission_GET_APP_OPS_STATS = \"android.permission.GET_APP_OPS_STATS\"\n\nval getAppOpsStatsState by lazy {\n    PermissionState(\n        name = \"获取应用权限状态\",\n        check = {\n            app.checkGrantedPermission(Manifest_permission_GET_APP_OPS_STATS)\n        },\n    )\n}\n\nprivate var canRestrictsRead = true\nval accessRestrictedSettingsState by lazy {\n    PermissionState(\n        name = \"访问受限设置\",\n        check = {\n            if (canRestrictsRead && AndroidTarget.UPSIDE_DOWN_CAKE && getAppOpsStatsState.updateAndGet()) {\n                try {\n                    // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r55:frameworks/base/services/core/java/com/android/server/appop/AppOpsService.java;l=4237\n                    checkAllowedOp(AppOpsManagerHidden.OPSTR_ACCESS_RESTRICTED_SETTINGS)\n                } catch (_: SecurityException) {\n                    // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r54:frameworks/base/services/core/java/com/android/server/appop/AppOpsService.java;l=4227\n                    canRestrictsRead = false\n                    true\n                }\n            } else {\n                true\n            }\n        },\n    )\n}\n\nval appOpsRestrictStateList by lazy {\n    arrayOf(\n        accessA11yState,\n        createA11yOverlayState,\n        accessRestrictedSettingsState,\n        foregroundServiceSpecialUseState,\n    )\n}\n\nval appOpsRestrictedFlow by lazy {\n    combine(\n        *appOpsRestrictStateList.map { it.stateFlow }.toTypedArray(),\n    ) { list ->\n        list.any { !it }\n    }.stateIn(appScope, SharingStarted.Eagerly, false)\n}\n\nval notificationState by lazy {\n    val permission = PermissionLists.getNotificationServicePermission()\n    PermissionState(\n        name = \"通知权限\",\n        check = {\n            XXPermissions.isGrantedPermission(app, permission)\n        },\n        request = { asyncRequestPermission(it, permission) },\n        reason = AuthReason(\n            text = { \"当前操作需要「通知权限」\\n请先前往权限页面授权\" },\n            confirm = {\n                XXPermissions.startPermissionActivity(app, permission)\n            }\n        ),\n    )\n}\n\nval canQueryPkgState by lazy {\n    val permission = PermissionLists.getGetInstalledAppsPermission()\n    val supported by lazy { permission.isSupportRequestPermission(app) }\n    PermissionState(\n        name = \"读取应用列表权限\",\n        check = {\n            if (supported) {\n                // 此框架内部有两个 printStackTrace 导致每次检测都会打印日志污染控制台\n                XXPermissions.isGrantedPermission(app, permission)\n            } else {\n                true\n            }\n        },\n        request = {\n            asyncRequestPermission(it, permission)\n        },\n        reason = AuthReason(\n            text = { \"当前操作需要「读取应用列表权限」\\n请先前往权限页面授权\" },\n            confirm = {\n                XXPermissions.startPermissionActivity(app, permission)\n            }\n        ),\n    )\n}\n\nval canDrawOverlaysState by lazy {\n    PermissionState(\n        name = \"悬浮窗权限\",\n        check = {\n            // https://developer.android.com/security/fraud-prevention/activities?hl=zh-cn#hide_overlay_windows\n            Settings.canDrawOverlays(app)\n        },\n        reason = AuthReason(\n            text = {\n                \"当前操作需要「悬浮窗权限」\\n请先前往权限页面授权\"\n            },\n            confirm = {\n                XXPermissions.startPermissionActivity(\n                    app,\n                    PermissionLists.getSystemAlertWindowPermission()\n                )\n            }\n        ),\n    )\n}\n\nval canWriteExternalStorage by lazy {\n    PermissionState(\n        name = \"写入外部存储权限\",\n        check = {\n            if (AndroidTarget.Q) {\n                true\n            } else {\n                app.checkGrantedPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)\n            }\n        },\n        request = {\n            if (AndroidTarget.Q) {\n                PermissionResult.Granted\n            } else {\n                asyncRequestPermission(it, PermissionLists.getWriteExternalStoragePermission())\n            }\n        },\n        reason = AuthReason(\n            text = { \"当前操作需要「写入外部存储权限」\\n请先前往权限页面授权\" },\n            confirm = {\n                XXPermissions.startPermissionActivity(\n                    app,\n                    PermissionLists.getWriteExternalStoragePermission()\n                )\n            }\n        ),\n    )\n}\n\nval ignoreBatteryOptimizationsState by lazy {\n    val permission = PermissionLists.getRequestIgnoreBatteryOptimizationsPermission()\n    PermissionState(\n        name = \"忽略电池优化权限\",\n        check = {\n            app.powerManager.isIgnoringBatteryOptimizations(app.packageName)\n        },\n        request = {\n            asyncRequestPermission(it, permission)\n        },\n        reason = AuthReason(\n            text = { \"当前操作需要「忽略电池优化权限」\\n请先前往权限页面授权\" },\n            confirm = {\n                XXPermissions.startPermissionActivity(\n                    app,\n                    permission\n                )\n            }\n        ),\n    )\n}\n\nval writeSecureSettingsState by lazy {\n    PermissionState(\n        name = \"写入安全设置权限\",\n        check = { app.checkGrantedPermission(Manifest.permission.WRITE_SECURE_SETTINGS) },\n    )\n}\n\nprivate fun shizukuCheckGranted(): Boolean {\n    if (Shizuku.getBinder()?.isBinderAlive != true) return false\n    val granted = try {\n        Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED\n    } catch (_: Throwable) {\n        false\n    }\n    if (!granted) return false\n    val u = shizukuContextFlow.value.packageManager ?: SafePackageManager.newBinder()\n    return u?.isSafeMode != null\n}\n\nval shizukuGrantedState by lazy {\n    PermissionState(\n        name = \"Shizuku 权限\",\n        check = { shizukuCheckGranted() },\n    )\n}\n\nval allPermissionStates by lazy {\n    listOf(\n        notificationState,\n        foregroundServiceSpecialUseState,\n        accessA11yState,\n        createA11yOverlayState,\n        getAppOpsStatsState,\n        accessRestrictedSettingsState,\n        canDrawOverlaysState,\n        canWriteExternalStorage,\n        ignoreBatteryOptimizationsState,\n        writeSecureSettingsState,\n        canQueryPkgState,\n        shizukuGrantedState,\n    )\n}\n\nfun updatePermissionState() {\n    allPermissionStates.forEach {\n        if (it === canQueryPkgState && !updateAppMutex.mutex.isLocked) {\n            if (canQueryPkgState.updateChanged()) {\n                updateAllAppInfo()\n            }\n        } else {\n            it.updateAndGet()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/A11yService.kt",
    "content": "package li.songe.gkd.service\n\nimport android.accessibilityservice.AccessibilityService\nimport android.annotation.SuppressLint\nimport android.content.Context.WINDOW_SERVICE\nimport android.graphics.Bitmap\nimport android.graphics.PixelFormat\nimport android.view.Display\nimport android.view.Gravity\nimport android.view.View\nimport android.view.WindowManager\nimport android.view.accessibility.AccessibilityEvent\nimport android.view.accessibility.AccessibilityNodeInfo\nimport android.view.accessibility.AccessibilityWindowInfo\nimport com.google.android.accessibility.selecttospeak.SelectToSpeakService\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.a11y.A11yCommonImpl\nimport li.songe.gkd.a11y.A11yRuleEngine\nimport li.songe.gkd.a11y.topActivityFlow\nimport li.songe.gkd.a11y.updateTopActivity\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.store.updateEnableAutomator\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.AutomatorModeOption\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.OnA11yLife\nimport li.songe.gkd.util.componentName\nimport li.songe.gkd.util.runMainPost\nimport li.songe.gkd.util.toast\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\n\n@SuppressLint(\"AccessibilityPolicy\")\nopen class A11yService : AccessibilityService(), OnA11yLife, A11yCommonImpl {\n    override val mode get() = AutomatorModeOption.A11yMode\n    override val scope = useScope()\n    override val windowNodeInfo: AccessibilityNodeInfo? get() = rootInActiveWindow\n    override val windowInfos: List<AccessibilityWindowInfo> get() = windows\n    override suspend fun screenshot(): Bitmap? = suspendCoroutine { continuation ->\n        if (AndroidTarget.R) {\n            takeScreenshot(\n                Display.DEFAULT_DISPLAY,\n                application.mainExecutor,\n                object : TakeScreenshotCallback {\n                    override fun onFailure(errorCode: Int) = continuation.resume(null)\n                    override fun onSuccess(screenshot: ScreenshotResult) = try {\n                        continuation.resume(\n                            Bitmap.wrapHardwareBuffer(\n                                screenshot.hardwareBuffer, screenshot.colorSpace\n                            )\n                        )\n                    } finally {\n                        screenshot.hardwareBuffer.close()\n                    }\n                }\n            )\n        } else {\n            continuation.resume(null)\n        }\n    }\n\n    override val ruleEngine by lazy { A11yRuleEngine(this) }\n\n    override fun onCreate() = onCreated()\n    override fun onServiceConnected() = onA11yConnected()\n    override fun onInterrupt() {}\n    override fun onDestroy() = onDestroyed()\n    override fun onAccessibilityEvent(event: AccessibilityEvent?) = ruleEngine.onA11yEvent(event)\n\n    val startTime = System.currentTimeMillis()\n    override var justStarted: Boolean = true\n        get() {\n            if (field) {\n                field = System.currentTimeMillis() - startTime < 3_000\n            }\n            return field\n        }\n\n    private var tempShutdownFlag = false\n\n    override fun shutdown(temp: Boolean) {\n        if (temp) {\n            tempShutdownFlag = true\n        }\n        disableSelf()\n    }\n\n    private var destroyed = false\n    private var connected = false\n\n    val wm by lazy { getSystemService(WINDOW_SERVICE) as WindowManager }\n\n    init {\n        useLogLifecycle()\n        useAliveFlow(isRunning)\n        onA11yConnected { instance = this }\n        onDestroyed { instance = null }\n        onCreated {\n            if (currentAppUseA11y) {\n                updateEnableAutomator(true)\n            } else {\n                toast(\"当前为自动化模式，无障碍将自动关闭\", forced = true)\n                runMainPost(1) { shutdown(true) }\n            }\n        }\n        onDestroyed {\n            if (tempShutdownFlag) {\n                toast(\"无障碍局部关闭\")\n            } else {\n                toast(\"无障碍已关闭\")\n                updateEnableAutomator(false)\n            }\n        }\n        useAliveOverlayView()\n        onCreated { StatusService.autoStart() }\n        onDestroyed {\n            shizukuContextFlow.value.topCpn()?.let { cpn ->\n                // com.android.systemui\n                if (!topActivityFlow.value.sameAs(cpn.packageName, cpn.className)) {\n                    updateTopActivity(cpn.packageName, cpn.className)\n                }\n            }\n        }\n        onDestroyed { destroyed = true }\n        onA11yConnected {\n            connected = true\n            toast(\"无障碍已启动\")\n            if (currentAppUseA11y) {\n                ruleEngine.onA11yConnected()\n            }\n        }\n        onCreated {\n            runMainPost(3000) {\n                if (!(destroyed || connected)) {\n                    toast(\"无障碍启动超时，请尝试关闭重启\", forced = true)\n                }\n            }\n        }\n    }\n\n    companion object {\n        val a11yCn by lazy { SelectToSpeakService::class.componentName }\n        val isRunning = MutableStateFlow(false)\n\n        @Volatile\n        var instance: A11yService? = null\n            private set\n    }\n}\n\nprivate fun A11yService.useAliveOverlayView() {\n    val context = this\n    var aliveView: View? = null\n    val wm by lazy { getSystemService(WINDOW_SERVICE) as WindowManager }\n    fun removeA11View() {\n        if (aliveView != null) {\n            wm.removeView(aliveView)\n            aliveView = null\n        }\n    }\n\n    fun addA11View() {\n        removeA11View()\n        val tempView = View(context)\n        val lp = WindowManager.LayoutParams().apply {\n            type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY\n            format = PixelFormat.TRANSLUCENT\n            flags =\n                flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE\n            gravity = Gravity.START or Gravity.TOP\n            width = 1\n            height = 1\n            packageName = context.packageName\n        }\n        try {\n            // 某些设备 android.view.WindowManager$BadTokenException\n            wm.addView(tempView, lp)\n            aliveView = tempView\n        } catch (e: Throwable) {\n            aliveView = null\n            LogUtils.d(e)\n            toast(\"添加无障碍保活失败\\n请尝试重启无障碍\")\n        }\n    }\n    onA11yConnected { addA11View() }\n    onDestroyed { removeA11View() }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/ActivityService.kt",
    "content": "package li.songe.gkd.service\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.contentColorFor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.zIndex\nimport androidx.lifecycle.lifecycleScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.a11y.ActivityScene\nimport li.songe.gkd.a11y.topActivityFlow\nimport li.songe.gkd.a11y.updateTopActivity\nimport li.songe.gkd.notif.StopServiceReceiver\nimport li.songe.gkd.notif.recordNotif\nimport li.songe.gkd.permission.canDrawOverlaysState\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.style.iconTextSize\nimport li.songe.gkd.util.copyText\nimport li.songe.gkd.util.startForegroundServiceByClass\nimport li.songe.gkd.util.stopServiceByClass\n\n\nclass ActivityService : OverlayWindowService(\n    positionKey = \"activity\"\n) {\n    val activityOkFlow by lazy {\n        combine(A11yService.isRunning, shizukuContextFlow) { a, b ->\n            a || b.ok\n        }.stateIn(scope = lifecycleScope, started = SharingStarted.Eagerly, initialValue = false)\n    }\n\n    @Composable\n    override fun ComposeContent() {\n        val bgColor = MaterialTheme.colorScheme.surface\n        Column(\n            modifier = Modifier\n                .clip(MaterialTheme.shapes.small)\n                .background(bgColor.copy(alpha = 0.9f))\n                .width(IntrinsicSize.Max)\n                .padding(4.dp)\n        ) {\n            CompositionLocalProvider(LocalContentColor provides contentColorFor(bgColor)) {\n                val topActivity by topActivityFlow.collectAsState()\n                val hasAuth by activityOkFlow.collectAsState()\n                ClosableTitle(\n                    title = if (hasAuth) \"记录服务\" else \"记录服务(无权限)\"\n                )\n                if (hasAuth) {\n                    Box {\n                        Column(\n                            modifier = Modifier.padding(start = 4.dp)\n                        ) {\n                            RowText(text = topActivity.appId)\n                            RowText(\n                                text = topActivity.shortActivityId,\n                                color = MaterialTheme.colorScheme.secondary\n                            )\n                        }\n                        if (topActivity.number > 0) {\n                            Text(\n                                text = topActivity.number.toString(),\n                                style = MaterialTheme.typography.labelMedium,\n                                color = MaterialTheme.colorScheme.tertiary,\n                                modifier = Modifier\n                                    .align(Alignment.TopEnd)\n                                    .zIndex(1f)\n                                    .clip(MaterialTheme.shapes.extraSmall)\n                                    .padding(end = 4.dp),\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    init {\n        useLogLifecycle()\n        useAliveFlow(isRunning)\n        useAliveToast(\"记录服务\")\n        StopServiceReceiver.autoRegister()\n        onCreated { recordNotif.notifyService() }\n        onCreated {\n            lifecycleScope.launch {\n                topActivityFlow.collect {\n                    recordNotif.copy(text = it.format()).notifyService()\n                }\n            }\n            if (!A11yService.isRunning.value) {\n                shizukuContextFlow.value.topCpn()?.let { cpn ->\n                    updateTopActivity(\n                        appId = cpn.packageName,\n                        activityId = cpn.className,\n                        scene = ActivityScene.TaskStack,\n                    )\n                }\n            }\n        }\n    }\n\n    companion object {\n        val isRunning = MutableStateFlow(false)\n        fun start() {\n            if (!canDrawOverlaysState.checkOrToast()) return\n            startForegroundServiceByClass(ActivityService::class)\n        }\n\n        fun stop() = stopServiceByClass(ActivityService::class)\n    }\n}\n\n@Composable\nprivate fun RowText(text: String?, color: Color = Color.Unspecified) {\n    Row {\n        Text(text = text ?: \"null\", color = color, modifier = Modifier.weight(1f, false))\n        if (text != null) {\n            Spacer(modifier = Modifier.width(4.dp))\n            PerfIcon(\n                imageVector = PerfIcon.ContentCopy,\n                modifier = Modifier\n                    .clip(MaterialTheme.shapes.extraSmall)\n                    .clickable(onClick = {\n                        copyText(text)\n                    })\n                    .iconTextSize(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/ActivityTileService.kt",
    "content": "package li.songe.gkd.service\n\nclass ActivityTileService : BaseTileService() {\n    override val activeFlow = ActivityService.isRunning\n\n    init {\n        onTileClicked {\n            if (ActivityService.isRunning.value) {\n                ActivityService.stop()\n            } else {\n                ActivityService.start()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/BaseTileService.kt",
    "content": "package li.songe.gkd.service\n\nimport android.service.quicksettings.Tile\nimport android.service.quicksettings.TileService\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.util.OnTileLife\n\nabstract class BaseTileService : TileService(), OnTileLife {\n    override fun onCreate() = onCreated()\n    override fun onStartListening() = onStartListened()\n    override fun onClick() = onTileClicked()\n    override fun onStopListening() = onStopListened()\n    override fun onDestroy() = onDestroyed()\n\n    abstract val activeFlow: StateFlow<Boolean>\n\n    override val scope = useScope()\n    val listeningFlow = MutableStateFlow(false).apply {\n        onStartListened { value = true }\n        onStopListened { value = false }\n    }\n\n    init {\n        onStartListened {\n            val t = System.currentTimeMillis()\n            if (t - lastA11yFixTime > 3_000L) {\n                lastA11yFixTime = t\n                fixRestartAutomatorService()\n            }\n        }\n        onTileClicked { StatusService.autoStart() }\n        scope.launch {\n            combine(\n                activeFlow,\n                listeningFlow\n            ) { v1, v2 -> v1 to v2 }.collect { (active, listening) ->\n                if (listening) {\n                    qsTile.state = if (active) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE\n                    qsTile.updateTile()\n                }\n            }\n        }\n    }\n}\n\nprivate var lastA11yFixTime = 0L\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/ButtonService.kt",
    "content": "package li.songe.gkd.service\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.appScope\nimport li.songe.gkd.notif.StopServiceReceiver\nimport li.songe.gkd.notif.buttonNotif\nimport li.songe.gkd.permission.canDrawOverlaysState\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.util.SnapshotExt\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.startForegroundServiceByClass\nimport li.songe.gkd.util.stopServiceByClass\n\nclass ButtonService : OverlayWindowService(\n    positionKey = \"button\"\n) {\n    override fun onClickView() = appScope.launchTry {\n        SnapshotExt.captureSnapshot()\n    }.let { }\n\n    override fun onLongClickView() = stopSelf()\n\n    @Composable\n    override fun ComposeContent() {\n        val alpha = 0.75f\n        PerfIcon(\n            imageVector = PerfIcon.CenterFocusWeak,\n            modifier = Modifier\n                .clip(MaterialTheme.shapes.small)\n                .background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = alpha))\n                .size(40.dp),\n            tint = MaterialTheme.colorScheme.primary.copy(alpha = alpha),\n        )\n    }\n\n    init {\n        useAliveFlow(isRunning)\n        useAliveToast(\"快照按钮服务\")\n        onCreated { buttonNotif.notifyService() }\n        StopServiceReceiver.autoRegister()\n    }\n\n    companion object {\n        val isRunning = MutableStateFlow(false)\n        fun start() {\n            if (!canDrawOverlaysState.checkOrToast()) return\n            startForegroundServiceByClass(ButtonService::class)\n        }\n\n        fun stop() = stopServiceByClass(ButtonService::class)\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/ButtonTileService.kt",
    "content": "package li.songe.gkd.service\n\nclass ButtonTileService : BaseTileService() {\n    override val activeFlow = ButtonService.isRunning\n\n    init {\n        onTileClicked {\n            if (ButtonService.isRunning.value) {\n                ButtonService.stop()\n            } else {\n                ButtonService.start()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/EventService.kt",
    "content": "package li.songe.gkd.service\n\nimport android.view.accessibility.AccessibilityEvent\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.contentColorFor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.getAndUpdate\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.META\nimport li.songe.gkd.appScope\nimport li.songe.gkd.data.A11yEventLog\nimport li.songe.gkd.data.toA11yEventLog\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.notif.StopServiceReceiver\nimport li.songe.gkd.notif.eventNotif\nimport li.songe.gkd.permission.canDrawOverlaysState\nimport li.songe.gkd.ui.EventLogCard\nimport li.songe.gkd.ui.component.LocalNumberCharWidth\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.isAtBottom\nimport li.songe.gkd.ui.component.measureNumberTextWidth\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.startForegroundServiceByClass\nimport li.songe.gkd.util.stopServiceByClass\n\nclass EventService : OverlayWindowService(positionKey = \"event\") {\n\n    val eventLogs = mutableStateListOf<A11yEventLog>()\n    private var tempEventId = 0\n    private var firstToBottom = false\n\n    @Composable\n    override fun ComposeContent() {\n        val bgColor = MaterialTheme.colorScheme.surface\n        CompositionLocalProvider(\n            LocalContentColor provides contentColorFor(bgColor),\n        ) {\n            val listState = key(eventLogs.isEmpty()) { rememberLazyListState() }\n            val isAtBottom by listState.isAtBottom()\n            val subScope = rememberCoroutineScope()\n            SideEffect {\n                val latestId = eventLogs.lastOrNull()?.id ?: 0\n                if (tempEventId != latestId) {\n                    tempEventId = latestId\n                    if (isAtBottom) {\n                        subScope.launch { listState.scrollToItem(eventLogs.lastIndex) }\n                    }\n                }\n            }\n            Column(\n                modifier = Modifier\n                    .clip(MaterialTheme.shapes.small)\n                    .background(bgColor.copy(alpha = 0.9f))\n                    .width(256.dp)\n                    .padding(4.dp)\n            ) {\n                ClosableTitle(\n                    title = if (A11yService.isRunning.collectAsState().value) \"事件服务\" else \"事件服务(无权限)\"\n                )\n                val textStyle = MaterialTheme.typography.labelSmall\n                val numCharWidth = measureNumberTextWidth(textStyle)\n                CompositionLocalProvider(\n                    LocalTextStyle provides textStyle,\n                    LocalNumberCharWidth provides numCharWidth,\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .height(300.dp)\n                    ) {\n                        LazyColumn(\n                            modifier = Modifier.fillMaxSize(),\n                            state = listState,\n                            verticalArrangement = Arrangement.spacedBy(4.dp),\n                        ) {\n                            items(eventLogs, { it.id }) {\n                                EventLogCard(\n                                    eventLog = it,\n                                    modifier = Modifier.padding(horizontal = 2.dp)\n                                )\n                            }\n                            item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                                Spacer(modifier = Modifier.height(2.dp))\n                            }\n                        }\n                        if (eventLogs.isNotEmpty() && !isAtBottom) {\n                            if (!firstToBottom) {\n                                firstToBottom = true\n                                SideEffect {\n                                    subScope.launch { listState.scrollToItem(eventLogs.lastIndex) }\n                                }\n                            }\n                            var count by remember { mutableIntStateOf(-1) }\n                            LaunchedEffect(eventLogs.last().id) { count++ }\n                            Column(\n                                modifier = Modifier\n                                    .align(Alignment.BottomEnd)\n                                    .width(IntrinsicSize.Min),\n                                horizontalAlignment = Alignment.CenterHorizontally,\n                            ) {\n                                if (count > 0) {\n                                    Text(text = \"+$count\")\n                                }\n                                PerfIconButton(\n                                    imageVector = PerfIcon.ArrowDownward,\n                                    onClick = {\n                                        subScope.launch {\n                                            listState.scrollToItem(eventLogs.lastIndex)\n                                        }\n                                    },\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    val tempEventListFlow = MutableStateFlow(emptyList<A11yEventLog>()).apply {\n        appScope.launch {\n            while (scope.isActive) {\n                delay(1000)\n                val list = getAndUpdate { emptyList() }\n                if (list.isNotEmpty()) {\n                    DbSet.a11yEventLogDao.insert(list)\n                }\n            }\n        }\n    }\n\n    init {\n        logAutoId = 0\n        instance = this\n        onDestroyed {\n            instance = null\n            logAutoId = 0\n        }\n        scope.launch {\n            logAutoId = (DbSet.a11yEventLogDao.maxId() ?: 0).coerceAtLeast(1)\n        }\n\n        useLogLifecycle()\n        useAliveFlow(isRunning)\n        useAliveToast(\"事件服务\")\n        StopServiceReceiver.autoRegister()\n        onCreated { eventNotif.notifyService() }\n    }\n\n    companion object {\n        private var instance: EventService? = null\n        private var logAutoId = 0\n\n        fun logEvent(event: AccessibilityEvent) {\n            val service = instance ?: return\n            if (event.packageName == META.appId) return\n            if (logAutoId == 0) return\n            logAutoId++\n            val eventLog = event.toA11yEventLog(logAutoId)\n            service.eventLogs.add(eventLog)\n            service.tempEventListFlow.update { it + eventLog }\n            if (service.eventLogs.size >= 256) {\n                service.eventLogs.removeRange(0, 64)\n            }\n            if (eventLog.id % 100 == 0) {\n                appScope.launchTry { DbSet.a11yEventLogDao.deleteKeepLatest() }\n            }\n        }\n\n        val isRunning = MutableStateFlow(false)\n        fun start() {\n            if (!canDrawOverlaysState.checkOrToast()) return\n            startForegroundServiceByClass(EventService::class)\n        }\n\n        fun stop() = stopServiceByClass(EventService::class)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/EventTileService.kt",
    "content": "package li.songe.gkd.service\n\nclass EventTileService : BaseTileService() {\n    override val activeFlow = EventService.isRunning\n\n    init {\n        onTileClicked {\n            if (EventService.isRunning.value) {\n                EventService.stop()\n            } else {\n                EventService.start()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/ExposeService.kt",
    "content": "package li.songe.gkd.service\n\nimport android.app.Service\nimport android.content.Intent\nimport android.os.Binder\nimport li.songe.gkd.app\nimport li.songe.gkd.appScope\nimport li.songe.gkd.notif.exposeNotif\nimport li.songe.gkd.syncFixState\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.SnapshotExt\nimport li.songe.gkd.util.componentName\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.runMainPost\nimport li.songe.gkd.util.shFolder\nimport li.songe.gkd.util.toast\n\nclass ExposeService : Service() {\n    override fun onBind(intent: Intent?): Binder? = null\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        appScope.launchTry {\n            try {\n                handleIntent(intent)\n            } finally {\n                runMainPost(1000) { stopSelf() }\n            }\n        }\n        return super.onStartCommand(intent, flags, startId)\n    }\n\n    suspend fun handleIntent(intent: Intent?) {\n        val expose = intent?.getIntExtra(\"expose\", 0) ?: 0\n        val data = intent?.getStringExtra(\"data\")\n        LogUtils.d(\"ExposeService::handleIntent\", expose, data)\n        when (expose) {\n            -1 -> StatusService.autoStart()\n            0 -> SnapshotExt.captureSnapshot()\n            1 -> {\n                toast(\"执行成功\", forced = true)\n                syncFixState()\n            }\n\n            else -> {\n                toast(\"未知调用: expose=$expose data=$data\", forced = true)\n            }\n        }\n    }\n\n    override fun onCreate() {\n        super.onCreate()\n        exposeNotif.notifyService()\n    }\n\n    companion object {\n        fun initCommandFile() {\n            val commandText = template\n                .replace(\"{service}\", ExposeService::class.componentName.flattenToShortString())\n            shFolder.resolve(\"expose.sh\").writeText(commandText)\n        }\n\n        fun exposeIntent(expose: Int, data: String? = null): Intent {\n            return Intent(app, ExposeService::class.java).apply {\n                putExtra(\"expose\", expose)\n                if (data != null) {\n                    putExtra(\"data\", data)\n                }\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n            }\n        }\n    }\n}\n\nprivate const val template = $$\"\"\"set -euo pipefail\necho '> start expose.sh'\np=''\nif [ -n \"${1:-}\" ]; then\n  p+=\" --ei expose $1\"\nfi\nif [ -n \"${2:-}\" ]; then\n  p+=\" --es data $2\"\nfi\nam start-foreground-service -n {service} $p\necho '> expose.sh end'\n\"\"\""
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/GkdTileService.kt",
    "content": "package li.songe.gkd.service\n\nimport android.provider.Settings\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport li.songe.gkd.META\nimport li.songe.gkd.a11y.systemRecentCn\nimport li.songe.gkd.a11y.topActivityFlow\nimport li.songe.gkd.accessRestrictedSettingsShowFlow\nimport li.songe.gkd.app\nimport li.songe.gkd.appScope\nimport li.songe.gkd.isActivityVisible\nimport li.songe.gkd.permission.writeSecureSettingsState\nimport li.songe.gkd.shizuku.AutomationService\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.shizuku.uiAutomationFlow\nimport li.songe.gkd.store.actualA11yScopeAppList\nimport li.songe.gkd.store.actualBlockA11yAppList\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.mapState\nimport li.songe.gkd.util.runMainPost\nimport li.songe.gkd.util.toast\n\nclass GkdTileService : BaseTileService() {\n    override val activeFlow = combine(A11yService.isRunning, uiAutomationFlow) { a11y, automator ->\n        a11y || automator != null\n    }.stateIn(scope, SharingStarted.Eagerly, false)\n\n    init {\n        onTileClicked { switchAutomatorService() }\n    }\n}\n\nprivate val modifyA11yMutex = Mutex()\nprivate const val A11Y_AWAIT_START_TIME = 2000L\nprivate const val A11Y_AWAIT_FIX_TIME = 1000L\n\nprivate fun modifyA11yRun(block: suspend () -> Unit) {\n    if (modifyA11yMutex.isLocked) return\n    appScope.launchTry(Dispatchers.IO) {\n        if (modifyA11yMutex.isLocked) return@launchTry\n        modifyA11yMutex.withLock { block() }\n    }\n}\n\nprivate suspend fun switchA11yService() {\n    if (A11yService.isRunning.value) {\n        A11yService.instance?.disableSelf()\n    } else {\n        if (!writeSecureSettingsState.updateAndGet()) {\n            if (!writeSecureSettingsState.value) {\n                toast(\"请先授予「写入安全设置权限」\")\n                return\n            }\n        }\n        val names = app.getSecureA11yServices()\n        app.putSecureInt(Settings.Secure.ACCESSIBILITY_ENABLED, 1)\n        if (names.contains(A11yService.a11yCn)) { // 当前无障碍异常, 重启服务\n            names.remove(A11yService.a11yCn)\n            app.putSecureA11yServices(names)\n            delay(A11Y_AWAIT_FIX_TIME)\n        }\n        names.add(A11yService.a11yCn)\n        app.putSecureA11yServices(names)\n        delay(A11Y_AWAIT_START_TIME)\n        // https://github.com/orgs/gkd-kit/discussions/799\n        if (!A11yService.isRunning.value) {\n            toast(\"开启无障碍失败\")\n            accessRestrictedSettingsShowFlow.value = true\n            return\n        }\n    }\n}\n\nprivate fun switchAutomationService() {\n    val newEnabled = uiAutomationFlow.value == null\n    uiAutomationFlow.value?.shutdown()\n    if (newEnabled && shizukuContextFlow.value.ok) {\n        AutomationService.tryConnect()\n    }\n}\n\nfun switchAutomatorService() = modifyA11yRun {\n    if (currentAppUseA11y) {\n        switchA11yService()\n    } else {\n        switchAutomationService()\n    }\n}\n\nprivate fun skipBlockApp(): Boolean {\n    if (storeFlow.value.enableBlockA11yAppList) {\n        val topAppId = if (isActivityVisible || app.justStarted) {\n            META.appId\n        } else {\n            shizukuContextFlow.value.topCpn()?.packageName\n        }\n        if (topAppId != null && topAppId in actualBlockA11yAppList) {\n            return true\n        }\n    }\n    return false\n}\n\nprivate suspend fun fixA11yService() {\n    if (!A11yService.isRunning.value && writeSecureSettingsState.updateAndGet()) {\n        if (skipBlockApp()) return\n        val names = app.getSecureA11yServices()\n        val a11yBroken = names.contains(A11yService.a11yCn)\n        if (a11yBroken) {\n            // 无障碍出现故障, 重启服务\n            names.remove(A11yService.a11yCn)\n            app.putSecureA11yServices(names)\n            // 必须等待一段时间, 否则概率不会触发系统重启无障碍\n            delay(A11Y_AWAIT_FIX_TIME)\n            if (!currentAppUseA11y) return\n        }\n        names.add(A11yService.a11yCn)\n        app.putSecureA11yServices(names)\n        delay(A11Y_AWAIT_START_TIME)\n        if (currentAppUseA11y && !A11yService.isRunning.value) {\n            toast(\"重启无障碍失败\")\n            accessRestrictedSettingsShowFlow.value = true\n        }\n    }\n}\n\nprivate fun fixAutomationService() {\n    if (uiAutomationFlow.value == null && shizukuContextFlow.value.ok) {\n        if (skipBlockApp()) return\n        if (currentAppUseA11y) return\n        AutomationService.tryConnect(true)\n    }\n}\n\nfun fixRestartAutomatorService() = modifyA11yRun {\n    if (storeFlow.value.enableAutomator) {\n        if (currentAppUseA11y) {\n            fixA11yService()\n        } else {\n            fixAutomationService()\n        }\n    }\n}\n\nval currentAppUseA11y\n    get() = storeFlow.value.useA11y || topAppIdFlow.value in actualA11yScopeAppList\n\nval currentAppBlocked\n    get() = storeFlow.value.enableBlockA11yAppList && topAppIdFlow.value in actualBlockA11yAppList\n\nprivate fun innerForcedUpdateA11yService(disabled: Boolean) {\n    if (!storeFlow.value.enableAutomator) {\n        return\n    }\n    if (disabled) {\n        A11yService.instance?.shutdown(true)\n        uiAutomationFlow.value?.shutdown(true)\n        return\n    }\n    if (currentAppUseA11y) {\n        if (A11yService.isRunning.value) {\n            return\n        }\n        if (!writeSecureSettingsState.stateFlow.value) {\n            return\n        }\n        val names = app.getSecureA11yServices()\n        names.add(A11yService.a11yCn)\n        app.putSecureA11yServices(names)\n    } else {\n        AutomationService.tryConnect(true)\n    }\n}\n\nprivate fun forcedUpdateA11yService(disabled: Boolean) = modifyA11yRun {\n    innerForcedUpdateA11yService(disabled)\n}\n\nconst val A11Y_WHITE_APP_AWAIT_TIME = 3000L\n\n@Volatile\nprivate var lastAppIdChangeTime = 0L\nval topAppIdFlow = MutableStateFlow(\"\")\nval a11yPartDisabledFlow by lazy {\n    topAppIdFlow.mapState(appScope) {\n        actualBlockA11yAppList.contains(it)\n    }\n}\n\nfun updateTopTaskAppId(value: String) {\n    if (storeFlow.value.enableBlockA11yAppList || actualA11yScopeAppList.isNotEmpty()) {\n        topAppIdFlow.value = value\n    }\n}\n\nfun initA11yWhiteAppList() {\n    val actualFlow = topAppIdFlow.drop(1)\n    appScope.launch(Dispatchers.Main) {\n        actualFlow.collect {\n            lastAppIdChangeTime = System.currentTimeMillis()\n            if (!currentAppBlocked) {\n                if (topActivityFlow.value.sameAs(systemRecentCn) && currentAppUseA11y) {\n                    // 切换无障碍会造成卡顿，在最近任务界面时，延迟这个卡顿\n                    val tempTime = lastAppIdChangeTime\n                    runMainPost(A11Y_WHITE_APP_AWAIT_TIME) {\n                        if (tempTime == lastAppIdChangeTime) {\n                            forcedUpdateA11yService(false)\n                        }\n                    }\n                } else {\n                    // 切换自动化不会卡顿，直接启动\n                    forcedUpdateA11yService(false)\n                }\n            }\n        }\n    }\n    appScope.launch(Dispatchers.Main) {\n        actualFlow.debounce(A11Y_WHITE_APP_AWAIT_TIME).collect {\n            if (currentAppBlocked) {\n                forcedUpdateA11yService(true)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/HttpService.kt",
    "content": "package li.songe.gkd.service\n\nimport android.app.Service\nimport android.content.Intent\nimport android.util.Log\nimport io.ktor.http.ContentType\nimport io.ktor.http.HttpHeaders\nimport io.ktor.http.HttpMethod\nimport io.ktor.serialization.kotlinx.json.json\nimport io.ktor.server.application.createApplicationPlugin\nimport io.ktor.server.application.hooks.CallFailed\nimport io.ktor.server.application.install\nimport io.ktor.server.cio.CIO\nimport io.ktor.server.cio.CIOApplicationEngine\nimport io.ktor.server.engine.EmbeddedServer\nimport io.ktor.server.engine.embeddedServer\nimport io.ktor.server.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.server.plugins.origin\nimport io.ktor.server.request.httpMethod\nimport io.ktor.server.request.receive\nimport io.ktor.server.request.receiveText\nimport io.ktor.server.request.uri\nimport io.ktor.server.response.header\nimport io.ktor.server.response.respond\nimport io.ktor.server.response.respondFile\nimport io.ktor.server.response.respondText\nimport io.ktor.server.routing.get\nimport io.ktor.server.routing.post\nimport io.ktor.server.routing.route\nimport io.ktor.server.routing.routing\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.a11y.A11yRuleEngine\nimport li.songe.gkd.appScope\nimport li.songe.gkd.data.AppInfo\nimport li.songe.gkd.data.DeviceInfo\nimport li.songe.gkd.data.GkdAction\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.data.RpcError\nimport li.songe.gkd.data.SubsItem\nimport li.songe.gkd.data.selfAppInfo\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.notif.StopServiceReceiver\nimport li.songe.gkd.notif.httpNotif\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.util.LOCAL_HTTP_SUBS_ID\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.OnSimpleLife\nimport li.songe.gkd.util.SERVER_SCRIPT_URL\nimport li.songe.gkd.util.SnapshotExt\nimport li.songe.gkd.util.SnapshotExt.getMinSnapshot\nimport li.songe.gkd.util.deleteSubscription\nimport li.songe.gkd.util.getIpAddressInLocalNetwork\nimport li.songe.gkd.util.isPortAvailable\nimport li.songe.gkd.util.keepNullJson\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.mapState\nimport li.songe.gkd.util.startForegroundServiceByClass\nimport li.songe.gkd.util.stopServiceByClass\nimport li.songe.gkd.util.subsItemsFlow\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateSubscription\n\n\nclass HttpService : Service(), OnSimpleLife {\n    override fun onBind(intent: Intent?) = null\n\n    override fun onCreate() {\n        super.onCreate()\n        onCreated()\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        onDestroyed()\n    }\n\n    override val scope = useScope()\n\n    val httpServerPortFlow = storeFlow.mapState(scope) { s -> s.httpServerPort }\n\n    init {\n        useLogLifecycle()\n        useAliveFlow(isRunning)\n        useAliveToast(\"HTTP服务\")\n        StopServiceReceiver.autoRegister()\n        onCreated {\n            scope.launchTry(Dispatchers.IO) {\n                httpServerPortFlow.collect {\n                    localNetworkIpsFlow.value = getIpAddressInLocalNetwork()\n                }\n            }\n        }\n        onDestroyed {\n            if (storeFlow.value.autoClearMemorySubs) {\n                deleteSubscription(LOCAL_HTTP_SUBS_ID)\n            }\n            httpServerFlow.value = null\n        }\n        onCreated {\n            httpNotif.notifyService()\n            scope.launchTry(Dispatchers.IO) {\n                httpServerPortFlow.collect { port ->\n                    val isReboot = httpServerFlow.value != null\n                    httpServerFlow.apply {\n                        value?.stop()\n                        value = null\n                    }\n                    if (!isPortAvailable(port)) {\n                        toast(\"端口 $port 被占用，请更换后重试\")\n                        stopSelf()\n                        return@collect\n                    }\n                    httpServerFlow.value = try {\n                        scope.createServer(port).apply { start() }\n                    } catch (e: Exception) {\n                        toast(\"HTTP服务启动失败:${e.stackTraceToString()}\")\n                        LogUtils.d(\"HTTP服务启动失败\", e)\n                        null\n                    }\n                    if (httpServerFlow.value == null) {\n                        stopSelf()\n                    } else if (isReboot) {\n                        toast(\"HTTP服务重启成功\")\n                    }\n                }\n            }\n        }\n    }\n\n    companion object {\n        val httpServerFlow = MutableStateFlow<ServerType?>(null)\n        val isRunning = MutableStateFlow(false)\n        val localNetworkIpsFlow = MutableStateFlow(emptyList<String>())\n        fun stop() = stopServiceByClass(HttpService::class)\n        fun start() = startForegroundServiceByClass(HttpService::class)\n    }\n}\n\ntypealias ServerType = EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>\n\n\n@Serializable\ndata class RpcOk(\n    val message: String? = null,\n)\n\n@Serializable\ndata class ReqId(\n    val id: Long,\n)\n\n@Serializable\ndata class ServerInfo(\n    val device: DeviceInfo = DeviceInfo(),\n    val gkdAppInfo: AppInfo = selfAppInfo\n)\n\nfun clearHttpSubs() {\n    // 如果 app 被直接在任务列表划掉, HTTP订阅会没有清除, 所以在后续的第一次启动时清除\n    if (HttpService.isRunning.value) return\n    appScope.launchTry {\n        delay(1000)\n        if (storeFlow.value.autoClearMemorySubs) {\n            deleteSubscription(LOCAL_HTTP_SUBS_ID)\n        }\n    }\n}\n\nprivate val httpSubsItem = SubsItem(\n    id = LOCAL_HTTP_SUBS_ID,\n    order = -1,\n    enableUpdate = false,\n)\n\nprivate fun CoroutineScope.createServer(port: Int) = embeddedServer(CIO, port) {\n    install(getKtorCorsPlugin())\n    install(getKtorErrorPlugin())\n    install(ContentNegotiation) { json(keepNullJson) }\n    routing {\n        get(\"/\") { call.respondText(ContentType.Text.Html) { \"<script type='module' src='$SERVER_SCRIPT_URL'></script>\" } }\n        route(\"/api\") {\n            post(\"/getServerInfo\") { call.respond(ServerInfo()) }\n            post(\"/getSnapshot\") {\n                val data = call.receive<ReqId>()\n                val fp = SnapshotExt.snapshotFile(data.id)\n                if (!fp.exists()) {\n                    throw RpcError(\"对应快照不存在\")\n                }\n                call.respondFile(fp)\n            }\n            post(\"/getScreenshot\") {\n                val data = call.receive<ReqId>()\n                val fp = SnapshotExt.screenshotFile(data.id)\n                if (!fp.exists()) {\n                    throw RpcError(\"对应截图不存在\")\n                }\n                call.respondFile(fp)\n            }\n            post(\"/captureSnapshot\") {\n                call.respond(SnapshotExt.captureSnapshot())\n            }\n            post(\"/getSnapshots\") {\n                val list = DbSet.snapshotDao.query().first().mapNotNull {\n                    try {\n                        getMinSnapshot(it.id)\n                    } catch (_: Throwable) {\n                        null\n                    }\n                }\n                call.respond(list)\n            }\n            post(\"/deleteSnapshot\") {\n                val data = call.receive<ReqId>()\n                val allSnapshots = DbSet.snapshotDao.query().first()\n                val snapshot = allSnapshots.find { it.id == data.id }\n                if (snapshot != null) {\n                    SnapshotExt.removeSnapshot(data.id)\n                    DbSet.snapshotDao.delete(snapshot)\n                    call.respond(RpcOk(\"快照删除成功\"))\n                } else {\n                    throw RpcError(\"快照不存在或已被删除\")\n                }\n            }\n            post(\"/updateSubscription\") {\n                val subscription =\n                    RawSubscription.parse(call.receiveText(), json5 = false)\n                        .copy(\n                            id = LOCAL_HTTP_SUBS_ID,\n                            name = \"内存订阅\",\n                            version = 0,\n                            author = \"@gkd-kit/inspect\"\n                        )\n                updateSubscription(subscription)\n                DbSet.subsItemDao.insert((subsItemsFlow.value.find { s -> s.id == httpSubsItem.id }\n                    ?: httpSubsItem).copy(mtime = System.currentTimeMillis()))\n                call.respond(RpcOk())\n            }\n            post(\"/execSelector\") {\n                if (!A11yService.isRunning.value) {\n                    throw RpcError(\"无障碍没有运行\")\n                }\n                val gkdAction = call.receive<GkdAction>()\n                call.respond(A11yRuleEngine.execAction(gkdAction))\n            }\n        }\n    }\n}\n\nprivate fun getKtorCorsPlugin() = createApplicationPlugin(name = \"KtorCorsPlugin\") {\n    onCall { call ->\n        mapOf(\n            HttpHeaders.AccessControlAllowOrigin to \"*\",\n            HttpHeaders.AccessControlAllowMethods to \"*\",\n            HttpHeaders.AccessControlAllowHeaders to \"*\",\n            HttpHeaders.AccessControlExposeHeaders to \"*\",\n            \"Access-Control-Allow-Private-Network\" to \"true\",\n        ).forEach { (k, v) ->\n            if (!call.response.headers.contains(k)) {\n                call.response.header(k, v)\n            }\n        }\n        if (call.request.httpMethod == HttpMethod.Options) {\n            call.respond(\"all-cors-ok\")\n        }\n    }\n}\n\nprivate fun getKtorErrorPlugin() = createApplicationPlugin(name = \"KtorErrorPlugin\") {\n    onCall { call ->\n        if (call.request.uri == \"/\" || call.request.uri.startsWith(\"/api/\")) {\n            Log.d(\"Ktor\", \"onCall: ${call.request.origin.remoteAddress} -> ${call.request.uri}\")\n        }\n    }\n    on(CallFailed) { call, cause ->\n        when (cause) {\n            is RpcError -> {\n                // 主动抛出的错误\n                LogUtils.d(call.request.uri, cause.message)\n                call.respond(cause)\n            }\n\n            is Exception -> {\n                // 未知错误\n                LogUtils.d(call.request.uri, cause.message)\n                cause.printStackTrace()\n                call.respond(RpcError(message = cause.message ?: \"unknown error\", unknown = true))\n            }\n\n            else -> {\n                cause.printStackTrace()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/HttpTileService.kt",
    "content": "package li.songe.gkd.service\n\nclass HttpTileService : BaseTileService() {\n    override val activeFlow = HttpService.isRunning\n\n    init {\n        onTileClicked {\n            if (HttpService.isRunning.value) {\n                HttpService.stop()\n            } else {\n                HttpService.start()\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/MatchTileService.kt",
    "content": "package li.songe.gkd.service\n\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.store.switchStoreEnableMatch\nimport li.songe.gkd.util.mapState\n\nclass MatchTileService : BaseTileService() {\n    override val activeFlow = storeFlow.mapState(scope) { it.enableMatch }\n\n    init {\n        onTileClicked { switchStoreEnableMatch() }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/OverlayWindowService.kt",
    "content": "package li.songe.gkd.service\n\n\nimport android.animation.ValueAnimator\nimport android.annotation.SuppressLint\nimport android.content.res.Configuration\nimport android.graphics.PixelFormat\nimport android.view.Gravity\nimport android.view.MotionEvent\nimport android.view.ViewConfiguration\nimport android.view.WindowManager\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.platform.ComposeView\nimport androidx.compose.ui.unit.dp\nimport androidx.core.animation.doOnEnd\nimport androidx.lifecycle.LifecycleService\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.setViewTreeLifecycleOwner\nimport androidx.savedstate.SavedStateRegistryController\nimport androidx.savedstate.SavedStateRegistryOwner\nimport androidx.savedstate.setViewTreeSavedStateRegistryOwner\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.a11y.topActivityFlow\nimport li.songe.gkd.permission.canDrawOverlaysState\nimport li.songe.gkd.store.createAnyFlow\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.icon.DragPan\nimport li.songe.gkd.ui.style.AppTheme\nimport li.songe.gkd.ui.style.iconTextSize\nimport li.songe.gkd.util.BarUtils\nimport li.songe.gkd.util.OnSimpleLife\nimport li.songe.gkd.util.ScreenUtils\nimport li.songe.gkd.util.mapState\nimport li.songe.gkd.util.px\nimport li.songe.gkd.util.runMainPost\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\nimport kotlin.math.abs\n\nprivate var tempShareContext: ShareContext? = null\nprivate fun OverlayWindowService.useShareContext(): ShareContext {\n    val shareContext = tempShareContext ?: ShareContext().apply { tempShareContext = this }\n    shareContext.count++\n    onDestroyed {\n        shareContext.count--\n        if (shareContext.count == 0) {\n            shareContext.scope.cancel()\n            tempShareContext = null\n        }\n    }\n    return shareContext\n}\n\nprivate class ShareContext {\n    var count = 0\n    val scope = MainScope()\n    val positionMapFlow = createAnyFlow<Map<String, List<Int>>>(\n        key = \"overlay_position\",\n        default = { emptyMap() },\n        scope = scope,\n    )\n\n    init {\n        scope.launch {\n            var canDrawOverlays = canDrawOverlaysState.updateAndGet()\n            topActivityFlow\n                .mapState(scope) { it.appId to it.activityId }\n                .collectLatest {\n                    var i = 0\n                    while (i < 6 && isActive) {\n                        val oldV = canDrawOverlays\n                        val newV = canDrawOverlaysState.updateAndGet()\n                        canDrawOverlays = newV\n                        if (!newV && oldV) {\n                            toast(\"当前界面拒绝显示悬浮窗\")\n                            break\n                        }\n                        delay(500)\n                        i++\n                    }\n                }\n        }\n    }\n}\n\nabstract class OverlayWindowService(\n    val positionKey: String,\n) : LifecycleService(), SavedStateRegistryOwner, OnSimpleLife {\n    companion object {\n        private var aliveSize = 0\n        val isAnyAlive: Boolean\n            get() = aliveSize > 0\n    }\n\n    override fun onCreate() {\n        super.onCreate()\n        onCreated()\n    }\n\n    override val scope get() = lifecycleScope\n\n    private val resizeFlow = MutableSharedFlow<Unit>()\n\n    override fun onConfigurationChanged(newConfig: Configuration) {\n        lifecycleScope.launch { resizeFlow.emit(Unit) }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        onDestroyed()\n    }\n\n    val registryController = SavedStateRegistryController.create(this).apply {\n        performAttach()\n        performRestore(null)\n    }\n    override val savedStateRegistry = registryController.savedStateRegistry\n\n    private val windowManager by lazy { getSystemService(WINDOW_SERVICE) as WindowManager }\n\n    @Composable\n    abstract fun ComposeContent()\n\n    @Composable\n    fun ClosableTitle(title: String) {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            PerfIcon(imageVector = DragPan, modifier = Modifier.iconTextSize())\n            Text(text = title, modifier = Modifier.weight(1f))\n            PerfIcon(\n                imageVector = PerfIcon.Close,\n                modifier = Modifier\n                    .clip(MaterialTheme.shapes.extraSmall)\n                    .clickable(onClick = throttle {\n                        stopSelf()\n                    })\n                    .iconTextSize()\n            )\n        }\n    }\n\n    open fun onClickView() {}\n\n    open fun onLongClickView() {}\n\n    val view by lazy {\n        ComposeView(this).apply {\n            setViewTreeSavedStateRegistryOwner(this@OverlayWindowService)\n            setViewTreeLifecycleOwner(this@OverlayWindowService)\n            setContent {\n                AppTheme(invertedTheme = true) {\n                    ComposeContent()\n                }\n            }\n        }\n    }\n\n    private val minMargin get() = 10.dp.px.toInt()\n    private val defaultPosition get() = listOf(minMargin, BarUtils.getStatusBarHeight())\n\n    private val shareContext = useShareContext()\n\n    private val positionFlow = MutableStateFlow(\n        shareContext.positionMapFlow.value[positionKey].let {\n            if (it != null && it.size >= 2) {\n                it\n            } else {\n                defaultPosition\n            }\n        }\n    )\n\n    init {\n        aliveSize++\n        onDestroyed {\n            runMainPost(1000) { aliveSize-- }\n        }\n        lifecycleScope.launch {\n            positionFlow.drop(1).debounce(300).collect { pos ->\n                shareContext.positionMapFlow.update {\n                    it.toMutableMap().apply {\n                        set(positionKey, pos)\n                    }\n                }\n            }\n        }\n        onCreated {\n            val marginX = minMargin\n            val marginY = minMargin\n            val layoutParams = WindowManager.LayoutParams(\n                WindowManager.LayoutParams.WRAP_CONTENT,\n                WindowManager.LayoutParams.WRAP_CONTENT,\n                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,\n                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or\n                        WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or\n                        WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,\n                PixelFormat.TRANSLUCENT\n            ).apply {\n                windowAnimations = android.R.style.Animation_Dialog\n                gravity = Gravity.START or Gravity.TOP\n                x = positionFlow.value.first()\n                y = positionFlow.value.last()\n            }\n            var screenWidth = ScreenUtils.getScreenWidth()\n            var screenHeight = ScreenUtils.getScreenHeight()\n            var paramsXy = layoutParams.x to layoutParams.y\n            var fixMoveFlag = 0\n            val fixLimitXy = {\n                screenWidth = ScreenUtils.getScreenWidth()\n                screenHeight = ScreenUtils.getScreenHeight()\n                val x = layoutParams.x.coerceIn(marginX, screenWidth - view.width - marginX)\n                val y = layoutParams.y.coerceIn(\n                    marginY,\n                    screenHeight - view.height - marginY\n                )\n                if (x != layoutParams.x || y != layoutParams.y) {\n                    positionFlow.value = listOf(x, y)\n                    val startX = layoutParams.x\n                    val startY = layoutParams.y\n                    fixMoveFlag++\n                    val tempFlag = fixMoveFlag\n                    ValueAnimator.ofFloat(0f, 1f).apply {\n                        duration = 300\n                        addUpdateListener { animator ->\n                            if (tempFlag == fixMoveFlag) {\n                                val fraction = animator.animatedValue as Float\n                                layoutParams.x = (startX + (x - startX) * fraction).toInt()\n                                layoutParams.y = (startY + (y - startY) * fraction).toInt()\n                                windowManager.updateViewLayout(view, layoutParams)\n                            } else {\n                                pause()\n                            }\n                        }\n                        doOnEnd {\n                            if (tempFlag == fixMoveFlag) {\n                                fixMoveFlag = 0\n                            }\n                        }\n                    }.start()\n                }\n            }\n            lifecycleScope.launch {\n                view.viewTreeObserver.addOnGlobalLayoutListener { launch { resizeFlow.emit(Unit) } }\n                resizeFlow.debounce(100).collect { fixLimitXy() }\n            }\n            var downXy: Pair<Float, Float>? = null\n            var longClickJob: kotlinx.coroutines.Job? = null\n            @SuppressLint(\"ClickableViewAccessibility\")\n            view.setOnTouchListener { _, event ->\n                if (fixMoveFlag > 0) return@setOnTouchListener true\n                when (event.action) {\n                    MotionEvent.ACTION_DOWN -> {\n                        downXy = event.rawX to event.rawY\n                        screenWidth = ScreenUtils.getScreenWidth()\n                        screenHeight = ScreenUtils.getScreenHeight()\n                        paramsXy = layoutParams.x to layoutParams.y\n                        longClickJob = null\n                        longClickJob = scope.launch {\n                            delay(500)\n                            longClickJob = null\n                            if (downXy != null) {\n                                onLongClickView()\n                            }\n                        }\n                        true\n                    }\n\n                    MotionEvent.ACTION_MOVE -> {\n                        downXy?.let { downEvent ->\n                            val dx = (event.rawX - downEvent.first).toInt()\n                            val dy = (event.rawY - downEvent.second).toInt()\n                            val x = dx + paramsXy.first\n                            val y = dy + paramsXy.second\n                            layoutParams.x = x.coerceIn(marginX, screenWidth - view.width - marginX)\n                            layoutParams.y = y.coerceIn(\n                                marginY,\n                                screenHeight - view.height - marginY\n                            )\n                            positionFlow.value = listOf(layoutParams.x, layoutParams.y)\n                            windowManager.updateViewLayout(view, layoutParams)\n                            longClickJob?.let {\n                                val maxBreakLongOffset = 10\n                                if (abs(dx) > maxBreakLongOffset || abs(dy) > maxBreakLongOffset) {\n                                    longClickJob?.cancel()\n                                    longClickJob = null\n                                }\n                            }\n                        }\n                        true\n                    }\n\n                    MotionEvent.ACTION_UP -> {\n                        val gapTime = event.eventTime - event.downTime\n                        if (gapTime <= ViewConfiguration.getTapTimeout()) {\n                            onClickView()\n                        }\n                        downXy = null\n                        longClickJob = null\n                        true\n                    }\n\n                    else -> false\n                }\n            }\n            windowManager.addView(view, layoutParams)\n        }\n        onDestroyed { windowManager.removeView(view) }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/ScreenshotService.kt",
    "content": "package li.songe.gkd.service\n\nimport android.app.Service\nimport android.content.Intent\nimport coil3.Bitmap\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.withTimeoutOrNull\nimport li.songe.gkd.app\nimport li.songe.gkd.notif.StopServiceReceiver\nimport li.songe.gkd.notif.screenshotNotif\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.OnSimpleLife\nimport li.songe.gkd.util.ScreenshotUtil\nimport li.songe.gkd.util.componentName\nimport li.songe.gkd.util.stopServiceByClass\n\nclass ScreenshotService : Service(), OnSimpleLife {\n    override val scope: CoroutineScope\n        get() = throw NotImplementedError()\n\n    override fun onBind(intent: Intent?) = null\n    override fun onCreate() = onCreated()\n    override fun onDestroy() = onDestroyed()\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        try {\n            return super.onStartCommand(intent, flags, startId)\n        } finally {\n            intent?.let {\n                screenshotUtil?.destroy()\n                screenshotUtil = ScreenshotUtil(this, intent)\n                LogUtils.d(\"screenshot restart\")\n            }\n        }\n    }\n\n    private var screenshotUtil: ScreenshotUtil? = null\n\n    init {\n        useLogLifecycle()\n        useAliveFlow(isRunning)\n        useAliveToast(\"截屏服务\")\n        StopServiceReceiver.autoRegister()\n        onCreated { screenshotNotif.notifyService() }\n        onCreated { instance = this }\n        onDestroyed {\n            screenshotUtil?.destroy()\n            instance = null\n        }\n    }\n\n    companion object {\n        private var instance: ScreenshotService? = null\n        val isRunning = MutableStateFlow(false)\n        suspend fun screenshot(): Bitmap? {\n            if (!isRunning.value) return null\n            return withTimeoutOrNull(5_000) {\n                instance?.screenshotUtil?.execute()\n            }\n        }\n\n        fun start(intent: Intent) {\n            intent.component = ScreenshotService::class.componentName\n            app.startForegroundService(intent)\n        }\n\n        fun stop() = stopServiceByClass(ScreenshotService::class)\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/SnapshotTileService.kt",
    "content": "package li.songe.gkd.service\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.isActive\nimport li.songe.gkd.a11y.A11yRuleEngine\nimport li.songe.gkd.appScope\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.SnapshotExt\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.toast\n\nclass SnapshotTileService() : BaseTileService() {\n    override val activeFlow = MutableStateFlow(false)\n\n    init {\n        onTileClicked { execSnapshot() }\n    }\n}\n\nprivate fun execSnapshot() {\n    LogUtils.d(\"SnapshotTileService::onClick\")\n    val service = A11yRuleEngine.instance\n    if (service == null) {\n        A11yRuleEngine.performActionBack()\n        toast(\"服务未连接\", forced = true)\n        return\n    }\n    appScope.launchTry(Dispatchers.IO) {\n        val oldAppId = service.safeActiveWindowAppId\n\n        if (oldAppId == null) {\n            A11yRuleEngine.performActionBack()\n            toast(\"获取信息根节点失败\", forced = true)\n            return@launchTry\n        }\n\n        val startTime = System.currentTimeMillis()\n        fun timeout(): Boolean {\n            return System.currentTimeMillis() - startTime > 3000L\n        }\n\n        var ok = false\n        while (isActive) {\n            val latestAppId = service.safeActiveWindowAppId\n            if (latestAppId == null) {\n                // https://github.com/gkd-kit/gkd/issues/713\n                delay(250)\n                if (timeout()) {\n                    toast(\"当前应用没有无障碍信息，捕获失败\", forced = true)\n                    break\n                }\n            } else if (latestAppId != oldAppId) {\n                ok = true\n                LogUtils.d(\"SnapshotTileService::eventExecutor.execute\")\n                appScope.launchTry { SnapshotExt.captureSnapshot(forcedCropStatusBar = true) }\n                break\n            } else {\n                A11yRuleEngine.performActionBack()\n                delay(500)\n                if (timeout()) {\n                    toast(\"未检测到界面切换，捕获失败\", forced = true)\n                    break\n                }\n            }\n        }\n        if (!ok) {\n            A11yRuleEngine.performActionBack()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/service/StatusService.kt",
    "content": "package li.songe.gkd.service\n\nimport android.app.Service\nimport android.content.Intent\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.META\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.a11y.useA11yServiceEnabledFlow\nimport li.songe.gkd.app\nimport li.songe.gkd.notif.abNotif\nimport li.songe.gkd.permission.appOpsRestrictedFlow\nimport li.songe.gkd.permission.foregroundServiceSpecialUseState\nimport li.songe.gkd.permission.notificationState\nimport li.songe.gkd.permission.requiredPermission\nimport li.songe.gkd.permission.shizukuGrantedState\nimport li.songe.gkd.permission.writeSecureSettingsState\nimport li.songe.gkd.shizuku.uiAutomationFlow\nimport li.songe.gkd.store.actionCountFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.util.OnSimpleLife\nimport li.songe.gkd.util.RuleSummary\nimport li.songe.gkd.util.appInfoMapFlow\nimport li.songe.gkd.util.getSubsStatus\nimport li.songe.gkd.util.ruleSummaryFlow\nimport li.songe.gkd.util.startForegroundServiceByClass\nimport li.songe.gkd.util.stopServiceByClass\n\nclass StatusService : Service(), OnSimpleLife {\n    override fun onBind(intent: Intent?) = null\n    override fun onCreate() = onCreated()\n    override fun onDestroy() = onDestroyed()\n\n    override val scope = useScope()\n\n    val shizukuWarnFlow = combine(\n        shizukuGrantedState.stateFlow,\n        storeFlow.map { it.enableShizuku },\n    ) { a, b ->\n        !a && b\n    }.stateIn(scope, SharingStarted.Eagerly, false)\n\n    val a11yServiceEnabledFlow = useA11yServiceEnabledFlow()\n\n    fun statusTriple(): Triple<String, String, String?> {\n        val abRunning = A11yService.isRunning.value\n        val automationRunning = uiAutomationFlow.value != null\n        val store = storeFlow.value\n        val ruleSummary = ruleSummaryFlow.value\n        val count = actionCountFlow.value\n        val shizukuWarn = shizukuWarnFlow.value\n        val title = if (store.useCustomNotifText) {\n            store.customNotifTitle.replaceTemplate(ruleSummary, count)\n        } else {\n            META.appName\n        }\n        return if (appOpsRestrictedFlow.value) {\n            Triple(title, \"权限受限，请解除限制\", \"gkd://page/3\")\n        } else if (shizukuWarn) {\n            Triple(title, \"Shizuku 未连接，请授权或关闭优化\", \"gkd://page/1\")\n        } else if (!automationRunning && !abRunning) {\n            if (currentAppUseA11y) {\n                val text = if (a11yServiceEnabledFlow.value) {\n                    \"无障碍发生故障\"\n                } else if (writeSecureSettingsState.updateAndGet()) {\n                    if (store.enableAutomator && store.enableBlockA11yAppList && a11yPartDisabledFlow.value) {\n                        val name =\n                            appInfoMapFlow.value[topAppIdFlow.value]?.name ?: topAppIdFlow.value\n                        \"局部关闭 · $name\"\n                    } else {\n                        \"无障碍已关闭\"\n                    }\n                } else {\n                    \"无障碍未授权\"\n                }\n                Triple(title, text, abNotif.uri)\n            } else {\n                val text =\n                    if (store.enableAutomator && store.enableBlockA11yAppList && a11yPartDisabledFlow.value) {\n                        val name =\n                            appInfoMapFlow.value[topAppIdFlow.value]?.name ?: topAppIdFlow.value\n                        \"局部关闭 · $name\"\n                    } else {\n                        \"自动化已关闭\"\n                    }\n                Triple(title, text, abNotif.uri)\n            }\n        } else if (!store.enableMatch) {\n            Triple(title, \"暂停规则匹配\", \"gkd://page?tab=1\")\n        } else if (store.useCustomNotifText) {\n            Triple(\n                title,\n                store.customNotifText.replaceTemplate(ruleSummary, count),\n                abNotif.uri\n            )\n        } else {\n            Triple(title, getSubsStatus(ruleSummary, count), abNotif.uri)\n        }\n    }\n\n    init {\n        useAliveFlow(isRunning)\n        useAliveToast(\n            name = \"常驻通知\",\n            delayMillis = if (app.justStarted) 1000 else 0,\n        )\n        onCreated {\n            abNotif.notifyService()\n            scope.launch {\n                combine(\n                    A11yService.isRunning,\n                    uiAutomationFlow,\n                    storeFlow,\n                    ruleSummaryFlow,\n                    shizukuWarnFlow,\n                    a11yServiceEnabledFlow,\n                    writeSecureSettingsState.stateFlow,\n                    appOpsRestrictedFlow,\n                    topAppIdFlow,\n                    actionCountFlow.debounce(1000L),\n                ) {\n                    statusTriple()\n                }\n                    .stateIn(\n                        scope,\n                        SharingStarted.Eagerly,\n                        Triple(abNotif.title, abNotif.text, abNotif.uri)\n                    )\n                    .collect {\n                        abNotif.copy(\n                            title = it.first,\n                            text = it.second,\n                            uri = it.third,\n                        ).notifyService()\n                    }\n            }\n        }\n    }\n\n    companion object {\n        val isRunning = MutableStateFlow(false)\n        val needRestart\n            get() = storeFlow.value.enableStatusService\n                    && !isRunning.value\n                    && notificationState.updateAndGet()\n                    && foregroundServiceSpecialUseState.updateAndGet()\n\n        fun start() = startForegroundServiceByClass(StatusService::class)\n        fun stop() = stopServiceByClass(StatusService::class)\n        suspend fun requestStart(context: MainActivity) {\n            requiredPermission(context, foregroundServiceSpecialUseState)\n            requiredPermission(context, notificationState)\n            start()\n            storeFlow.update { it.copy(enableStatusService = true) }\n        }\n\n        private var lastAutoStart = 0L\n        fun autoStart() {\n            if (System.currentTimeMillis() - lastAutoStart < 1000) return\n            // 重启自动打开通知栏状态服务\n            // 需要已有服务或前台才能自主启动，否则报错 startForegroundService() not allowed due to mAllowStartForeground false\n            if (needRestart) {\n                start()\n                lastAutoStart = System.currentTimeMillis()\n            }\n        }\n    }\n}\n\nprivate fun String.replaceTemplate(ruleSummary: RuleSummary, count: Long): String {\n    return replace($$\"${i}\", ruleSummary.globalGroups.size.toString())\n        .replace($$\"${k}\", ruleSummary.appSize.toString())\n        .replace($$\"${u}\", ruleSummary.appGroupSize.toString())\n        .replace($$\"${n}\", count.toString())\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/AccessibilityManager.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.content.Context\nimport android.view.accessibility.IAccessibilityManager\n\nclass SafeAccessibilityManager(val value: IAccessibilityManager) {\n    companion object {\n        fun newBinder() = getShizukuService(Context.ACCESSIBILITY_SERVICE)?.let {\n            SafeAccessibilityManager(IAccessibilityManager.Stub.asInterface(it))\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/ActivityManager.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.app.ActivityManager\nimport android.app.IActivityManager\nimport android.content.Context\nimport android.content.Intent\nimport li.songe.gkd.util.AndroidTarget\n\nclass SafeActivityManager(private val value: IActivityManager) {\n    companion object {\n        fun newBinder() = getShizukuService(Context.ACTIVITY_SERVICE)?.let {\n            SafeActivityManager(IActivityManager.Stub.asInterface(it))\n        }\n    }\n\n    fun getTasks(maxNum: Int = 1): List<ActivityManager.RunningTaskInfo> = safeInvokeShizuku {\n        if (AndroidTarget.P) {\n            value.getTasks(maxNum)\n        } else {\n            value.getTasks(maxNum, 0)\n        }\n    } ?: emptyList()\n\n    fun startForegroundService(intent: Intent) {\n        // 被启动的服务必须设置 android:exported=\"true\"\n        // https://github.com/android-cs/16/blob/main/services/core/java/com/android/server/am/ActivityManagerShellCommand.java#L982\n        val requireForeground = true\n        val callingPackage = \"com.android.shell\"\n        val callingFeatureId: String? = null\n        if (AndroidTarget.R) {\n            value.startService(\n                null,\n                intent,\n                intent.type,\n                requireForeground,\n                callingPackage,\n                callingFeatureId,\n                currentUserId\n            )\n        } else {\n            value.startService(\n                null,\n                intent,\n                intent.type,\n                requireForeground,\n                callingPackage,\n                currentUserId\n            )\n        }\n    }\n\n    fun registerDefault() {\n        safeInvokeShizuku {\n            value.registerTaskStackListener(FixedTaskStackListener)\n        }\n    }\n\n    fun unregisterDefault() {\n        safeInvokeShizuku {\n            value.unregisterTaskStackListener(FixedTaskStackListener)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/ActivityTaskManager.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.app.ActivityManager\nimport android.app.IActivityTaskManager\nimport android.content.ContextHidden\nimport android.view.Display\nimport li.songe.gkd.util.AndroidTarget\n\nclass SafeActivityTaskManager(private val value: IActivityTaskManager) {\n    companion object {\n        fun newBinder() = if (AndroidTarget.Q) {\n            getShizukuService(ContextHidden.ACTIVITY_TASK_SERVICE)?.let {\n                SafeActivityTaskManager(IActivityTaskManager.Stub.asInterface(it))\n            }\n        } else {\n            null\n        }\n\n        private val getTasksType by lazy {\n            IActivityTaskManager::class.java.detectHiddenMethod(\n                \"getTasks\",\n                1 to listOf(Int::class.java),\n                2 to listOf(Int::class.java, Boolean::class.java, Boolean::class.java),\n                3 to listOf(\n                    Int::class.java,\n                    Boolean::class.java,\n                    Boolean::class.java,\n                    Int::class.java\n                ),\n            )\n        }\n    }\n\n    fun getTasks(maxNum: Int = 1): List<ActivityManager.RunningTaskInfo>? = safeInvokeShizuku {\n        when (getTasksType) {\n            1 -> value.getTasks(maxNum)\n            2 -> value.getTasks(maxNum, false, false)\n            3 -> value.getTasks(maxNum, false, false, Display.INVALID_DISPLAY)\n            else -> value.getTasks(maxNum)\n        }\n    }\n\n    fun registerDefault() {\n        safeInvokeShizuku {\n            value.registerTaskStackListener(FixedTaskStackListener)\n        }\n    }\n\n    fun unregisterDefault() {\n        safeInvokeShizuku {\n            value.unregisterTaskStackListener(FixedTaskStackListener)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/AppOpsService.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.app.AppOpsManager\nimport android.app.AppOpsManagerHidden\nimport android.content.Context\nimport android.os.Build\nimport androidx.annotation.ChecksSdkIntAtLeast\nimport com.android.internal.app.IAppOpsService\nimport li.songe.gkd.META\nimport li.songe.gkd.util.AndroidTarget\n\nclass SafeAppOpsService(\n    private val value: IAppOpsService\n) {\n    companion object {\n\n        fun newBinder() = getShizukuService(Context.APP_OPS_SERVICE)?.let {\n            SafeAppOpsService(IAppOpsService.Stub.asInterface(it))\n        }\n\n        // https://diff.songe.li/?ref=AppOpsManager.OP_CREATE_ACCESSIBILITY_OVERLAY\n        private val a11yOverlayOk by lazy {\n            AndroidTarget.UPSIDE_DOWN_CAKE && try {\n                AppOpsManager::class.java.getField(\"OP_CREATE_ACCESSIBILITY_OVERLAY\")\n            } catch (_: NoSuchFieldException) {\n                null\n            } != null\n        }\n\n        @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n        val supportCreateA11yOverlay get() = a11yOverlayOk\n    }\n\n    fun checkOperation(code: Int): Int? = safeInvokeShizuku {\n        value.checkOperation(code, android.os.Process.myUid(), META.appId)\n    }\n\n    fun setMode(code: Int, mode: Int) = safeInvokeShizuku {\n        value.setMode(code, android.os.Process.myUid(), META.appId, mode)\n    }\n\n    private fun setAllowSelfMode(code: Int) {\n        val m = checkOperation(code = code) ?: return\n        if (m == AppOpsManager.MODE_ALLOWED) {\n            return\n        }\n        setMode(code = code, mode = AppOpsManager.MODE_ALLOWED)\n    }\n\n    fun allowAllSelfMode() {\n        setAllowSelfMode(AppOpsManagerHidden.OP_POST_NOTIFICATION)\n        setAllowSelfMode(AppOpsManagerHidden.OP_SYSTEM_ALERT_WINDOW)\n        if (AndroidTarget.Q) {\n            setAllowSelfMode(AppOpsManagerHidden.OP_ACCESS_ACCESSIBILITY)\n        }\n        if (AndroidTarget.TIRAMISU) {\n            setAllowSelfMode(AppOpsManagerHidden.OP_ACCESS_RESTRICTED_SETTINGS)\n        }\n        if (AndroidTarget.UPSIDE_DOWN_CAKE) {\n            setAllowSelfMode(AppOpsManagerHidden.OP_FOREGROUND_SERVICE_SPECIAL_USE)\n        }\n        if (supportCreateA11yOverlay) {\n            setAllowSelfMode(AppOpsManagerHidden.OP_CREATE_ACCESSIBILITY_OVERLAY)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/AutomationService.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.app.UiAutomation\nimport android.app.UiAutomationHidden\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.os.HandlerThread\nimport android.view.accessibility.AccessibilityNodeInfo\nimport android.view.accessibility.AccessibilityWindowInfo\nimport kotlinx.atomicfu.atomic\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.withContext\nimport li.songe.gkd.a11y.A11yCommonImpl\nimport li.songe.gkd.a11y.A11yRuleEngine\nimport li.songe.gkd.store.updateEnableAutomator\nimport li.songe.gkd.util.AutomatorModeOption\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.createGkdTempDir\nimport li.songe.gkd.util.toast\n\nclass AutomationService private constructor() : A11yCommonImpl {\n    override val mode get() = AutomatorModeOption.AutomationMode\n    private val handlerThread = HandlerThread(\"UiAutomatorHandlerThread\")\n    private val uiAutomation by lazy {\n        UiAutomationHidden(\n            handlerThread.looper,\n            ProxyUiAutomationConnection(),\n        ).castedHidden\n    }\n\n    override val scope = MainScope()\n\n    override val ruleEngine by lazy { A11yRuleEngine(this) }\n\n    private val listener = UiAutomation.OnAccessibilityEventListener {\n        ruleEngine.onA11yEvent(it)\n    }\n\n    override suspend fun screenshot(): Bitmap? = withContext(Dispatchers.IO) {\n        try {\n            uiAutomation.takeScreenshot()\n        } catch (e: Throwable) {\n            LogUtils.d(\"takeScreenshot failed, rollback to screencapFile\", e)\n            val tempDir = createGkdTempDir()\n            val fp = tempDir.resolve(\"screenshot.png\")\n            val ok = shizukuContextFlow.value.serviceWrapper?.screencapFile(fp.absolutePath)\n            if (ok == true && fp.exists()) {\n                BitmapFactory.decodeFile(fp.absolutePath).apply {\n                    tempDir.deleteRecursively()\n                }\n            } else {\n                null\n            }\n        }\n    }\n\n    override val windowNodeInfo: AccessibilityNodeInfo? get() = uiAutomation.rootInActiveWindow\n    override val windowInfos: List<AccessibilityWindowInfo> get() = uiAutomation.windows\n    private val startTime = System.currentTimeMillis()\n    override var justStarted: Boolean = true\n        get() {\n            if (field) {\n                field = System.currentTimeMillis() - startTime < 3_000\n            }\n            return field\n        }\n\n    private var connected = false\n\n    // https://github.com/android-cs/16/blob/main/cmds/uiautomator/library/testrunner-src/com/android/uiautomator/core/UiAutomationShellWrapper.java#L25\n    private fun connect() {\n        handlerThread.start()\n        uiAutomation.casted.connect(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES)\n        uiAutomation.setOnAccessibilityEventListener(listener)\n        connected = true\n        toast(\"自动化已启动\")\n        updateEnableAutomator(true)\n        ruleEngine.onA11yConnected()\n    }\n\n    private fun disconnect() {\n        scope.cancel()\n        handlerThread.quit()\n        if (!connected) return\n        uiAutomation.setOnAccessibilityEventListener(null)\n        safeInvokeShizuku {\n            uiAutomation.casted.disconnect()\n        }\n        if (tempShutdownFlag) {\n            toast(\"自动化局部关闭\")\n        } else {\n            toast(\"自动化已关闭\")\n            updateEnableAutomator(false)\n        }\n    }\n\n    private var tempShutdownFlag = false\n    override fun shutdown(temp: Boolean) {\n        if (temp) {\n            tempShutdownFlag = true\n        }\n        disconnect()\n        uiAutomationFlow.value = null\n    }\n\n    companion object {\n        private val loading = atomic(false)\n        fun tryConnect(silent: Boolean = false) {\n            if (loading.value) return\n            loading.value = true\n            try {\n                automationRegisteredExceptionFlow.value = null\n                if (uiAutomationFlow.value?.connected == true) {\n                    return\n                }\n                uiAutomationFlow.value?.shutdown()\n                val instance = AutomationService()\n                try {\n                    instance.connect()\n                    uiAutomationFlow.value = instance\n                } catch (e: Exception) {\n                    instance.disconnect()\n                    uiAutomationFlow.value = null\n                    // https://github.com/android-cs/16/blob/main/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java#L110\n                    if (e is IllegalStateException && e.message?.contains(\"already registered\") == true) {\n                        toast(\"自动化启动失败，被其他应用占用\")\n                        if (!silent) {\n                            automationRegisteredExceptionFlow.value = e\n                        }\n                        LogUtils.d(e.message)\n                    } else {\n                        toast(\"自动化启动失败：${e.message}\")\n                        LogUtils.d(e)\n                    }\n                }\n            } finally {\n                loading.value = false\n            }\n        }\n    }\n}\n\nval uiAutomationFlow = MutableStateFlow<AutomationService?>(null)\nval automationRegisteredExceptionFlow = MutableStateFlow<Exception?>(null)\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/CommandResult.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class CommandResult(\n    val code: Int?,\n    val result: String,\n    val error: String?\n) : Parcelable {\n    val ok: Boolean\n        get() = code == 0\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/HiddenCast.kt",
    "content": "@file:Suppress(\"CAST_NEVER_SUCCEEDS\")\n\npackage li.songe.gkd.shizuku\n\nimport android.accessibilityservice.AccessibilityServiceInfo\nimport android.accessibilityservice.AccessibilityServiceInfoHidden\nimport android.app.UiAutomation\nimport android.app.UiAutomationHidden\nimport android.content.pm.PackageInfo\nimport android.content.pm.PackageInfoHidden\nimport android.view.KeyEvent\nimport android.view.KeyEventHidden\nimport android.view.MotionEvent\nimport android.view.MotionEventHidden\nimport android.view.accessibility.AccessibilityNodeInfo\nimport android.view.accessibility.AccessibilityNodeInfoHidden\n\n// Ignoring an implementation of the method `a.getCasted(b)` because it has multiple definitions\n\ninline val UiAutomationHidden.castedHidden get() = this as UiAutomation\ninline val UiAutomation.casted get() = this as UiAutomationHidden\n\ninline val AccessibilityNodeInfo.casted get() = this as AccessibilityNodeInfoHidden\n\ninline val AccessibilityServiceInfo.casted get() = this as AccessibilityServiceInfoHidden\n\ninline val KeyEvent.casted get() = this as KeyEventHidden\n\ninline val MotionEvent.casted get() = this as MotionEventHidden\n\ninline val PackageInfo.casted get() = this as PackageInfoHidden\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/InputManager.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.content.Context\nimport android.hardware.input.IInputManager\nimport android.view.InputEvent\nimport androidx.annotation.WorkerThread\nimport li.songe.gkd.util.AndroidTarget\n\n\nclass SafeInputManager(private val value: IInputManager) {\n    companion object {\n        fun newBinder() = getShizukuService(Context.INPUT_SERVICE)?.let {\n            SafeInputManager(IInputManager.Stub.asInterface(it))\n        }\n    }\n\n    private val command = InputShellCommand(this)\n\n    fun compatInjectInputEvent(\n        ev: InputEvent,\n        mode: Int,\n    ) = safeInvokeShizuku {\n        if (AndroidTarget.TIRAMISU) {\n            // https://github.com/android-cs/16/blob/main/core/java/android/hardware/input/InputManagerGlobal.java#L1707\n            value.injectInputEventToTarget(ev, mode, android.os.Process.INVALID_UID)\n        } else {\n            value.injectInputEvent(ev, mode)\n        }\n    }\n\n    @WorkerThread\n    fun tap(x: Float, y: Float, duration: Long = 0) {\n        if (duration > 0) {\n            command.runSwipe(x, y, x, y, duration)\n        } else {\n            command.runTap(x, y)\n        }\n    }\n\n    fun key(keyCode: Int) = command.runKeyEvent(keyCode)\n\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/InputShellCommand.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.hardware.input.InputManagerHidden\nimport android.os.Build\nimport android.os.SystemClock\nimport android.view.Display\nimport android.view.InputDevice\nimport android.view.KeyCharacterMap\nimport android.view.KeyEvent\nimport android.view.MotionEvent\nimport android.view.MotionEvent.PointerCoords\nimport android.view.MotionEvent.PointerProperties\nimport android.view.MotionEventHidden\nimport android.view.ViewConfiguration\nimport androidx.annotation.RequiresApi\nimport li.songe.gkd.util.AndroidTarget\nimport java.util.Map\nimport kotlin.math.floor\n\n\n// https://github.com/android-cs/16/blob/main/services/core/java/com/android/server/input/InputShellCommand.java\n@Suppress(\"SameParameterValue\")\nclass InputShellCommand(val safeInputManager: SafeInputManager) {\n    companion object {\n        private const val DEFAULT_DEVICE_ID = 0\n        private const val DEFAULT_SIZE = 1.0f\n        private const val DEFAULT_META_STATE = 0\n        private const val DEFAULT_PRECISION_X = 1.0f\n        private const val DEFAULT_PRECISION_Y = 1.0f\n        private const val DEFAULT_EDGE_FLAGS = 0\n        private const val DEFAULT_BUTTON_STATE = 0\n        private const val DEFAULT_FLAGS = 0\n        private const val SECOND_IN_MILLISECONDS = 1000L\n        private const val SWIPE_EVENT_HZ_DEFAULT = 120\n    }\n\n    fun runTap(x: Float, y: Float) {\n        sendTap(InputDevice.SOURCE_TOUCHSCREEN, x, y, Display.INVALID_DISPLAY)\n    }\n\n    fun runSwipe(x1: Float, y1: Float, x2: Float, y2: Float, duration: Long) {\n        sendSwipe(\n            InputDevice.SOURCE_TOUCHSCREEN,\n            x1,\n            y1,\n            x2,\n            y2,\n            duration,\n            Display.INVALID_DISPLAY,\n            false,\n        )\n    }\n\n    private fun sendSwipe(\n        inputSource: Int,\n        x1: Float,\n        y1: Float,\n        x2: Float,\n        y2: Float,\n        duration: Long,\n        displayId: Int,\n        isDragDrop: Boolean,\n    ) {\n        val down = SystemClock.uptimeMillis()\n        injectMotionEvent(\n            inputSource, MotionEvent.ACTION_DOWN, down, down, x1, y1, 1.0f,\n            displayId\n        )\n        if (isDragDrop) {\n            // long press until drag start.\n            sleep(ViewConfiguration.getLongPressTimeout().toLong())\n        }\n        var now = SystemClock.uptimeMillis()\n        val endTime = down + duration\n        val swipeEventPeriodMillis: Float =\n            SECOND_IN_MILLISECONDS.toFloat() / SWIPE_EVENT_HZ_DEFAULT\n        var injected = 1\n        while (now < endTime) {\n            // Ensure that we inject at most at the frequency of SWIPE_EVENT_HZ_DEFAULT\n            // by waiting an additional delta between the actual time and expected time.\n            var elapsedTime = now - down\n            val errorMillis =\n                floor((injected * swipeEventPeriodMillis - elapsedTime).toDouble()).toLong()\n            if (errorMillis > 0) {\n                // Make sure not to exceed the duration and inject an extra event.\n                if (errorMillis > endTime - now) {\n                    sleep(endTime - now)\n                    break\n                }\n                sleep(errorMillis)\n            }\n            now = SystemClock.uptimeMillis()\n            elapsedTime = now - down\n            val alpha = elapsedTime.toFloat() / duration\n            injectMotionEvent(\n                inputSource, MotionEvent.ACTION_MOVE, down, now,\n                lerp(x1, x2, alpha), lerp(y1, y2, alpha), 1.0f, displayId\n            )\n            injected++\n            now = SystemClock.uptimeMillis()\n        }\n        injectMotionEvent(\n            inputSource, MotionEvent.ACTION_UP, down, now, x2, y2, 0.0f,\n            displayId\n        )\n    }\n\n    private fun sendTap(\n        inputSource: Int,\n        x: Float,\n        y: Float,\n        displayId: Int,\n    ) {\n        val now = SystemClock.uptimeMillis()\n        injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, now, x, y, 1.0f, displayId)\n        injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, now, x, y, 0.0f, displayId)\n    }\n\n    private fun injectMotionEvent(\n        inputSource: Int,\n        action: Int,\n        downTime: Long,\n        mWhen: Long,\n        x: Float,\n        y: Float,\n        pressure: Float,\n        displayId: Int\n    ) {\n        if (AndroidTarget.S) {\n            val axisValues = Map.of<Int, Float>(\n                MotionEvent.AXIS_X, x, MotionEvent.AXIS_Y, y, MotionEvent.AXIS_PRESSURE, pressure\n            )\n            injectMotionEvent(inputSource, action, downTime, mWhen, axisValues, displayId)\n        } else {\n            // https://github.com/android-cs/11/blob/main/cmds/input/src/com/android/commands/input/Input.java#L382\n            val event = MotionEvent.obtain(\n                downTime, mWhen, action, x, y, pressure, DEFAULT_SIZE,\n                DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y,\n                getInputDeviceId(inputSource), DEFAULT_EDGE_FLAGS\n            )\n            event.setSource(inputSource)\n            // https://github.com/android-cs/9/blob/main/cmds/input/src/com/android/commands/input/Input.java#L298\n            if (AndroidTarget.Q) {\n                var mDisplayId = displayId\n                if (mDisplayId == Display.INVALID_DISPLAY && (inputSource and InputDevice.SOURCE_CLASS_POINTER) != 0) {\n                    mDisplayId = Display.DEFAULT_DISPLAY\n                }\n                event.casted.setDisplayId(mDisplayId)\n            }\n            safeInputManager.compatInjectInputEvent(\n                event, InputManagerHidden.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH\n            )\n        }\n    }\n\n    @RequiresApi(Build.VERSION_CODES.Q)\n    @Suppress(\"KotlinConstantConditions\")\n    private fun injectMotionEvent(\n        inputSource: Int,\n        action: Int,\n        downTime: Long,\n        mWhen: Long,\n        axisValues: MutableMap<Int, Float>,\n        displayId: Int\n    ) {\n        val pointerCount = 1\n        val pointerProperties = arrayOfNulls<PointerProperties>(pointerCount)\n        for (i in 0..<pointerCount) {\n            pointerProperties[i] = PointerProperties().apply {\n                id = i\n                toolType = getToolType(inputSource)\n            }\n        }\n        val pointerCoords = arrayOfNulls<PointerCoords>(pointerCount)\n        for (i in 0..<pointerCount) {\n            pointerCoords[i] = PointerCoords().apply {\n                size = DEFAULT_SIZE\n                for (entry in axisValues.entries) {\n                    setAxisValue(entry.key, entry.value)\n                }\n            }\n        }\n        var mDisplayId = displayId\n        if (mDisplayId == Display.INVALID_DISPLAY && (inputSource and InputDevice.SOURCE_CLASS_POINTER) != 0) {\n            mDisplayId = Display.DEFAULT_DISPLAY\n        }\n        val event = MotionEventHidden.obtain(\n            downTime,\n            mWhen,\n            action,\n            pointerCount,\n            pointerProperties,\n            pointerCoords,\n            DEFAULT_META_STATE,\n            DEFAULT_BUTTON_STATE,\n            DEFAULT_PRECISION_X,\n            DEFAULT_PRECISION_Y,\n            getInputDeviceId(inputSource),\n            DEFAULT_EDGE_FLAGS,\n            inputSource,\n            mDisplayId,\n            DEFAULT_FLAGS,\n        )\n        safeInputManager.compatInjectInputEvent(\n            event, InputManagerHidden.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH\n        )\n    }\n\n    private fun getInputDeviceId(inputSource: Int): Int {\n        val devIds = InputDevice.getDeviceIds()\n        for (devId in devIds) {\n            val inputDev = InputDevice.getDevice(devId)!!\n            if (inputDev.supportsSource(inputSource)) {\n                return devId\n            }\n        }\n        return DEFAULT_DEVICE_ID\n    }\n\n    private fun getToolType(inputSource: Int): Int = when (inputSource) {\n        InputDevice.SOURCE_MOUSE, InputDevice.SOURCE_MOUSE_RELATIVE, InputDevice.SOURCE_TRACKBALL -> MotionEvent.TOOL_TYPE_MOUSE\n        InputDevice.SOURCE_STYLUS, InputDevice.SOURCE_BLUETOOTH_STYLUS -> MotionEvent.TOOL_TYPE_STYLUS\n        InputDevice.SOURCE_TOUCHPAD, InputDevice.SOURCE_TOUCHSCREEN, InputDevice.SOURCE_TOUCH_NAVIGATION -> MotionEvent.TOOL_TYPE_FINGER\n        else -> MotionEvent.TOOL_TYPE_UNKNOWN\n    }\n\n    private fun sleep(milliseconds: Long) {\n        try {\n            Thread.sleep(milliseconds)\n        } catch (e: InterruptedException) {\n            throw RuntimeException(e)\n        }\n    }\n\n    private fun lerp(a: Float, b: Float, alpha: Float): Float {\n        return (b - a) * alpha + a\n    }\n\n    fun runKeyEvent(keyCode: Int) {\n        sendKeyEvent(keyCode)\n    }\n\n    private fun sendKeyEvent(keyCode: Int) {\n        val inputSource = InputDevice.SOURCE_UNKNOWN\n        val displayId = Display.INVALID_DISPLAY\n        val async = false\n\n        val now = SystemClock.uptimeMillis()\n        val event = KeyEvent(\n            now, now, KeyEvent.ACTION_DOWN, keyCode, 0,\n            0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,\n            inputSource\n        )\n        if (AndroidTarget.Q) {\n            event.casted.setDisplayId(displayId)\n        }\n        injectKeyEvent(event, async)\n        val event2 = KeyEvent.changeTimeRepeat(event, SystemClock.uptimeMillis(), 0)\n        injectKeyEvent(KeyEvent.changeAction(event2, KeyEvent.ACTION_UP), async)\n    }\n\n    private fun injectKeyEvent(event: KeyEvent, async: Boolean) {\n        val injectMode: Int = if (async) {\n            InputManagerHidden.INJECT_INPUT_EVENT_MODE_ASYNC\n        } else {\n            InputManagerHidden.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH\n        }\n        safeInputManager.compatInjectInputEvent(event, injectMode)\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/PackageManager.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.Manifest\nimport android.content.pm.IPackageManager\nimport android.content.pm.PackageInfo\nimport li.songe.gkd.META\nimport li.songe.gkd.app\nimport li.songe.gkd.permission.Manifest_permission_GET_APP_OPS_STATS\nimport li.songe.gkd.permission.canQueryPkgState\nimport li.songe.gkd.util.AndroidTarget\n\n\nclass SafePackageManager(private val value: IPackageManager) {\n    companion object {\n\n        fun newBinder() = getShizukuService(\"package\")?.let {\n            SafePackageManager(IPackageManager.Stub.asInterface(it))\n        }\n\n        private var canUseGetInstalledApps = true\n    }\n\n    val isSafeMode get() = safeInvokeShizuku { value.isSafeMode }\n\n    fun getInstalledPackages(\n        flags: Int,\n        userId: Int = currentUserId,\n    ): List<PackageInfo> = safeInvokeShizuku {\n        if (AndroidTarget.TIRAMISU) {\n            value.getInstalledPackages(flags.toLong(), userId).list\n        } else {\n            value.getInstalledPackages(flags, userId).list\n        }\n    } ?: emptyList()\n\n    @Suppress(\"unused\")\n    fun getPackageInfo(\n        packageName: String,\n        flags: Int,\n        userId: Int,\n    ): PackageInfo? = safeInvokeShizuku {\n        if (AndroidTarget.TIRAMISU) {\n            value.getPackageInfo(packageName, flags.toLong(), userId)\n        } else {\n            value.getPackageInfo(packageName, flags, userId)\n        }\n    }\n\n    fun getApplicationEnabledSetting(\n        packageName: String,\n        userId: Int,\n    ): Int = safeInvokeShizuku {\n        value.getApplicationEnabledSetting(packageName, userId)\n    } ?: 0\n\n    private fun grantRuntimePermission(\n        packageName: String,\n        permissionName: String,\n        userId: Int = currentUserId,\n    ) = safeInvokeShizuku {\n        value.grantRuntimePermission(\n            packageName,\n            permissionName,\n            userId\n        )\n    }\n\n    private fun grantSelfPermission(name: String, skipCheck: Boolean = false) {\n        if (!skipCheck) {\n            if (app.checkGrantedPermission(name)) return\n        }\n        grantRuntimePermission(\n            packageName = META.appId,\n            permissionName = name,\n        )\n    }\n\n    fun allowAllSelfPermission() {\n        if (canUseGetInstalledApps && !canQueryPkgState.value) {\n            try {\n                grantSelfPermission(\"com.android.permission.GET_INSTALLED_APPS\", skipCheck = true)\n            } catch (_: IllegalArgumentException) {\n                canUseGetInstalledApps = false\n            }\n        }\n        grantSelfPermission(Manifest_permission_GET_APP_OPS_STATS)\n        grantSelfPermission(Manifest.permission.WRITE_SECURE_SETTINGS)\n        if (AndroidTarget.TIRAMISU) {\n            grantSelfPermission(Manifest.permission.POST_NOTIFICATIONS)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/ProxyUiAutomationConnection.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.accessibilityservice.AccessibilityServiceInfo\nimport android.accessibilityservice.AccessibilityServiceInfoHidden\nimport android.accessibilityservice.IAccessibilityServiceClient\nimport android.app.IUiAutomationConnection\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport android.os.Binder\nimport android.os.Build\nimport android.os.RemoteException\nimport android.view.Display.DEFAULT_DISPLAY\nimport android.view.accessibility.AccessibilityEvent\nimport android.window.ScreenCapture\nimport androidx.annotation.RequiresApi\nimport li.songe.gkd.util.AndroidTarget\nimport rikka.shizuku.Shizuku\n\n\n// https://diff.songe.li/?ref=UiAutomationConnection\nclass ProxyUiAutomationConnection : IUiAutomationConnection.Stub() {\n    companion object {\n        private const val INITIAL_FROZEN_ROTATION_UNSPECIFIED = -1\n    }\n\n    private val mLock = Any()\n    private val mToken = Binder()\n    private var mClient: IAccessibilityServiceClient? = null\n    private var mInitialFrozenRotation = INITIAL_FROZEN_ROTATION_UNSPECIFIED\n    private var mIsShutdown = false\n    private var mOwningUid = 0\n    private val mWindowManager\n        get() = shizukuContextFlow.value.wmManager?.value ?: throw ShizukuOffException()\n    private val manager\n        get() = shizukuContextFlow.value.a11yManager?.value ?: throw ShizukuOffException()\n\n    override fun connect(\n        client: IAccessibilityServiceClient?,\n        flags: Int,\n    ) {\n        if (client == null) {\n            throw IllegalArgumentException(\"Client cannot be null!\")\n        }\n        synchronized(mLock) {\n            throwIfShutdownLocked()\n            if (isConnectedLocked()) {\n                throw IllegalStateException(\"Already connected.\")\n            }\n            mOwningUid = Shizuku.getUid() // Binder.getCallingUid()\n            registerUiTestAutomationServiceLocked(client, currentUserId, flags)\n            storeRotationStateLocked()\n        }\n    }\n\n\n    override fun disconnect() {\n        synchronized(mLock) {\n            throwIfCalledByNotTrustedUidLocked()\n            throwIfShutdownLocked()\n            if (!isConnectedLocked()) {\n                throw IllegalStateException(\"Already disconnected.\")\n            }\n            mOwningUid = -1\n            unregisterUiTestAutomationServiceLocked()\n            restoreRotationStateLocked()\n        }\n    }\n\n    override fun shutdown() {\n        synchronized(mLock) {\n            if (isConnectedLocked()) {\n                throwIfCalledByNotTrustedUidLocked()\n            }\n            throwIfShutdownLocked()\n            mIsShutdown = true\n            if (isConnectedLocked()) {\n                disconnect()\n            }\n        }\n    }\n\n    // https://diff.songe.li/?ref=UiAutomationConnection.takeScreenshot\n    override fun takeScreenshot(width: Int, height: Int): Bitmap? {\n        synchronized(mLock) {\n            throwIfCalledByNotTrustedUidLocked()\n            throwIfShutdownLocked()\n            throwIfNotConnectedLocked()\n        }\n        val identity = clearCallingIdentity()\n        try {\n            return shizukuContextFlow.value.serviceWrapper?.run {\n                userService.takeScreenshot1(width, height)\n            }\n        } finally {\n            restoreCallingIdentity(identity)\n        }\n    }\n\n    override fun takeScreenshot(\n        crop: Rect,\n        rotation: Int,\n    ): Bitmap? {\n        synchronized(mLock) {\n            throwIfCalledByNotTrustedUidLocked()\n            throwIfShutdownLocked()\n            throwIfNotConnectedLocked()\n        }\n        val identity = clearCallingIdentity()\n        try {\n            return shizukuContextFlow.value.serviceWrapper?.run {\n                userService.takeScreenshot2(crop, rotation)\n            }\n        } finally {\n            restoreCallingIdentity(identity)\n        }\n    }\n\n    override fun takeScreenshot(crop: Rect): Bitmap? {\n        synchronized(mLock) {\n            throwIfCalledByNotTrustedUidLocked()\n            throwIfShutdownLocked()\n            throwIfNotConnectedLocked()\n        }\n        val identity = clearCallingIdentity()\n        try {\n            if (AndroidTarget.UPSIDE_DOWN_CAKE) {\n                val captureArgs = ScreenCapture.CaptureArgs.Builder()\n                    .setSourceCrop(crop)\n                    .build()\n                val syncScreenCapture = ScreenCapture.createSyncCaptureListener()\n                mWindowManager.captureDisplay(DEFAULT_DISPLAY, captureArgs, syncScreenCapture)\n                val screenshotBuffer = syncScreenCapture.buffer\n                return screenshotBuffer?.asBitmap()\n            } else {\n                return shizukuContextFlow.value.serviceWrapper?.run {\n                    userService.takeScreenshot3(crop)\n                }\n            }\n        } catch (re: RemoteException) {\n            throw re.rethrowAsRuntimeException()\n        } finally {\n            restoreCallingIdentity(identity)\n        }\n    }\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    override fun takeScreenshot(\n        crop: Rect,\n        listener: ScreenCapture.ScreenCaptureListener,\n    ): Boolean {\n        synchronized(mLock) {\n            throwIfCalledByNotTrustedUidLocked()\n            throwIfShutdownLocked()\n            throwIfNotConnectedLocked()\n        }\n        val identity = clearCallingIdentity()\n        try {\n            val captureArgs = ScreenCapture.CaptureArgs.Builder()\n                .setSourceCrop(crop)\n                .build()\n            mWindowManager.captureDisplay(DEFAULT_DISPLAY, captureArgs, listener)\n        } catch (re: RemoteException) {\n            throw re.rethrowAsRuntimeException()\n        } finally {\n            restoreCallingIdentity(identity)\n        }\n        return true\n    }\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    override fun takeScreenshot(\n        crop: Rect,\n        listener: ScreenCapture.ScreenCaptureListener,\n        displayId: Int,\n    ): Boolean {\n        synchronized(mLock) {\n            throwIfCalledByNotTrustedUidLocked()\n            throwIfShutdownLocked()\n            throwIfNotConnectedLocked()\n        }\n        val identity = clearCallingIdentity()\n        try {\n            val captureArgs = ScreenCapture.CaptureArgs.Builder()\n                .setSourceCrop(crop)\n                .build()\n            mWindowManager.captureDisplay(displayId, captureArgs, listener)\n        } catch (re: RemoteException) {\n            throw re.rethrowAsRuntimeException()\n        } finally {\n            restoreCallingIdentity(identity)\n        }\n        return true\n    }\n\n    private fun registerUiTestAutomationServiceLocked(\n        client: IAccessibilityServiceClient,\n        userId: Int,\n        flags: Int,\n    ) {\n        // see app/src/main/res/xml/ab_desc.xml\n        val info = AccessibilityServiceInfo().apply {\n            eventTypes =\n                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED or AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED\n            feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK\n            this.flags = (this.flags or\n                    AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS or\n                    AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS or\n                    AccessibilityServiceInfo.DEFAULT or\n                    AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS or\n                    AccessibilityServiceInfoHidden.FLAG_FORCE_DIRECT_BOOT_AWARE)\n        }\n        info.casted.apply {\n            setCapabilities(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT)\n            if (AndroidTarget.UPSIDE_DOWN_CAKE) {\n                setAccessibilityTool(true)\n            }\n        }\n        try {\n            if (AndroidTarget.UPSIDE_DOWN_CAKE) {\n                manager.registerUiTestAutomationService(mToken, client, info, userId, flags)\n            } else {\n                manager.registerUiTestAutomationService(mToken, client, info, flags)\n            }\n            mClient = client\n        } catch (re: RemoteException) {\n            throw IllegalStateException(\n                \"Error while registering UiTestAutomationService for \"\n                        + \"user \" + userId + \".\", re\n            )\n        }\n    }\n\n    private fun unregisterUiTestAutomationServiceLocked() {\n        manager.unregisterUiTestAutomationService(mClient)\n        mClient = null\n    }\n\n    private fun storeRotationStateLocked() {\n        try {\n            if (mWindowManager.isRotationFrozen()) {\n                mInitialFrozenRotation = mWindowManager.getDefaultDisplayRotation()\n            }\n        } catch (_: RemoteException) {\n        }\n    }\n\n    private fun restoreRotationStateLocked() {\n        try {\n            if (mInitialFrozenRotation != INITIAL_FROZEN_ROTATION_UNSPECIFIED) {\n                if (AndroidTarget.UPSIDE_DOWN_CAKE) {\n                    mWindowManager.freezeRotation(\n                        mInitialFrozenRotation,\n                        \"UiAutomationConnection#restoreRotationStateLocked\"\n                    )\n                } else {\n                    mWindowManager.freezeRotation(mInitialFrozenRotation)\n                }\n            } else {\n                if (AndroidTarget.UPSIDE_DOWN_CAKE) {\n                    mWindowManager.thawRotation(\"UiAutomationConnection#restoreRotationStateLocked\")\n                } else {\n                    mWindowManager.thawRotation()\n                }\n            }\n        } catch (_: RemoteException) {\n        }\n    }\n\n    private fun throwIfShutdownLocked() {\n        if (mIsShutdown) {\n            throw IllegalStateException(\"Connection shutdown!\")\n        }\n    }\n\n    private fun isConnectedLocked(): Boolean = mClient != null\n\n    private fun throwIfCalledByNotTrustedUidLocked() {\n        val callingUid = Shizuku.getUid()\n        if (callingUid != mOwningUid && mOwningUid != android.os.Process.SYSTEM_UID && callingUid != 0) {\n            throw SecurityException(\"Calling from not trusted UID!\")\n        }\n    }\n\n    private fun throwIfNotConnectedLocked() {\n        if (!isConnectedLocked()) {\n            throw IllegalStateException(\"Not connected!\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/ShizukuApi.kt",
    "content": "package li.songe.gkd.shizuku\n\n\nimport android.content.ComponentName\nimport android.content.pm.PackageManager\nimport androidx.annotation.WorkerThread\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport li.songe.gkd.app\nimport li.songe.gkd.appScope\nimport li.songe.gkd.isActivityVisible\nimport li.songe.gkd.permission.shizukuGrantedState\nimport li.songe.gkd.permission.updatePermissionState\nimport li.songe.gkd.service.ExposeService\nimport li.songe.gkd.service.StatusService\nimport li.songe.gkd.service.currentAppBlocked\nimport li.songe.gkd.service.currentAppUseA11y\nimport li.songe.gkd.service.updateTopTaskAppId\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.MutexState\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.toast\nimport rikka.shizuku.Shizuku\nimport rikka.shizuku.ShizukuBinderWrapper\nimport rikka.shizuku.SystemServiceHelper\nimport java.lang.reflect.Method\nimport kotlin.system.exitProcess\n\ninline fun <T> safeInvokeShizuku(\n    block: () -> T\n): T? = try {\n    block()\n} catch (_: ShizukuOffException) {\n    null\n} catch (e: IllegalStateException) {\n    // https://github.com/RikkaApps/Shizuku-API/blob/a27f6e4151ba7b39965ca47edb2bf0aeed7102e5/api/src/main/java/rikka/shizuku/Shizuku.java#L430\n    if (e.message == \"binder haven't been received\") {\n        null\n    } else {\n        throw e\n    }\n}\n\nclass ShizukuOffException : IllegalStateException(\"Shizuku is off\")\n\nfun getShizukuService(name: String): ShizukuBinderWrapper? {\n    return SystemServiceHelper.getSystemService(name)?.let(::ShizukuBinderWrapper)\n}\n\nprivate fun Method.simpleString(): String {\n    return \"${name}(${parameterTypes.joinToString(\",\") { it.name }}):${returnType.name}\"\n}\n\nfun Class<*>.detectHiddenMethod(\n    methodName: String,\n    vararg args: Pair<Int, List<Class<*>>>,\n): Int {\n    val methodsVal = methods\n    methodsVal.forEach { method ->\n        if (method.name == methodName) {\n            val types = method.parameterTypes.toList()\n            args.forEach { (value, argTypes) ->\n                if (types == argTypes) {\n                    return value\n                }\n            }\n        }\n    }\n    val result = methodsVal.filter { it.name == methodName }\n    if (result.isEmpty()) {\n        throw NoSuchMethodException(\"${name}::${methodName} not found\")\n    } else {\n        LogUtils.d(\"detectHiddenMethod\", *result.map { it.simpleString() }.toTypedArray())\n        throw NoSuchMethodException(\"${name}::${methodName} not match\")\n    }\n}\n\n// https://github.com/android-cs/16/blob/main/packages/Shell/AndroidManifest.xml\nprivate fun checkRemotePermission(permission: String): Boolean {\n    return Shizuku.checkRemotePermission(permission) == PackageManager.PERMISSION_GRANTED\n}\n\nprivate val isAdbRestricted: Boolean\n    get() {\n        if (!checkRemotePermission(\"android.permission.GRANT_RUNTIME_PERMISSIONS\")) {\n            return true\n        }\n        if (AndroidTarget.P && !checkRemotePermission(\"android.permission.MANAGE_APP_OPS_MODES\")) {\n            return true\n        }\n        return false\n    }\n\nclass ShizukuContext(\n    val serviceWrapper: UserServiceWrapper?,\n    val packageManager: SafePackageManager?,\n    val userManager: SafeUserManager?,\n    val activityManager: SafeActivityManager?,\n    val activityTaskManager: SafeActivityTaskManager?,\n    val appOpsService: SafeAppOpsService?,\n    val inputManager: SafeInputManager?,\n    val a11yManager: SafeAccessibilityManager?,\n    val wmManager: SafeWindowManager?,\n) {\n    val ok get() = this !== defaultShizukuContext\n    fun destroy() {\n        serviceWrapper?.destroy()\n        if (activityTaskManager != null) {\n            activityTaskManager.unregisterDefault()\n        } else {\n            activityManager?.unregisterDefault()\n        }\n    }\n\n    val states = listOf(\n        \"IUserService\" to serviceWrapper,\n        \"IActivityManager\" to activityManager,\n        \"IActivityTaskManager\" to activityTaskManager,\n        \"IAppOpsService\" to appOpsService,\n        \"IInputManager\" to inputManager,\n        \"IPackageManager\" to packageManager,\n        \"IUserManager\" to userManager,\n        \"IAccessibilityManager\" to a11yManager,\n        \"IWindowManager\" to wmManager,\n    )\n\n    fun grantSelf() {\n        packageManager ?: return\n        appOpsService ?: return\n        if (isAdbRestricted) return\n        appOpsService.allowAllSelfMode()\n        packageManager.allowAllSelfPermission()\n    }\n\n    @WorkerThread\n    fun tap(x: Float, y: Float, duration: Long = 0): Boolean {\n        return serviceWrapper?.tap(x, y, duration) ?: (inputManager?.tap(x, y, duration) != null)\n    }\n\n    fun topCpn(): ComponentName? {\n        return (activityTaskManager?.getTasks()\n            ?: activityManager?.getTasks())?.firstOrNull()?.topActivity\n    }\n\n    init {\n        if (activityTaskManager != null) {\n            activityTaskManager.registerDefault()\n        } else {\n            activityManager?.registerDefault()\n        }\n        grantSelf()\n        // 某些情况下存在残留进程\n        val size = serviceWrapper?.userService?.killLegacyService()\n        if (size != null && size > 0) {\n            LogUtils.d(\"killLegacyService $size\")\n        }\n    }\n}\n\nprivate val defaultShizukuContext by lazy {\n    ShizukuContext(\n        serviceWrapper = null,\n        packageManager = null,\n        userManager = null,\n        activityManager = null,\n        activityTaskManager = null,\n        appOpsService = null,\n        inputManager = null,\n        a11yManager = null,\n        wmManager = null,\n    )\n}\n\nval currentUserId by lazy { android.os.Process.myUserHandle().hashCode() }\n\nval shizukuContextFlow by lazy { MutableStateFlow(defaultShizukuContext) }\n\nval shizukuUsedFlow by lazy {\n    combine(\n        shizukuGrantedState.stateFlow,\n        storeFlow.map { it.enableShizuku },\n    ) { a, b ->\n        a && b\n    }.stateIn(appScope, SharingStarted.Eagerly, false)\n}\n\nval updateBinderMutex = MutexState()\nprivate fun updateShizukuBinder() = updateBinderMutex.launchTry(appScope, Dispatchers.IO) {\n    if (shizukuUsedFlow.value) {\n        if (!app.justStarted) {\n            toast(\"正在连接 Shizuku 服务...\")\n        }\n        val shizukuContext = ShizukuContext(\n            serviceWrapper = buildServiceWrapper(),\n            packageManager = SafePackageManager.newBinder(),\n            userManager = SafeUserManager.newBinder(),\n            activityManager = SafeActivityManager.newBinder(),\n            activityTaskManager = SafeActivityTaskManager.newBinder(),\n            appOpsService = SafeAppOpsService.newBinder(),\n            inputManager = SafeInputManager.newBinder(),\n            a11yManager = SafeAccessibilityManager.newBinder(),\n            wmManager = SafeWindowManager.newBinder(),\n        )\n        shizukuContextFlow.value = shizukuContext\n        shizukuContext.topCpn()?.let { cpn ->\n            updateTopTaskAppId(cpn.packageName)\n        }\n        if (\n            storeFlow.value.useAutomation &&\n            !currentAppBlocked &&\n            !currentAppUseA11y\n        ) {\n            AutomationService.tryConnect(true)\n        }\n        updatePermissionState()\n        if (StatusService.needRestart) {\n            //\n            shizukuContext.activityManager?.startForegroundService(ExposeService.exposeIntent(expose = -1))\n        }\n        val delayMillis = if (app.justStarted) 1200L else 0L\n        if (shizukuContext.serviceWrapper == null) {\n            if (shizukuContext.packageManager != null) {\n                toast(\"Shizuku 服务连接部分失败\", delayMillis = delayMillis)\n            } else {\n                toast(\"Shizuku 服务连接失败\", delayMillis = delayMillis)\n            }\n        } else {\n            toast(\"Shizuku 服务连接成功\", delayMillis = delayMillis)\n        }\n    } else if (shizukuContextFlow.value.ok) {\n        val willRelaunch = uiAutomationFlow.value != null && !shizukuGrantedState.updateAndGet()\n        if (willRelaunch) {\n            // 需要重启应用让系统释放 UiAutomation\n            killRelaunchApp()\n        } else {\n            uiAutomationFlow.value?.shutdown(true)\n            shizukuContextFlow.value.destroy()\n            shizukuContextFlow.value = defaultShizukuContext\n            toast(\"Shizuku 服务已断开\")\n        }\n    }\n}\n\nprivate suspend fun killRelaunchApp() {\n    if (isActivityVisible) {\n        toast(\"Shizuku 断开，重启应用以释放自动化服务\", forced = true)\n        delay(1500)\n        app.startLaunchActivity()\n    } else {\n        toast(\"Shizuku 断开，结束应用以释放自动化服务\", forced = true)\n        delay(1500)\n    }\n    android.os.Process.killProcess(android.os.Process.myPid())\n    exitProcess(0)\n}\n\nfun initShizuku() {\n    Shizuku.addBinderReceivedListener {\n        LogUtils.d(\"Shizuku.addBinderReceivedListener\")\n        appScope.launchTry(Dispatchers.IO) {\n            shizukuGrantedState.updateAndGet()\n        }\n    }\n    Shizuku.addBinderDeadListener {\n        LogUtils.d(\"Shizuku.addBinderDeadListener\")\n        shizukuGrantedState.stateFlow.value = false\n    }\n    appScope.launchTry {\n        shizukuUsedFlow.collect { updateShizukuBinder() }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/TaskStackListener.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.app.ActivityManager\nimport android.app.ITaskStackListener\nimport android.content.ComponentName\nimport android.os.Parcel\nimport li.songe.gkd.a11y.ActivityScene\nimport li.songe.gkd.a11y.updateTopActivity\n\nobject FixedTaskStackListener : ITaskStackListener.Stub() {\n\n    // https://github.com/gkd-kit/gkd/issues/941#issuecomment-2784035441\n    override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = try {\n        super.onTransact(code, data, reply, flags)\n    } catch (_: Throwable) {\n        true\n    }\n\n    override fun onTaskStackChanged() {\n        val cpn = shizukuContextFlow.value.topCpn() ?: return\n        synchronized(this) {\n            if (lastFront.first > 0 && lastFront.second == cpn && System.currentTimeMillis() - lastFront.first > 200) {\n                lastFront = defaultFront\n                return\n            }\n        }\n        updateTopActivity(\n            appId = cpn.packageName,\n            activityId = cpn.className,\n            scene = ActivityScene.TaskStack,\n        )\n    }\n\n    private val defaultFront = 0L to ComponentName(\"\", \"\")\n    private var lastFront = defaultFront\n    private fun onTaskMovedToFrontCompat(cpn: ComponentName? = null) {\n        val cpn = cpn ?: shizukuContextFlow.value.topCpn() ?: return\n        synchronized(this) {\n            lastFront = System.currentTimeMillis() to cpn\n        }\n        updateTopActivity(\n            appId = cpn.packageName,\n            activityId = cpn.className,\n            scene = ActivityScene.TaskStack,\n        )\n    }\n\n    override fun onTaskMovedToFront(taskId: Int) {\n        onTaskMovedToFrontCompat()\n    }\n\n    override fun onTaskMovedToFront(taskInfo: ActivityManager.RunningTaskInfo) {\n        onTaskMovedToFrontCompat(taskInfo.topActivity)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/UserManager.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.content.Context\nimport android.os.IUserManager\nimport li.songe.gkd.data.UserInfo\n\nclass SafeUserManager(private val value: IUserManager) {\n    companion object {\n\n        fun newBinder() = getShizukuService(Context.USER_SERVICE)?.let {\n            SafeUserManager(IUserManager.Stub.asInterface(it))\n        }\n\n        private val getUsersType by lazy {\n            IUserManager::class.java.detectHiddenMethod(\n                \"getUsers\",\n                1 to listOf(Boolean::class.java),\n                2 to listOf(Boolean::class.java, Boolean::class.java, Boolean::class.java),\n            )\n        }\n    }\n\n    fun getUsers(\n        excludePartial: Boolean = true,\n        excludeDying: Boolean = true,\n        excludePreCreated: Boolean = true\n    ): List<UserInfo> = safeInvokeShizuku {\n        when (getUsersType) {\n            1 -> value.getUsers(excludeDying)\n            2 -> value.getUsers(excludePartial, excludeDying, excludePreCreated)\n            else -> value.getUsers(excludeDying)\n        }.map { UserInfo(id = it.id, name = it.name.trim()) }\n    } ?: emptyList()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/UserService.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.ServiceConnection\nimport android.graphics.Bitmap\nimport android.graphics.Rect\nimport android.os.IBinder\nimport android.util.Log\nimport android.view.SurfaceControlHidden\nimport androidx.annotation.Keep\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport kotlinx.coroutines.withTimeoutOrNull\nimport li.songe.gkd.META\nimport li.songe.gkd.permission.shizukuGrantedState\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.componentName\nimport rikka.shizuku.Shizuku\nimport java.io.DataOutputStream\nimport java.io.File\nimport kotlin.coroutines.resume\nimport kotlin.system.exitProcess\n\n\n// https://github.com/RikkaApps/Shizuku/issues/1171#issuecomment-2952442340\n@Keep\nclass UserService(val context: Context) : IUserService.Stub() {\n\n    init {\n        Log.d(\n            \"UserService\",\n            \"constructor(context=${context.packageName},pid=${android.os.Process.myPid()},uid=${android.os.Process.myUid()})\"\n        )\n    }\n\n    override fun destroy() {\n        Log.d(\"UserService\", \"destroy\")\n        exitProcess(0)\n    }\n\n    override fun exit() {\n        destroy()\n    }\n\n    override fun execCommand(command: String): CommandResult {\n        Log.d(\"UserService\", \"execCommand(command=$command)\")\n        val process = Runtime.getRuntime().exec(\"sh\")\n        val outputStream = DataOutputStream(process.outputStream)\n        val commandResult = try {\n            command.split('\\n').filter { it.isNotBlank() }.forEach {\n                outputStream.write(it.toByteArray())\n                outputStream.writeBytes('\\n'.toString())\n                outputStream.flush()\n            }\n            outputStream.writeBytes(\"exit\\n\")\n            outputStream.flush()\n            CommandResult(\n                code = process.waitFor(),\n                result = process.inputStream.bufferedReader().readText(),\n                error = process.errorStream.bufferedReader().readText(),\n            )\n        } catch (e: Exception) {\n            e.printStackTrace()\n            val message = e.message\n            val aimErrStr = \"error=\"\n            val index = message?.indexOf(aimErrStr)\n            val code = if (index != null) {\n                message.substring(index + aimErrStr.length)\n                    .takeWhile { c -> c.isDigit() }\n                    .toIntOrNull()\n            } else {\n                null\n            } ?: 1\n            CommandResult(\n                code = code,\n                result = \"\",\n                error = e.message,\n            )\n        } finally {\n            outputStream.close()\n            process.inputStream.close()\n            process.outputStream.close()\n            process.destroy()\n        }\n        return commandResult\n    }\n\n    override fun takeScreenshot1(width: Int, height: Int): Bitmap? {\n        return SurfaceControlHidden.screenshot(width, height)\n    }\n\n    override fun takeScreenshot2(\n        crop: Rect,\n        rotation: Int\n    ): Bitmap? {\n        val width = crop.width()\n        val height = crop.height()\n        return SurfaceControlHidden.screenshot(crop, width, height, rotation)\n    }\n\n    override fun takeScreenshot3(crop: Rect): Bitmap? {\n        val width = crop.width()\n        val height = crop.height()\n        val displayToken = SurfaceControlHidden.getInternalDisplayToken()\n        val captureArgs = SurfaceControlHidden.DisplayCaptureArgs.Builder(displayToken)\n            .setSourceCrop(crop)\n            .setSize(width, height)\n            .build()\n        val screenshotBuffer = SurfaceControlHidden.captureDisplay(captureArgs)\n        return screenshotBuffer?.asBitmap()\n    }\n\n    override fun killLegacyService(): Int {\n        val pid = android.os.Process.myPid()\n        val idReg = \"\\\\d+\".toRegex()\n        val legacyPids = execCommand(\"ps | grep '${context.packageName}:$shizukuPsSuffix'\")\n            .result.lineSequence()\n            .mapNotNull { idReg.find(it)?.value?.toInt() }\n            .filter { it != pid }.toList()\n        if (legacyPids.isNotEmpty()) {\n            execCommand(legacyPids.joinToString(\";\") { \"kill $it\" })\n        }\n        return legacyPids.size\n    }\n}\n\nprivate const val shizukuPsSuffix = \"shizuku-user-service\"\n\nprivate fun unbindUserService(\n    serviceArgs: Shizuku.UserServiceArgs,\n    connection: ServiceConnection,\n    reason: String? = null,\n) {\n    if (!shizukuGrantedState.stateFlow.value) return\n    LogUtils.d(serviceArgs, reason)\n    // https://github.com/RikkaApps/Shizuku-API/blob/master/server-shared/src/main/java/rikka/shizuku/server/UserServiceManager.java#L62\n    try {\n        Shizuku.unbindUserService(serviceArgs, connection, false)\n        Shizuku.unbindUserService(serviceArgs, connection, true)\n    } catch (e: Exception) {\n        e.printStackTrace()\n    }\n}\n\ndata class UserServiceWrapper(\n    val userService: IUserService,\n    val connection: ServiceConnection,\n    val serviceArgs: Shizuku.UserServiceArgs\n) {\n    fun destroy() = unbindUserService(serviceArgs, connection)\n\n    fun execCommandForResult(command: String): CommandResult = try {\n        userService.execCommand(command)\n    } catch (e: Throwable) {\n        e.printStackTrace()\n        CommandResult(code = null, result = \"\", error = e.message)\n    }\n\n    fun tap(x: Float, y: Float, duration: Long = 0): Boolean {\n        val command = if (duration > 0) {\n            \"input swipe $x $y $x $y $duration\"\n        } else {\n            \"input tap $x $y\"\n        }\n        return execCommandForResult(command).ok\n    }\n\n    fun screencapFile(filePath: String): Boolean {\n        val tempPath = \"/data/local/tmp/screencap_${System.currentTimeMillis()}.png\"\n        val command = \"screencap -p $tempPath\"\n        val r = execCommandForResult(command)\n        if (r.ok) {\n            File(tempPath).copyTo(File(filePath), overwrite = true)\n            execCommandForResult(\"rm $tempPath\")\n        }\n        return r.ok\n    }\n}\n\nsuspend fun buildServiceWrapper(): UserServiceWrapper? {\n    val serviceArgs = Shizuku\n        .UserServiceArgs(UserService::class.componentName)\n        .daemon(false)\n        .processNameSuffix(shizukuPsSuffix)\n        .debuggable(META.debuggable)\n        .version(META.versionCode)\n        .tag(\"default\")\n    LogUtils.d(\"buildServiceWrapper\", serviceArgs)\n    var resumeCallback: ((UserServiceWrapper) -> Unit)? = null\n    val connection = object : ServiceConnection {\n        override fun onServiceConnected(componentName: ComponentName, binder: IBinder?) {\n            LogUtils.d(\"onServiceConnected\", componentName)\n            resumeCallback ?: return\n            if (binder?.pingBinder() == true) {\n                resumeCallback?.invoke(\n                    UserServiceWrapper(\n                        IUserService.Stub.asInterface(binder),\n                        this,\n                        serviceArgs\n                    )\n                )\n                resumeCallback = null\n            } else {\n                LogUtils.d(\"invalid binder for $componentName received\")\n            }\n        }\n\n        override fun onServiceDisconnected(componentName: ComponentName) {\n            LogUtils.d(\"onServiceDisconnected\", componentName)\n        }\n    }\n    return withTimeoutOrNull(3000) {\n        suspendCancellableCoroutine { continuation ->\n            resumeCallback = { continuation.resume(it) }\n            try {\n                Shizuku.bindUserService(serviceArgs, connection)\n            } catch (_: Throwable) {\n                resumeCallback = null\n                continuation.resume(null)\n            }\n        }\n    }.apply {\n        if (this == null) {\n            unbindUserService(serviceArgs, connection, \"connect timeout\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/shizuku/WindowManager.kt",
    "content": "package li.songe.gkd.shizuku\n\nimport android.content.Context\nimport android.view.IWindowManager\n\nclass SafeWindowManager(val value: IWindowManager) {\n    companion object {\n        fun newBinder() = getShizukuService(Context.WINDOW_SERVICE)?.let {\n            SafeWindowManager(IWindowManager.Stub.asInterface(it))\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/store/SettingsStore.kt",
    "content": "package li.songe.gkd.store\n\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.META\nimport li.songe.gkd.util.AppGroupOption\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.AutomatorModeOption\nimport li.songe.gkd.util.RuleSortOption\nimport li.songe.gkd.util.UpdateChannelOption\nimport li.songe.gkd.util.UpdateTimeOption\n\n@Serializable\ndata class SettingsStore(\n    val enableAutomator: Boolean = false,\n    val automatorMode: Int = AutomatorModeOption.A11yMode.value,\n    val enableShizuku: Boolean = false,\n    val enableMatch: Boolean = true,\n    val enableStatusService: Boolean = false,\n    val excludeFromRecents: Boolean = false,\n    val captureScreenshot: Boolean = false,\n    val screenshotTargetAppId: String = \"\",\n    val screenshotEventSelector: String = \"\",\n    val httpServerPort: Int = 8888,\n    val updateSubsInterval: Long = UpdateTimeOption.Everyday.value,\n    val captureVolumeChange: Boolean = false,\n    val toastWhenClick: Boolean = true,\n    val actionToast: String = META.appName,\n    val autoClearMemorySubs: Boolean = true,\n    val hideSnapshotStatusBar: Boolean = false,\n    val enableDarkTheme: Boolean? = null,\n    val enableDynamicColor: Boolean = true,\n    val showSaveSnapshotToast: Boolean = true,\n    val useSystemToast: Boolean = false,\n    val useCustomNotifText: Boolean = false,\n    val customNotifTitle: String = META.appName,\n    val customNotifText: String = $$\"${i}全局/${k}应用/${u}规则组/${n}触发\",\n    val updateChannel: Int = if (META.isBeta) UpdateChannelOption.Beta.value else UpdateChannelOption.Stable.value,\n    val appSort: Int = AppSortOption.ByUsedTime.value,\n    val showBlockApp: Boolean = true,\n    val appRuleSort: Int = RuleSortOption.ByDefault.value,\n    val subsAppSort: Int = AppSortOption.ByUsedTime.value,\n    val subsAppShowUninstall: Boolean = false,\n    val subsAppGroupType: Int = AppGroupOption.UserGroup.value or AppGroupOption.SystemGroup.value,\n    val subsAppShowBlock: Boolean = false,\n    val subsExcludeSort: Int = AppSortOption.ByUsedTime.value,\n    val subsExcludeShowBlockApp: Boolean = true,\n    val subsExcludeShowInnerDisabledApp: Boolean = true,\n    val subsPowerWarn: Boolean = true,\n    val enableBlockA11yAppList: Boolean = false,\n    val blockA11yAppListFollowMatch: Boolean = true,\n    val a11yAppSort: Int = AppSortOption.ByUsedTime.value,\n    val a11yScopeAppSort: Int = AppSortOption.ByUsedTime.value,\n    val appGroupType: Int = (1 shl AppGroupOption.normalObjects.size) - 1,\n    val a11yAppGroupType: Int = appGroupType,\n    val a11yScopeAppGroupType: Int = appGroupType,\n    val subsExcludeAppGroupType: Int = appGroupType,\n) {\n    val useA11y get() = automatorMode == AutomatorModeOption.A11yMode.value\n    val useAutomation get() = automatorMode == AutomatorModeOption.AutomationMode.value\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/store/StorageExt.kt",
    "content": "package li.songe.gkd.store\r\n\r\nimport kotlinx.coroutines.CoroutineScope\r\nimport kotlinx.coroutines.Dispatchers\r\nimport kotlinx.coroutines.flow.MutableStateFlow\r\nimport kotlinx.coroutines.flow.conflate\r\nimport kotlinx.coroutines.flow.debounce\r\nimport kotlinx.coroutines.flow.drop\r\nimport kotlinx.coroutines.launch\r\nimport kotlinx.coroutines.withContext\r\nimport li.songe.gkd.appScope\r\nimport li.songe.gkd.util.json\r\nimport li.songe.gkd.util.privateStoreFolder\r\nimport li.songe.gkd.util.storeFolder\r\nimport java.io.File\r\nimport java.nio.file.Files\r\nimport java.nio.file.StandardCopyOption\r\n\r\n\r\nprivate fun readStoreText(\r\n    file: File\r\n): String? = file.run {\r\n    if (exists()) {\r\n        readText()\r\n    } else {\r\n        null\r\n    }\r\n}\r\n\r\nprivate fun writeStoreText(file: File, text: String) {\r\n    val tempFile = File(\"${file.absolutePath}.tmp\")\r\n    tempFile.outputStream().use {\r\n        it.write(text.toByteArray(Charsets.UTF_8))\r\n        it.fd.sync()\r\n    }\r\n    Files.move(\r\n        tempFile.toPath(),\r\n        file.toPath(),\r\n        StandardCopyOption.REPLACE_EXISTING,\r\n        StandardCopyOption.ATOMIC_MOVE\r\n    )\r\n}\r\n\r\nfun <T> createTextFlow(\r\n    key: String,\r\n    decode: (String?) -> T,\r\n    encode: (T) -> String,\r\n    private: Boolean = false,\r\n    scope: CoroutineScope = appScope,\r\n    debounceMillis: Long = 0,\r\n): MutableStateFlow<T> {\r\n    val name = if (key.contains('.')) key else \"$key.txt\"\r\n    val file = (if (private) privateStoreFolder else storeFolder).resolve(name)\r\n    val initText = readStoreText(file)\r\n    val initValue = decode(initText)\r\n    val stateFlow = MutableStateFlow(initValue)\r\n    scope.launch {\r\n        stateFlow.drop(1).conflate().debounce(debounceMillis).collect {\r\n            withContext(Dispatchers.IO) {\r\n                writeStoreText(file, encode(it))\r\n            }\r\n        }\r\n    }\r\n    return stateFlow\r\n}\r\n\r\ninline fun <reified T> createAnyFlow(\r\n    key: String,\r\n    crossinline default: () -> T,\r\n    crossinline initialize: (T) -> T = { it },\r\n    private: Boolean = false,\r\n    scope: CoroutineScope = appScope,\r\n    debounceMillis: Long = 0,\r\n): MutableStateFlow<T> {\r\n    return createTextFlow(\r\n        key = \"$key.json\",\r\n        decode = {\r\n            val initValue = it?.let {\r\n                runCatching { json.decodeFromString<T>(it) }.getOrNull()\r\n            }\r\n            initialize(initValue ?: default())\r\n        },\r\n        encode = {\r\n            json.encodeToString(it)\r\n        },\r\n        private = private,\r\n        scope = scope,\r\n        debounceMillis = debounceMillis,\r\n    )\r\n}\r\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/store/StoreExt.kt",
    "content": "package li.songe.gkd.store\r\n\r\nimport kotlinx.coroutines.Dispatchers\r\nimport kotlinx.coroutines.flow.MutableStateFlow\r\nimport kotlinx.coroutines.flow.update\r\nimport li.songe.gkd.appScope\r\nimport li.songe.gkd.service.ExposeService\r\nimport li.songe.gkd.ui.gkdStartCommandText\r\nimport li.songe.gkd.util.AppListString\r\nimport li.songe.gkd.util.launchTry\r\nimport li.songe.gkd.util.toast\r\n\r\nval storeFlow: MutableStateFlow<SettingsStore> by lazy {\r\n    createAnyFlow(\r\n        key = \"store\",\r\n        default = { SettingsStore() }\r\n    )\r\n}\r\n\r\nval actionCountFlow: MutableStateFlow<Long> by lazy {\r\n    createTextFlow(\r\n        key = \"action_count\",\r\n        decode = { it?.toLongOrNull() ?: 0L },\r\n        encode = { it.toString() },\r\n    )\r\n}\r\n\r\nval blockMatchAppListFlow: MutableStateFlow<Set<String>> by lazy {\r\n    createTextFlow(\r\n        key = \"block_match_app_list\",\r\n        decode = { it?.let(AppListString::decode) ?: AppListString.getDefaultBlockList() },\r\n        encode = AppListString::encode,\r\n    )\r\n}\r\n\r\nval blockA11yAppListFlow: MutableStateFlow<Set<String>> by lazy {\r\n    createTextFlow(\r\n        key = \"block_a11y_app_list\",\r\n        decode = { it?.let(AppListString::decode) ?: emptySet() },\r\n        encode = AppListString::encode,\r\n    )\r\n}\r\n\r\nval actualBlockA11yAppList: Set<String>\r\n    get() = if (storeFlow.value.blockA11yAppListFollowMatch) {\r\n        blockMatchAppListFlow.value\r\n    } else {\r\n        blockA11yAppListFlow.value\r\n    }\r\n\r\nval a11yScopeAppListFlow: MutableStateFlow<Set<String>> by lazy {\r\n    createTextFlow(\r\n        key = \"a11y_scope_app_list\",\r\n        decode = { it?.let(AppListString::decode) ?: setOf(\"com.tencent.mm\") },\r\n        encode = AppListString::encode,\r\n    )\r\n}\r\n\r\nval actualA11yScopeAppList: Set<String>\r\n    get() = if (storeFlow.value.useAutomation) {\r\n        a11yScopeAppListFlow.value\r\n    } else {\r\n        emptySet()\r\n    }\r\n\r\nfun checkAppBlockMatch(appId: String): Boolean {\r\n    if (blockMatchAppListFlow.value.contains(appId)) {\r\n        return true\r\n    }\r\n    if (storeFlow.value.enableBlockA11yAppList) {\r\n        return actualBlockA11yAppList.contains(appId)\r\n    }\r\n    return false\r\n}\r\n\r\nfun initStore() = appScope.launchTry(Dispatchers.IO) {\r\n    // preload\r\n    storeFlow.value\r\n    actionCountFlow.value\r\n    blockMatchAppListFlow.value\r\n    blockA11yAppListFlow.value\r\n    a11yScopeAppListFlow.value\r\n    gkdStartCommandText\r\n    ExposeService.initCommandFile()\r\n}\r\n\r\nfun switchStoreEnableMatch() {\r\n    if (storeFlow.value.enableMatch) {\r\n        toast(\"暂停规则匹配\")\r\n    } else {\r\n        toast(\"开启规则匹配\")\r\n    }\r\n    storeFlow.update { it.copy(enableMatch = !it.enableMatch) }\r\n}\r\n\r\nfun updateEnableAutomator(value: Boolean) {\r\n    if (value == storeFlow.value.enableAutomator) return\r\n    storeFlow.update { it.copy(enableAutomator = value) }\r\n}\r\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/A11yEventLogPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport androidx.paging.LoadState\nimport androidx.paging.compose.collectAsLazyPagingItems\nimport androidx.paging.compose.itemKey\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonArray\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.JsonPrimitive\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.data.A11yEventLog\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.component.AppNameText\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.FixedTimeText\nimport li.songe.gkd.ui.component.LocalNumberCharWidth\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.measureNumberTextWidth\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalDarkTheme\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.getJson5AnnotatedString\nimport li.songe.gkd.ui.style.iconTextSize\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.copyText\nimport li.songe.gkd.util.format\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toJson5String\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata object A11yEventLogRoute : NavKey\n\n@Composable\nfun A11yEventLogPage() {\n    val context = LocalActivity.current as MainActivity\n    val mainVm = context.mainVm\n    val vm = viewModel<A11yEventLogVm>()\n\n    val logCount by vm.logCountFlow.collectAsState()\n    val list = vm.pagingDataFlow.collectAsLazyPagingItems()\n    val (scrollBehavior, listState) = useListScrollState(vm.resetKey, list.itemCount > 0)\n\n    Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {\n        PerfTopAppBar(\n            scrollBehavior = scrollBehavior,\n            navigationIcon = {\n                PerfIconButton(imageVector = PerfIcon.ArrowBack, onClick = {\n                    mainVm.popPage()\n                })\n            },\n            title = {\n                Text(\n                    text = \"事件日志\",\n                    modifier = Modifier.noRippleClickable { vm.resetKey.intValue++ },\n                )\n            },\n            actions = {\n                if (logCount > 0) {\n                    PerfIconButton(\n                        imageVector = PerfIcon.Delete,\n                        onClick = throttle(fn = vm.viewModelScope.launchAsFn {\n                            mainVm.dialogFlow.waitResult(\n                                title = \"删除日志\",\n                                text = \"确定删除所有事件日志?\",\n                                error = true,\n                            )\n                            DbSet.a11yEventLogDao.deleteAll()\n                            toast(\"删除成功\")\n                        })\n                    )\n                }\n            }\n        )\n    }) { contentPadding ->\n        CompositionLocalProvider(\n            LocalNumberCharWidth provides measureNumberTextWidth(),\n        ) {\n            LazyColumn(\n                modifier = Modifier.scaffoldPadding(contentPadding),\n                state = listState,\n                verticalArrangement = Arrangement.spacedBy(12.dp),\n            ) {\n                items(\n                    count = list.itemCount,\n                    key = list.itemKey { it.id }\n                ) { i ->\n                    val eventLog = list[i] ?: return@items\n                    EventLogCard(\n                        eventLog = eventLog,\n                        modifier = Modifier\n                            .padding(horizontal = 16.dp)\n                            .clickable(onClick = {\n                                vm.showEventLogFlow.value = eventLog\n                            })\n                    )\n                }\n                item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                    Spacer(modifier = Modifier.height(EmptyHeight))\n                    if (logCount == 0 && list.loadState.refresh !is LoadState.Loading) {\n                        EmptyText(text = \"暂无数据\")\n                    }\n                }\n            }\n        }\n    }\n\n    vm.showEventLogFlow.collectAsState().value?.let { eventLog ->\n        val onDismissRequest = { vm.showEventLogFlow.value = null }\n        val dark = LocalDarkTheme.current\n        val eventText = remember(dark) {\n            getJson5AnnotatedString(\n                toJson5String(\n                    JsonObject(\n                        mapOf(\n                            \"name\" to JsonPrimitive(eventLog.name),\n                            \"desc\" to JsonPrimitive(eventLog.desc),\n                            \"text\" to JsonArray(eventLog.text.map(::JsonPrimitive)),\n                        )\n                    )\n                ),\n                dark,\n            )\n        }\n        AlertDialog(\n            onDismissRequest = onDismissRequest,\n            title = { Text(text = \"事件详情\") },\n            text = {\n                val textModifier = Modifier\n                    .background(\n                        color = MaterialTheme.colorScheme.tertiaryContainer,\n                        shape = MaterialTheme.shapes.extraSmall,\n                    )\n                    .padding(horizontal = 4.dp)\n                Column {\n                    Text(text = \"类型: \" + if (eventLog.isStateChanged) \"状态变化\" else \"内容变化\")\n                    Spacer(modifier = Modifier.height(12.dp))\n                    Text(text = \"应用ID\")\n                    Row {\n                        Text(\n                            text = eventLog.appId,\n                            modifier = textModifier\n                        )\n                        Spacer(modifier = Modifier.width(4.dp))\n                        CopyIcon(onClick = {\n                            copyText(eventLog.appId)\n                        })\n                    }\n                    Spacer(modifier = Modifier.height(12.dp))\n                    Text(text = \"事件数据\")\n                    Box(\n                        modifier = Modifier.fillMaxWidth()\n                    ) {\n                        SelectionContainer(modifier = Modifier.fillMaxWidth()) {\n                            Text(\n                                text = eventText,\n                                modifier = textModifier.fillMaxWidth()\n                            )\n                        }\n                        CopyIcon(\n                            modifier = Modifier\n                                .align(Alignment.TopEnd)\n                                .padding(4.dp),\n                            onClick = {\n                                copyText(eventText.text)\n                            })\n                    }\n                    if (eventLog.isStateChanged) {\n                        Spacer(modifier = Modifier.height(12.dp))\n                        val selectorText = remember(eventLog.id) {\n                            (listOf(\n                                \"name\" to eventLog.name,\n                                \"desc\" to eventLog.desc,\n                                \"text.size\" to eventLog.text.size,\n                            ) + eventLog.text.mapIndexed { i, s -> \"text.get($i)\" to s }).joinToString(\n                                \"\"\n                            ) { (key, value) ->\n                                val v =\n                                    if (value is String) toJson5String(value) else value.toString()\n                                \"[${key}=${v}]\"\n                            }\n                        }\n                        Text(text = \"特征选择器\")\n                        Row(\n                            modifier = Modifier.fillMaxWidth()\n                        ) {\n                            Text(\n                                text = selectorText,\n                                modifier = textModifier.weight(1f)\n                            )\n                            Spacer(modifier = Modifier.width(4.dp))\n                            CopyIcon(onClick = {\n                                copyText(selectorText)\n                            })\n                        }\n                    }\n                }\n            },\n            confirmButton = {\n                TextButton(onClick = onDismissRequest) {\n                    Text(text = \"关闭\")\n                }\n            },\n        )\n    }\n}\n\n@Composable\nfun EventLogCard(eventLog: A11yEventLog, modifier: Modifier = Modifier) {\n    var parentHeight by remember { mutableIntStateOf(0) }\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .onSizeChanged {\n                parentHeight = it.height\n            }\n    ) {\n        Spacer(\n            modifier = Modifier\n                .background(MaterialTheme.colorScheme.secondary)\n                .width(2.dp)\n                .height((parentHeight / LocalDensity.current.density).dp)\n        )\n        Spacer(modifier = Modifier.width(4.dp))\n        Column(\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                FixedTimeText(\n                    text = eventLog.ctime.format(\"HH:mm:ss SSS\"),\n                )\n                Spacer(\n                    modifier = Modifier\n                        .padding(horizontal = 8.dp)\n                        .background(MaterialTheme.colorScheme.tertiary)\n                        .size(height = 8.dp, width = 1.dp)\n                )\n                AppNameText(\n                    appId = eventLog.appId,\n                )\n            }\n            Text(\n                text = eventLog.fixedName,\n                color = if (eventLog.isStateChanged) MaterialTheme.colorScheme.primary else Color.Unspecified,\n                maxLines = 1,\n                softWrap = false,\n                overflow = TextOverflow.MiddleEllipsis,\n            )\n            if (eventLog.desc != null) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.spacedBy(2.dp)\n                ) {\n                    PerfIcon(\n                        imageVector = PerfIcon.Title,\n                        modifier = Modifier.iconTextSize(\n                            square = false\n                        ),\n                    )\n                    Text(\n                        text = eventLog.desc,\n                        modifier = Modifier\n                            .background(\n                                color = MaterialTheme.colorScheme.secondaryContainer,\n                                shape = MaterialTheme.shapes.extraSmall,\n                            )\n                            .padding(horizontal = 2.dp),\n                    )\n                }\n            }\n            if (eventLog.text.isNotEmpty()) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.spacedBy(2.dp),\n                ) {\n                    PerfIcon(\n                        imageVector = PerfIcon.TextFields,\n                        modifier = Modifier.iconTextSize(\n                            square = false\n                        ),\n                    )\n                    // 如果祖先容器有设置了 height(IntrinsicSize.Min) 会导致 FlowRow 不会自动换行\n                    FlowRow(\n                        modifier = Modifier.weight(1f),\n                        horizontalArrangement = Arrangement.spacedBy(2.dp),\n                        verticalArrangement = Arrangement.spacedBy(2.dp),\n                    ) {\n                        eventLog.text.forEach { subText ->\n                            Text(\n                                text = subText,\n                                modifier = Modifier\n                                    .background(\n                                        color = MaterialTheme.colorScheme.tertiaryContainer,\n                                        shape = MaterialTheme.shapes.extraSmall,\n                                    )\n                                    .padding(horizontal = 2.dp),\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun CopyIcon(modifier: Modifier = Modifier, onClick: () -> Unit) {\n    PerfIcon(\n        imageVector = PerfIcon.ContentCopy,\n        modifier = modifier\n            .clip(MaterialTheme.shapes.extraSmall)\n            .clickable(onClick = onClick)\n            .iconTextSize(),\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/A11yEventLogVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.Pager\nimport androidx.paging.PagingConfig\nimport androidx.paging.cachedIn\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.data.A11yEventLog\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.share.BaseViewModel\n\nclass A11yEventLogVm : BaseViewModel() {\n    val pagingDataFlow =\n        Pager(PagingConfig(pageSize = 100)) { DbSet.a11yEventLogDao.pagingSource() }\n            .flow.cachedIn(viewModelScope)\n\n    val logCountFlow = DbSet.a11yEventLogDao.count().stateInit(0)\n    val resetKey = mutableIntStateOf(0)\n    val showEventLogFlow = MutableStateFlow<A11yEventLog?>(null)\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/A11yScopeAppListPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.flow.update\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.store.a11yScopeAppListFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.component.AnimatedBooleanContent\nimport li.songe.gkd.ui.component.AnimatedIconButton\nimport li.songe.gkd.ui.component.AnimationFloatingActionButton\nimport li.songe.gkd.ui.component.AppBarTextField\nimport li.songe.gkd.ui.component.AppCheckBoxCard\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.MenuGroupCard\nimport li.songe.gkd.ui.component.MenuItemCheckbox\nimport li.songe.gkd.ui.component.MenuItemRadioButton\nimport li.songe.gkd.ui.component.MultiTextField\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.autoFocus\nimport li.songe.gkd.ui.component.isFullVisible\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.icon.BackCloseIcon\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.asMutableState\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.AppGroupOption\nimport li.songe.gkd.util.AppListString\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.switchItem\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata object A11YScopeAppListRoute : NavKey\n\n@Composable\nfun A11yScopeAppListPage() {\n    val store by storeFlow.collectAsState()\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    val vm = viewModel<A11yScopeAppListVm>()\n    val appInfos by vm.appInfosFlow.collectAsState()\n    val searchStr by vm.searchStrFlow.collectAsState()\n    var showSearchBar by vm.showSearchBarFlow.asMutableState()\n    var editable by vm.editableFlow.asMutableState()\n    val (scrollBehavior, listState) = useListScrollState(vm.resetKey, canScroll = { !editable })\n    BackHandler(editable, vm.viewModelScope.launchAsFn {\n        context.justHideSoftInput()\n        if (vm.textChanged) {\n            mainVm.dialogFlow.waitResult(\n                title = \"提示\",\n                text = \"当前内容未保存，是否放弃编辑？\",\n            )\n        }\n        editable = false\n    })\n    Scaffold(\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(\n                scrollBehavior = scrollBehavior,\n                canScroll = !editable && !store.blockA11yAppListFollowMatch,\n                navigationIcon = {\n                    IconButton(\n                        onClick = throttle(vm.viewModelScope.launchAsFn {\n                            if (editable) {\n                                if (vm.textChanged) {\n                                    context.justHideSoftInput()\n                                    mainVm.dialogFlow.waitResult(\n                                        title = \"提示\",\n                                        text = \"当前内容未保存，是否放弃编辑？\",\n                                    )\n                                }\n                                editable = !editable\n                            } else {\n                                context.hideSoftInput()\n                                mainVm.popPage()\n                            }\n                        })\n                    ) {\n                        BackCloseIcon(backOrClose = !editable)\n                    }\n                },\n                title = {\n                    val firstShowSearchBar = remember { showSearchBar }\n                    if (showSearchBar) {\n                        BackHandler {\n                            if (!context.justHideSoftInput()) {\n                                showSearchBar = false\n                            }\n                        }\n                        AppBarTextField(\n                            value = searchStr,\n                            onValueChange = { newValue ->\n                                vm.searchStrFlow.value = newValue.trim()\n                            },\n                            hint = \"请输入应用名称/ID\",\n                            modifier = if (firstShowSearchBar) Modifier else Modifier.autoFocus(),\n                        )\n                    } else {\n                        val titleModifier = Modifier\n                            .noRippleClickable(\n                                onClick = throttle {\n                                    vm.resetKey.intValue++\n                                }\n                            )\n                        Text(\n                            modifier = titleModifier,\n                            text = \"局部无障碍\",\n                        )\n                    }\n                },\n                actions = {\n                    AnimatedBooleanContent(\n                        targetState = editable,\n                        contentAlignment = Alignment.TopEnd,\n                        contentTrue = {\n                            PerfIconButton(\n                                imageVector = PerfIcon.Save,\n                                onClick = throttle {\n                                    if (vm.textChanged) {\n                                        a11yScopeAppListFlow.value =\n                                            AppListString.decode(vm.textFlow.value)\n                                        toast(\"更新成功\")\n                                    } else {\n                                        toast(\"未修改\")\n                                    }\n                                    context.justHideSoftInput()\n                                    editable = false\n                                },\n                            )\n                        },\n                        contentFalse = {\n                            Row {\n                                var expanded by remember { mutableStateOf(false) }\n                                AnimatedIconButton(\n                                    onClick = throttle {\n                                        if (showSearchBar) {\n                                            if (vm.searchStrFlow.value.isEmpty()) {\n                                                showSearchBar = false\n                                            } else {\n                                                vm.searchStrFlow.value = \"\"\n                                            }\n                                        } else {\n                                            showSearchBar = true\n                                        }\n                                    },\n                                    id = R.drawable.ic_anim_search_close,\n                                    atEnd = showSearchBar,\n                                )\n                                PerfIconButton(imageVector = PerfIcon.Sort, onClick = {\n                                    expanded = true\n                                })\n                                Box(\n                                    modifier = Modifier\n                                        .wrapContentSize(Alignment.TopStart)\n                                ) {\n                                    DropdownMenu(\n                                        expanded = expanded,\n                                        onDismissRequest = { expanded = false }\n                                    ) {\n                                        MenuGroupCard(inTop = true, title = \"排序\") {\n                                            var sortType by vm.sortTypeFlow.asMutableState()\n                                            AppSortOption.objects.forEach { option ->\n                                                MenuItemRadioButton(\n                                                    text = option.label,\n                                                    selected = sortType == option,\n                                                    onClick = { sortType = option },\n                                                )\n                                            }\n                                        }\n                                        MenuGroupCard(inTop = true, title = \"筛选\") {\n                                            var appGroupType by vm.appGroupTypeFlow.asMutableState()\n                                            AppGroupOption.normalObjects.forEach { option ->\n                                                val newValue = option.invert(appGroupType)\n                                                MenuItemCheckbox(\n                                                    enabled = newValue != 0,\n                                                    text = option.label,\n                                                    checked = option.include(appGroupType),\n                                                    onClick = { appGroupType = newValue },\n                                                )\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        },\n                    )\n                })\n        },\n        floatingActionButton = {\n            AnimationFloatingActionButton(\n                visible = !editable && scrollBehavior.isFullVisible,\n                onClickLabel = \"进入文本编辑模式\",\n                onClick = {\n                    editable = !editable\n                },\n                imageVector = PerfIcon.Edit,\n                contentDescription = \"编辑文本\"\n            )\n        },\n    ) { contentPadding ->\n        if (editable) {\n            MultiTextField(\n                modifier = Modifier.scaffoldPadding(contentPadding),\n                textFlow = vm.textFlow,\n                immediateFocus = true,\n                placeholderText = \"请输入应用ID列表\\n示例:\\ncom.android.systemui\\ncom.android.settings\",\n                indicatorSize = vm.indicatorSizeFlow.collectAsState().value,\n            )\n        } else {\n            val a11yScopeAppList by a11yScopeAppListFlow.collectAsState()\n            LazyColumn(\n                modifier = Modifier.scaffoldPadding(contentPadding),\n                state = listState,\n            ) {\n                items(appInfos, { it.id }) { appInfo ->\n                    val checked = a11yScopeAppList.contains(appInfo.id)\n                    AppCheckBoxCard(\n                        appInfo = appInfo,\n                        checked = checked,\n                        onCheckedChange = {\n                            a11yScopeAppListFlow.update {\n                                it.switchItem(appInfo.id)\n                            }\n                        },\n                    )\n                }\n                item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                    Spacer(modifier = Modifier.height(EmptyHeight))\n                    if (appInfos.isEmpty() && searchStr.isNotEmpty()) {\n                        EmptyText(text = \"暂无搜索结果\")\n                        Spacer(modifier = Modifier.height(EmptyHeight / 2))\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/A11yScopeAppListVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.compose.runtime.mutableIntStateOf\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.map\nimport li.songe.gkd.store.a11yScopeAppListFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.share.BaseViewModel\nimport li.songe.gkd.ui.share.asMutableStateFlow\nimport li.songe.gkd.ui.share.useAppFilter\nimport li.songe.gkd.util.AppListString\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.findOption\n\nclass A11yScopeAppListVm : BaseViewModel() {\n    val sortTypeFlow = storeFlow.asMutableStateFlow(\n        getter = { AppSortOption.objects.findOption(it.a11yScopeAppSort) },\n        setter = {\n            storeFlow.value.copy(a11yScopeAppSort = it.value)\n        }\n    )\n    val appGroupTypeFlow = storeFlow.asMutableStateFlow(\n        getter = { it.a11yScopeAppGroupType },\n        setter = {\n            storeFlow.value.copy(a11yScopeAppGroupType = it)\n        }\n    )\n    val appFilter = useAppFilter(\n        appGroupTypeFlow = appGroupTypeFlow,\n        sortTypeFlow = sortTypeFlow,\n    )\n    val searchStrFlow = appFilter.searchStrFlow\n\n    val showSearchBarFlow = MutableStateFlow(false)\n    val appInfosFlow = appFilter.appListFlow\n\n    val resetKey = mutableIntStateOf(0)\n    val editableFlow = MutableStateFlow(false)\n\n    val textFlow = MutableStateFlow(\"\")\n    val textChanged get() = a11yScopeAppListFlow.value != AppListString.decode(textFlow.value)\n\n    val indicatorSizeFlow = textFlow.debounce(500).map {\n        AppListString.decode(it).size\n    }.stateInit(0)\n\n    init {\n        showSearchBarFlow.launchCollect {\n            if (!it) {\n                searchStrFlow.value = \"\"\n            }\n        }\n        editableFlow.launchOnChange {\n            if (it) {\n                showSearchBarFlow.value = false\n                textFlow.value = AppListString.encode(a11yScopeAppListFlow.value, append = true)\n            }\n        }\n        appInfosFlow.launchOnChange {\n            resetKey.intValue++\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/AboutPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.animation.graphics.res.animatedVectorResource\nimport androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter\nimport androidx.compose.animation.graphics.vector.AnimatedImageVector\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.colorResource\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.LinkAnnotation\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextLinkStyles\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.withLink\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.isActive\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.META\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.RotatingLoadingIcon\nimport li.songe.gkd.ui.component.SettingItem\nimport li.songe.gkd.ui.component.TextMenu\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.share.LocalDarkTheme\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.asMutableState\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.itemPadding\nimport li.songe.gkd.ui.style.titleItemPadding\nimport li.songe.gkd.util.ISSUES_URL\nimport li.songe.gkd.util.PLAY_STORE_URL\nimport li.songe.gkd.util.REPOSITORY_URL\nimport li.songe.gkd.util.ShortUrlSet\nimport li.songe.gkd.util.UpdateChannelOption\nimport li.songe.gkd.util.buildLogFile\nimport li.songe.gkd.util.findOption\nimport li.songe.gkd.util.format\nimport li.songe.gkd.util.getShareApkFile\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.openUri\nimport li.songe.gkd.util.saveFileToDownloads\nimport li.songe.gkd.util.shareFile\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata object AboutRoute : NavKey\n\n@Composable\nfun AboutPage() {\n    val context = LocalActivity.current as MainActivity\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel<AboutVm>()\n    val store by storeFlow.collectAsState()\n\n    var showInfoDlg by vm.showInfoDlgFlow.asMutableState()\n    if (showInfoDlg) {\n        AlertDialog(\n            onDismissRequest = { showInfoDlg = false },\n            title = { Text(text = \"版本信息\") },\n            text = {\n                Column(\n                    verticalArrangement = Arrangement.spacedBy(12.dp),\n                ) {\n                    Column {\n                        Text(text = \"构建渠道\")\n                        Text(text = META.channel)\n                    }\n                    Column {\n                        Text(text = \"版本代码\")\n                        Text(text = META.versionCode.toString())\n                    }\n                    Column {\n                        Text(text = \"版本名称\")\n                        Text(text = META.versionName)\n                    }\n                    Column {\n                        Text(text = \"代码记录\")\n                        Text(\n                            modifier = Modifier.clickable { openUri(META.commitUrl) },\n                            text = META.tagName ?: META.commitId.substring(0, 16),\n                            color = MaterialTheme.colorScheme.primary,\n                            style = LocalTextStyle.current.copy(textDecoration = TextDecoration.Underline),\n                        )\n                    }\n                    Column {\n                        Text(text = \"提交时间\")\n                        Text(text = META.commitTime.format(\"yyyy-MM-dd HH:mm:ss ZZ\"))\n                    }\n                }\n            },\n            confirmButton = {\n                TextButton(onClick = {\n                    showInfoDlg = false\n                }) {\n                    Text(text = \"关闭\")\n                }\n            },\n        )\n    }\n    var showShareLogDlg by vm.showShareLogDlgFlow.asMutableState()\n    var showShareAppDlg by vm.showShareAppDlgFlow.asMutableState()\n    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()\n    Scaffold(\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(\n                scrollBehavior = scrollBehavior,\n                navigationIcon = {\n                    PerfIconButton(\n                        imageVector = PerfIcon.ArrowBack,\n                        onClick = {\n                            mainVm.popPage()\n                        },\n                    )\n                },\n                title = { Text(text = \"关于\") },\n                actions = {\n                    PerfIconButton(\n                        imageVector = PerfIcon.Share,\n                        onClick = {\n                            showShareAppDlg = true\n                        },\n                    )\n                }\n            )\n        }\n    ) { contentPadding ->\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .verticalScroll(rememberScrollState())\n                .padding(contentPadding),\n        ) {\n            Column(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalAlignment = Alignment.CenterHorizontally\n            ) {\n                AnimatedLogoIcon(\n                    modifier = Modifier\n                        .clickable(\n                            indication = null,\n                            interactionSource = remember { MutableInteractionSource() },\n                            onClick = throttle { toast(\"你干嘛~ 哎呦~\") }\n                        )\n                        .fillMaxWidth(0.33f)\n                        .aspectRatio(1f)\n                )\n                Column(\n                    modifier = Modifier\n                        .clip(MaterialTheme.shapes.extraSmall)\n                        .clickable(onClick = { showInfoDlg = true })\n                        .padding(horizontal = 4.dp),\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                ) {\n                    Text(text = META.appName, style = MaterialTheme.typography.titleMedium)\n                    Text(\n                        text = META.versionName,\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n                }\n                Spacer(modifier = Modifier.height(32.dp))\n            }\n\n            SettingItem(\n                imageVector = null,\n                title = \"开源代码\",\n                onClick = {\n                    mainVm.openUrl(REPOSITORY_URL)\n                },\n            )\n            if (META.isGkdChannel) {\n                SettingItem(\n                    imageVector = null,\n                    title = \"捐赠支持\",\n                    onClick = {\n                        mainVm.navigateWebPage(ShortUrlSet.URL10)\n                    },\n                )\n            }\n            SettingItem(\n                imageVector = null,\n                title = \"使用协议\",\n                onClick = {\n                    mainVm.navigateWebPage(ShortUrlSet.URL12)\n                },\n            )\n            SettingItem(\n                imageVector = null,\n                title = \"隐私政策\",\n                onClick = {\n                    mainVm.navigateWebPage(ShortUrlSet.URL11)\n                },\n            )\n\n            Text(\n                text = \"反馈\",\n                modifier = Modifier.titleItemPadding(),\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.primary,\n            )\n            Column(\n                modifier = Modifier\n                    .clickable(onClick = throttle(mainVm.viewModelScope.launchAsFn {\n                        mainVm.dialogFlow.waitResult(\n                            title = \"反馈须知\",\n                            textContent = {\n                                Text(text = buildAnnotatedString {\n                                    val highlightStyle = SpanStyle(\n                                        fontWeight = FontWeight.Bold,\n                                        color = MaterialTheme.colorScheme.primary,\n                                    )\n                                    append(\"感谢您愿意花时间反馈，\")\n                                    withStyle(style = highlightStyle) {\n                                        append(\"GKD 默认不携带任何规则，只接受应用本体功能相关的反馈\")\n                                    }\n                                    append(\"\\n\\n\")\n                                    append(\"请先判断是不是第三方规则订阅的问题，如果是，您应该向规则提供者反馈，而不是在此处反馈。\")\n                                    withStyle(style = highlightStyle) {\n                                        append(\"如果您已经确信是 GKD 应用本体的问题\")\n                                    }\n                                    append(\"，可点击下方继续反馈\")\n                                })\n                            },\n                            confirmText = \"继续\",\n                            dismissRequest = true,\n                        )\n                        mainVm.openUrl(ISSUES_URL)\n                    }))\n                    .fillMaxWidth()\n                    .itemPadding()\n            ) {\n                Text(\n                    text = \"问题反馈\",\n                    style = MaterialTheme.typography.bodyLarge,\n                )\n            }\n            SettingItem(\n                title = \"导出日志\",\n                imageVector = PerfIcon.Share,\n                onClick = {\n                    showShareLogDlg = true\n                }\n            )\n            if (mainVm.updateStatus != null) {\n                Text(\n                    text = \"更新\",\n                    modifier = Modifier.titleItemPadding(),\n                    style = MaterialTheme.typography.titleSmall,\n                    color = MaterialTheme.colorScheme.primary,\n                )\n                TextMenu(\n                    title = \"更新渠道\",\n                    option = UpdateChannelOption.objects.findOption(store.updateChannel)\n                ) {\n                    if (mainVm.updateStatus.checkUpdatingFlow.value) return@TextMenu\n                    if (it.value == UpdateChannelOption.Beta.value) {\n                        mainVm.viewModelScope.launchTry {\n                            mainVm.dialogFlow.waitResult(\n                                title = \"版本渠道\",\n                                text = \"测试版本渠道更新快\\n但不稳定可能存在较多BUG\\n请谨慎使用\",\n                            )\n                            storeFlow.update { s -> s.copy(updateChannel = it.value) }\n                        }\n                    } else {\n                        storeFlow.update { s -> s.copy(updateChannel = it.value) }\n                    }\n                }\n                Row(\n                    modifier = Modifier\n                        .clickable(\n                            onClick = throttle {\n                                mainVm.updateStatus.checkUpdate(true)\n                            }\n                        )\n                        .fillMaxWidth()\n                        .itemPadding(),\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        text = \"检查更新\",\n                        style = MaterialTheme.typography.bodyLarge,\n                    )\n                    RotatingLoadingIcon(loading = mainVm.updateStatus.checkUpdatingFlow.collectAsState().value)\n                }\n            }\n            Spacer(modifier = Modifier.height(EmptyHeight))\n        }\n    }\n\n    if (showShareLogDlg) {\n        Dialog(onDismissRequest = { showShareLogDlg = false }) {\n            Card(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n                shape = RoundedCornerShape(16.dp),\n            ) {\n                val modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp)\n                Text(\n                    text = \"分享到其他应用\", modifier = Modifier\n                        .clickable(onClick = throttle {\n                            showShareLogDlg = false\n                            mainVm.viewModelScope.launchTry(Dispatchers.IO) {\n                                val logZipFile = buildLogFile()\n                                context.shareFile(logZipFile, \"分享日志文件\")\n                            }\n                        })\n                        .then(modifier)\n                )\n                Text(\n                    text = \"保存到下载\", modifier = Modifier\n                        .clickable(onClick = throttle {\n                            showShareLogDlg = false\n                            mainVm.viewModelScope.launchTry(Dispatchers.IO) {\n                                val logZipFile = buildLogFile()\n                                context.saveFileToDownloads(logZipFile)\n                            }\n                        })\n                        .then(modifier)\n                )\n                Text(\n                    text = \"生成链接(需科学上网)\",\n                    modifier = Modifier\n                        .clickable(onClick = throttle {\n                            showShareLogDlg = false\n                            mainVm.uploadOptions.startTask(\n                                getFile = { buildLogFile() }\n                            )\n                        })\n                        .then(modifier)\n                )\n            }\n        }\n    }\n\n    if (showShareAppDlg) {\n        Dialog(onDismissRequest = { showShareAppDlg = false }) {\n            Card(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n                shape = RoundedCornerShape(16.dp),\n            ) {\n                val modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp)\n                Text(\n                    text = \"分享到其他应用\", modifier = Modifier\n                        .clickable(onClick = throttle {\n                            showShareAppDlg = false\n                            mainVm.viewModelScope.launchTry(Dispatchers.IO) {\n                                if (!META.isGkdChannel) {\n                                    mainVm.dialogFlow.waitResult(\n                                        title = \"分享提示\",\n                                        textContent = { Text(text = exportPlayTipTemplate()) },\n                                        confirmText = \"继续\",\n                                    )\n                                }\n                                context.shareFile(getShareApkFile(), \"分享安装文件\")\n                            }\n                        })\n                        .then(modifier)\n                )\n                Text(\n                    text = \"保存到下载\", modifier = Modifier\n                        .clickable(onClick = throttle {\n                            showShareAppDlg = false\n                            mainVm.viewModelScope.launchTry(Dispatchers.IO) {\n                                if (!META.isGkdChannel) {\n                                    mainVm.dialogFlow.waitResult(\n                                        title = \"保存提示\",\n                                        textContent = { Text(text = exportPlayTipTemplate()) },\n                                        confirmText = \"继续\",\n                                    )\n                                }\n                                context.saveFileToDownloads(getShareApkFile())\n                            }\n                        })\n                        .then(modifier)\n                )\n                Text(\n                    text = \"Google Play\", modifier = Modifier\n                        .clickable(onClick = throttle {\n                            showShareAppDlg = false\n                            mainVm.openUrl(PLAY_STORE_URL)\n                        })\n                        .then(modifier)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun exportPlayTipTemplate(): AnnotatedString {\n    return buildAnnotatedString {\n        append(\"当前导出的 APK 文件只能在已安装 Google 框架的设备上才能使用，否则安装打开后会提示报错，\")\n        withLink(\n            LinkAnnotation.Url(\n                ShortUrlSet.URL13,\n                TextLinkStyles(\n                    style = SpanStyle(\n                        fontWeight = FontWeight.Bold,\n                        color = MaterialTheme.colorScheme.primary,\n                    )\n                )\n            )\n        ) {\n            append(\"建议点此从官网下载\")\n        }\n        append(\"，或点击下方继续操作\")\n    }\n}\n\n@Composable\nprivate fun AnimatedLogoIcon(\n    modifier: Modifier = Modifier\n) {\n    val darkTheme = LocalDarkTheme.current\n    val colorRid = if (darkTheme) R.color.better_white else R.color.better_black\n    var atEnd by remember { mutableStateOf(false) }\n    val animation = AnimatedImageVector.animatedVectorResource(id = R.drawable.ic_anim_logo)\n    val painter = rememberAnimatedVectorPainter(\n        animation,\n        atEnd\n    )\n    LaunchedEffect(Unit) {\n        while (isActive) {\n            atEnd = !atEnd\n            delay(animation.totalDuration.toLong())\n        }\n    }\n    Icon(\n        modifier = modifier,\n        painter = painter,\n        contentDescription = null,\n        tint = colorResource(colorRid),\n    )\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/AboutVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.lifecycle.ViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\n\nclass AboutVm : ViewModel() {\n    val showInfoDlgFlow = MutableStateFlow(false)\n    val showShareLogDlgFlow = MutableStateFlow(false)\n    val showShareAppDlgFlow = MutableStateFlow(false)\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/ActionLogPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport androidx.paging.LoadState\nimport androidx.paging.compose.collectAsLazyPagingItems\nimport androidx.paging.compose.itemKey\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.data.ActionLog\nimport li.songe.gkd.data.ExcludeData\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.data.SubsConfig\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.component.AppNameText\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.FixedTimeText\nimport li.songe.gkd.ui.component.GroupNameText\nimport li.songe.gkd.ui.component.LocalNumberCharWidth\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.TowLineText\nimport li.songe.gkd.ui.component.animateListItem\nimport li.songe.gkd.ui.component.measureNumberTextWidth\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.component.useSubs\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.iconTextSize\nimport li.songe.gkd.ui.style.itemHorizontalPadding\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.mapState\nimport li.songe.gkd.util.subsMapFlow\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata class ActionLogRoute(\n    val subsId: Long? = null,\n    val appId: String? = null,\n) : NavKey\n\n@Composable\nfun ActionLogPage(route: ActionLogRoute) {\n    val subsId = route.subsId\n    val appId = route.appId\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel { ActionLogVm(route) }\n    val resetKey = rememberSaveable { mutableIntStateOf(0) }\n    val list = vm.pagingDataFlow.collectAsLazyPagingItems()\n    val (scrollBehavior, listState) = useListScrollState(resetKey, list.itemCount > 0)\n    val timeTextWidth = measureNumberTextWidth(MaterialTheme.typography.bodySmall)\n\n    Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {\n        PerfTopAppBar(\n            scrollBehavior = scrollBehavior,\n            navigationIcon = {\n                PerfIconButton(\n                    imageVector = PerfIcon.ArrowBack,\n                    onClick = {\n                        mainVm.popPage()\n                    },\n                )\n            },\n            title = {\n                val title = \"触发记录\"\n                val titleModifier = Modifier.noRippleClickable {\n                    resetKey.intValue++\n                }\n                if (subsId != null) {\n                    TowLineText(\n                        title = title,\n                        subtitle = useSubs(subsId)?.name ?: subsId.toString(),\n                        modifier = titleModifier,\n                    )\n                } else if (appId != null) {\n                    TowLineText(\n                        title = title,\n                        subtitle = appId,\n                        showApp = true,\n                        modifier = titleModifier,\n                    )\n                } else {\n                    Text(\n                        text = title,\n                        modifier = titleModifier,\n                    )\n                }\n            },\n            actions = {\n                if (list.itemCount > 0) {\n                    PerfIconButton(\n                        imageVector = PerfIcon.Delete,\n                        onClick = throttle(fn = mainVm.viewModelScope.launchAsFn {\n                            val text = if (subsId != null) {\n                                \"确定删除当前订阅所有触发记录?\"\n                            } else if (appId != null) {\n                                \"确定删除当前应用所有触发记录?\"\n                            } else {\n                                \"确定删除所有触发记录?\"\n                            }\n                            mainVm.dialogFlow.waitResult(\n                                title = \"删除记录\",\n                                text = text,\n                                error = true,\n                            )\n                            if (subsId != null) {\n                                DbSet.actionLogDao.deleteSubsAll(subsId)\n                            } else if (appId != null) {\n                                DbSet.actionLogDao.deleteAppAll(appId)\n                            } else {\n                                DbSet.actionLogDao.deleteAll()\n                            }\n                            toast(\"删除成功\")\n                        })\n                    )\n                }\n            })\n    }, content = { contentPadding ->\n        CompositionLocalProvider(\n            LocalNumberCharWidth provides timeTextWidth\n        ) {\n            LazyColumn(\n                modifier = Modifier.scaffoldPadding(contentPadding),\n                state = listState,\n            ) {\n                items(\n                    count = list.itemCount,\n                    key = list.itemKey { c -> c.first.id }\n                ) { i ->\n                    val item = list[i] ?: return@items\n                    val lastItem = if (i > 0) list[i - 1] else null\n                    ActionLogCard(\n                        modifier = Modifier.animateListItem(),\n                        i = i,\n                        item = item,\n                        lastItem = lastItem,\n                        onClick = {\n                            vm.showActionLogFlow.value = item.first\n                        },\n                        subsId = subsId,\n                        appId = appId,\n                    )\n                }\n                item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                    Spacer(modifier = Modifier.height(EmptyHeight))\n                    if (list.itemCount == 0 && list.loadState.refresh !is LoadState.Loading) {\n                        EmptyText(text = \"暂无数据\")\n                    }\n                }\n            }\n        }\n    })\n\n    vm.showActionLogFlow.collectAsState().value?.let {\n        ActionLogDialog(\n            vm = vm,\n            actionLog = it,\n            onDismissRequest = {\n                vm.showActionLogFlow.value = null\n            }\n        )\n    }\n}\n\n\n@Composable\nprivate fun ActionLogCard(\n    modifier: Modifier = Modifier,\n    i: Int,\n    item: Triple<ActionLog, RawSubscription.RawGroupProps?, RawSubscription.RawRuleProps?>,\n    lastItem: Triple<ActionLog, RawSubscription.RawGroupProps?, RawSubscription.RawRuleProps?>?,\n    onClick: () -> Unit,\n    subsId: Long?,\n    appId: String?,\n) {\n    val mainVm = LocalMainViewModel.current\n    val (actionLog, group, rule) = item\n    val lastActionLog = lastItem?.first\n    val isDiffApp = actionLog.appId != lastActionLog?.appId\n    val verticalPadding = if (i == 0) 0.dp else if (isDiffApp) 12.dp else 8.dp\n    val subsIdToRaw by subsMapFlow.collectAsState()\n    val subscription = subsIdToRaw[actionLog.subsId]\n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(\n                start = itemHorizontalPadding / 2,\n                end = itemHorizontalPadding / 2,\n                top = verticalPadding\n            )\n    ) {\n        if (isDiffApp && appId == null) {\n            Row(\n                modifier = Modifier\n                    .padding(start = itemHorizontalPadding / 4)\n                    .clip(MaterialTheme.shapes.extraSmall)\n                    .clickable(onClick = throttle {\n                        mainVm.navigatePage(\n                            AppConfigRoute(\n                                appId = actionLog.appId,\n                            )\n                        )\n                    })\n                    .fillMaxWidth()\n                    .padding(start = 5.dp),\n                horizontalArrangement = Arrangement.spacedBy(4.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {\n                    Spacer(\n                        modifier = Modifier\n                            .clip(CircleShape)\n                            .background(MaterialTheme.colorScheme.secondary)\n                            .size(4.dp)\n                    )\n                    AppNameText(appId = actionLog.appId, modifier = Modifier.weight(1f))\n                    PerfIcon(\n                        imageVector = PerfIcon.KeyboardArrowRight,\n                        modifier = Modifier\n                            .iconTextSize()\n                    )\n                }\n            }\n        }\n        Row(\n            modifier = Modifier\n                .padding(start = itemHorizontalPadding / 4)\n                .clickable(onClick = onClick)\n                .fillMaxWidth()\n                .height(IntrinsicSize.Min)\n                .padding(start = itemHorizontalPadding / 4)\n        ) {\n            if (appId == null) {\n                Spacer(modifier = Modifier.width(2.dp))\n            }\n            Spacer(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .width(2.dp)\n                    .background(MaterialTheme.colorScheme.primaryContainer),\n            )\n            Spacer(modifier = Modifier.width(8.dp))\n            Column(\n                modifier = Modifier.weight(1f)\n            ) {\n                FixedTimeText(\n                    text = actionLog.date,\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.secondary,\n                )\n                CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyMedium) {\n                    val showActivityId = actionLog.showActivityId\n                    if (showActivityId != null) {\n                        Text(\n                            text = showActivityId,\n                            softWrap = false,\n                            maxLines = 1,\n                            overflow = TextOverflow.MiddleEllipsis,\n                        )\n                    } else {\n                        Text(\n                            text = \"null\",\n                            color = LocalContentColor.current.copy(alpha = 0.5f),\n                        )\n                    }\n                    if (subsId == null) {\n                        Row {\n                            Text(text = subscription?.name ?: \"id=${actionLog.subsId}\")\n                            val lineHeightDp = LocalDensity.current.run {\n                                LocalTextStyle.current.lineHeight.toDp()\n                            }\n                            Row(\n                                modifier = Modifier\n                                    .height(lineHeightDp)\n                                    .padding(start = 4.dp),\n                                verticalAlignment = Alignment.CenterVertically\n                            ) {\n                                Text(\n                                    text = \"v${item.first.subsVersion}\",\n                                    style = MaterialTheme.typography.labelMedium,\n                                    color = MaterialTheme.colorScheme.tertiary,\n                                    modifier = Modifier\n                                        .clip(MaterialTheme.shapes.extraSmall)\n                                        .background(MaterialTheme.colorScheme.tertiaryContainer)\n                                        .padding(horizontal = 2.dp),\n                                )\n                            }\n                        }\n                    }\n                    Row(\n                        modifier = Modifier.fillMaxWidth()\n                    ) {\n                        val groupDesc = group?.name.toString()\n                        val textColor = LocalContentColor.current.let {\n                            if (group?.name == null) it.copy(alpha = 0.5f) else it\n                        }\n                        GroupNameText(\n                            isGlobal = actionLog.groupType == SubsConfig.GlobalGroupType,\n                            text = groupDesc,\n                            color = textColor,\n                        )\n                        val ruleDesc = rule?.name ?: (if ((group?.rules?.size ?: 0) > 1) {\n                            val keyDesc = actionLog.ruleKey?.let { \"key=$it, \" } ?: \"\"\n                            \"${keyDesc}index=${actionLog.ruleIndex}\"\n                        } else {\n                            null\n                        })\n                        if (ruleDesc != null) {\n                            Text(\n                                text = ruleDesc,\n                                modifier = Modifier.padding(start = 8.dp),\n                                color = LocalContentColor.current.copy(alpha = 0.8f),\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ActionLogDialog(\n    vm: ViewModel,\n    actionLog: ActionLog,\n    onDismissRequest: () -> Unit,\n) {\n    val mainVm = LocalMainViewModel.current\n    val scope = rememberCoroutineScope()\n    val subsConfig = remember(actionLog) {\n        (if (actionLog.groupType == SubsConfig.AppGroupType) {\n            DbSet.subsConfigDao.queryAppGroupTypeConfig(\n                actionLog.subsId, actionLog.appId, actionLog.groupKey\n            )\n        } else {\n            DbSet.subsConfigDao.queryGlobalGroupTypeConfig(actionLog.subsId, actionLog.groupKey)\n        }).stateIn(vm.viewModelScope, SharingStarted.Eagerly, null)\n    }.collectAsState().value\n\n    val oldExclude = remember(subsConfig?.exclude) {\n        ExcludeData.parse(subsConfig?.exclude)\n    }\n\n    Dialog(onDismissRequest = onDismissRequest) {\n        Card(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(16.dp),\n            shape = RoundedCornerShape(16.dp),\n        ) {\n            ItemText(\n                text = \"查看规则组\",\n                onClick = {\n                    onDismissRequest()\n                    if (actionLog.groupType == SubsConfig.AppGroupType) {\n                        mainVm.navigatePage(\n                            SubsAppGroupListRoute(\n                                actionLog.subsId, actionLog.appId, actionLog.groupKey\n                            )\n                        )\n                    } else if (actionLog.groupType == SubsConfig.GlobalGroupType) {\n                        mainVm.navigatePage(\n                            SubsGlobalGroupListRoute(\n                                actionLog.subsId, actionLog.groupKey\n                            )\n                        )\n                    }\n                }\n            )\n            HorizontalDivider()\n\n            if (actionLog.groupType == SubsConfig.GlobalGroupType) {\n                val subs = remember(actionLog.subsId) {\n                    subsMapFlow.mapState(scope) { it[actionLog.subsId] }\n                }.collectAsState().value\n                val group = subs?.globalGroups?.find { g -> g.key == actionLog.groupKey }\n                val appChecked = if (group != null) {\n                    getGlobalGroupChecked(\n                        subs,\n                        oldExclude,\n                        group,\n                        actionLog.appId,\n                    )\n                } else {\n                    null\n                }\n                if (appChecked != null) {\n                    ItemText(\n                        text = if (appChecked) \"在此应用禁用\" else \"移除在此应用的禁用\",\n                        onClick = vm.viewModelScope.launchAsFn {\n                            val subsConfig = subsConfig ?: SubsConfig(\n                                type = SubsConfig.GlobalGroupType,\n                                subsId = actionLog.subsId,\n                                groupKey = actionLog.groupKey,\n                            )\n                            val newSubsConfig = subsConfig.copy(\n                                exclude = oldExclude\n                                    .copy(\n                                        appIds = oldExclude.appIds\n                                            .toMutableMap()\n                                            .apply {\n                                                set(actionLog.appId, appChecked)\n                                            })\n                                    .stringify()\n                            )\n                            DbSet.subsConfigDao.insert(newSubsConfig)\n                            toast(\"更新成功\")\n                        }\n                    )\n                    HorizontalDivider()\n                }\n            }\n\n            if (actionLog.activityId != null) {\n                val disabled =\n                    oldExclude.activityIds.contains(actionLog.appId to actionLog.activityId)\n                ItemText(\n                    text = if (disabled) \"移除在此页面的禁用\" else \"在此页面禁用\",\n                    onClick = vm.viewModelScope.launchAsFn {\n                        val subsConfig = if (actionLog.groupType == SubsConfig.AppGroupType) {\n                            subsConfig ?: SubsConfig(\n                                type = SubsConfig.AppGroupType,\n                                subsId = actionLog.subsId,\n                                appId = actionLog.appId,\n                                groupKey = actionLog.groupKey,\n                            )\n                        } else {\n                            subsConfig ?: SubsConfig(\n                                type = SubsConfig.GlobalGroupType,\n                                subsId = actionLog.subsId,\n                                groupKey = actionLog.groupKey,\n                            )\n                        }\n                        val newSubsConfig = subsConfig.copy(\n                            exclude = oldExclude\n                                .switch(\n                                    actionLog.appId,\n                                    actionLog.activityId\n                                )\n                                .stringify()\n                        )\n                        DbSet.subsConfigDao.insert(newSubsConfig)\n                        toast(\"更新成功\")\n                    }\n                )\n                HorizontalDivider()\n            }\n        }\n    }\n}\n\n@Composable\nfun ItemText(\n    text: String,\n    color: Color = Color.Unspecified,\n    onClick: () -> Unit\n) {\n    val modifier = Modifier\n        .clickable(onClick = throttle(onClick))\n        .fillMaxWidth()\n        .padding(16.dp)\n    Text(\n        modifier = modifier,\n        text = text,\n        color = color,\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/ActionLogVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.Pager\nimport androidx.paging.PagingConfig\nimport androidx.paging.cachedIn\nimport androidx.paging.map\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.combine\nimport li.songe.gkd.data.ActionLog\nimport li.songe.gkd.data.SubsConfig\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.util.subsMapFlow\n\nclass ActionLogVm(val route: ActionLogRoute) : ViewModel() {\n\n    val pagingDataFlow = Pager(PagingConfig(pageSize = 100)) {\n        if (route.subsId != null) {\n            DbSet.actionLogDao.pagingSubsSource(subsId = route.subsId)\n        } else if (route.appId != null) {\n            DbSet.actionLogDao.pagingAppSource(appId = route.appId)\n        } else {\n            DbSet.actionLogDao.pagingSource()\n        }\n    }\n        .flow\n        .cachedIn(viewModelScope)\n        .combine(subsMapFlow) { pagingData, subsMap ->\n            pagingData.map { c ->\n                val group = if (c.groupType == SubsConfig.AppGroupType) {\n                    val app = subsMap[c.subsId]?.apps?.find { a -> a.id == c.appId }\n                    app?.groups?.find { g -> g.key == c.groupKey }\n                } else {\n                    subsMap[c.subsId]?.globalGroups?.find { g -> g.key == c.groupKey }\n                }\n                val rule = group?.rules?.run {\n                    if (c.ruleKey != null) {\n                        find { r -> r.key == c.ruleKey }\n                    } else {\n                        getOrNull(c.ruleIndex)\n                    }\n                }\n                Triple(c, group, rule)\n            }\n        }\n\n    val showActionLogFlow = MutableStateFlow<ActionLog?>(null)\n\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/ActivityLogPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport androidx.paging.LoadState\nimport androidx.paging.compose.collectAsLazyPagingItems\nimport androidx.paging.compose.itemKey\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.data.ActivityLog\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.component.AppNameText\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.FixedTimeText\nimport li.songe.gkd.ui.component.LocalNumberCharWidth\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.measureNumberTextWidth\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.iconTextSize\nimport li.songe.gkd.ui.style.itemHorizontalPadding\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.appInfoMapFlow\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata object ActivityLogRoute : NavKey\n\n@Composable\nfun ActivityLogPage() {\n    val context = LocalActivity.current as MainActivity\n    val mainVm = context.mainVm\n    val vm = viewModel<ActivityLogVm>()\n\n    val logCount by vm.logCountFlow.collectAsState()\n    val list = vm.pagingDataFlow.collectAsLazyPagingItems()\n    val resetKey = rememberSaveable { mutableIntStateOf(0) }\n    val (scrollBehavior, listState) = useListScrollState(resetKey, list.itemCount > 0)\n    val timeTextWidth = measureNumberTextWidth(MaterialTheme.typography.bodySmall)\n\n    Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {\n        PerfTopAppBar(\n            scrollBehavior = scrollBehavior,\n            navigationIcon = {\n                PerfIconButton(imageVector = PerfIcon.ArrowBack, onClick = {\n                    mainVm.popPage()\n                })\n            },\n            title = {\n                Text(\n                    text = \"界面日志\",\n                    modifier = Modifier.noRippleClickable { resetKey.intValue++ },\n                )\n            },\n            actions = {\n                if (logCount > 0) {\n                    PerfIconButton(\n                        imageVector = PerfIcon.Delete,\n                        onClick = throttle(fn = vm.viewModelScope.launchAsFn {\n                            mainVm.dialogFlow.waitResult(\n                                title = \"删除日志\",\n                                text = \"确定删除所有界面日志?\",\n                                error = true,\n                            )\n                            DbSet.activityLogDao.deleteAll()\n                            toast(\"删除成功\")\n                        })\n                    )\n                }\n            }\n        )\n    }) { contentPadding ->\n        LazyColumn(\n            modifier = Modifier.scaffoldPadding(contentPadding),\n            state = listState,\n        ) {\n            items(\n                count = list.itemCount,\n                key = list.itemKey { it.id }\n            ) { i ->\n                val actionLog = list[i] ?: return@items\n                val lastActionLog = if (i > 0) list[i - 1] else null\n                CompositionLocalProvider(\n                    LocalNumberCharWidth provides timeTextWidth\n                ) {\n                    ActivityLogCard(i = i, activityLog = actionLog, lastActivityLog = lastActionLog)\n                }\n            }\n            item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                Spacer(modifier = Modifier.height(EmptyHeight))\n                if (logCount == 0 && list.loadState.refresh !is LoadState.Loading) {\n                    EmptyText(text = \"暂无数据\")\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ActivityLogCard(\n    i: Int,\n    activityLog: ActivityLog,\n    lastActivityLog: ActivityLog?,\n) {\n    val mainVm = LocalMainViewModel.current\n    val isDiffApp = activityLog.appId != lastActivityLog?.appId\n    val verticalPadding = if (i == 0) 0.dp else if (isDiffApp) 12.dp else 8.dp\n    val showActivityId = activityLog.showActivityId\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(\n                start = itemHorizontalPadding / 2,\n                end = itemHorizontalPadding / 2,\n                top = verticalPadding\n            )\n    ) {\n        if (isDiffApp) {\n            Row(\n                modifier = Modifier\n                    .padding(start = itemHorizontalPadding / 4)\n                    .clip(MaterialTheme.shapes.extraSmall)\n                    .clickable(onClick = throttle {\n                        mainVm.navigatePage(\n                            AppConfigRoute(\n                                appId = activityLog.appId,\n                            )\n                        )\n                    })\n                    .fillMaxWidth()\n                    .padding(start = 5.dp),\n                horizontalArrangement = Arrangement.spacedBy(4.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {\n                    Spacer(\n                        modifier = Modifier\n                            .clip(CircleShape)\n                            .background(MaterialTheme.colorScheme.secondary)\n                            .size(4.dp)\n                    )\n                    AppNameText(appId = activityLog.appId, modifier = Modifier.weight(1f))\n                    PerfIcon(\n                        imageVector = PerfIcon.KeyboardArrowRight,\n                        modifier = Modifier\n                            .iconTextSize()\n                    )\n                }\n            }\n        }\n        Row(\n            modifier = Modifier\n                .padding(start = itemHorizontalPadding / 4)\n                .clickable(onClick = {\n                    mainVm.textFlow.value = listOfNotNull(\n                        appInfoMapFlow.value[activityLog.appId]?.name,\n                        activityLog.appId,\n                        activityLog.showActivityId,\n                    ).joinToString(\"\\n\")\n                })\n                .fillMaxWidth()\n                .height(IntrinsicSize.Min)\n                .padding(start = itemHorizontalPadding / 4)\n        ) {\n            Spacer(modifier = Modifier.width(2.dp))\n            Spacer(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .width(2.dp)\n                    .background(MaterialTheme.colorScheme.primaryContainer),\n            )\n            Spacer(modifier = Modifier.width(8.dp))\n            Column(\n                modifier = Modifier.weight(1f)\n            ) {\n                FixedTimeText(\n                    text = activityLog.date,\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.secondary,\n                )\n                CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyMedium) {\n                    if (showActivityId != null) {\n                        Text(\n                            text = showActivityId,\n                            softWrap = false,\n                            maxLines = 1,\n                            overflow = TextOverflow.MiddleEllipsis,\n                        )\n                    } else {\n                        Text(\n                            text = \"null\",\n                            color = LocalContentColor.current.copy(alpha = 0.5f),\n                        )\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/ActivityLogVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.lifecycle.viewModelScope\nimport androidx.paging.Pager\nimport androidx.paging.PagingConfig\nimport androidx.paging.cachedIn\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.share.BaseViewModel\n\nclass ActivityLogVm : BaseViewModel() {\n    val pagingDataFlow = Pager(PagingConfig(pageSize = 100)) { DbSet.activityLogDao.pagingSource() }\n        .flow.cachedIn(viewModelScope)\n\n    val logCountFlow =\n        DbSet.activityLogDao.count().stateInit(0)\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/AdvancedPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport android.app.Activity\nimport android.content.Context\nimport android.media.projection.MediaProjectionManager\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport com.dylanc.activityresult.launcher.launchForResult\nimport kotlinx.coroutines.flow.update\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.permission.canDrawOverlaysState\nimport li.songe.gkd.permission.foregroundServiceSpecialUseState\nimport li.songe.gkd.permission.notificationState\nimport li.songe.gkd.permission.requiredPermission\nimport li.songe.gkd.permission.shizukuGrantedState\nimport li.songe.gkd.service.ActivityService\nimport li.songe.gkd.service.ButtonService\nimport li.songe.gkd.service.EventService\nimport li.songe.gkd.service.HttpService\nimport li.songe.gkd.service.ScreenshotService\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.shizuku.updateBinderMutex\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.component.AuthCard\nimport li.songe.gkd.ui.component.CustomOutlinedTextField\nimport li.songe.gkd.ui.component.PerfCustomIconButton\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfSwitch\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.SettingItem\nimport li.songe.gkd.ui.component.TextSwitch\nimport li.songe.gkd.ui.component.autoFocus\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.asMutableState\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.iconTextSize\nimport li.songe.gkd.ui.style.itemPadding\nimport li.songe.gkd.ui.style.titleItemPadding\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.ShortUrlSet\nimport li.songe.gkd.util.appInfoMapFlow\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\nimport li.songe.selector.Selector\n\n@Serializable\ndata object AdvancedPageRoute : NavKey\n\n@Composable\nfun AdvancedPage() {\n    val context = LocalActivity.current as MainActivity\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel<AdvancedVm>()\n    val store by storeFlow.collectAsState()\n\n    var showEditPortDlg by vm.showEditPortDlgFlow.asMutableState()\n    if (showEditPortDlg) {\n        val portRange = remember { 1000 to 65535 }\n        val placeholderText = remember { \"请输入 ${portRange.first}-${portRange.second} 的整数\" }\n        var value by remember {\n            mutableStateOf(store.httpServerPort.toString())\n        }\n        AlertDialog(\n            properties = DialogProperties(dismissOnClickOutside = false),\n            title = { Text(text = \"服务端口\") },\n            text = {\n                OutlinedTextField(\n                    value = value,\n                    placeholder = {\n                        Text(text = placeholderText)\n                    },\n                    onValueChange = {\n                        value = it.filter { c -> c.isDigit() }.take(5)\n                    },\n                    singleLine = true,\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .autoFocus(),\n                    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),\n                    supportingText = {\n                        Text(\n                            text = \"${value.length} / 5\",\n                            modifier = Modifier.fillMaxWidth(),\n                            textAlign = TextAlign.End,\n                        )\n                    },\n                )\n            },\n            onDismissRequest = {\n                showEditPortDlg = false\n            },\n            confirmButton = {\n                TextButton(\n                    enabled = value.isNotEmpty(),\n                    onClick = {\n                        val newPort = value.toIntOrNull()\n                        if (newPort == null || !(portRange.first <= newPort && newPort <= portRange.second)) {\n                            toast(placeholderText)\n                            return@TextButton\n                        }\n                        showEditPortDlg = false\n                        if (newPort != store.httpServerPort) {\n                            storeFlow.value = store.copy(\n                                httpServerPort = newPort\n                            )\n                            toast(\"更新成功\")\n                        }\n                    }\n                ) {\n                    Text(\n                        text = \"确认\", modifier = Modifier\n                    )\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = { showEditPortDlg = false }) {\n                    Text(\n                        text = \"取消\"\n                    )\n                }\n            }\n        )\n    }\n\n    var showShizukuState by vm.showShizukuStateFlow.asMutableState()\n    if (showShizukuState) {\n        val onDismissRequest = { showShizukuState = false }\n        AlertDialog(\n            title = { Text(text = \"授权状态\") },\n            text = {\n                val states = shizukuContextFlow.collectAsState().value.states\n                Column {\n                    states.forEach { (name, value) ->\n                        Text(\n                            text = name,\n                            textDecoration = if (value != null) null else TextDecoration.LineThrough,\n                        )\n                    }\n                }\n            },\n            onDismissRequest = onDismissRequest,\n            confirmButton = {\n                TextButton(onClick = onDismissRequest) {\n                    Text(text = \"我知道了\")\n                }\n            },\n        )\n    }\n\n    var showCaptureScreenshotDlg by vm.showCaptureScreenshotDlgFlow.asMutableState()\n    if (showCaptureScreenshotDlg) {\n        var appIdValue by remember { mutableStateOf(store.screenshotTargetAppId) }\n        var eventSelectorValue by remember { mutableStateOf(store.screenshotEventSelector) }\n        AlertDialog(\n            properties = DialogProperties(dismissOnClickOutside = false),\n            title = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    Text(text = \"截屏快照\")\n                    PerfIconButton(\n                        imageVector = PerfIcon.HelpOutline,\n                        onClick = throttle {\n                            showCaptureScreenshotDlg = false\n                            mainVm.navigateWebPage(ShortUrlSet.URL15)\n                        },\n                    )\n                }\n            },\n            text = {\n                Column(\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    CustomOutlinedTextField(\n                        label = { Text(\"应用ID\") },\n                        value = appIdValue,\n                        placeholder = { Text(text = \"请输入目标应用ID\") },\n                        onValueChange = {\n                            appIdValue = it\n                        },\n                        singleLine = true,\n                        modifier = Modifier.fillMaxWidth(),\n                    )\n                    Spacer(modifier = Modifier.height(8.dp))\n                    CustomOutlinedTextField(\n                        label = { Text(\"特征事件选择器\") },\n                        value = eventSelectorValue,\n                        placeholder = { Text(text = \"请输入特征事件选择器\") },\n                        onValueChange = {\n                            eventSelectorValue = it\n                        },\n                        maxLines = 4,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .autoFocus(),\n                    )\n                }\n            },\n            onDismissRequest = {\n                showCaptureScreenshotDlg = false\n            },\n            confirmButton = {\n                TextButton(onClick = throttle {\n                    if (appIdValue == store.screenshotTargetAppId && eventSelectorValue == store.screenshotEventSelector) {\n                        showCaptureScreenshotDlg = false\n                        return@throttle\n                    }\n                    if (appIdValue.isNotEmpty() && !appInfoMapFlow.value.contains(appIdValue)) {\n                        toast(\"无效应用ID\")\n                        return@throttle\n                    }\n                    if (eventSelectorValue.isNotEmpty()) {\n                        val s = Selector.parseOrNull(eventSelectorValue)\n                        if (s == null) {\n                            toast(\"无效事件选择器\")\n                            return@throttle\n                        }\n                    }\n                    storeFlow.update {\n                        it.copy(\n                            screenshotTargetAppId = appIdValue,\n                            screenshotEventSelector = eventSelectorValue,\n                        )\n                    }\n                    toast(\"更新成功\")\n                    showCaptureScreenshotDlg = false\n                }) {\n                    Text(\n                        text = \"确认\",\n                    )\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = { showCaptureScreenshotDlg = false }) {\n                    Text(\n                        text = \"取消\",\n                    )\n                }\n            })\n    }\n\n    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()\n    Scaffold(\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(\n                scrollBehavior = scrollBehavior,\n                navigationIcon = {\n                    PerfIconButton(imageVector = PerfIcon.ArrowBack, onClick = {\n                        mainVm.popPage()\n                    })\n                },\n                title = { Text(text = \"高级设置\") },\n            )\n        }\n    ) { contentPadding ->\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .verticalScroll(rememberScrollState())\n                .padding(contentPadding),\n        ) {\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .titleItemPadding(showTop = false),\n                horizontalArrangement = Arrangement.SpaceBetween,\n            ) {\n                Text(\n                    modifier = Modifier,\n                    text = \"Shizuku\",\n                    style = MaterialTheme.typography.titleSmall,\n                    color = MaterialTheme.colorScheme.primary,\n                )\n                PerfIcon(\n                    modifier = Modifier\n                        .clip(MaterialTheme.shapes.extraSmall)\n                        .clickable(onClickLabel = \"打开 Shizuku 状态弹窗\", onClick = throttle {\n                            showShizukuState = true\n                        })\n                        .iconTextSize(textStyle = MaterialTheme.typography.titleSmall),\n                    imageVector = PerfIcon.Api,\n                    tint = MaterialTheme.colorScheme.primary,\n                    contentDescription = \"Shizuku 状态\",\n                )\n            }\n            val shizukuGranted by shizukuGrantedState.stateFlow.collectAsState()\n            AnimatedVisibility(store.enableShizuku && !shizukuGranted) {\n                AuthCard(\n                    title = \"未授权\",\n                    subtitle = \"点击授权以优化体验\",\n                    onAuthClick = {\n                        mainVm.requestShizuku()\n                    }\n                )\n            }\n            TextSwitch(\n                title = \"启用优化\",\n                subtitle = \"提升权限优化体验\",\n                suffix = \"了解更多\",\n                suffixUnderline = true,\n                onSuffixClick = { mainVm.navigateWebPage(ShortUrlSet.URL14) },\n                checked = store.enableShizuku,\n                suffixIcon = {\n                    if (updateBinderMutex.state.collectAsState().value) {\n                        CircularProgressIndicator(\n                            modifier = Modifier\n                                .size(20.dp),\n                        )\n                    }\n                },\n                onCheckedChange = {\n                    mainVm.switchEnableShizuku(it)\n                },\n                onClick = null,\n            )\n\n            val server by HttpService.httpServerFlow.collectAsState()\n            val httpServerRunning = server != null\n            val localNetworkIps by HttpService.localNetworkIpsFlow.collectAsState()\n\n            Text(\n                text = \"HTTP\",\n                modifier = Modifier.titleItemPadding(),\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.primary,\n            )\n            Row(\n                modifier = Modifier.itemPadding(),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Column(modifier = Modifier.weight(1f)) {\n                    Text(\n                        text = \"HTTP服务\",\n                        style = MaterialTheme.typography.bodyLarge,\n                    )\n                    CompositionLocalProvider(\n                        LocalTextStyle provides MaterialTheme.typography.bodyMedium\n                    ) {\n                        Text(text = if (httpServerRunning) \"点击链接打开即可自动连接\" else \"在浏览器下连接调试工具\")\n                        AnimatedVisibility(httpServerRunning) {\n                            Column {\n                                Row {\n                                    val localUrl = \"http://127.0.0.1:${store.httpServerPort}\"\n                                    Text(\n                                        text = localUrl,\n                                        color = MaterialTheme.colorScheme.primary,\n                                        style = LocalTextStyle.current.copy(textDecoration = TextDecoration.Underline),\n                                        modifier = Modifier.clickable(onClick = throttle {\n                                            mainVm.openUrl(localUrl)\n                                        }),\n                                    )\n                                    Spacer(modifier = Modifier.width(2.dp))\n                                    Text(text = \"仅本设备可访问\")\n                                }\n                                localNetworkIps.forEach { host ->\n                                    val lanUrl = \"http://${host}:${store.httpServerPort}\"\n                                    Text(\n                                        text = lanUrl,\n                                        color = MaterialTheme.colorScheme.primary,\n                                        style = LocalTextStyle.current.copy(textDecoration = TextDecoration.Underline),\n                                        modifier = Modifier.clickable(onClick = throttle {\n                                            mainVm.openUrl(lanUrl)\n                                        })\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n                PerfSwitch(\n                    checked = httpServerRunning,\n                    onCheckedChange = throttle(fn = vm.viewModelScope.launchAsFn<Boolean> {\n                        if (it) {\n                            requiredPermission(context, foregroundServiceSpecialUseState)\n                            requiredPermission(context, notificationState)\n                            HttpService.start()\n                        } else {\n                            HttpService.stop()\n                        }\n                    })\n                )\n            }\n\n            SettingItem(\n                title = \"服务端口\",\n                subtitle = store.httpServerPort.toString(),\n                imageVector = PerfIcon.Edit,\n                onClickLabel = \"编辑服务端口\",\n                onClick = {\n                    showEditPortDlg = true\n                }\n            )\n\n            TextSwitch(\n                title = \"清除订阅\",\n                subtitle = \"关闭服务时删除内存订阅\",\n                checked = store.autoClearMemorySubs,\n                onCheckedChange = {\n                    storeFlow.update {\n                        it.copy(autoClearMemorySubs = !it.autoClearMemorySubs)\n                    }\n                }\n            )\n\n            Text(\n                text = \"快照\",\n                modifier = Modifier.titleItemPadding(),\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.primary,\n            )\n\n            SettingItem(\n                title = \"快照记录\",\n                subtitle = \"应用界面节点信息及截图\",\n                onClick = {\n                    mainVm.navigatePage(SnapshotPageRoute)\n                }\n            )\n\n            if (!AndroidTarget.R) {\n                val screenshotRunning by ScreenshotService.isRunning.collectAsState()\n                TextSwitch(\n                    title = \"截屏服务\",\n                    subtitle = \"生成快照需要获取屏幕截图\",\n                    checked = screenshotRunning,\n                    onCheckedChange = vm.viewModelScope.launchAsFn<Boolean> {\n                        if (it) {\n                            requiredPermission(context, notificationState)\n                            val mediaProjectionManager =\n                                context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager\n                            val activityResult =\n                                context.launcher.launchForResult(mediaProjectionManager.createScreenCaptureIntent())\n                            if (activityResult.resultCode == Activity.RESULT_OK && activityResult.data != null) {\n                                ScreenshotService.start(intent = activityResult.data!!)\n                            }\n                        } else {\n                            ScreenshotService.stop()\n                        }\n                    }\n                )\n            }\n\n            TextSwitch(\n                title = \"快照按钮\",\n                subtitle = \"显示按钮点击保存快照\",\n                checked = ButtonService.isRunning.collectAsState().value,\n                onCheckedChange = vm.viewModelScope.launchAsFn<Boolean> {\n                    if (it) {\n                        requiredPermission(context, foregroundServiceSpecialUseState)\n                        requiredPermission(context, notificationState)\n                        requiredPermission(context, canDrawOverlaysState)\n                        ButtonService.start()\n                    } else {\n                        ButtonService.stop()\n                    }\n                },\n            )\n\n            TextSwitch(\n                title = \"音量快照\",\n                subtitle = \"音量变化时保存快照\",\n                checked = store.captureVolumeChange,\n                onCheckedChange = {\n                    storeFlow.value = store.copy(\n                        captureVolumeChange = it\n                    )\n                },\n            )\n\n            TextSwitch(\n                title = \"截屏快照\",\n                subtitle = \"截屏时保存快照\",\n                checked = store.captureScreenshot,\n                suffixIcon = {\n                    PerfCustomIconButton(\n                        size = 32.dp,\n                        iconSize = 20.dp,\n                        onClickLabel = \"打开配置截屏快照弹窗\",\n                        onClick = throttle {\n                            showCaptureScreenshotDlg = true\n                        },\n                        id = R.drawable.ic_page_info,\n                        contentDescription = \"截屏快照设置\",\n                    )\n                },\n                onCheckedChange = {\n                    storeFlow.value = store.copy(\n                        captureScreenshot = it\n                    )\n                    if (it && store.screenshotTargetAppId.isEmpty() || store.screenshotEventSelector.isEmpty()) {\n                        toast(\"请配置目标应用和特征事件选择器\")\n                    }\n                }\n            )\n\n            TextSwitch(\n                title = \"隐藏状态栏\",\n                subtitle = \"隐藏快照截图状态栏\",\n                checked = store.hideSnapshotStatusBar,\n                onCheckedChange = {\n                    storeFlow.value = store.copy(\n                        hideSnapshotStatusBar = it\n                    )\n                }\n            )\n\n            TextSwitch(\n                title = \"保存提示\",\n                subtitle = \"提示「正在保存快照」\",\n                checked = store.showSaveSnapshotToast,\n                onCheckedChange = {\n                    storeFlow.value = store.copy(\n                        showSaveSnapshotToast = it\n                    )\n                }\n            )\n\n            SettingItem(\n                title = \"Github Cookie\",\n                subtitle = \"生成快照/日志链接\",\n                suffix = \"获取教程\",\n                suffixUnderline = true,\n                onSuffixClick = {\n                    mainVm.navigateWebPage(ShortUrlSet.URL1)\n                },\n                imageVector = PerfIcon.Edit,\n                onClick = {\n                    mainVm.showEditCookieDlgFlow.value = true\n                }\n            )\n\n            Text(\n                text = \"日志\",\n                modifier = Modifier.titleItemPadding(),\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.primary,\n            )\n            SettingItem(\n                title = \"界面日志\",\n                subtitle = \"界面切换日志\",\n                onClick = {\n                    mainVm.navigatePage(ActivityLogRoute)\n                }\n            )\n            TextSwitch(\n                title = \"界面服务\",\n                subtitle = \"显示当前界面信息\",\n                checked = ActivityService.isRunning.collectAsState().value,\n                onCheckedChange = vm.viewModelScope.launchAsFn<Boolean> {\n                    if (it) {\n                        requiredPermission(context, foregroundServiceSpecialUseState)\n                        requiredPermission(context, notificationState)\n                        requiredPermission(context, canDrawOverlaysState)\n                        ActivityService.start()\n                    } else {\n                        ActivityService.stop()\n                    }\n                }\n            )\n            SettingItem(\n                title = \"事件日志\",\n                subtitle = \"无障碍事件日志\",\n                onClick = {\n                    mainVm.navigatePage(A11yEventLogRoute)\n                }\n            )\n            TextSwitch(\n                title = \"事件服务\",\n                subtitle = \"显示无障碍事件\",\n                checked = EventService.isRunning.collectAsState().value,\n                onCheckedChange = vm.viewModelScope.launchAsFn<Boolean> {\n                    if (it) {\n                        requiredPermission(context, foregroundServiceSpecialUseState)\n                        requiredPermission(context, notificationState)\n                        requiredPermission(context, canDrawOverlaysState)\n                        EventService.start()\n                    } else {\n                        EventService.stop()\n                    }\n                }\n            )\n\n            Spacer(modifier = Modifier.height(EmptyHeight))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/AdvancedVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.lifecycle.ViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\n\nclass AdvancedVm : ViewModel() {\n\n    val showEditPortDlgFlow = MutableStateFlow(false)\n    val showShizukuStateFlow = MutableStateFlow(false)\n    val showCaptureScreenshotDlgFlow = MutableStateFlow(false)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/AppConfigPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.update\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.data.ActionLog\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.component.AnimatedBooleanContent\nimport li.songe.gkd.ui.component.AnimationFloatingActionButton\nimport li.songe.gkd.ui.component.AppNameText\nimport li.songe.gkd.ui.component.BatchActionButtonGroup\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.MenuGroupCard\nimport li.songe.gkd.ui.component.MenuItemRadioButton\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.RuleGroupCard\nimport li.songe.gkd.ui.component.animateListItem\nimport li.songe.gkd.ui.component.toGroupState\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.icon.BackCloseIcon\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.iconTextSize\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.LOCAL_SUBS_ID\nimport li.songe.gkd.util.RuleSortOption\nimport li.songe.gkd.util.appInfoMapFlow\nimport li.songe.gkd.util.copyText\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.switchItem\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toJson5String\n\n@Serializable\ndata class AppConfigRoute(\n    val appId: String,\n    val focusLog: ActionLog? = null,\n) : NavKey\n\n@Composable\nfun AppConfigPage(route: AppConfigRoute) {\n    val appId = route.appId\n    val focusLog = route.focusLog\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel { AppConfigVm(route) }\n\n    val ruleSortType by vm.ruleSortTypeFlow.collectAsState()\n    val groupSize by vm.groupSizeFlow.collectAsState()\n    val firstLoading by vm.firstLoadingFlow.collectAsState()\n    val resetKey = rememberSaveable { mutableIntStateOf(0) }\n    val (scrollBehavior, listState) = useListScrollState(\n        resetKey,\n        groupSize > 0,\n        ruleSortType.value\n    )\n    if (focusLog != null && groupSize > 0) {\n        LaunchedEffect(null) {\n            if (vm.focusGroupFlow?.value != null) {\n                val i = vm.subsPairsFlow.value.run {\n                    var j = 0\n                    forEach { (entry, groups) ->\n                        groups.forEach {\n                            if (entry.subsItem.id == focusLog.subsId && it.groupType == focusLog.groupType && it.key == focusLog.groupKey) {\n                                return@run j\n                            }\n                            j++\n                        }\n                    }\n                    -1\n                }\n                if (i >= 0) {\n                    listState.scrollToItem(i)\n                }\n            }\n        }\n    }\n\n    val isSelectedMode = vm.isSelectedModeFlow.collectAsState().value\n    val selectedDataSet = vm.selectedDataSetFlow.collectAsState().value\n    LaunchedEffect(key1 = isSelectedMode) {\n        if (!isSelectedMode) {\n            vm.selectedDataSetFlow.value = emptySet()\n        }\n    }\n    LaunchedEffect(key1 = selectedDataSet.isEmpty()) {\n        if (selectedDataSet.isEmpty()) {\n            vm.isSelectedModeFlow.value = false\n        }\n    }\n    BackHandler(isSelectedMode) {\n        vm.isSelectedModeFlow.value = false\n    }\n    Scaffold(\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {\n                IconButton(onClick = throttle {\n                    if (isSelectedMode) {\n                        vm.isSelectedModeFlow.value = false\n                    } else {\n                        mainVm.popPage()\n                    }\n                }) {\n                    BackCloseIcon(backOrClose = !isSelectedMode)\n                }\n            }, title = {\n                val titleModifier = Modifier.noRippleClickable {\n                    resetKey.intValue++\n                }\n                if (isSelectedMode) {\n                    Text(\n                        modifier = titleModifier,\n                        text = if (selectedDataSet.isNotEmpty()) selectedDataSet.size.toString() else \"\",\n                    )\n                } else {\n                    AppNameText(\n                        modifier = titleModifier,\n                        appId = appId\n                    )\n                }\n            }, actions = {\n                var expanded by remember { mutableStateOf(false) }\n                AnimatedBooleanContent(\n                    targetState = isSelectedMode,\n                    contentAlignment = Alignment.TopEnd,\n                    contentTrue = {\n                        Row {\n                            PerfIconButton(\n                                imageVector = PerfIcon.ContentCopy,\n                                enabled = selectedDataSet.any { a -> a.appId != null },\n                                onClick = throttle(vm.viewModelScope.launchAsFn(Dispatchers.Default) {\n                                    val selectGroups = mutableListOf<RawSubscription.RawAppGroup>()\n                                    vm.subsPairsFlow.value.forEach { (entry, groups) ->\n                                        groups.forEach { g ->\n                                            if (g is RawSubscription.RawAppGroup && selectedDataSet.any { v -> entry.subsItem.id == v.subsId && g.key == v.groupKey }) {\n                                                selectGroups.add(g)\n                                            }\n                                        }\n                                    }\n                                    val a = RawSubscription.RawApp(\n                                        id = appId,\n                                        name = appInfoMapFlow.value[appId]?.name,\n                                        groups = selectGroups,\n                                    )\n                                    copyText(toJson5String(a))\n                                })\n                            )\n                            BatchActionButtonGroup(vm, selectedDataSet)\n                            PerfIconButton(imageVector = PerfIcon.MoreVert, onClick = {\n                                expanded = true\n                            })\n                        }\n                    },\n                    contentFalse = {\n                        Row {\n                            PerfIconButton(imageVector = PerfIcon.History, onClick = throttle {\n                                mainVm.navigatePage(ActionLogRoute(appId = appId))\n                            })\n                            PerfIconButton(imageVector = PerfIcon.Sort, onClick = {\n                                expanded = true\n                            })\n                        }\n                    },\n                )\n                Box(\n                    modifier = Modifier.wrapContentSize(Alignment.TopStart)\n                ) {\n                    key(isSelectedMode) {\n                        DropdownMenu(\n                            expanded = expanded,\n                            onDismissRequest = { expanded = false }\n                        ) {\n                            if (isSelectedMode) {\n                                DropdownMenuItem(\n                                    text = {\n                                        Text(text = \"全选\")\n                                    },\n                                    onClick = {\n                                        expanded = false\n                                        vm.selectAll()\n                                    }\n                                )\n                                DropdownMenuItem(\n                                    text = {\n                                        Text(text = \"反选\")\n                                    },\n                                    onClick = {\n                                        expanded = false\n                                        vm.invertSelect()\n                                    }\n                                )\n                            } else {\n                                MenuGroupCard(inTop = true, title = \"排序\") {\n                                    val handleItem: (RuleSortOption) -> Unit = throttle { v ->\n                                        storeFlow.update { s -> s.copy(appRuleSort = v.value) }\n                                    }\n                                    RuleSortOption.objects.forEach { s ->\n                                        MenuItemRadioButton(\n                                            text = s.label,\n                                            selected = ruleSortType == s,\n                                            onClick = {\n                                                handleItem(s)\n                                            },\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            })\n        },\n        floatingActionButton = {\n            AnimationFloatingActionButton(\n                visible = !isSelectedMode,\n                onClick = {\n                    mainVm.navigatePage(\n                        UpsertRuleGroupRoute(\n                            subsId = LOCAL_SUBS_ID,\n                            groupKey = null,\n                            appId = appId\n                        )\n                    )\n                },\n                imageVector = PerfIcon.Add,\n                contentDescription = \"添加规则\"\n            )\n        },\n    ) { contentPadding ->\n        val globalSubsConfigs by vm.globalSubsConfigsFlow.collectAsState()\n        val categoryConfigs by vm.categoryConfigsFlow.collectAsState()\n        val appSubsConfigs by vm.appSubsConfigsFlow.collectAsState()\n        val subsPairs by vm.subsPairsFlow.collectAsState()\n        LazyColumn(\n            modifier = Modifier.scaffoldPadding(contentPadding),\n            state = listState,\n            verticalArrangement = Arrangement.spacedBy(4.dp),\n        ) {\n            subsPairs.forEach { (entry, groups) ->\n                val subsId = entry.subsItem.id\n                stickyHeader(entry.subsItem.id) {\n                    Row(\n                        modifier = Modifier\n                            .background(MaterialTheme.colorScheme.background)\n                            .padding(horizontal = 8.dp)\n                            .clip(MaterialTheme.shapes.extraSmall)\n                            .clickable(onClick = throttle {\n                                mainVm.navigatePage(\n                                    SubsAppGroupListRoute(\n                                        subsItemId = subsId,\n                                        appId = appId,\n                                    )\n                                )\n                            })\n                            .fillMaxWidth()\n                            .padding(4.dp),\n                        horizontalArrangement = Arrangement.spacedBy(4.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        Text(\n                            modifier = Modifier.weight(1f),\n                            text = entry.subscription.name,\n                            style = MaterialTheme.typography.titleSmall,\n                            color = MaterialTheme.colorScheme.primary,\n                            maxLines = 1,\n                            softWrap = false,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                        PerfIcon(\n                            imageVector = PerfIcon.KeyboardArrowRight,\n                            tint = MaterialTheme.colorScheme.primary,\n                            modifier = Modifier.iconTextSize()\n                        )\n                    }\n                }\n                items(groups, { Triple(subsId, it.groupType, it.key) }) { group ->\n                    val subsConfig = when (group) {\n                        is RawSubscription.RawAppGroup -> appSubsConfigs\n                        is RawSubscription.RawGlobalGroup -> globalSubsConfigs\n                    }.find { it.subsId == entry.subsItem.id && it.groupKey == group.key }\n                    val category = when (group) {\n                        is RawSubscription.RawAppGroup -> entry.subscription.groupToCategoryMap[group]\n                        is RawSubscription.RawGlobalGroup -> null\n                    }\n                    val categoryConfig = if (category != null) {\n                        categoryConfigs.find { it.subsId == subsId && it.categoryKey == category.key }\n                    } else {\n                        null\n                    }\n                    val isSelected = selectedDataSet.any {\n                        it.subsId == subsId && it.groupType == group.groupType && it.groupKey == group.key\n                    }\n                    val onLongClick = {\n                        if (groupSize > 1 && !isSelectedMode) {\n                            vm.isSelectedModeFlow.value = true\n                            vm.selectedDataSetFlow.value = setOf(\n                                group.toGroupState(\n                                    subsId = subsId,\n                                    appId = appId,\n                                )\n                            )\n                        }\n                    }\n                    val onSelectedChange = {\n                        vm.selectedDataSetFlow.value =\n                            selectedDataSet.switchItem(\n                                group.toGroupState(\n                                    subsId = subsId,\n                                    appId = appId,\n                                )\n                            )\n                    }\n                    RuleGroupCard(\n                        modifier = Modifier.animateListItem(),\n                        subs = entry.subscription,\n                        appId = appId,\n                        group = group,\n                        subsConfig = subsConfig,\n                        category = category,\n                        categoryConfig = categoryConfig,\n                        onLongClick = onLongClick,\n                        isSelectedMode = isSelectedMode,\n                        isSelected = isSelected,\n                        onSelectedChange = onSelectedChange,\n                        focusGroupFlow = vm.focusGroupFlow,\n                    )\n                }\n            }\n            item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                Spacer(modifier = Modifier.height(EmptyHeight))\n                if (groupSize == 0 && !firstLoading) {\n                    EmptyText(text = \"暂无规则\")\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/AppConfigVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.flattenConcat\nimport kotlinx.coroutines.flow.flowOf\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.data.SubsConfig\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.component.ShowGroupState\nimport li.songe.gkd.ui.component.toGroupState\nimport li.songe.gkd.ui.share.BaseViewModel\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.RuleSortOption\nimport li.songe.gkd.util.collator\nimport li.songe.gkd.util.findOption\nimport li.songe.gkd.util.subsItemsFlow\nimport li.songe.gkd.util.usedSubsEntriesFlow\n\n\nclass AppConfigVm(val route: AppConfigRoute) : BaseViewModel() {\n    val ruleSortTypeFlow = storeFlow.mapNew {\n        RuleSortOption.objects.findOption(it.appRuleSort)\n    }\n\n    private val usedSubsIdsFlow = subsItemsFlow.mapNew { list ->\n        list.filter { it.enable }.map { it.id }.sorted()\n    }\n\n    private val appConfigsFlow = DbSet.appConfigDao.queryAppUsedList(route.appId).attachLoad()\n\n    private val appUsedSubsIdsFlow = combine(usedSubsIdsFlow, appConfigsFlow) { ids, configs ->\n        ids.filter {\n            configs.find { c -> c.subsId == it }?.enable != false\n        }\n    }.stateInit(usedSubsIdsFlow.value)\n\n    private val latestLogsFlow = ruleSortTypeFlow.map {\n        if (it == RuleSortOption.ByActionTime) {\n            DbSet.actionLogDao.queryLatestByAppId(route.appId)\n        } else {\n            flowOf(emptyList())\n        }\n    }.flattenConcat().attachLoad().stateInit(emptyList())\n\n    val globalSubsConfigsFlow = DbSet.subsConfigDao.queryUsedGlobalConfig().attachLoad()\n        .stateInit(emptyList())\n\n    val appSubsConfigsFlow = appUsedSubsIdsFlow.map {\n        DbSet.subsConfigDao.queryAppConfig(it, route.appId)\n    }.flattenConcat().attachLoad()\n        .stateInit(emptyList())\n\n    val categoryConfigsFlow = appUsedSubsIdsFlow.map {\n        DbSet.categoryConfigDao.queryBySubsIds(it)\n    }.flattenConcat().attachLoad()\n        .stateInit(emptyList())\n\n    private val temp1ListFlow = combine(\n        appUsedSubsIdsFlow,\n        usedSubsEntriesFlow,\n        globalSubsConfigsFlow,\n    ) { usedSubsIds, list, configs ->\n        list.map { e ->\n            val globalGroups = e.subscription.globalGroups\n                .filter { g -> configs.find { it.subsId == e.subsItem.id && it.groupKey == g.key }?.enable != false }\n            val appGroups = if (usedSubsIds.contains(e.subsItem.id)) {\n                e.subscription.getAppGroups(route.appId)\n            } else {\n                emptyList()\n            }\n            e to (globalGroups + appGroups)\n        }.filter { it.second.isNotEmpty() }\n    }.stateInit(emptyList())\n\n    val subsPairsFlow = combine(\n        temp1ListFlow,\n        latestLogsFlow,\n        ruleSortTypeFlow\n    ) { list, logs, sortType ->\n        when (sortType) {\n            RuleSortOption.ByDefault -> list\n            RuleSortOption.ByRuleName -> list.map { e ->\n                e.first to e.second.sortedWith { a, b ->\n                    collator.compare(\n                        a.name,\n                        b.name\n                    )\n                }\n            }\n\n            RuleSortOption.ByActionTime -> list.map { e ->\n                e.first to e.second.sortedBy { a ->\n                    -(logs.find { c ->\n                        c.subsId == e.first.subsItem.id && c.groupType == a.groupType && c.groupKey == a.key\n                    }?.id ?: 0)\n                }\n            }\n        }\n    }.combine(firstLoadingFlow) { list, firstLoading ->\n        if (firstLoading) {\n            emptyList()\n        } else {\n            list\n        }\n    }.stateInit(emptyList())\n\n    val groupSizeFlow = subsPairsFlow.mapNew { list ->\n        list.sumOf { it.second.size }\n    }\n\n    val isSelectedModeFlow = MutableStateFlow(false)\n    val selectedDataSetFlow = MutableStateFlow(emptySet<ShowGroupState>())\n\n    private fun getAllSelectedDataSet() = subsPairsFlow.value.map { e ->\n        e.second.map { g ->\n            g.toGroupState(subsId = e.first.subsItem.id, appId = route.appId)\n        }\n    }.flatten().toSet()\n\n    fun selectAll() {\n        selectedDataSetFlow.value = getAllSelectedDataSet()\n    }\n\n    fun invertSelect() {\n        selectedDataSetFlow.value = getAllSelectedDataSet() - selectedDataSetFlow.value\n    }\n\n    val focusGroupFlow = route.focusLog?.let {\n        MutableStateFlow<Triple<Long, String?, Int>?>(\n            Triple(\n                it.subsId,\n                if (it.groupType == SubsConfig.AppGroupType) it.appId else null,\n                it.groupKey,\n            )\n        )\n    }\n\n    init {\n        viewModelScope.launch {\n            appUsedSubsIdsFlow.collect {\n                LogUtils.d(it)\n            }\n        }\n    }\n\n}\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/AppOpsAllowPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.permission.PermissionState\nimport li.songe.gkd.permission.appOpsRestrictStateList\nimport li.songe.gkd.permission.appOpsRestrictedFlow\nimport li.songe.gkd.ui.component.AuthButtonGroup\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.ManualAuthDialog\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.updateDialogOptions\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.itemHorizontalPadding\nimport li.songe.gkd.util.getShareApkFile\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.saveFileToDownloads\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata object AppOpsAllowRoute : NavKey\n\n@Composable\nfun AppOpsAllowPage() {\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    val vm = viewModel<AppOpsAllowVm>()\n    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()\n    val appOpsRestricted by appOpsRestrictedFlow.collectAsState()\n    Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {\n        PerfTopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {\n            PerfIconButton(imageVector = PerfIcon.ArrowBack, onClick = {\n                mainVm.popPage()\n            })\n        }, title = {\n            Text(text = \"解除限制\")\n        })\n    }) { contentPadding ->\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .verticalScroll(rememberScrollState())\n                .padding(contentPadding)\n        ) {\n            if (appOpsRestricted) {\n                Column(\n                    modifier = Modifier\n                        .padding(itemHorizontalPadding, 0.dp)\n                        .fillMaxWidth(),\n                ) {\n                    Text(\n                        text = \"下列权限应默认授予，但可能因某些操作如系统升级，备份迁移等被限制\",\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n                    Spacer(modifier = Modifier.height(24.dp))\n                    Column(\n                        modifier = Modifier.padding(horizontal = 8.dp),\n                        verticalArrangement = Arrangement.spacedBy(12.dp),\n                    ) {\n                        appOpsRestrictStateList.forEach { RestrictItem(it) }\n                    }\n                    Spacer(modifier = Modifier.height(16.dp))\n                    AuthButtonGroup(\n                        modifier = Modifier.fillMaxWidth(),\n                        buttons = listOf(\n                            \"Shizuku 授权\" to vm.viewModelScope.launchAsFn(Dispatchers.IO) {\n                                mainVm.guardShizukuContext()\n                                toast(\"授权成功\")\n                            },\n                            \"命令授权\" to {\n                                vm.showCopyDlgFlow.value = true\n                            },\n                            \"卸载重装\" to {\n                                mainVm.dialogFlow.updateDialogOptions(\n                                    title = \"卸载重装\",\n                                    text = \"卸载后重新安装可让应用权限回归初始状态解除限制，先点击下方「导出应用」可将应用提前保存至下载，然后卸载应用，到文件管理中重新安装即可\\n\\n注意：卸载会删除所有数据，请自行备份数据\",\n                                    dismissText = \"导出应用\",\n                                    dismissAction = {\n                                        mainVm.viewModelScope.launchTry(Dispatchers.IO) {\n                                            context.saveFileToDownloads(getShareApkFile())\n                                        }\n                                    },\n                                    confirmText = \"关闭\",\n                                )\n                            }\n                        )\n                    )\n                }\n            }\n            Spacer(modifier = Modifier.height(EmptyHeight))\n            if (!appOpsRestricted) {\n                Spacer(modifier = Modifier.height(EmptyHeight))\n                EmptyText(text = \"状态正常, 无需操作\")\n            }\n        }\n    }\n\n    val showCopyDlg by vm.showCopyDlgFlow.collectAsState()\n    ManualAuthDialog(\n        commandText = gkdStartCommandText,\n        show = showCopyDlg,\n        onUpdateShow = {\n            vm.showCopyDlgFlow.value = it\n        }\n    )\n}\n\n@Composable\nprivate fun RestrictItem(state: PermissionState) {\n    if (!state.stateFlow.collectAsState().value) {\n        Row {\n            val lineHeightDp = LocalDensity.current.run { LocalTextStyle.current.lineHeight.toDp() }\n            val size = 5.dp\n            Spacer(\n                modifier = Modifier\n                    .padding(vertical = (lineHeightDp - size) / 2)\n                    .clip(CircleShape)\n                    .background(MaterialTheme.colorScheme.tertiary)\n                    .size(size)\n            )\n            Spacer(modifier = Modifier.width(8.dp))\n            Text(\n                style = MaterialTheme.typography.titleMedium,\n                text = state.name,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/AppOpsAllowVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.permission.foregroundServiceSpecialUseState\n\nclass AppOpsAllowVm : ViewModel() {\n    val showCopyDlgFlow = MutableStateFlow(false)\n\n    init {\n        viewModelScope.launch(Dispatchers.IO) {\n            while (isActive) {\n                foregroundServiceSpecialUseState.updateAndGet()\n                delay(1000)\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/AuthA11yPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport android.Manifest\nimport android.app.AppOpsManagerHidden\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.META\nimport li.songe.gkd.permission.Manifest_permission_GET_APP_OPS_STATS\nimport li.songe.gkd.permission.writeSecureSettingsState\nimport li.songe.gkd.service.A11yService\nimport li.songe.gkd.service.fixRestartAutomatorService\nimport li.songe.gkd.shizuku.SafeAppOpsService\nimport li.songe.gkd.shizuku.shizukuUsedFlow\nimport li.songe.gkd.store.updateEnableAutomator\nimport li.songe.gkd.ui.component.AnimatedBooleanContent\nimport li.songe.gkd.ui.component.ManualAuthDialog\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.updateDialogOptions\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.cardHorizontalPadding\nimport li.songe.gkd.ui.style.itemHorizontalPadding\nimport li.songe.gkd.ui.style.surfaceCardColors\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.AutomatorModeOption\nimport li.songe.gkd.util.ShortUrlSet\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.openA11ySettings\nimport li.songe.gkd.util.shFolder\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata object AuthA11yRoute : NavKey\n\n@Composable\nfun AuthA11yPage() {\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel<AuthA11yVm>()\n    val showCopyDlg by vm.showCopyDlgFlow.collectAsState()\n    val writeSecureSettings by writeSecureSettingsState.stateFlow.collectAsState()\n    val a11yRunning by A11yService.isRunning.collectAsState()\n    val automatorMode by mainVm.automatorModeFlow.collectAsState()\n    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()\n    Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {\n        PerfTopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {\n            PerfIconButton(\n                imageVector = PerfIcon.ArrowBack,\n                onClick = {\n                    mainVm.popPage()\n                })\n        }, title = {\n            Text(text = \"工作模式\")\n        })\n    }) { contentPadding ->\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .verticalScroll(rememberScrollState())\n                .padding(contentPadding)\n        ) {\n            Card(\n                modifier = Modifier\n                    .padding(horizontal = itemHorizontalPadding)\n                    .fillMaxWidth(),\n                onClick = throttle { mainVm.updateAutomatorMode(AutomatorModeOption.A11yMode) },\n                colors = surfaceCardColors,\n            ) {\n                Row(\n                    modifier = Modifier.padding(12.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    RadioButton(\n                        selected = automatorMode == AutomatorModeOption.A11yMode,\n                        onClick = null,\n                    )\n                    Text(\n                        modifier = Modifier.padding(start = 12.dp),\n                        text = AutomatorModeOption.A11yMode.label,\n                        style = MaterialTheme.typography.titleMedium,\n                    )\n                }\n                Text(\n                    modifier = Modifier\n                        .padding(horizontal = cardHorizontalPadding)\n                        .padding(start = 4.dp),\n                    text = \"基础\",\n                    style = MaterialTheme.typography.titleSmall\n                )\n                TextListItem(\n                    modifier = Modifier\n                        .padding(horizontal = cardHorizontalPadding)\n                        .padding(start = 8.dp, top = 4.dp),\n                    style = MaterialTheme.typography.bodyMedium,\n                    list = listOf(\n                        \"授予「无障碍权限」\",\n                        \"无障碍关闭后需重新授权\"\n                    ),\n                )\n                AnimatedBooleanContent(\n                    targetState = writeSecureSettings || a11yRunning,\n                    contentTrue = {\n                        Text(\n                            modifier = Modifier\n                                .padding(horizontal = cardHorizontalPadding)\n                                .padding(start = 8.dp, top = 4.dp),\n                            text = \"已持有「无障碍权限」可继续使用\",\n                            style = MaterialTheme.typography.bodySmall,\n                        )\n                    },\n                    contentFalse = {\n                        Row(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = cardHorizontalPadding),\n                            verticalAlignment = Alignment.Bottom,\n                            horizontalArrangement = Arrangement.SpaceBetween,\n                        ) {\n                            TextButton(\n                                onClick = throttle { openA11ySettings() },\n                            ) {\n                                Text(\n                                    text = \"手动授权\",\n                                    style = MaterialTheme.typography.bodyLarge,\n                                )\n                            }\n                            Text(\n                                modifier = Modifier\n                                    .padding(bottom = 12.dp)\n                                    .clip(MaterialTheme.shapes.extraSmall)\n                                    .clickable(onClick = throttle {\n                                        mainVm.navigateWebPage(ShortUrlSet.URL2)\n                                    })\n                                    .padding(horizontal = 4.dp),\n                                text = \"无法开启无障碍?\",\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.primary,\n                            )\n                        }\n                    }\n                )\n                Text(\n                    modifier = Modifier\n                        .padding(horizontal = cardHorizontalPadding)\n                        .padding(start = 4.dp, top = 8.dp),\n                    text = \"增强\",\n                    style = MaterialTheme.typography.titleSmall,\n                )\n                TextListItem(\n                    modifier = Modifier\n                        .padding(horizontal = cardHorizontalPadding)\n                        .padding(start = 8.dp, top = 4.dp),\n                    style = MaterialTheme.typography.bodyMedium,\n                    list = listOf(\n                        \"授予「写入安全设置权限」\",\n                        \"应用可自行控制开关无障碍\",\n                    ),\n                )\n                AnimatedBooleanContent(\n                    targetState = writeSecureSettings,\n                    contentTrue = {\n                        Text(\n                            modifier = Modifier\n                                .padding(horizontal = cardHorizontalPadding)\n                                .padding(start = 8.dp, top = 4.dp),\n                            text = \"已持有「写入安全设置权限」 优先使用此项\",\n                            style = MaterialTheme.typography.bodySmall,\n                        )\n                    },\n                    contentFalse = {\n                        Row(\n                            modifier = Modifier\n                                .padding(horizontal = cardHorizontalPadding),\n                            horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        ) {\n                            ShizukuAuthButton()\n                            TextButton(onClick = { vm.showCopyDlgFlow.value = true }) {\n                                Text(\n                                    text = \"命令授权\",\n                                    style = MaterialTheme.typography.bodyLarge,\n                                )\n                            }\n                        }\n                    }\n                )\n                TextButton(\n                    modifier = Modifier\n                        .padding(horizontal = cardHorizontalPadding),\n                    onClick = throttle {\n                        if (!writeSecureSettings) {\n                            toast(\"请先授予「${writeSecureSettingsState.name}」\")\n                        }\n                        mainVm.dialogFlow.updateDialogOptions(\n                            title = \"无感保活\",\n                            text = \"添加通知栏快捷开关\\n\\n1. 下拉通知栏至「快捷开关」标界面\\n2. 找到名称为 ${META.appName} 的快捷开关\\n3. 添加此开关到通知面板 \\n\\n只要此快捷开关在通知面板可见\\n无论是系统杀后台还是自身崩溃\\n简单下拉打开通知即可重启\"\n                        )\n                    }\n                ) {\n                    Text(\n                        text = \"无感保活\",\n                        style = MaterialTheme.typography.bodyLarge,\n                    )\n                }\n                Spacer(modifier = Modifier.height(12.dp))\n            }\n            Spacer(modifier = Modifier.height(12.dp))\n            Card(\n                modifier = Modifier\n                    .padding(horizontal = itemHorizontalPadding)\n                    .fillMaxWidth(),\n                onClick = throttle { mainVm.updateAutomatorMode(AutomatorModeOption.AutomationMode) },\n                colors = surfaceCardColors,\n            ) {\n                Row(\n                    modifier = Modifier.padding(12.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    RadioButton(\n                        selected = automatorMode == AutomatorModeOption.AutomationMode,\n                        onClick = null,\n                    )\n                    Text(\n                        modifier = Modifier.padding(start = 12.dp),\n                        text = AutomatorModeOption.AutomationMode.label,\n                        style = MaterialTheme.typography.titleMedium,\n                    )\n                }\n                TextListItem(\n                    modifier = Modifier\n                        .padding(horizontal = cardHorizontalPadding)\n                        .padding(start = 8.dp),\n                    style = MaterialTheme.typography.bodyMedium,\n                    list = listOf(\n                        \"自动化驱动的无障碍\",\n                        \"不会导致界面显示异常\",\n                        \"不会被其它应用检测为无障碍\",\n                        \"部分应用仍需切换至无障碍模式\",\n                    ),\n                )\n                AnimatedBooleanContent(\n                    targetState = shizukuUsedFlow.collectAsState().value,\n                    contentTrue = {\n                        Text(\n                            modifier = Modifier\n                                .padding(horizontal = cardHorizontalPadding)\n                                .padding(start = 8.dp, top = 8.dp),\n                            text = \"已连接 Shizuku 服务，可继续使用\",\n                            style = MaterialTheme.typography.bodySmall,\n                        )\n                    },\n                    contentFalse = {\n                        ShizukuAuthButton(\n                            modifier = Modifier.padding(\n                                start = cardHorizontalPadding\n                            )\n                        )\n                    }\n                )\n                TextButton(\n                    modifier = Modifier.padding(start = cardHorizontalPadding),\n                    onClick = throttle {\n                        mainVm.navigatePage(A11YScopeAppListRoute)\n                    },\n                ) {\n                    Text(\n                        text = \"局部无障碍\",\n                        style = MaterialTheme.typography.bodyLarge,\n                    )\n                }\n                Spacer(modifier = Modifier.height(12.dp))\n            }\n            Spacer(modifier = Modifier.height(EmptyHeight))\n        }\n    }\n\n    ManualAuthDialog(\n        commandText = gkdStartCommandText,\n        show = showCopyDlg,\n        onUpdateShow = {\n            vm.showCopyDlgFlow.value = it\n        },\n    )\n}\n\n@Composable\nprivate fun ShizukuAuthButton(\n    modifier: Modifier = Modifier,\n) {\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel<AuthA11yVm>()\n    TextButton(\n        modifier = modifier,\n        onClick = throttle(vm.viewModelScope.launchAsFn(Dispatchers.IO) {\n            mainVm.guardShizukuContext()\n            if (writeSecureSettingsState.value) {\n                toast(\"授权成功\")\n                updateEnableAutomator(true)\n                fixRestartAutomatorService()\n            }\n        })\n    ) {\n        Text(\n            text = \"Shizuku 授权\",\n            style = MaterialTheme.typography.bodyLarge,\n        )\n    }\n}\n\nprivate val Int.appopsAllow get() = \"appops set ${META.appId} ${AppOpsManagerHidden.opToName(this)} allow\"\nprivate val String.pmGrant get() = \"pm grant ${META.appId} $this\"\n\nval gkdStartCommandText by lazy {\n    val commandText = listOfNotNull(\n        \"set -euo pipefail\",\n        \"echo '> start start.sh'\",\n        Manifest.permission.WRITE_SECURE_SETTINGS.pmGrant,\n        Manifest_permission_GET_APP_OPS_STATS.pmGrant,\n        if (AndroidTarget.TIRAMISU) Manifest.permission.POST_NOTIFICATIONS.pmGrant else null,\n        AppOpsManagerHidden.OP_POST_NOTIFICATION.appopsAllow,\n        AppOpsManagerHidden.OP_SYSTEM_ALERT_WINDOW.appopsAllow,\n        if (AndroidTarget.Q) AppOpsManagerHidden.OP_ACCESS_ACCESSIBILITY.appopsAllow else null,\n        if (AndroidTarget.TIRAMISU) AppOpsManagerHidden.OP_ACCESS_RESTRICTED_SETTINGS.appopsAllow else null,\n        if (AndroidTarget.UPSIDE_DOWN_CAKE) AppOpsManagerHidden.OP_FOREGROUND_SERVICE_SPECIAL_USE.appopsAllow else null,\n        if (SafeAppOpsService.supportCreateA11yOverlay) AppOpsManagerHidden.OP_CREATE_ACCESSIBILITY_OVERLAY.appopsAllow else null,\n        \"sh ${shFolder.absolutePath}/expose.sh 1\",\n        \"echo '> start.sh end'\",\n    ).joinToString(\"\\n\")\n    val file = shFolder.resolve(\"start.sh\")\n    file.writeText(commandText)\n    \"adb shell sh ${file.absolutePath}\"\n}\n\n@Composable\nprivate fun TextListItem(\n    list: List<String>,\n    modifier: Modifier = Modifier,\n    style: TextStyle = LocalTextStyle.current,\n) {\n    val lineHeightDp = LocalDensity.current.run { style.lineHeight.toDp() }\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(2.dp),\n    ) {\n        list.forEach { text ->\n            Row {\n                Spacer(\n                    modifier = Modifier\n                        .padding(vertical = (lineHeightDp - 4.dp) / 2)\n                        .clip(CircleShape)\n                        .background(MaterialTheme.colorScheme.tertiary)\n                        .size(4.dp)\n                )\n                Spacer(modifier = Modifier.width(8.dp))\n                Text(text = text, style = style)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/AuthA11yVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.permission.writeSecureSettingsState\n\nclass AuthA11yVm : ViewModel() {\n    init {\n        viewModelScope.launch(Dispatchers.IO) {\n            while (isActive) {\n                writeSecureSettingsState.updateAndGet()\n                delay(1000)\n            }\n        }\n    }\n\n    val showCopyDlgFlow = MutableStateFlow(false)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/BlockA11yAppListPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.flow.update\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.service.fixRestartAutomatorService\nimport li.songe.gkd.store.blockA11yAppListFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.component.AnimatedBooleanContent\nimport li.songe.gkd.ui.component.AnimatedIconButton\nimport li.songe.gkd.ui.component.AnimationFloatingActionButton\nimport li.songe.gkd.ui.component.AppBarTextField\nimport li.songe.gkd.ui.component.AppCheckBoxCard\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.MenuGroupCard\nimport li.songe.gkd.ui.component.MenuItemCheckbox\nimport li.songe.gkd.ui.component.MenuItemRadioButton\nimport li.songe.gkd.ui.component.MultiTextField\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.autoFocus\nimport li.songe.gkd.ui.component.isFullVisible\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.icon.BackCloseIcon\nimport li.songe.gkd.ui.icon.LockOpenRight\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.asMutableState\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.AppGroupOption\nimport li.songe.gkd.util.AppListString\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.switchItem\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata object BlockA11yAppListRoute : NavKey\n\n@Composable\nfun BlockA11yAppListPage() {\n    val store by storeFlow.collectAsState()\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    val vm = viewModel<BlockA11yAppListVm>()\n    val appInfos by vm.appInfosFlow.collectAsState()\n    val searchStr by vm.searchStrFlow.collectAsState()\n    var showSearchBar by vm.showSearchBarFlow.asMutableState()\n    var editable by vm.editableFlow.asMutableState()\n    val (scrollBehavior, listState) = useListScrollState(vm.resetKey, canScroll = { !editable })\n    BackHandler(editable, vm.viewModelScope.launchAsFn {\n        context.justHideSoftInput()\n        if (vm.textChanged) {\n            mainVm.dialogFlow.waitResult(\n                title = \"提示\",\n                text = \"当前内容未保存，是否放弃编辑？\",\n            )\n        }\n        editable = false\n    })\n    Scaffold(\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(\n                scrollBehavior = scrollBehavior,\n                canScroll = !editable && !store.blockA11yAppListFollowMatch,\n                navigationIcon = {\n                    IconButton(\n                        onClick = throttle(vm.viewModelScope.launchAsFn {\n                            if (editable) {\n                                if (vm.textChanged) {\n                                    context.justHideSoftInput()\n                                    mainVm.dialogFlow.waitResult(\n                                        title = \"提示\",\n                                        text = \"当前内容未保存，是否放弃编辑？\",\n                                    )\n                                }\n                                editable = !editable\n                            } else {\n                                context.hideSoftInput()\n                                mainVm.popPage()\n                            }\n                        })\n                    ) {\n                        BackCloseIcon(backOrClose = !editable)\n                    }\n                },\n                title = {\n                    val firstShowSearchBar = remember { showSearchBar }\n                    if (showSearchBar) {\n                        BackHandler {\n                            if (!context.justHideSoftInput()) {\n                                showSearchBar = false\n                            }\n                        }\n                        AppBarTextField(\n                            value = searchStr,\n                            onValueChange = { newValue ->\n                                vm.searchStrFlow.value = newValue.trim()\n                            },\n                            hint = \"请输入应用名称/ID\",\n                            modifier = if (firstShowSearchBar) Modifier else Modifier.autoFocus(),\n                        )\n                    } else {\n                        val titleModifier = Modifier\n                            .noRippleClickable(\n                                onClick = throttle {\n                                    vm.resetKey.intValue++\n                                }\n                            )\n                        Text(\n                            modifier = titleModifier,\n                            text = \"无障碍白名单\",\n                        )\n                    }\n                },\n                actions = {\n                    AnimatedBooleanContent(\n                        targetState = editable,\n                        contentAlignment = Alignment.TopEnd,\n                        contentTrue = {\n                            PerfIconButton(\n                                imageVector = PerfIcon.Save,\n                                onClick = throttle {\n                                    if (vm.textChanged) {\n                                        blockA11yAppListFlow.value =\n                                            AppListString.decode(vm.textFlow.value)\n                                        toast(\"更新成功\")\n                                    } else {\n                                        toast(\"未修改\")\n                                    }\n                                    context.justHideSoftInput()\n                                    editable = false\n                                },\n                            )\n                        },\n                        contentFalse = {\n                            Row {\n                                PerfIconButton(\n                                    imageVector = if (store.blockA11yAppListFollowMatch) PerfIcon.Lock else LockOpenRight,\n                                    contentDescription = if (store.blockA11yAppListFollowMatch) \"已设置为跟随应用白名单\" else \"已设置为独立无障碍白名单\",\n                                    onClickLabel = \"切换模式\",\n                                    onClick = throttle {\n                                        showSearchBar = false\n                                        storeFlow.update { it.copy(blockA11yAppListFollowMatch = !it.blockA11yAppListFollowMatch) }\n                                        fixRestartAutomatorService()\n                                    }\n                                )\n\n                                var expanded by remember { mutableStateOf(false) }\n                                AnimatedVisibility(!store.blockA11yAppListFollowMatch) {\n                                    Row {\n                                        AnimatedIconButton(\n                                            onClick = throttle {\n                                                if (showSearchBar) {\n                                                    if (vm.searchStrFlow.value.isEmpty()) {\n                                                        showSearchBar = false\n                                                    } else {\n                                                        vm.searchStrFlow.value = \"\"\n                                                    }\n                                                } else {\n                                                    showSearchBar = true\n                                                }\n                                            },\n                                            id = R.drawable.ic_anim_search_close,\n                                            atEnd = showSearchBar,\n                                        )\n                                        PerfIconButton(imageVector = PerfIcon.Sort, onClick = {\n                                            expanded = true\n                                        })\n                                    }\n                                }\n                                Box(\n                                    modifier = Modifier\n                                        .wrapContentSize(Alignment.TopStart)\n                                ) {\n                                    DropdownMenu(\n                                        expanded = expanded,\n                                        onDismissRequest = { expanded = false }\n                                    ) {\n                                        MenuGroupCard(inTop = true, title = \"排序\") {\n                                            var sortType by vm.sortTypeFlow.asMutableState()\n                                            AppSortOption.objects.forEach { option ->\n                                                MenuItemRadioButton(\n                                                    text = option.label,\n                                                    selected = sortType == option,\n                                                    onClick = { sortType = option },\n                                                )\n                                            }\n                                        }\n                                        MenuGroupCard(inTop = true, title = \"筛选\") {\n                                            var appGroupType by vm.appGroupTypeFlow.asMutableState()\n                                            AppGroupOption.normalObjects.forEach { option ->\n                                                val newValue = option.invert(appGroupType)\n                                                MenuItemCheckbox(\n                                                    enabled = newValue != 0,\n                                                    text = option.label,\n                                                    checked = option.include(appGroupType),\n                                                    onClick = { appGroupType = newValue },\n                                                )\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        },\n                    )\n                })\n        },\n        floatingActionButton = {\n            AnimationFloatingActionButton(\n                visible = !editable && scrollBehavior.isFullVisible && !store.blockA11yAppListFollowMatch,\n                onClickLabel = \"进入白名单文本编辑模式\",\n                onClick = {\n                    editable = !editable\n                },\n                imageVector = PerfIcon.Edit,\n                contentDescription = \"编辑白名单文本\"\n            )\n        },\n    ) { contentPadding ->\n        if (store.blockA11yAppListFollowMatch) {\n            Column(\n                modifier = Modifier.scaffoldPadding(contentPadding),\n            ) {\n                Spacer(modifier = Modifier.height(EmptyHeight))\n                Text(\n                    text = \"已设置为跟随应用白名单\",\n                    textAlign = TextAlign.Center,\n                    modifier = Modifier.fillMaxWidth(),\n                    color = MaterialTheme.colorScheme.tertiary,\n                )\n            }\n        } else if (editable) {\n            MultiTextField(\n                modifier = Modifier.scaffoldPadding(contentPadding),\n                textFlow = vm.textFlow,\n                immediateFocus = true,\n                placeholderText = \"请输入应用ID列表\\n示例:\\ncom.android.systemui\\ncom.android.settings\",\n                indicatorSize = vm.indicatorSizeFlow.collectAsState().value,\n            )\n        } else {\n            val blockA11yAppList by blockA11yAppListFlow.collectAsState()\n            LazyColumn(\n                modifier = Modifier.scaffoldPadding(contentPadding),\n                state = listState,\n            ) {\n                items(appInfos, { it.id }) { appInfo ->\n                    AppCheckBoxCard(\n                        appInfo = appInfo,\n                        checked = blockA11yAppList.contains(appInfo.id),\n                        onCheckedChange = {\n                            blockA11yAppListFlow.update {\n                                it.switchItem(appInfo.id)\n                            }\n                        },\n                    )\n                }\n                item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                    Spacer(modifier = Modifier.height(EmptyHeight))\n                    if (appInfos.isEmpty() && searchStr.isNotEmpty()) {\n                        EmptyText(text = \"暂无搜索结果\")\n                        Spacer(modifier = Modifier.height(EmptyHeight / 2))\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/BlockA11yAppListVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.compose.runtime.mutableIntStateOf\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.map\nimport li.songe.gkd.store.blockA11yAppListFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.share.BaseViewModel\nimport li.songe.gkd.ui.share.asMutableStateFlow\nimport li.songe.gkd.ui.share.useAppFilter\nimport li.songe.gkd.util.AppListString\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.findOption\n\nclass BlockA11yAppListVm : BaseViewModel() {\n    val sortTypeFlow = storeFlow.asMutableStateFlow(\n        getter = { AppSortOption.objects.findOption(it.a11yAppSort) },\n        setter = {\n            storeFlow.value.copy(a11yAppSort = it.value)\n        }\n    )\n    val appGroupTypeFlow = storeFlow.asMutableStateFlow(\n        getter = { it.a11yAppGroupType },\n        setter = {\n            storeFlow.value.copy(a11yAppGroupType = it)\n        }\n    )\n    val appFilter = useAppFilter(\n        appGroupTypeFlow = appGroupTypeFlow,\n        sortTypeFlow = sortTypeFlow,\n    )\n    val searchStrFlow = appFilter.searchStrFlow\n\n    val showSearchBarFlow = MutableStateFlow(false)\n    val appInfosFlow = appFilter.appListFlow\n\n    val resetKey = mutableIntStateOf(0)\n    val editableFlow = MutableStateFlow(false)\n\n    val textFlow = MutableStateFlow(\"\")\n    val textChanged get() = blockA11yAppListFlow.value != AppListString.decode(textFlow.value)\n\n    val indicatorSizeFlow = textFlow.debounce(500).map {\n        AppListString.decode(it).size\n    }.stateInit(0)\n\n    init {\n        showSearchBarFlow.launchCollect {\n            if (!it) {\n                searchStrFlow.value = \"\"\n            }\n        }\n        editableFlow.launchOnChange {\n            if (it) {\n                showSearchBarFlow.value = false\n                textFlow.value = AppListString.encode(blockA11yAppListFlow.value, append = true)\n            }\n        }\n        appInfosFlow.launchOnChange {\n            resetKey.intValue++\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/EditBlockAppListPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.store.blockMatchAppListFlow\nimport li.songe.gkd.ui.component.MultiTextField\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata object EditBlockAppListRoute : NavKey\n\n@Composable\nfun EditBlockAppListPage() {\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    val vm = viewModel<EditBlockAppListVm>()\n    val onBack = throttle(vm.viewModelScope.launchAsFn {\n        if (vm.getChangedSet() != null) {\n            context.justHideSoftInput()\n            mainVm.dialogFlow.waitResult(\n                title = \"提示\",\n                text = \"当前内容未保存，是否放弃编辑？\",\n            )\n        } else {\n            context.hideSoftInput()\n        }\n        mainVm.popPage()\n    })\n    BackHandler(onBack = onBack)\n    Scaffold(modifier = Modifier, topBar = {\n        PerfTopAppBar(\n            modifier = Modifier.fillMaxWidth(),\n            navigationIcon = {\n                PerfIconButton(\n                    imageVector = PerfIcon.ArrowBack,\n                    onClick = onBack,\n                )\n            },\n            title = { Text(text = \"应用白名单\") },\n            actions = {\n                PerfIconButton(\n                    imageVector = PerfIcon.Save,\n                    onClick = throttle(vm.viewModelScope.launchAsFn {\n                        val newSet = vm.getChangedSet()\n                        if (newSet != null) {\n                            blockMatchAppListFlow.value = newSet\n                            toast(\"更新成功\")\n                        } else {\n                            toast(\"未修改\")\n                        }\n                        context.hideSoftInput()\n                        mainVm.popPage()\n                    })\n                )\n            }\n        )\n    }) { contentPadding ->\n        MultiTextField(\n            modifier = Modifier.scaffoldPadding(contentPadding),\n            textFlow = vm.textFlow,\n            indicatorSize = vm.indicatorSizeFlow.collectAsState().value\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/EditBlockAppListVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.map\nimport li.songe.gkd.store.blockMatchAppListFlow\nimport li.songe.gkd.ui.share.BaseViewModel\nimport li.songe.gkd.util.AppListString\n\nclass EditBlockAppListVm : BaseViewModel() {\n\n    val textFlow = MutableStateFlow(\n        AppListString.encode(\n            blockMatchAppListFlow.value,\n            append = true,\n        )\n    )\n\n    val indicatorSizeFlow = textFlow.debounce(500).map {\n        AppListString.decode(it).size\n    }.stateInit(0)\n\n    fun getChangedSet(): Set<String>? {\n        val newSet = AppListString.decode(textFlow.value)\n        if (blockMatchAppListFlow.value != newSet) {\n            return newSet\n        }\n        return null\n    }\n\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/ImagePreviewPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport android.webkit.URLUtil\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shadow\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.zIndex\nimport androidx.core.view.WindowCompat\nimport androidx.core.view.WindowInsetsCompat\nimport androidx.navigation3.runtime.NavKey\nimport coil3.ImageLoader\nimport coil3.compose.AsyncImagePainter\nimport coil3.compose.rememberAsyncImagePainter\nimport coil3.disk.DiskCache\nimport coil3.gif.AnimatedImageDecoder\nimport coil3.gif.GifDecoder\nimport coil3.network.okhttp.OkHttpNetworkFetcherFactory\nimport coil3.request.CachePolicy\nimport coil3.request.ImageRequest\nimport coil3.request.crossfade\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.app\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.coilCacheDir\nimport li.songe.gkd.util.throttle\nimport okhttp3.OkHttpClient\nimport okio.Path.Companion.toOkioPath\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.toJavaDuration\n\n@Serializable\ndata class ImagePreviewRoute(\n    val title: String? = null,\n    val uri: String? = null,\n    val uris: List<String> = emptyList(),\n) : NavKey\n\n@Composable\nfun ImagePreviewPage(route: ImagePreviewRoute) {\n    val title = route.title\n    val uri = route.uri\n    val uris = route.uris\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    DisposableEffect(null) {\n        val controller = WindowCompat.getInsetsController(context.window, context.window.decorView)\n        controller.hide(WindowInsetsCompat.Type.statusBars())\n        onDispose {\n            controller.show(WindowInsetsCompat.Type.statusBars())\n        }\n    }\n    Box(\n        modifier = Modifier\n            .background(MaterialTheme.colorScheme.background)\n            .fillMaxSize()\n    ) {\n        val showUri = uri ?: if (uris.size == 1) uris.first() else null\n        val state = rememberPagerState { uris.size }\n        PerfTopAppBar(\n            modifier = Modifier\n                .zIndex(1f)\n                .fillMaxWidth(),\n            navigationIcon = {\n                PerfIconButton(imageVector = PerfIcon.ArrowBack, onClick = {\n                    mainVm.popPage()\n                })\n            },\n            title = {\n                if (title != null) {\n                    Text(\n                        text = title,\n                        maxLines = 1,\n                        softWrap = false,\n                        overflow = TextOverflow.MiddleEllipsis,\n                        style = MaterialTheme.typography.titleLarge.copy(\n                            color = MaterialTheme.colorScheme.onBackground,\n                            shadow = Shadow(\n                                color = Color.Black.copy(alpha = 0.7f),\n                                blurRadius = with(LocalDensity.current) { 2.dp.toPx() },\n                                offset = with(LocalDensity.current) {\n                                    Offset( 1.dp.toPx(), 1.dp.toPx() )\n                                }\n                            )\n                        )\n                    )\n                }\n            },\n            actions = {\n                val currentUri = showUri ?: uris.getOrNull(state.currentPage)\n                if (currentUri != null && URLUtil.isNetworkUrl(currentUri)) {\n                    PerfIconButton(imageVector = PerfIcon.OpenInNew, onClick = throttle(fn = {\n                        mainVm.openUrl(currentUri)\n                    }))\n                }\n            },\n            colors = TopAppBarDefaults.topAppBarColors(\n                containerColor = MaterialTheme.colorScheme.background.copy(alpha = 0.1f)\n            )\n        )\n        if (showUri != null) {\n            UriImage(showUri)\n        } else if (uris.isNotEmpty()) {\n            Box(\n                modifier = Modifier.fillMaxSize()\n            ) {\n                HorizontalPager(\n                    modifier = Modifier.fillMaxSize(),\n                    state = state,\n                    pageContent = {\n                        UriImage(uris[it])\n                    }\n                )\n                Box(\n                    Modifier\n                        .align(Alignment.BottomCenter)\n                        .fillMaxWidth()\n                        .padding(bottom = 150.dp),\n                    contentAlignment = Alignment.BottomCenter\n                ) {\n                    Text(\n                        text = \"${state.currentPage + 1}/${uris.size}\",\n                        style = MaterialTheme.typography.titleLarge.copy(\n                            color = MaterialTheme.colorScheme.onPrimary,\n                            shadow = Shadow(\n                                color = Color.Black.copy(alpha = 0.8f),\n                                blurRadius = with(LocalDensity.current) { 3.dp.toPx() }\n                            )\n                        )\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun UriImage(uri: String) {\n    val context = LocalContext.current\n    val model = remember(uri) {\n        ImageRequest.Builder(context).data(uri)\n            .crossfade(DefaultDurationMillis).run {\n                if (URLUtil.isNetworkUrl(uri)) {\n                    this\n                } else {\n                    diskCachePolicy(CachePolicy.DISABLED).memoryCachePolicy(CachePolicy.DISABLED)\n                }\n            }\n            .build().apply {\n                imageLoader.enqueue(this)\n            }\n    }\n    val painter = rememberAsyncImagePainter(model)\n    val state by painter.state.collectAsState()\n    when (state) {\n        AsyncImagePainter.State.Empty -> {}\n        is AsyncImagePainter.State.Loading -> {\n            Column(\n                modifier = Modifier.fillMaxSize(),\n                horizontalAlignment = Alignment.CenterHorizontally,\n                verticalArrangement = Arrangement.Center,\n            ) {\n                CircularProgressIndicator(modifier = Modifier.size(40.dp))\n            }\n        }\n\n        is AsyncImagePainter.State.Success -> {\n            Column(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .verticalScroll(rememberScrollState()),\n                verticalArrangement = Arrangement.Center,\n            ) {\n                Image(\n                    painter = painter,\n                    contentDescription = null,\n                    modifier = Modifier.fillMaxWidth(),\n                    contentScale = ContentScale.FillWidth,\n                    alignment = Alignment.Center,\n                )\n            }\n        }\n\n        is AsyncImagePainter.State.Error -> {\n            Column(\n                modifier = Modifier.fillMaxSize(),\n                horizontalAlignment = Alignment.CenterHorizontally,\n                verticalArrangement = Arrangement.Center,\n            ) {\n                Text(\n                    modifier = Modifier.clickable(onClick = throttle { painter.restart() }),\n                    text = \"加载失败, 点击重试\",\n                    color = MaterialTheme.colorScheme.error,\n                    style = MaterialTheme.typography.bodyMedium\n                )\n            }\n        }\n    }\n}\n\nprivate val imageLoader by lazy {\n    ImageLoader.Builder(app)\n        .diskCache {\n            DiskCache.Builder()\n                .directory(coilCacheDir.toOkioPath())\n                .maxSizePercent(0.1)\n                .build()\n        }\n        .components {\n            if (AndroidTarget.P) {\n                add(AnimatedImageDecoder.Factory())\n            } else {\n                add(GifDecoder.Factory())\n            }\n            add(\n                OkHttpNetworkFetcherFactory(\n                    callFactory = {\n                        OkHttpClient.Builder()\n                            .connectTimeout(30.seconds.toJavaDuration())\n                            .readTimeout(30.seconds.toJavaDuration())\n                            .writeTimeout(30.seconds.toJavaDuration())\n                            .build()\n                    }\n                ))\n        }\n        .build()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SlowGroupPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.updateDialogOptions\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.itemPadding\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.appInfoMapFlow\nimport li.songe.gkd.util.ruleSummaryFlow\nimport li.songe.gkd.util.throttle\n\n@Serializable\ndata object SlowGroupRoute : NavKey\n\n@Composable\nfun SlowGroupPage() {\n    val mainVm = LocalMainViewModel.current\n    val ruleSummary by ruleSummaryFlow.collectAsState()\n    val appInfoCache by appInfoMapFlow.collectAsState()\n\n    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()\n    Scaffold(\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(\n                scrollBehavior = scrollBehavior,\n                navigationIcon = {\n                    PerfIconButton(imageVector = PerfIcon.ArrowBack, onClick = {\n                        mainVm.popPage()\n                    })\n                },\n                title = { Text(text = \"缓慢查询\") },\n                actions = {\n                    PerfIconButton(imageVector = PerfIcon.Info, onClick = throttle {\n                        mainVm.dialogFlow.updateDialogOptions(\n                            title = \"缓慢查询\",\n                            text = arrayOf(\n                                \"任意单个规则同时满足以下 3 个条件即判定为缓慢查询\",\n                                \"1. 选择器右侧无法快速查询且不是主动查询, 或内部使用<<且无法快速查询\\n2. preKeys 为空\\n3. matchTime 为空或大于 10s\",\n                                \"缓慢查询可能导致触发缓慢或更多耗电, 一些可能优化的建议操作\\n1. 降低选择器获取新节点次数\\n2. 降低或限制规则查询时间或次数\"\n                            ).joinToString(\"\\n\\n\"),\n                        )\n                    })\n                }\n            )\n        }\n    ) { contentPadding ->\n        LazyColumn(\n            modifier = Modifier.scaffoldPadding(contentPadding)\n        ) {\n            items(\n                ruleSummary.slowGlobalGroups,\n                { (_, r) -> r.subsItem.id to r.group.key }\n            ) { (group, rule) ->\n                SlowGroupCard(\n                    modifier = Modifier\n                        .clickable(onClick = throttle {\n                            mainVm.navigatePage(\n                                SubsGlobalGroupListRoute(\n                                    rule.subsItem.id,\n                                    group.key\n                                )\n                            )\n                        })\n                        .itemPadding(),\n                    title = group.name,\n                    desc = \"${rule.rawSubs.name}/全局规则\"\n                )\n            }\n            items(\n                ruleSummary.slowAppGroups,\n                { (_, r) -> Triple(r.subsItem.id, r.appId, r.group.key) }\n            ) { (group, rule) ->\n                SlowGroupCard(\n                    modifier = Modifier\n                        .clickable(onClick = throttle {\n                            mainVm.navigatePage(\n                                SubsAppGroupListRoute(\n                                    rule.subsItem.id,\n                                    rule.app.id,\n                                    group.key\n                                )\n                            )\n                        })\n                        .itemPadding(),\n                    title = group.name,\n                    desc = \"${rule.rawSubs.name}/应用规则/${appInfoCache[rule.app.id]?.name ?: rule.app.name ?: rule.app.id}\"\n                )\n            }\n            item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                Spacer(modifier = Modifier.height(EmptyHeight))\n                if (ruleSummary.slowGroupCount == 0) {\n                    EmptyText(text = \"暂无规则\")\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun SlowGroupCard(title: String, desc: String, modifier: Modifier = Modifier) {\n    Row(\n        horizontalArrangement = Arrangement.SpaceBetween,\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier,\n    ) {\n        Column(modifier = Modifier.weight(1f)) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.bodyLarge,\n                maxLines = 1,\n                softWrap = false,\n                overflow = TextOverflow.Ellipsis,\n            )\n            Text(\n                text = desc,\n                style = MaterialTheme.typography.bodyMedium,\n                maxLines = 1,\n                softWrap = false,\n                overflow = TextOverflow.Ellipsis,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n        PerfIcon(\n            imageVector = PerfIcon.KeyboardArrowRight,\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SnapshotPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport android.graphics.BitmapFactory\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.data.Snapshot\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.permission.canWriteExternalStorage\nimport li.songe.gkd.permission.requiredPermission\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.FixedTimeText\nimport li.songe.gkd.ui.component.LocalNumberCharWidth\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.animateListItem\nimport li.songe.gkd.ui.component.measureNumberTextWidth\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.itemHorizontalPadding\nimport li.songe.gkd.ui.style.itemVerticalPadding\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.IMPORT_SHORT_URL\nimport li.songe.gkd.util.ImageUtils\nimport li.songe.gkd.util.SnapshotExt\nimport li.songe.gkd.util.UriUtils\nimport li.songe.gkd.util.appInfoMapFlow\nimport li.songe.gkd.util.copyText\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.saveFileToDownloads\nimport li.songe.gkd.util.shareFile\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata object SnapshotPageRoute : NavKey\n\n@Composable\nfun SnapshotPage() {\n    val context = LocalActivity.current as MainActivity\n    val mainVm = LocalMainViewModel.current\n    val colorScheme = MaterialTheme.colorScheme\n    val vm = viewModel<SnapshotVm>()\n\n    val firstLoading by vm.firstLoadingFlow.collectAsState()\n    val snapshots by vm.snapshotsState.collectAsState()\n    var selectedSnapshot by remember { mutableStateOf<Snapshot?>(null) }\n    val resetKey = rememberSaveable { mutableIntStateOf(0) }\n    val (scrollBehavior, listState) = useListScrollState(\n        resetKey,\n        snapshots.isEmpty(),\n        firstLoading,\n    )\n    val timeTextWidth = measureNumberTextWidth(MaterialTheme.typography.bodySmall)\n\n    Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {\n        PerfTopAppBar(\n            scrollBehavior = scrollBehavior,\n            navigationIcon = {\n                PerfIconButton(imageVector = PerfIcon.ArrowBack, onClick = {\n                    mainVm.popPage()\n                })\n            },\n            title = {\n                Text(\n                    text = \"快照记录\",\n                    modifier = Modifier.noRippleClickable { resetKey.intValue++ },\n                )\n            },\n            actions = {\n                if (snapshots.isNotEmpty()) {\n                    PerfIconButton(\n                        imageVector = PerfIcon.Delete,\n                        onClick = throttle(fn = vm.viewModelScope.launchAsFn(Dispatchers.IO) {\n                            mainVm.dialogFlow.waitResult(\n                                title = \"删除快照\",\n                                text = \"确定删除所有快照记录?\",\n                                error = true,\n                            )\n                            snapshots.forEach { s ->\n                                SnapshotExt.removeSnapshot(s.id)\n                            }\n                            DbSet.snapshotDao.deleteAll()\n                        })\n                    )\n                }\n            })\n    }, content = { contentPadding ->\n        CompositionLocalProvider(\n            LocalNumberCharWidth provides timeTextWidth\n        ) {\n            LazyColumn(\n                modifier = Modifier.scaffoldPadding(contentPadding),\n                state = listState,\n            ) {\n                items(snapshots, { it.id }) { snapshot ->\n                    SnapshotCard(\n                        modifier = Modifier.animateListItem(),\n                        snapshot = snapshot,\n                        onClick = {\n                            selectedSnapshot = snapshot\n                        }\n                    )\n                }\n                item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                    Spacer(modifier = Modifier.height(EmptyHeight))\n                    if (snapshots.isEmpty() && !firstLoading) {\n                        EmptyText(text = \"暂无数据\")\n                    }\n                }\n            }\n        }\n    })\n\n    selectedSnapshot?.let { snapshotVal ->\n        Dialog(onDismissRequest = { selectedSnapshot = null }) {\n            Card(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n                shape = RoundedCornerShape(16.dp),\n            ) {\n                val modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp)\n                Text(\n                    text = \"查看\", modifier = Modifier\n                        .clickable(onClick = throttle(fn = vm.viewModelScope.launchAsFn {\n                            selectedSnapshot = null\n                            mainVm.navigatePage(\n                                ImagePreviewRoute(\n                                    title = appInfoMapFlow.value[snapshotVal.appId]?.name\n                                        ?: snapshotVal.appId,\n                                    uri = snapshotVal.screenshotFile.absolutePath,\n                                )\n                            )\n                        }))\n                        .then(modifier)\n                )\n                HorizontalDivider()\n                Text(\n                    text = \"分享到其他应用\",\n                    modifier = Modifier\n                        .clickable(onClick = throttle(fn = vm.viewModelScope.launchAsFn {\n                            selectedSnapshot = null\n                            val zipFile = SnapshotExt.snapshotZipFile(\n                                snapshotVal.id,\n                                snapshotVal.appId,\n                                snapshotVal.activityId\n                            )\n                            context.shareFile(zipFile, \"分享快照文件\")\n                        }))\n                        .then(modifier)\n                )\n                HorizontalDivider()\n                Text(\n                    text = \"保存到下载\",\n                    modifier = Modifier\n                        .clickable(onClick = throttle(fn = vm.viewModelScope.launchAsFn(Dispatchers.IO) {\n                            selectedSnapshot = null\n                            toast(\"正在保存...\")\n                            val zipFile = SnapshotExt.snapshotZipFile(\n                                snapshotVal.id,\n                                snapshotVal.appId,\n                                snapshotVal.activityId\n                            )\n                            context.saveFileToDownloads(zipFile)\n                        }))\n                        .then(modifier)\n                )\n                HorizontalDivider()\n                if (snapshotVal.githubAssetId != null) {\n                    Text(\n                        text = \"复制链接\", modifier = Modifier\n                            .clickable(onClick = throttle {\n                                selectedSnapshot = null\n                                copyText(IMPORT_SHORT_URL + snapshotVal.githubAssetId)\n                            })\n                            .then(modifier)\n                    )\n                } else {\n                    Text(\n                        text = \"生成链接(需科学上网)\", modifier = Modifier\n                            .clickable(onClick = throttle {\n                                selectedSnapshot = null\n                                mainVm.uploadOptions.startTask(\n                                    getFile = { SnapshotExt.snapshotZipFile(snapshotVal.id) },\n                                    showHref = { IMPORT_SHORT_URL + it.id },\n                                    onSuccessResult = {\n                                        DbSet.snapshotDao.update(snapshotVal.copy(githubAssetId = it.id))\n                                    }\n                                )\n                            })\n                            .then(modifier)\n                    )\n                }\n                HorizontalDivider()\n\n                Text(\n                    text = \"保存截图到相册\",\n                    modifier = Modifier\n                        .clickable(onClick = throttle(fn = vm.viewModelScope.launchAsFn(Dispatchers.IO) {\n                            toast(\"正在保存...\")\n                            selectedSnapshot = null\n                            requiredPermission(context, canWriteExternalStorage)\n                            ImageUtils.save2Album(BitmapFactory.decodeFile(snapshotVal.screenshotFile.absolutePath))\n                            toast(\"保存成功\")\n                        }))\n                        .then(modifier)\n                )\n                HorizontalDivider()\n                Text(\n                    text = \"替换截图(去除隐私)\",\n                    modifier = Modifier\n                        .clickable(onClick = throttle(fn = vm.viewModelScope.launchAsFn(Dispatchers.IO) {\n                            val uri = context.pickContentLauncher.launchForImageResult()\n                            val oldBitmap =\n                                BitmapFactory.decodeFile(snapshotVal.screenshotFile.absolutePath)\n                            val newBytes = UriUtils.uri2Bytes(uri)\n                            val newBitmap =\n                                BitmapFactory.decodeByteArray(newBytes, 0, newBytes.size)\n                            if (oldBitmap.width == newBitmap.width && oldBitmap.height == newBitmap.height) {\n                                snapshotVal.screenshotFile.writeBytes(newBytes)\n                                if (snapshotVal.githubAssetId != null) {\n                                    // 当本地快照变更时, 移除快照链接\n                                    DbSet.snapshotDao.deleteGithubAssetId(snapshotVal.id)\n                                }\n                                toast(\"替换成功\")\n                                selectedSnapshot = null\n                            } else {\n                                toast(\"截图尺寸不一致, 无法替换\")\n                            }\n                        }))\n                        .then(modifier)\n                )\n                HorizontalDivider()\n                Text(\n                    text = \"删除\", modifier = Modifier\n                        .clickable(onClick = throttle(fn = vm.viewModelScope.launchAsFn {\n                            selectedSnapshot = null\n                            mainVm.dialogFlow.waitResult(\n                                title = \"删除快照\",\n                                text = \"确定删除当前快照吗?\",\n                                error = true,\n                            )\n                            DbSet.snapshotDao.delete(snapshotVal)\n                            withContext(Dispatchers.IO) {\n                                SnapshotExt.removeSnapshot(snapshotVal.id)\n                            }\n                            toast(\"删除成功\")\n                        }))\n                        .then(modifier), color = colorScheme.error\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SnapshotCard(\n    modifier: Modifier = Modifier,\n    snapshot: Snapshot,\n    onClick: () -> Unit,\n) {\n    Row(\n        modifier = modifier\n            .clickable(onClick = onClick)\n            .fillMaxWidth()\n            .height(IntrinsicSize.Min)\n            .padding(horizontal = itemHorizontalPadding, vertical = itemVerticalPadding / 2)\n    ) {\n        Spacer(\n            modifier = Modifier\n                .fillMaxHeight()\n                .width(2.dp)\n                .background(MaterialTheme.colorScheme.primaryContainer),\n        )\n        Spacer(modifier = Modifier.width(8.dp))\n        Column(\n            modifier = Modifier.weight(1f),\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.SpaceBetween,\n            ) {\n                val appInfo = appInfoMapFlow.collectAsState().value[snapshot.appId]\n                val showAppName = appInfo?.name ?: snapshot.appId\n                Text(\n                    text = showAppName,\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 1,\n                    softWrap = false,\n                )\n                FixedTimeText(\n                    text = snapshot.date,\n                    style = MaterialTheme.typography.bodySmall,\n                )\n            }\n            val showActivityId = if (snapshot.activityId != null) {\n                if (snapshot.activityId.startsWith(snapshot.appId)) {\n                    snapshot.activityId.substring(snapshot.appId.length)\n                } else {\n                    snapshot.activityId\n                }\n            } else {\n                null\n            }\n            if (showActivityId != null) {\n                Text(\n                    modifier = Modifier.height(MaterialTheme.typography.bodyMedium.lineHeight.value.dp),\n                    text = showActivityId,\n                    style = MaterialTheme.typography.bodyMedium,\n                    softWrap = false,\n                    maxLines = 1,\n                    overflow = TextOverflow.MiddleEllipsis,\n                )\n            } else {\n                Text(\n                    text = \"null\",\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.typography.bodyMedium.color.copy(alpha = 0.5f)\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SnapshotVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.share.BaseViewModel\n\nclass SnapshotVm : BaseViewModel() {\n    val snapshotsState = DbSet.snapshotDao.query().attachLoad()\n        .stateInit(emptyList())\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SubsAppGroupListPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.component.AnimationFloatingActionButton\nimport li.songe.gkd.ui.component.BatchActionButtonGroup\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.RuleGroupCard\nimport li.songe.gkd.ui.component.TowLineText\nimport li.songe.gkd.ui.component.animateListItem\nimport li.songe.gkd.ui.component.toGroupState\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.icon.BackCloseIcon\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.copyText\nimport li.songe.gkd.util.getUpDownTransform\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.switchItem\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toJson5String\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateSubscription\n\n@Serializable\ndata class SubsAppGroupListRoute(\n    val subsItemId: Long,\n    val appId: String,\n    val focusGroupKey: Int? = null, // 背景/边框高亮一下\n) : NavKey\n\n@Composable\nfun SubsAppGroupListPage(route: SubsAppGroupListRoute) {\n    val subsItemId = route.subsItemId\n    val appId = route.appId\n    val focusGroupKey = route.focusGroupKey\n\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel { SubsAppGroupListVm(route) }\n    val subs = vm.subsFlow.collectAsState().value\n    val subsConfigs by vm.subsConfigsFlow.collectAsState()\n    val categoryConfigs by vm.categoryConfigsFlow.collectAsState()\n    val app by vm.subsAppFlow.collectAsState()\n\n    val groupToCategoryMap = subs.groupToCategoryMap\n\n    val editable = subsItemId < 0\n    val isSelectedMode = vm.isSelectedModeFlow.collectAsState().value\n    val selectedDataSet = vm.selectedDataSetFlow.collectAsState().value\n    LaunchedEffect(key1 = isSelectedMode) {\n        if (!isSelectedMode) {\n            vm.selectedDataSetFlow.value = emptySet()\n        }\n    }\n    LaunchedEffect(key1 = selectedDataSet.isEmpty()) {\n        if (selectedDataSet.isEmpty()) {\n            vm.isSelectedModeFlow.value = false\n        }\n    }\n    BackHandler(isSelectedMode) {\n        vm.isSelectedModeFlow.value = false\n    }\n    val resetKey = rememberSaveable { mutableIntStateOf(0) }\n    val (scrollBehavior, listState) = useListScrollState(resetKey, app.groups.isEmpty())\n    if (focusGroupKey != null) {\n        LaunchedEffect(null) {\n            if (vm.focusGroupFlow?.value != null) {\n                val i = app.groups.indexOfFirst { it.key == focusGroupKey }\n                if (i >= 0) {\n                    listState.scrollToItem(i)\n                }\n            }\n        }\n    }\n    Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {\n        PerfTopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {\n            IconButton(onClick = throttle {\n                if (isSelectedMode) {\n                    vm.isSelectedModeFlow.value = false\n                } else {\n                    mainVm.popPage()\n                }\n            }) {\n                BackCloseIcon(backOrClose = !isSelectedMode)\n            }\n        }, title = {\n            val titleModifier = Modifier.noRippleClickable { resetKey.intValue++ }\n            if (isSelectedMode) {\n                Text(\n                    modifier = titleModifier,\n                    text = if (selectedDataSet.isNotEmpty()) selectedDataSet.size.toString() else \"\",\n                )\n            } else {\n                TowLineText(\n                    modifier = titleModifier,\n                    title = subs.name,\n                    subtitle = appId,\n                    showApp = true,\n                )\n            }\n        }, actions = {\n            var expanded by remember { mutableStateOf(false) }\n            AnimatedContent(\n                targetState = isSelectedMode,\n                transitionSpec = { getUpDownTransform() },\n                contentAlignment = Alignment.TopEnd,\n            ) {\n                if (it) {\n                    Row {\n                        PerfIconButton(\n                            imageVector = PerfIcon.ContentCopy,\n                            onClick = throttle(vm.viewModelScope.launchAsFn(Dispatchers.Default) {\n                                val copyGroups = app.groups.filter { g ->\n                                    selectedDataSet.any { s -> s.groupKey == g.key }\n                                }\n                                val str = toJson5String(app.copy(groups = copyGroups))\n                                copyText(str)\n                            })\n                        )\n                        BatchActionButtonGroup(vm, selectedDataSet)\n                        if (editable) {\n                            PerfIconButton(\n                                imageVector = PerfIcon.Delete,\n                                onClick = throttle(vm.viewModelScope.launchAsFn {\n                                    mainVm.dialogFlow.waitResult(\n                                        title = \"删除规则组\",\n                                        text = \"删除当前所选规则组?\",\n                                        error = true,\n                                    )\n                                    val keys = selectedDataSet.mapNotNull { g -> g.groupKey }\n                                    vm.isSelectedModeFlow.value = false\n                                    if (keys.size == app.groups.size) {\n                                        updateSubscription(\n                                            subs.copy(\n                                                apps = subs.apps.filter { a -> a.id != appId }\n                                            )\n                                        )\n                                        DbSet.subsConfigDao.deleteAppConfig(subsItemId, appId)\n                                    } else {\n                                        updateSubscription(\n                                            subs.copy(\n                                                apps = subs.apps.toMutableList().apply {\n                                                    set(\n                                                        indexOfFirst { a -> a.id == appId },\n                                                        app.copy(groups = app.groups.filterNot { g ->\n                                                            keys.contains(\n                                                                g.key\n                                                            )\n                                                        })\n                                                    )\n                                                }\n                                            )\n                                        )\n                                        DbSet.subsConfigDao.batchDeleteAppGroupConfig(\n                                            subsItemId,\n                                            appId,\n                                            keys\n                                        )\n                                    }\n                                    toast(\"删除成功\")\n                                })\n                            )\n                        }\n                        PerfIconButton(imageVector = PerfIcon.MoreVert, onClick = {\n                            expanded = true\n                        })\n                    }\n                }\n            }\n            if (isSelectedMode) {\n                Box(\n                    modifier = Modifier\n                        .wrapContentSize(Alignment.TopStart)\n                ) {\n                    DropdownMenu(\n                        expanded = expanded,\n                        onDismissRequest = { expanded = false }\n                    ) {\n                        DropdownMenuItem(\n                            text = {\n                                Text(text = \"全选\")\n                            },\n                            onClick = {\n                                expanded = false\n                                vm.selectedDataSetFlow.value = app.groups.map {\n                                    it.toGroupState(\n                                        subsId = subsItemId,\n                                        appId = appId,\n                                    )\n                                }.toSet()\n                            }\n                        )\n                        DropdownMenuItem(\n                            text = {\n                                Text(text = \"反选\")\n                            },\n                            onClick = {\n                                expanded = false\n                                val newSelectedIds = app.groups.map {\n                                    it.toGroupState(\n                                        subsId = subsItemId,\n                                        appId = appId,\n                                    )\n                                }.toSet() - selectedDataSet\n                                vm.selectedDataSetFlow.value = newSelectedIds\n                            }\n                        )\n                    }\n                }\n            }\n        })\n    }, floatingActionButton = {\n        if (editable) {\n            AnimationFloatingActionButton(\n                visible = !isSelectedMode,\n                onClick = {\n                    mainVm.navigatePage(\n                        UpsertRuleGroupRoute(\n                            subsId = subsItemId,\n                            groupKey = null,\n                            appId = appId\n                        )\n                    )\n                },\n                contentDescription = \"添加规则\",\n                imageVector = PerfIcon.Add,\n            )\n        }\n    }) { contentPadding ->\n        LazyColumn(\n            modifier = Modifier.scaffoldPadding(contentPadding),\n            state = listState,\n            verticalArrangement = Arrangement.spacedBy(4.dp),\n        ) {\n            items(app.groups, { it.key }) { group ->\n                val category = groupToCategoryMap[group]\n                val subsConfig = subsConfigs.find { it.groupKey == group.key }\n                val categoryConfig = categoryConfigs.find {\n                    it.categoryKey == category?.key\n                }\n                RuleGroupCard(\n                    modifier = Modifier.animateListItem(),\n                    subs = subs,\n                    appId = appId,\n                    group = group,\n                    category = category,\n                    subsConfig = subsConfig,\n                    categoryConfig = categoryConfig,\n                    focusGroupFlow = vm.focusGroupFlow,\n                    isSelectedMode = isSelectedMode,\n                    isSelected = selectedDataSet.any { it.groupKey == group.key },\n                    onLongClick = {\n                        if (app.groups.size > 1) {\n                            vm.isSelectedModeFlow.value = true\n                            vm.selectedDataSetFlow.value = setOf(\n                                group.toGroupState(subsItemId, appId)\n                            )\n                        }\n                    },\n                    onSelectedChange = {\n                        vm.selectedDataSetFlow.value = selectedDataSet.switchItem(\n                            group.toGroupState(\n                                subsItemId,\n                                appId,\n                            )\n                        )\n                    }\n                )\n            }\n            item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                Spacer(modifier = Modifier.height(EmptyHeight))\n                if (app.groups.isEmpty()) {\n                    EmptyText(text = \"暂无规则\")\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SubsAppGroupListVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.component.ShowGroupState\nimport li.songe.gkd.ui.share.BaseViewModel\n\nclass SubsAppGroupListVm(val route: SubsAppGroupListRoute) : BaseViewModel() {\n\n    val subsFlow = mapSafeSubs(route.subsItemId)\n    val subsAppFlow = subsFlow.mapNew { it.getApp(route.appId) }\n\n    val subsConfigsFlow = DbSet.subsConfigDao.queryAppGroupTypeConfig(route.subsItemId, route.appId)\n        .stateInit(emptyList())\n\n    val categoryConfigsFlow = DbSet.categoryConfigDao.queryConfig(route.subsItemId)\n        .stateInit(emptyList())\n\n\n    val isSelectedModeFlow = MutableStateFlow(false)\n    val selectedDataSetFlow = MutableStateFlow(emptySet<ShowGroupState>())\n\n    val focusGroupFlow = route.focusGroupKey?.let {\n        MutableStateFlow<Triple<Long, String?, Int>?>(\n            Triple(\n                route.subsItemId,\n                route.appId,\n                route.focusGroupKey\n            )\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SubsAppListPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.data.AppConfig\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.component.AnimatedIconButton\nimport li.songe.gkd.ui.component.AppBarTextField\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.MenuGroupCard\nimport li.songe.gkd.ui.component.MenuItemCheckbox\nimport li.songe.gkd.ui.component.MenuItemRadioButton\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.SubsAppCard\nimport li.songe.gkd.ui.component.TowLineText\nimport li.songe.gkd.ui.component.autoFocus\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.component.useSubs\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.asMutableState\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.AppGroupOption\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.LOCAL_SUBS_IDS\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.throttle\n\n@Serializable\ndata class SubsAppListRoute(val subsItemId: Long) : NavKey\n\n@Composable\nfun SubsAppListPage(route: SubsAppListRoute) {\n    val subsItemId = route.subsItemId\n\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    val vm = viewModel { SubsAppListVm(route) }\n\n    val appTripleList by vm.appItemListFlow.collectAsState()\n    val searchStr by vm.searchStrFlow.collectAsState()\n\n    var showSearchBar by rememberSaveable { mutableStateOf(false) }\n    LaunchedEffect(key1 = showSearchBar, block = {\n        if (!showSearchBar) {\n            vm.searchStrFlow.value = \"\"\n        }\n    })\n    val (scrollBehavior, listState) = useListScrollState(\n        vm.resetKey,\n    )\n    var expanded by remember { mutableStateOf(false) }\n\n    Scaffold(\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {\n                PerfIconButton(\n                    imageVector = PerfIcon.ArrowBack,\n                    onClick = throttle(vm.viewModelScope.launchAsFn {\n                        context.hideSoftInput()\n                        mainVm.popPage()\n                    }),\n                )\n            }, title = {\n                val firstShowSearchBar = remember { showSearchBar }\n                if (showSearchBar) {\n                    BackHandler {\n                        if (!context.justHideSoftInput()) {\n                            showSearchBar = false\n                        }\n                    }\n                    AppBarTextField(\n                        value = searchStr,\n                        onValueChange = { newValue -> vm.searchStrFlow.value = newValue.trim() },\n                        hint = \"请输入应用名称/ID\",\n                        modifier = if (firstShowSearchBar) Modifier else Modifier.autoFocus(),\n                    )\n                } else {\n                    TowLineText(\n                        title = useSubs(subsItemId)?.name ?: subsItemId.toString(),\n                        subtitle = \"应用规则\",\n                        modifier = Modifier.noRippleClickable {\n                            vm.resetKey.intValue++\n                        }\n                    )\n                }\n            }, actions = {\n                AnimatedIconButton(\n                    onClick = {\n                        if (showSearchBar) {\n                            if (vm.searchStrFlow.value.isEmpty()) {\n                                showSearchBar = false\n                            } else {\n                                vm.searchStrFlow.value = \"\"\n                            }\n                        } else {\n                            showSearchBar = true\n                        }\n                    },\n                    id = R.drawable.ic_anim_search_close,\n                    atEnd = showSearchBar,\n                )\n                PerfIconButton(\n                    imageVector = PerfIcon.Sort,\n                    onClick = {\n                        expanded = true\n                    },\n                )\n                Box(\n                    modifier = Modifier.wrapContentSize(Alignment.TopStart)\n                ) {\n                    DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {\n                        MenuGroupCard(inTop = true, title = \"排序\") {\n                            var sortType by vm.sortTypeFlow.asMutableState()\n                            AppSortOption.objects.forEach { option ->\n                                MenuItemRadioButton(\n                                    text = option.label,\n                                    selected = sortType == option,\n                                    onClick = { sortType = option },\n                                )\n                            }\n                        }\n                        MenuGroupCard(title = \"分组\") {\n                            var appGroupType by vm.appGroupTypeFlow.asMutableState()\n                            AppGroupOption.allObjects.forEach { option ->\n                                val newValue = option.invert(appGroupType)\n                                MenuItemCheckbox(\n                                    enabled = newValue != 0,\n                                    text = option.label,\n                                    checked = option.include(appGroupType),\n                                    onClick = { appGroupType = newValue },\n                                )\n                            }\n                        }\n                        MenuGroupCard(title = \"筛选\") {\n                            MenuItemCheckbox(\n                                text = \"白名单\",\n                                stateFlow = vm.showBlockAppFlow,\n                            )\n                        }\n                    }\n                }\n            })\n        },\n        floatingActionButton = {\n            if (LOCAL_SUBS_IDS.contains(subsItemId)) {\n                FloatingActionButton(onClick = throttle {\n                    mainVm.navigatePage(\n                        UpsertRuleGroupRoute(\n                            subsId = subsItemId,\n                            groupKey = null,\n                            appId = \"\",\n                            forward = true,\n                        )\n                    )\n                }) {\n                    PerfIcon(\n                        imageVector = PerfIcon.Add,\n                    )\n                }\n            }\n        },\n    ) { contentPadding ->\n        LazyColumn(\n            modifier = Modifier.scaffoldPadding(contentPadding),\n            state = listState\n        ) {\n            items(appTripleList, { it.id }) { a ->\n                SubsAppCard(\n                    data = a,\n                    onClick = throttle {\n                        context.justHideSoftInput()\n                        mainVm.navigatePage(SubsAppGroupListRoute(subsItemId, a.id))\n                    },\n                    onValueChange = vm.viewModelScope.launchAsFn { enable ->\n                        val newItem = a.appConfig?.copy(\n                            enable = enable\n                        ) ?: AppConfig(\n                            enable = enable,\n                            subsId = subsItemId,\n                            appId = a.id,\n                        )\n                        DbSet.appConfigDao.insert(newItem)\n                    },\n                )\n            }\n            item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                Spacer(modifier = Modifier.height(EmptyHeight))\n                val firstLoading by vm.firstLoadingFlow.collectAsState()\n                if (appTripleList.isEmpty() && !firstLoading) {\n                    EmptyText(\n                        text = if (searchStr.isNotEmpty()) {\n                            if (vm.showAllAppFlow.collectAsState().value) \"暂无搜索结果\" else \"暂无搜索结果，或修改筛选\"\n                        } else {\n                            \"暂无规则\"\n                        }\n                    )\n                    Spacer(modifier = Modifier.height(EmptyHeight / 2))\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SubsAppListVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.compose.runtime.mutableIntStateOf\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.map\nimport li.songe.gkd.MainViewModel\nimport li.songe.gkd.data.AppConfig\nimport li.songe.gkd.data.AppInfo\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.store.blockMatchAppListFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.share.BaseViewModel\nimport li.songe.gkd.ui.share.asMutableStateFlow\nimport li.songe.gkd.util.AppGroupOption\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.appInfoMapFlow\nimport li.songe.gkd.util.collator\nimport li.songe.gkd.util.findOption\nimport li.songe.gkd.util.getGroupEnable\n\nclass SubsAppListVm(val route: SubsAppListRoute) : BaseViewModel() {\n\n    val subsFlow = mapSafeSubs(route.subsItemId)\n\n    private val appConfigsFlow = DbSet.appConfigDao.queryAppTypeConfig(route.subsItemId)\n        .attachLoad().stateInit(emptyList())\n\n    private val groupSubsConfigsFlow =\n        DbSet.subsConfigDao.querySubsGroupTypeConfig(route.subsItemId)\n            .attachLoad().stateInit(emptyList())\n\n    private val categoryConfigsFlow = DbSet.categoryConfigDao.queryConfig(route.subsItemId)\n        .attachLoad().stateInit(emptyList())\n\n\n    val appGroupTypeFlow = storeFlow.asMutableStateFlow(\n        getter = { it.subsAppGroupType },\n        setter = { storeFlow.value.copy(subsAppGroupType = it) },\n    )\n    val showBlockAppFlow = storeFlow.asMutableStateFlow(\n        getter = { it.subsAppShowBlock },\n        setter = { storeFlow.value.copy(subsAppShowBlock = it) },\n    )\n\n    private val temp1ListFlow = run {\n        var tempListFlow = combine(\n            subsFlow,\n            appInfoMapFlow,\n        ) { subs, appMap ->\n            subs.usedApps.map {\n                it to appMap[it.id]\n            }.sortedWith { a, b ->\n                // 默认顺序: 已安装(有名字->无名字)->未安装(有名字(来自订阅)->无名字)\n                val x = a.second?.name ?: a.first.name?.let { \"\\uFFFF\" + it }\n                ?: (\"\\uFFFF\\uFFFF\" + a.first.id)\n                val y = b.second?.name ?: b.first.name?.let { \"\\uFFFF\" + it }\n                ?: (\"\\uFFFF\\uFFFF\" + b.first.id)\n                collator.compare(x, y)\n            }\n        }\n        tempListFlow = combine(\n            tempListFlow,\n            appGroupTypeFlow,\n        ) { list, type ->\n            if (type == 0) {\n                return@combine emptyList()\n            }\n            if (AppGroupOption.allObjects.all { it.include(type) }) {\n                return@combine list\n            }\n            var resultList = list\n            if (!AppGroupOption.SystemGroup.include(type)) {\n                resultList = resultList.filterNot { it.second?.isSystem == true }\n            }\n            if (!AppGroupOption.UserGroup.include(type)) {\n                resultList = resultList.filterNot { it.second?.isSystem == false }\n            }\n            if (!AppGroupOption.UnInstalledGroup.include(type)) {\n                resultList = resultList.filterNot { it.second == null }\n            }\n            resultList\n        }\n        tempListFlow = combine(\n            tempListFlow,\n            showBlockAppFlow,\n            blockMatchAppListFlow\n        ) { list, showBlock, blockSet ->\n            if (showBlock) {\n                list\n            } else {\n                list.filterNot { it.first.id in blockSet }\n            }\n        }\n        tempListFlow\n    }\n\n    val showAllAppFlow = combine(subsFlow, temp1ListFlow) { subs, list ->\n        subs.apps.size == list.size\n    }.stateInit(false)\n\n    val sortTypeFlow = storeFlow.asMutableStateFlow(\n        getter = { AppSortOption.objects.findOption(it.subsAppSort) },\n        setter = {\n            storeFlow.value.copy(subsAppSort = it.value)\n        }\n    )\n    private val appActionOrderMapFlow = DbSet.actionLogDao\n        .queryLatestUniqueAppIds(route.subsItemId)\n        .map {\n            it.mapIndexed { i, appId -> appId to i }.toMap()\n        }\n    private val temp2ListFlow = combine(\n        temp1ListFlow,\n        appActionOrderMapFlow,\n        sortTypeFlow,\n        MainViewModel.instance.appVisitOrderMapFlow,\n    ) { apps, appIdToOrder, sortType, appVisitOrderMap ->\n        when (sortType) {\n\n            AppSortOption.ByActionTime -> {\n                apps.sortedBy { a -> appIdToOrder[a.first.id] ?: Int.MAX_VALUE }\n            }\n\n            AppSortOption.ByAppName -> {\n                apps\n            }\n\n            AppSortOption.ByUsedTime -> {\n                apps.sortedBy { a -> appVisitOrderMap[a.first.id] ?: Int.MAX_VALUE }\n            }\n        }\n    }\n\n    val searchStrFlow = MutableStateFlow(\"\")\n    private val debounceSearchStr = searchStrFlow.debounce(200).stateInit(searchStrFlow.value)\n    val temp3ListFlow = combine(\n        temp2ListFlow,\n        debounceSearchStr,\n    ) { apps, searchStr ->\n        if (searchStr.isBlank()) {\n            apps\n        } else {\n            val results = mutableListOf<Pair<RawSubscription.RawApp, AppInfo?>>()\n            val tempList = apps.toMutableList()\n            //1. 搜索已安装应用名称\n            tempList.toList().apply { tempList.clear() }.forEach { a ->\n                if (a.second?.name?.contains(searchStr, true) == true) {\n                    results.add(a)\n                } else {\n                    tempList.add(a)\n                }\n            }\n            //2. 搜索未安装应用名称\n            tempList.toList().apply { tempList.clear() }.forEach { a ->\n                val name = a.first.name\n                if (a.second == null && name?.contains(searchStr, true) == true) {\n                    results.add(a)\n                } else {\n                    tempList.add(a)\n                }\n            }\n            //3. 搜索应用 id\n            tempList.toList().apply { tempList.clear() }.forEach { a ->\n                if (a.first.id.contains(searchStr, true)) {\n                    results.add(a)\n                } else {\n                    tempList.add(a)\n                }\n            }\n            results\n        }\n    }.stateInit(emptyList())\n\n    val appItemListFlow = combine(\n        subsFlow,\n        temp3ListFlow,\n        categoryConfigsFlow,\n        appConfigsFlow,\n        groupSubsConfigsFlow,\n    ) { subsRaw, apps, categoryConfigs, appConfigs, groupSubsConfigs ->\n        val groupToCategoryMap = subsRaw.groupToCategoryMap\n        apps.map {\n            val appGroupSubsConfigs = groupSubsConfigs.filter { s -> s.appId == it.first.id }\n            val enableSize = it.first.groups.count { g ->\n                getGroupEnable(\n                    g,\n                    appGroupSubsConfigs.find { c -> c.groupKey == g.key },\n                    groupToCategoryMap[g],\n                    categoryConfigs.find { c -> c.categoryKey == groupToCategoryMap[g]?.key }\n                )\n            }\n            SubsAppInfoItem(\n                rawApp = it.first,\n                appInfo = it.second,\n                appConfig = appConfigs.find { s -> s.appId == it.first.id },\n                enableSize = enableSize,\n            )\n        }\n    }.stateInit(emptyList())\n\n    val resetKey = mutableIntStateOf(0)\n\n    init {\n        appItemListFlow.mapNew { it.map { a -> a.id } }.launchOnChange {\n            resetKey.intValue++\n        }\n    }\n}\n\ndata class SubsAppInfoItem(\n    val rawApp: RawSubscription.RawApp,\n    val appInfo: AppInfo?,\n    val appConfig: AppConfig?,\n    val enableSize: Int,\n) {\n    val id get() = rawApp.id\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SubsCategoryPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TriStateCheckbox\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.appScope\nimport li.songe.gkd.data.CategoryConfig\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.FullscreenDialog\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.TowLineText\nimport li.songe.gkd.ui.component.autoFocus\nimport li.songe.gkd.ui.component.updateDialogOptions\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.icon.ResetSettings\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.EnableGroupOption\nimport li.songe.gkd.util.findOption\nimport li.songe.gkd.util.getCategoryEnable\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toToggleableState\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateSubscription\n\n@Serializable\ndata class SubsCategoryRoute(val subsItemId: Long) : NavKey\n\n@Composable\nfun SubsCategoryPage(@Suppress(\"unused\") route: SubsCategoryRoute) {\n    val mainVm = LocalMainViewModel.current\n\n    val vm = viewModel { SubsCategoryVm(route) }\n    val subs = vm.subsRawFlow.collectAsState().value\n    val categoryConfigMap = vm.categoryConfigMapFlow.collectAsState().value\n\n    val categories = subs.categories\n\n    val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()\n    Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = {\n        PerfTopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {\n            PerfIconButton(\n                imageVector = PerfIcon.ArrowBack,\n                onClick = {\n                    mainVm.popPage()\n                },\n            )\n        }, title = {\n            TowLineText(\n                title = subs.name,\n                subtitle = \"规则类别\"\n            )\n        }, actions = {\n            PerfIconButton(imageVector = PerfIcon.Info, onClick = throttle {\n                mainVm.dialogFlow.updateDialogOptions(\n                    title = \"类别说明\",\n                    text = arrayOf(\n                        \"类别会捕获以当前类别开头的所有应用规则组, 因此可调整类别开关(分类手动配置)来批量开关规则组\",\n                        \"规则组开关优先级为:\\n规则手动配置 > 分类手动配置 > 分类默认 > 规则默认\",\n                        \"因此如果手动开关了规则组(规则手动配置), 则该规则组不会被批量开关, 可通过点击类别-重置规则组开关, 来移除类别下所有规则手动配置\",\n                    ).joinToString(\"\\n\\n\"),\n                )\n            })\n        })\n    }, floatingActionButton = {\n        if (subs.isLocal) {\n            FloatingActionButton(onClick = { vm.showAddCategoryFlow.value = true }) {\n                PerfIcon(\n                    imageVector = PerfIcon.Add,\n                )\n            }\n        }\n    }) { contentPadding ->\n        LazyColumn(\n            modifier = Modifier.scaffoldPadding(contentPadding)\n        ) {\n            items(categories, { it.key }) { category ->\n                CategoryItemCard(\n                    vm = vm,\n                    subs = subs,\n                    category = category,\n                    categoryConfig = categoryConfigMap[category.key],\n                )\n            }\n            item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                Spacer(modifier = Modifier.height(EmptyHeight))\n                if (categories.isEmpty()) {\n                    EmptyText(text = \"暂无类别\")\n                }\n            }\n        }\n    }\n\n    val editCategory by vm.editCategoryFlow.collectAsState()\n    if (editCategory != null) {\n        AddOrEditCategoryDialog(\n            subs = subs,\n            category = editCategory,\n        ) {\n            vm.editCategoryFlow.value = null\n        }\n    }\n    val showAddCategory by vm.showAddCategoryFlow.collectAsState()\n    if (showAddCategory) {\n        AddOrEditCategoryDialog(\n            subs = subs,\n            category = null,\n        ) {\n            vm.showAddCategoryFlow.value = false\n        }\n    }\n}\n\n@Composable\nprivate fun CategoryItemCard(\n    vm: SubsCategoryVm,\n    subs: RawSubscription,\n    category: RawSubscription.RawCategory,\n    categoryConfig: CategoryConfig?,\n) {\n    val groups = subs.categoryToGroupsMap[category] ?: emptyList()\n    var expanded by remember { mutableStateOf(false) }\n    val onClick = {\n        if (groups.isNotEmpty() || subs.isLocal) {\n            expanded = true\n        }\n    }\n    Card(\n        onClick = onClick,\n        shape = MaterialTheme.shapes.extraSmall,\n        modifier = Modifier.padding(\n            horizontal = 8.dp,\n            vertical = 2.dp,\n        ),\n        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(8.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Column(modifier = Modifier.weight(1f)) {\n                Text(\n                    text = category.name,\n                    style = MaterialTheme.typography.bodyLarge,\n                )\n                if (!category.desc.isNullOrBlank())\n                    Text(\n                        text = category.desc,\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                else if (groups.isNotEmpty()) {\n                    val appSize = subs.getCategoryApps(category.key).size\n                    Text(\n                        text = \"${appSize}应用/${groups.size}规则组\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                } else {\n                    Text(\n                        text = \"暂无规则\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)\n                    )\n                }\n            }\n            CategoryMenu(\n                vm = vm,\n                subs = subs,\n                category = category,\n                expanded = expanded,\n                onCheckedChange = { expanded = it }\n            )\n            Spacer(modifier = Modifier.width(8.dp))\n            val enable = getCategoryEnable(category, categoryConfig)\n            TriStateCheckbox(\n                state = EnableGroupOption.objects.findOption(enable).toToggleableState(),\n                onClick = throttle(appScope.launchAsFn {\n                    val option = when (enable) {\n                        false -> EnableGroupOption.FollowSubs\n                        null -> EnableGroupOption.AllEnable\n                        true -> EnableGroupOption.AllDisable\n                    }\n                    DbSet.categoryConfigDao.insert(\n                        (categoryConfig ?: CategoryConfig(\n                            enable = option.value,\n                            subsId = subs.id,\n                            categoryKey = category.key\n                        )).copy(enable = option.value)\n                    )\n                    toast(option.label)\n                })\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun CategoryMenu(\n    vm: SubsCategoryVm,\n    subs: RawSubscription,\n    category: RawSubscription.RawCategory,\n    expanded: Boolean,\n    onCheckedChange: ((Boolean) -> Unit),\n) {\n    val mainVm = LocalMainViewModel.current\n    val groups = subs.categoryToGroupsMap[category] ?: emptyList()\n    Box(\n        modifier = Modifier.wrapContentSize(Alignment.TopStart)\n    ) {\n        DropdownMenu(\n            expanded = expanded,\n            onDismissRequest = { onCheckedChange(false) }\n        ) {\n            if (groups.isNotEmpty()) {\n                DropdownMenuItem(\n                    leadingIcon = {\n                        PerfIcon(\n                            imageVector = ResetSettings,\n                        )\n                    },\n                    text = { Text(text = \"重置规则组开关\") },\n                    onClick = throttle(vm.viewModelScope.launchAsFn {\n                        onCheckedChange(false)\n                        val updatedList = DbSet.subsConfigDao.batchResetAppGroupEnable(\n                            subs.id,\n                            subs.categoryToGroupsMap[category] ?: emptyList(),\n                        )\n                        if (updatedList.isNotEmpty()) {\n                            toast(\"成功重置 ${updatedList.size} 规则组开关\")\n                        } else {\n                            toast(\"无可重置规则组\")\n                        }\n                    })\n                )\n            }\n            if (subs.isLocal) {\n                DropdownMenuItem(\n                    leadingIcon = {\n                        PerfIcon(\n                            imageVector = PerfIcon.Edit,\n                        )\n                    },\n                    text = { Text(text = \"编辑\") },\n                    onClick = {\n                        onCheckedChange(false)\n                        vm.editCategoryFlow.value = category\n                    }\n                )\n                DropdownMenuItem(\n                    text = { Text(text = \"删除\", color = MaterialTheme.colorScheme.error) },\n                    leadingIcon = {\n                        PerfIcon(\n                            imageVector = PerfIcon.Delete,\n                        )\n                    },\n                    onClick = throttle(vm.viewModelScope.launchAsFn {\n                        onCheckedChange(false)\n                        mainVm.dialogFlow.waitResult(\n                            title = \"删除类别\",\n                            text = \"确定删除 ${category.name} ?\",\n                            error = true,\n                        )\n                        updateSubscription(\n                            subs.copy(categories = subs.categories.toMutableList().apply {\n                                removeIf { it.key == category.key }\n                            })\n                        )\n                        DbSet.categoryConfigDao.deleteByCategoryKey(\n                            subs.id,\n                            category.key\n                        )\n                        toast(\"删除成功\")\n                    })\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun AddOrEditCategoryDialog(\n    subs: RawSubscription,\n    category: RawSubscription.RawCategory?,\n    onDismissRequest: () -> Unit,\n) {\n    var nameValue by remember { mutableStateOf(category?.name ?: \"\") }\n    var descValue by remember { mutableStateOf(category?.desc ?: \"\") }\n    val onClick = appScope.launchAsFn {\n        if (category != null) {\n            onDismissRequest()\n            val changed = category.name != nameValue || (category.desc ?: \"\") != descValue\n            if (changed) {\n                updateSubscription(\n                    subs.copy(categories = subs.categories.toMutableList().apply {\n                        set(\n                            indexOfFirst { c -> c.key == category.key },\n                            category.copy(name = nameValue, desc = descValue)\n                        )\n                    })\n                )\n                toast(\"更新成功\")\n            } else {\n                toast(\"未修改\")\n            }\n        } else {\n            if (subs.categories.any { c -> c.name == nameValue }) {\n                error(\"不可添加同名类别\")\n            }\n            onDismissRequest()\n            updateSubscription(\n                subs.copy(categories = subs.categories.toMutableList().apply {\n                    val c = RawSubscription.RawCategory(\n                        key = (subs.categories.maxOfOrNull { c -> c.key } ?: -1) + 1,\n                        enable = null,\n                        name = nameValue,\n                        desc = descValue,\n                    )\n                    add(c)\n                })\n            )\n            toast(\"添加成功\")\n        }\n    }\n    FullscreenDialog(onDismissRequest = onDismissRequest) {\n        Scaffold(\n            topBar = {\n                PerfTopAppBar(\n                    navigationIcon = {\n                        PerfIconButton(\n                            imageVector = PerfIcon.Close,\n                            onClick = throttle(onDismissRequest),\n                        )\n                    },\n                    title = { Text(text = if (category == null) \"添加类别\" else \"编辑类别\") },\n                    actions = {\n                        PerfIconButton(\n                            imageVector = PerfIcon.Save,\n                            enabled = nameValue.isNotEmpty(),\n                            onClick = throttle(onClick),\n                        )\n                    }\n                )\n            },\n        ) { paddingValues ->\n            Column(\n                modifier = Modifier\n                    .padding(paddingValues)\n                    .padding(horizontal = 16.dp),\n            ) {\n                OutlinedTextField(\n                    label = { Text(\"类别名称\") },\n                    value = nameValue,\n                    onValueChange = { nameValue = it.trim() },\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .autoFocus(),\n                    placeholder = { Text(text = \"请输入类别名称\") },\n                    singleLine = true,\n                )\n                Spacer(modifier = Modifier.height(12.dp))\n                OutlinedTextField(\n                    label = { Text(\"类别描述\") },\n                    value = descValue,\n                    onValueChange = { descValue = it.trim() },\n                    modifier = Modifier.fillMaxWidth(),\n                    placeholder = { Text(text = \"请输入类别描述\") },\n                    singleLine = true,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SubsCategoryVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.map\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.share.BaseViewModel\n\nclass SubsCategoryVm(val route: SubsCategoryRoute) : BaseViewModel() {\n    val subsRawFlow = mapSafeSubs(route.subsItemId)\n\n    val categoryConfigsFlow = DbSet.categoryConfigDao.queryConfig(route.subsItemId)\n        .stateInit(emptyList())\n\n    val categoryConfigMapFlow = categoryConfigsFlow.map { it.associateBy { c -> c.categoryKey } }\n        .stateInit(emptyMap())\n\n    val editCategoryFlow = MutableStateFlow<RawSubscription.RawCategory?>(null)\n    val showAddCategoryFlow = MutableStateFlow(false)\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SubsGlobalGroupExcludePage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.a11y.launcherAppId\nimport li.songe.gkd.data.ExcludeData\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.data.SubsConfig\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.store.blockMatchAppListFlow\nimport li.songe.gkd.ui.component.AnimatedBooleanContent\nimport li.songe.gkd.ui.component.AnimatedIconButton\nimport li.songe.gkd.ui.component.AnimationFloatingActionButton\nimport li.songe.gkd.ui.component.AppBarTextField\nimport li.songe.gkd.ui.component.AppIcon\nimport li.songe.gkd.ui.component.AppNameText\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.InnerDisableSwitch\nimport li.songe.gkd.ui.component.MenuGroupCard\nimport li.songe.gkd.ui.component.MenuItemCheckbox\nimport li.songe.gkd.ui.component.MenuItemRadioButton\nimport li.songe.gkd.ui.component.MultiTextField\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfSwitch\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.TowLineText\nimport li.songe.gkd.ui.component.autoFocus\nimport li.songe.gkd.ui.component.isFullVisible\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.icon.BackCloseIcon\nimport li.songe.gkd.ui.icon.ResetSettings\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.asMutableState\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.itemPadding\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.AppGroupOption\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.systemAppsFlow\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\n\n@Serializable\ndata class SubsGlobalGroupExcludeRoute(\n    val subsItemId: Long,\n    val groupKey: Int,\n) : NavKey\n\n@Composable\nfun SubsGlobalGroupExcludePage(route: SubsGlobalGroupExcludeRoute) {\n    val subsItemId = route.subsItemId\n    val groupKey = route.groupKey\n\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    val vm = viewModel { SubsGlobalGroupExcludeVm(route) }\n    val subs = vm.subsFlow.collectAsState().value\n    val group = vm.groupFlow.collectAsState().value ?: return\n    val excludeData = vm.excludeDataFlow.collectAsState().value\n    val showAppInfos = vm.showAppInfosFlow.collectAsState().value\n\n    var searchStr by vm.searchStrFlow.asMutableState()\n    var editable by vm.editableFlow.asMutableState()\n\n    var showSearchBar by rememberSaveable {\n        mutableStateOf(false)\n    }\n    LaunchedEffect(key1 = showSearchBar, block = {\n        if (!showSearchBar) {\n            searchStr = \"\"\n        }\n    })\n    val (scrollBehavior, listState) = useListScrollState(\n        vm.resetKey,\n        canScroll = { !editable }\n    )\n\n    BackHandler(editable, onBack = throttle(vm.viewModelScope.launchAsFn {\n        context.justHideSoftInput()\n        if (vm.changedValue != null) {\n            mainVm.dialogFlow.waitResult(\n                title = \"提示\",\n                text = \"当前内容未保存，是否放弃编辑？\",\n            )\n        }\n        editable = false\n    }))\n\n    Scaffold(\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(\n                scrollBehavior = scrollBehavior,\n                canScroll = !editable,\n                navigationIcon = {\n                    IconButton(onClick = throttle(vm.viewModelScope.launchAsFn {\n                        if (vm.editableFlow.value) {\n                            editable = false\n                            context.justHideSoftInput()\n                        } else {\n                            context.hideSoftInput()\n                            mainVm.popPage()\n                        }\n                    })) {\n                        BackCloseIcon(backOrClose = !editable)\n                    }\n                },\n                title = {\n                    if (showSearchBar) {\n                        BackHandler {\n                            if (!context.justHideSoftInput()) {\n                                showSearchBar = false\n                            }\n                        }\n                        AppBarTextField(\n                            value = searchStr,\n                            onValueChange = { newValue ->\n                                searchStr = newValue.trim()\n                            },\n                            hint = \"请输入应用名称/ID\",\n                            modifier = Modifier.autoFocus(),\n                        )\n                    } else {\n                        TowLineText(\n                            title = group.name,\n                            subtitle = \"编辑禁用\",\n                            modifier = Modifier.noRippleClickable { vm.resetKey.intValue++ }\n                        )\n                    }\n                },\n                actions = {\n                    AnimatedBooleanContent(\n                        targetState = editable,\n                        contentAlignment = Alignment.TopEnd,\n                        contentTrue = {\n                            PerfIconButton(\n                                imageVector = PerfIcon.Save,\n                                onClick = throttle(vm.viewModelScope.launchAsFn {\n                                    val newExclude = vm.changedValue\n                                    if (newExclude != null) {\n                                        val subsConfig = (vm.subsConfigFlow.value ?: SubsConfig(\n                                            type = SubsConfig.GlobalGroupType,\n                                            subsId = subsItemId,\n                                            groupKey = groupKey,\n                                        )).copy(\n                                            exclude = newExclude.stringify()\n                                        )\n                                        DbSet.subsConfigDao.insert(subsConfig)\n                                        toast(\"更新成功\")\n                                    } else {\n                                        toast(\"未修改\")\n                                    }\n                                    context.justHideSoftInput()\n                                    editable = false\n                                }),\n                            )\n                        },\n                        contentFalse = {\n                            Row {\n                                AnimatedIconButton(\n                                    onClick = {\n                                        if (showSearchBar) {\n                                            if (searchStr.isEmpty()) {\n                                                showSearchBar = false\n                                            } else {\n                                                searchStr = \"\"\n                                            }\n                                        } else {\n                                            showSearchBar = true\n                                        }\n                                    },\n                                    id = R.drawable.ic_anim_search_close,\n                                    atEnd = showSearchBar,\n                                )\n                                var expanded by remember { mutableStateOf(false) }\n                                PerfIconButton(\n                                    imageVector = PerfIcon.Sort,\n                                    onClick = {\n                                        expanded = true\n                                    },\n                                )\n                                Box(\n                                    modifier = Modifier\n                                        .wrapContentSize(Alignment.TopStart)\n                                ) {\n                                    DropdownMenu(\n                                        expanded = expanded,\n                                        onDismissRequest = { expanded = false }\n                                    ) {\n                                        MenuGroupCard(inTop = true, title = \"排序\") {\n                                            var sortType by vm.sortTypeFlow.asMutableState()\n                                            AppSortOption.objects.forEach { option ->\n                                                MenuItemRadioButton(\n                                                    text = option.label,\n                                                    selected = sortType == option,\n                                                    onClick = { sortType = option }\n                                                )\n                                            }\n                                        }\n                                        MenuGroupCard(title = \"分组\") {\n                                            var appGroupType by vm.appGroupTypeFlow.asMutableState()\n                                            AppGroupOption.normalObjects.forEach { option ->\n                                                val newValue = option.invert(appGroupType)\n                                                MenuItemCheckbox(\n                                                    enabled = newValue != 0,\n                                                    text = option.label,\n                                                    checked = option.include(appGroupType),\n                                                    onClick = { appGroupType = newValue },\n                                                )\n                                            }\n                                        }\n                                        MenuGroupCard(title = \"筛选\") {\n                                            MenuItemCheckbox(\n                                                text = \"内置禁用\",\n                                                stateFlow = vm.showInnerDisabledAppFlow,\n                                            )\n                                            MenuItemCheckbox(\n                                                text = \"白名单\",\n                                                stateFlow = vm.showBlockAppFlow,\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        },\n                    )\n                })\n        },\n        floatingActionButton = {\n            AnimationFloatingActionButton(\n                visible = !editable && scrollBehavior.isFullVisible,\n                onClick = {\n                    editable = !editable\n                },\n                imageVector = PerfIcon.Edit,\n                contentDescription = \"编辑禁用名单\"\n            )\n        }\n    ) { contentPadding ->\n        if (editable) {\n            MultiTextField(\n                modifier = Modifier.scaffoldPadding(contentPadding),\n                textFlow = vm.excludeTextFlow,\n                immediateFocus = true,\n                placeholderText = tipText,\n            )\n        } else {\n            LazyColumn(\n                modifier = Modifier.scaffoldPadding(contentPadding),\n                state = listState,\n            ) {\n                items(showAppInfos, { it.id }) { appInfo ->\n\n                    Row(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .itemPadding(),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(12.dp)\n                    ) {\n                        AppIcon(appId = appInfo.id)\n                        Column(\n                            modifier = Modifier.weight(1f),\n                        ) {\n                            AppNameText(appInfo = appInfo)\n                            Text(\n                                text = appInfo.id,\n                                maxLines = 1,\n                                softWrap = false,\n                                overflow = TextOverflow.Ellipsis,\n                                modifier = Modifier.fillMaxWidth(),\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                        val blockMatch =\n                            blockMatchAppListFlow.collectAsState().value.contains(appInfo.id)\n                        if (blockMatch) {\n                            PerfIcon(\n                                modifier = Modifier\n                                    .padding(2.dp)\n                                    .size(20.dp),\n                                imageVector = PerfIcon.Block,\n                                tint = MaterialTheme.colorScheme.secondary,\n                            )\n                        }\n                        val checked = getGlobalGroupChecked(\n                            subs,\n                            excludeData,\n                            group,\n                            appInfo.id\n                        )\n                        if (checked != null) {\n                            PerfSwitch(\n                                key = appInfo.id,\n                                checked = checked,\n                                onCheckedChange = vm.viewModelScope.launchAsFn { newChecked ->\n                                    val subsConfig = (vm.subsConfigFlow.value ?: SubsConfig(\n                                        type = SubsConfig.GlobalGroupType,\n                                        subsId = subsItemId,\n                                        groupKey = groupKey,\n                                    )).copy(\n                                        exclude = excludeData.copy(\n                                            appIds = excludeData.appIds.toMutableMap()\n                                                .apply {\n                                                    set(appInfo.id, !newChecked)\n                                                })\n                                            .stringify()\n                                    )\n                                    DbSet.subsConfigDao.insert(subsConfig)\n                                },\n                                thumbContent = if (excludeData.appIds.contains(appInfo.id)) ({\n                                    PerfIcon(\n                                        imageVector = ResetSettings,\n                                        modifier = Modifier.size(8.dp)\n                                    )\n                                }) else null,\n                            )\n                        } else {\n                            InnerDisableSwitch()\n                        }\n                    }\n                }\n                item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                    Spacer(modifier = Modifier.height(EmptyHeight))\n                    if (showAppInfos.isEmpty() && searchStr.isNotEmpty()) {\n                        EmptyText(text = if (vm.appFilter.showAllAppFlow.collectAsState().value) \"暂无搜索结果\" else \"暂无搜索结果，或修改筛选\")\n                        Spacer(modifier = Modifier.height(EmptyHeight / 2))\n                    }\n                }\n            }\n        }\n    }\n}\n\n// null - 内置禁用\n// true - 启用\n// false - 禁用\nfun getGlobalGroupChecked(\n    subscription: RawSubscription,\n    excludeData: ExcludeData,\n    group: RawSubscription.RawGlobalGroup,\n    appId: String,\n): Boolean? {\n    if (subscription.getGlobalGroupInnerDisabled(group, appId)) {\n        return null\n    }\n    excludeData.appIds[appId]?.let { return !it }\n    if (group.appIdEnable[appId] == true) return true\n    if (appId == launcherAppId) {\n        return group.matchLauncher ?: false\n    }\n    if (systemAppsFlow.value.contains(appId)) {\n        return group.matchSystemApp ?: false\n    }\n    return group.matchAnyApp ?: true\n}\n\nprivate val tipText = \"\"\"\n以换行或英文逗号分割每条禁用\n示例1-禁用单个页面\nappId/activityId\n示例2-禁用整个应用(移除/)\nappId\n示例3-开启此应用(前置!)\n!appId\n\"\"\".trimIndent()"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SubsGlobalGroupExcludeVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.compose.runtime.mutableIntStateOf\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.combine\nimport li.songe.gkd.data.ExcludeData\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.share.BaseViewModel\nimport li.songe.gkd.ui.share.asMutableStateFlow\nimport li.songe.gkd.ui.share.useAppFilter\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.findOption\n\nclass SubsGlobalGroupExcludeVm(val route: SubsGlobalGroupExcludeRoute) : BaseViewModel() {\n\n    val subsFlow = mapSafeSubs(route.subsItemId)\n    val groupFlow = subsFlow.mapNew { r -> r.globalGroups.find { g -> g.key == route.groupKey } }\n    val subsConfigFlow = DbSet.subsConfigDao\n        .queryGlobalGroupTypeConfig(route.subsItemId, route.groupKey)\n        .stateInit(null)\n    val excludeDataFlow = subsConfigFlow.mapNew { s -> ExcludeData.parse(s?.exclude) }\n\n    val sortTypeFlow = storeFlow.asMutableStateFlow(\n        getter = { AppSortOption.objects.findOption(it.subsExcludeSort) },\n        setter = {\n            storeFlow.value.copy(subsExcludeSort = it.value)\n        }\n    )\n    val showInnerDisabledAppFlow = storeFlow.asMutableStateFlow(\n        getter = { it.subsExcludeShowInnerDisabledApp },\n        setter = {\n            storeFlow.value.copy(subsExcludeShowInnerDisabledApp = it)\n        }\n    )\n    val showBlockAppFlow = storeFlow.asMutableStateFlow(\n        getter = { it.subsExcludeShowBlockApp },\n        setter = {\n            storeFlow.value.copy(subsExcludeShowBlockApp = it)\n        }\n    )\n    val appGroupTypeFlow = storeFlow.asMutableStateFlow(\n        getter = { it.subsExcludeAppGroupType },\n        setter = {\n            storeFlow.value.copy(subsExcludeAppGroupType = it)\n        }\n    )\n    val appFilter = useAppFilter(\n        appGroupTypeFlow = appGroupTypeFlow,\n        appOrderListFlow = DbSet.actionLogDao.queryLatestUniqueAppIds(\n            route.subsItemId,\n            route.groupKey\n        ).stateInit(emptyList()),\n        sortTypeFlow = sortTypeFlow,\n        showBlockAppFlow = showBlockAppFlow,\n    )\n    val searchStrFlow = appFilter.searchStrFlow\n\n    val showAppInfosFlow = combine(\n        appFilter.appListFlow,\n        showInnerDisabledAppFlow,\n        subsFlow,\n        groupFlow,\n    ) { apps, showDisabledApp, rawSubs, group ->\n        if (showDisabledApp || group == null) {\n            apps\n        } else {\n            apps.filter { a -> !rawSubs.getGlobalGroupInnerDisabled(group, a.id) }\n        }\n    }.stateInit(emptyList()).apply {\n        launchOnChange {\n            resetKey.intValue++\n        }\n    }\n    val resetKey = mutableIntStateOf(0)\n    val excludeTextFlow = MutableStateFlow(\"\")\n    val editableFlow = MutableStateFlow(false).apply {\n        launchOnChange {\n            if (it) {\n                excludeTextFlow.value = excludeDataFlow.value.stringify()\n            }\n        }\n    }\n\n    val changedValue: ExcludeData?\n        get() {\n            val newExclude = ExcludeData.parse(excludeTextFlow.value)\n            return if (newExclude != excludeDataFlow.value) {\n                newExclude\n            } else {\n                null\n            }\n        }\n\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SubsGlobalGroupListPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.component.AnimationFloatingActionButton\nimport li.songe.gkd.ui.component.BatchActionButtonGroup\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.RuleGroupCard\nimport li.songe.gkd.ui.component.TowLineText\nimport li.songe.gkd.ui.component.animateListItem\nimport li.songe.gkd.ui.component.toGroupState\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.icon.BackCloseIcon\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.getUpDownTransform\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.switchItem\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateSubscription\n\n\n@Serializable\ndata class SubsGlobalGroupListRoute(val subsItemId: Long, val focusGroupKey: Int? = null) : NavKey\n\n@Composable\nfun SubsGlobalGroupListPage(route: SubsGlobalGroupListRoute) {\n    val subsItemId = route.subsItemId\n    val focusGroupKey = route.focusGroupKey\n\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel { SubsGlobalGroupListVm(route) }\n    val subs = vm.subsRawFlow.collectAsState().value\n    val subsConfigs by vm.subsConfigsFlow.collectAsState()\n\n    val editable = subsItemId < 0\n    val globalGroups = subs.globalGroups\n\n    val isSelectedMode = vm.isSelectedModeFlow.collectAsState().value\n    val selectedDataSet = vm.selectedDataSetFlow.collectAsState().value\n    LaunchedEffect(key1 = isSelectedMode) {\n        if (!isSelectedMode) {\n            vm.selectedDataSetFlow.value = emptySet()\n        }\n    }\n    LaunchedEffect(key1 = selectedDataSet.isEmpty()) {\n        if (selectedDataSet.isEmpty()) {\n            vm.isSelectedModeFlow.value = false\n        }\n    }\n    BackHandler(isSelectedMode) {\n        vm.isSelectedModeFlow.value = false\n    }\n\n    val resetKey = rememberSaveable { mutableIntStateOf(0) }\n    val (scrollBehavior, listState) = useListScrollState(resetKey, globalGroups.isEmpty())\n    if (focusGroupKey != null) {\n        LaunchedEffect(null) {\n            if (vm.focusGroupFlow?.value != null) {\n                val i = globalGroups.indexOfFirst { it.key == focusGroupKey }\n                if (i >= 0) {\n                    listState.scrollToItem(i)\n                }\n            }\n        }\n    }\n    Scaffold(\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {\n                IconButton(onClick = throttle {\n                    if (isSelectedMode) {\n                        vm.isSelectedModeFlow.value = false\n                    } else {\n                        mainVm.popPage()\n                    }\n                }) {\n                    BackCloseIcon(backOrClose = !isSelectedMode)\n                }\n            }, title = {\n                val titleModifier = Modifier.noRippleClickable { resetKey.intValue++ }\n                if (isSelectedMode) {\n                    Text(\n                        modifier = titleModifier,\n                        text = selectedDataSet.size.toString(),\n                    )\n                } else {\n                    TowLineText(\n                        modifier = titleModifier,\n                        title = subs.name,\n                        subtitle = \"全局规则\"\n                    )\n                }\n            }, actions = {\n                var expanded by remember { mutableStateOf(false) }\n                AnimatedContent(\n                    targetState = isSelectedMode,\n                    transitionSpec = { getUpDownTransform() },\n                    contentAlignment = Alignment.TopEnd,\n                ) {\n                    if (it) {\n                        Row {\n                            BatchActionButtonGroup(vm, selectedDataSet)\n                            if (editable) {\n                                PerfIconButton(\n                                    imageVector = PerfIcon.Delete,\n                                    onClick = throttle(\n                                        vm.viewModelScope.launchAsFn(\n                                            Dispatchers.Default\n                                        ) {\n                                            mainVm.dialogFlow.waitResult(\n                                                title = \"删除规则组\",\n                                                text = \"删除当前所选规则组?\",\n                                                error = true,\n                                            )\n                                            val keys = selectedDataSet.mapNotNull { g ->\n                                                g.groupKey\n                                            }\n                                            vm.isSelectedModeFlow.value = false\n                                            updateSubscription(\n                                                subs.copy(\n                                                    globalGroups = globalGroups.filterNot { g ->\n                                                        keys.contains(g.key)\n                                                    }\n                                                )\n                                            )\n                                            DbSet.subsConfigDao.batchDeleteGlobalGroupConfig(\n                                                subsItemId,\n                                                keys\n                                            )\n                                            toast(\"删除成功\")\n                                        })\n                                )\n                            }\n                            PerfIconButton(\n                                imageVector = PerfIcon.MoreVert,\n                                onClick = {\n                                    expanded = true\n                                })\n                        }\n                    }\n                }\n                if (isSelectedMode) {\n                    Box(\n                        modifier = Modifier\n                            .wrapContentSize(Alignment.TopStart)\n                    ) {\n                        DropdownMenu(\n                            expanded = expanded,\n                            onDismissRequest = { expanded = false }\n                        ) {\n                            DropdownMenuItem(\n                                text = {\n                                    Text(text = \"全选\")\n                                },\n                                onClick = {\n                                    expanded = false\n                                    vm.selectedDataSetFlow.value = globalGroups.map {\n                                        it.toGroupState(\n                                            subsId = subsItemId,\n                                        )\n                                    }.toSet()\n                                }\n                            )\n                            DropdownMenuItem(\n                                text = {\n                                    Text(text = \"反选\")\n                                },\n                                onClick = {\n                                    expanded = false\n                                    val newSelectedIds = globalGroups.map {\n                                        it.toGroupState(\n                                            subsId = subsItemId,\n                                        )\n                                    }.toSet() - selectedDataSet\n                                    vm.selectedDataSetFlow.value = newSelectedIds\n                                }\n                            )\n                        }\n                    }\n                }\n            })\n        },\n        floatingActionButton = {\n            if (editable) {\n                AnimationFloatingActionButton(\n                    visible = !isSelectedMode,\n                    onClick = {\n                        mainVm.navigatePage(\n                            UpsertRuleGroupRoute(\n                                subsId = subsItemId,\n                                groupKey = null,\n                                appId = null,\n                            )\n                        )\n                    },\n                    imageVector = PerfIcon.Add,\n                    contentDescription = \"添加规则\"\n                )\n            }\n        },\n    ) { paddingValues ->\n        LazyColumn(\n            modifier = Modifier.scaffoldPadding(paddingValues),\n            state = listState,\n            verticalArrangement = Arrangement.spacedBy(4.dp),\n        ) {\n            items(globalGroups, { g -> g.key }) { group ->\n                val subsConfig = subsConfigs.find { it.groupKey == group.key }\n                RuleGroupCard(\n                    modifier = Modifier.animateListItem(),\n                    subs = subs,\n                    appId = null,\n                    group = group,\n                    focusGroupFlow = vm.focusGroupFlow,\n                    subsConfig = subsConfig,\n                    category = null,\n                    categoryConfig = null,\n                    isSelectedMode = isSelectedMode,\n                    isSelected = selectedDataSet.any { it.groupKey == group.key },\n                    onLongClick = {\n                        if (globalGroups.size > 1) {\n                            vm.isSelectedModeFlow.value = true\n                            vm.selectedDataSetFlow.value = setOf(\n                                group.toGroupState(subsId = subsItemId)\n                            )\n                        }\n                    },\n                    onSelectedChange = {\n                        vm.selectedDataSetFlow.value = selectedDataSet.switchItem(\n                            group.toGroupState(subsId = subsItemId)\n                        )\n                    }\n                )\n            }\n            item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                Spacer(modifier = Modifier.height(EmptyHeight))\n                if (globalGroups.isEmpty()) {\n                    EmptyText(text = \"暂无规则\")\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/SubsGlobalGroupListVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.component.ShowGroupState\nimport li.songe.gkd.ui.share.BaseViewModel\n\nclass SubsGlobalGroupListVm(val route: SubsGlobalGroupListRoute) : BaseViewModel() {\n    val subsRawFlow = mapSafeSubs(route.subsItemId)\n\n    val subsConfigsFlow = DbSet.subsConfigDao.queryGlobalGroupTypeConfig(route.subsItemId)\n        .stateInit(emptyList())\n\n    val isSelectedModeFlow = MutableStateFlow(false)\n    val selectedDataSetFlow = MutableStateFlow(emptySet<ShowGroupState>())\n    val focusGroupFlow = route.focusGroupKey?.let {\n        MutableStateFlow<Triple<Long, String?, Int>?>(\n            Triple(\n                route.subsItemId,\n                null,\n                route.focusGroupKey\n            )\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/UpsertRuleGroupPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.autoFocus\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.share.LocalDarkTheme\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.getJson5Transformation\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.throttle\n\n@Serializable\ndata class UpsertRuleGroupRoute(\n    val subsId: Long,\n    val groupKey: Int? = null,\n    val appId: String? = null,\n    val forward: Boolean = false,\n) : NavKey\n\n@Composable\nfun UpsertRuleGroupPage(route: UpsertRuleGroupRoute) {\n    val subsId = route.subsId\n    val appId = route.appId\n    val forward = route.forward\n\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    val vm = viewModel { UpsertRuleGroupVm(route) }\n    val text by vm.textFlow.collectAsState()\n\n    val checkIfSaveText = throttle(mainVm.viewModelScope.launchAsFn(Dispatchers.Default) {\n        if (vm.hasTextChanged()) {\n            context.justHideSoftInput()\n            mainVm.dialogFlow.waitResult(\n                title = \"提示\",\n                text = \"当前内容未保存，是否放弃编辑？\",\n            )\n        } else {\n            context.hideSoftInput()\n        }\n        mainVm.popPage()\n    })\n\n    val onClickSave = throttle(vm.viewModelScope.launchAsFn(Dispatchers.Main) {\n        withContext(Dispatchers.Default) { vm.saveRule() }\n        context.hideSoftInput()\n        if (forward) {\n            if (appId == null) {\n                mainVm.navigatePage(\n                    SubsGlobalGroupListRoute(subsItemId = subsId),\n                    replaced = true\n                )\n            } else {\n                mainVm.navigatePage(\n                    SubsAppGroupListRoute(\n                        subsItemId = subsId,\n                        vm.addAppId ?: appId\n                    ),\n                    replaced = true\n                )\n            }\n        } else {\n            mainVm.popPage()\n        }\n    })\n    BackHandler(true, checkIfSaveText)\n    Scaffold(modifier = Modifier, topBar = {\n        PerfTopAppBar(\n            modifier = Modifier.fillMaxWidth(),\n            navigationIcon = {\n                PerfIconButton(imageVector = PerfIcon.ArrowBack, onClick = checkIfSaveText)\n            },\n            title = {\n                Text(text = if (vm.isEdit) \"编辑规则组\" else \"添加规则组\")\n            },\n            actions = {\n                PerfIconButton(\n                    imageVector = PerfIcon.Save,\n                    onClick = onClickSave,\n                    enabled = text.isNotBlank()\n                )\n            }\n        )\n    }) { paddingValues ->\n        val textColors = TextFieldDefaults.colors(\n            focusedIndicatorColor = Color.Transparent,\n            unfocusedIndicatorColor = Color.Transparent,\n            errorIndicatorColor = Color.Transparent,\n            disabledIndicatorColor = Color.Transparent,\n        )\n        Box(\n            modifier = Modifier\n                .scaffoldPadding(paddingValues)\n                .fillMaxSize(),\n        ) {\n            CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {\n                val imeShowing by context.imePlayingFlow.collectAsState()\n                val modifier = Modifier\n                    .autoFocus()\n                    .fillMaxSize()\n                    .run {\n                        if (imeShowing) {\n                            this\n                        } else {\n                            imePadding()\n                        }\n                    }\n                TextField(\n                    value = text,\n                    onValueChange = { vm.textFlow.value = it },\n                    modifier = modifier,\n                    shape = RectangleShape,\n                    colors = textColors,\n                    visualTransformation = getJson5Transformation(LocalDarkTheme.current),\n                    placeholder = {\n                        Text(text = if (vm.isApp) \"请输入应用规则组\\n\" else \"请输入全局规则组\\n\")\n                    },\n                )\n            }\n            if (text.isNotEmpty()) {\n                Text(\n                    text = text.length.toString(),\n                    modifier = Modifier\n                        .padding(8.dp)\n                        .align(Alignment.TopEnd)\n                        .clip(MaterialTheme.shapes.extraSmall)\n                        .background(MaterialTheme.colorScheme.surfaceContainer)\n                        .padding(horizontal = 2.dp),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.tertiary,\n                )\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/UpsertRuleGroupVm.kt",
    "content": "package li.songe.gkd.ui\n\nimport androidx.lifecycle.ViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.serialization.json.JsonArray\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.JsonPrimitive\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.ui.style.clearJson5TransformationCache\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.subsMapFlow\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateSubscription\nimport li.songe.json5.Json5\n\nclass UpsertRuleGroupVm(val route: UpsertRuleGroupRoute) : ViewModel() {\n    val groupKey = route.groupKey\n    val appId = route.appId\n\n    val isEdit = groupKey != null\n    val isApp = appId != null\n    val isAddAnyApp = appId == \"\"\n\n    private val initialGroup: RawSubscription.RawGroupProps? = run {\n        val subs = subsMapFlow.value[route.subsId]\n        subs ?: return@run null\n        if (groupKey != null) {\n            if (appId != null) {\n                subs.getAppGroups(appId)\n            } else {\n                subs.globalGroups\n            }.find { it.key == route.groupKey }\n        } else {\n            null\n        }\n    }\n\n    private val initText = initialGroup?.cacheStr ?: \"\"\n    val textFlow = MutableStateFlow(initText)\n\n    fun hasTextChanged(): Boolean {\n        val text = textFlow.value\n        if (!isEdit) return !text.isBlank()\n        if (initText == text) return false\n        return initialGroup?.cacheJsonObject != runCatching { Json5.parseToJson5Element(text) }.getOrNull()\n    }\n\n\n    var addAppId: String? = null\n\n    fun saveRule() {\n        val subs = subsMapFlow.value[route.subsId] ?: error(\"订阅不存在\")\n        val text = textFlow.value\n        if (text.isBlank()) {\n            error(\"规则不能为空\")\n        }\n        if (text == initText) {\n            toast(\"规则无变动\")\n            return\n        }\n        val jsonObject = runCatching { Json5.parseToJson5Element(text) }.run {\n            if (isFailure) {\n                error(\"非法格式\\n${exceptionOrNull()?.message}\")\n            }\n            getOrThrow()\n        }\n        if (jsonObject !is JsonObject) {\n            error(\"规则应为对象格式\")\n        }\n        if (jsonObject == initialGroup?.cacheJsonObject) {\n            toast(\"规则无变动\")\n            return\n        }\n        if (groupKey != null) {\n            val newGroup = try {\n                if (appId != null) {\n                    if (jsonObject[\"groups\"] is JsonArray) {\n                        val id = jsonObject[\"id\"] ?: error(\"缺少id\")\n                        if (!(id is JsonPrimitive && id.isString && id.content == appId)) {\n                            error(\"id与当前应用不一致\")\n                        }\n                        RawSubscription.parseApp(jsonObject).let { newApp ->\n                            if (newApp.groups.isEmpty()) {\n                                error(\"至少输入一个规则组\")\n                            }\n                            newApp.groups.first()\n                        }\n                    } else {\n                        null\n                    } ?: RawSubscription.parseAppGroup(jsonObject)\n                } else {\n                    RawSubscription.parseGlobalGroup(jsonObject)\n                }\n            } catch (e: Exception) {\n                LogUtils.d(e)\n                error(\"非法规则\\n${e.message}\")\n            }\n            newGroup.errorDesc?.let(::error)\n            if (newGroup.key != groupKey) {\n                error(\"不能更改规则组的key\")\n            }\n            val newSubs = if (appId != null) {\n                newGroup as RawSubscription.RawAppGroup\n                val app = subs.apps.find { a -> a.id == appId } ?: error(\"应用不存在\")\n                subs.copy(apps = subs.apps.toMutableList().apply {\n                    set(\n                        indexOfFirst { a -> a.id == appId },\n                        app.copy(groups = app.groups.toMutableList().apply {\n                            set(\n                                indexOfFirst { g -> g.key == newGroup.key },\n                                newGroup\n                            )\n                        })\n                    )\n                })\n            } else {\n                newGroup as RawSubscription.RawGlobalGroup\n                subs.copy(globalGroups = subs.globalGroups.toMutableList().apply {\n                    set(indexOfFirst { g -> g.key == newGroup.key }, newGroup)\n                })\n            }\n            updateSubscription(newSubs)\n        } else {\n            if (isAddAnyApp) {\n                val newApp = try {\n                    RawSubscription.parseApp(jsonObject).apply {\n                        if (groups.isEmpty()) {\n                            error(\"至少输入一个规则组\")\n                        }\n                    }\n                } catch (e: Exception) {\n                    LogUtils.d(e)\n                    error(\"非法规则\\n${e.message}\")\n                }\n                val oldApp = subs.apps.find { it.id == newApp.id }\n                if (oldApp != null) {\n                    newApp.groups.forEach { g ->\n                        checkGroupKeyName(oldApp.groups, g)\n                    }\n                }\n                val newSubs = subs.copy(apps = subs.apps.toMutableList().apply {\n                    val i = indexOfFirst { a -> a.id == newApp.id }\n                    if (i >= 0) {\n                        set(\n                            i,\n                            get(i).copy(groups = get(i).groups + newApp.groups),\n                        )\n                    } else {\n                        add(newApp)\n                    }\n                })\n                addAppId = newApp.id\n                updateSubscription(newSubs)\n            } else if (appId != null) {\n                // add specified app group\n                val newGroups = try {\n                    if (jsonObject[\"groups\"] is JsonArray) {\n                        val id = jsonObject[\"id\"] ?: error(\"缺少id\")\n                        if (!(id is JsonPrimitive && id.isString && id.content == appId)) {\n                            error(\"id与当前应用不一致\")\n                        }\n                        RawSubscription.parseApp(jsonObject).apply {\n                            if (groups.isEmpty()) {\n                                error(\"至少输入一个规则组\")\n                            }\n                        }.groups\n                    } else {\n                        null\n                    } ?: listOf(RawSubscription.parseAppGroup(jsonObject))\n                } catch (e: Exception) {\n                    LogUtils.d(e)\n                    error(\"非法规则\\n${e.message}\")\n                }\n                val oldApp = subs.getApp(appId)\n                newGroups.forEach { g ->\n                    checkGroupKeyName(oldApp.groups, g)\n                    g.errorDesc?.let { error(it) }\n                }\n                val newSubs = subs.copy(apps = subs.apps.toMutableList().apply {\n                    val newApp = oldApp.copy(groups = oldApp.groups + newGroups)\n                    val i = indexOfFirst { a -> a.id == newApp.id }\n                    if (i >= 0) {\n                        set(\n                            i,\n                            newApp\n                        )\n                    } else {\n                        add(newApp)\n                    }\n                })\n                updateSubscription(newSubs)\n            } else {\n                // add global group\n                val newGroup = try {\n                    RawSubscription.parseGlobalGroup(jsonObject)\n                } catch (e: Exception) {\n                    LogUtils.d(e)\n                    error(\"非法规则\\n${e.message}\")\n                }\n                checkGroupKeyName(subs.globalGroups, newGroup)\n                updateSubscription(\n                    subs.copy(\n                        globalGroups = subs.globalGroups + newGroup\n                    )\n                )\n            }\n        }\n        if (isEdit) {\n            toast(\"更新成功\")\n        } else {\n            toast(\"添加成功\")\n        }\n    }\n\n    init {\n        addCloseable { clearJson5TransformationCache() }\n    }\n}\n\nprivate fun checkGroupKeyName(\n    groups: List<RawSubscription.RawGroupProps>,\n    newGroup: RawSubscription.RawGroupProps\n) {\n    if (groups.any { it.name == newGroup.name }) {\n        error(\"已存在同名「${newGroup.name}」规则组\")\n    }\n    if (groups.any { it.key == newGroup.key }) {\n        error(\"已存在同 key=${newGroup.key} 规则组\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/WebViewPage.kt",
    "content": "package li.songe.gkd.ui\n\nimport android.annotation.SuppressLint\nimport android.graphics.Bitmap\nimport android.webkit.JavascriptInterface\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebResourceResponse\nimport android.webkit.WebView\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.navigation3.runtime.NavKey\nimport com.kevinnzou.web.AccompanistWebViewClient\nimport com.kevinnzou.web.LoadingState\nimport com.kevinnzou.web.WebView\nimport com.kevinnzou.web.rememberWebViewState\nimport io.ktor.client.call.body\nimport io.ktor.client.request.get\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.META\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.data.Value\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.updateDialogOptions\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.iconTextSize\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.client\nimport li.songe.gkd.util.copyText\nimport li.songe.gkd.util.openUri\nimport li.songe.gkd.util.throttle\n\n@Serializable\ndata class WebViewRoute(val initUrl: String) : NavKey\n\n@Composable\nfun WebViewPage(route: WebViewRoute) {\n    val initUrl = route.initUrl\n    val mainVm = LocalMainViewModel.current\n    val webViewState = rememberWebViewState(url = initUrl)\n    val webViewClient = remember { GkdWebViewClient() }\n    val webView = remember { Value<WebView?>(null) }\n    Scaffold(modifier = Modifier, topBar = {\n        PerfTopAppBar(\n            modifier = Modifier.fillMaxWidth(),\n            navigationIcon = {\n                PerfIconButton(\n                    imageVector = PerfIcon.ArrowBack,\n                    onClick = { mainVm.popPage() },\n                )\n            },\n            title = {\n                val loadingState = webViewState.loadingState\n                if (loadingState is LoadingState.Loading) {\n                    CircularProgressIndicator(\n                        modifier = Modifier.iconTextSize(),\n                    )\n                } else {\n                    Text(\n                        // webViewState.pageTitle 在调用 reload 后会变成 null\n                        text = webViewState.pageTitle ?: webView.value?.title ?: \"\",\n                        maxLines = 1,\n                        softWrap = false,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n                }\n            },\n            actions = {\n                if (chromeVersion in 1..<MINI_CHROME_VERSION) {\n                    PerfIconButton(imageVector = PerfIcon.WarningAmber, onClick = throttle {\n                        mainVm.dialogFlow.updateDialogOptions(\n                            title = \"兼容性提示\",\n                            text = \"检测到您的系统内置浏览器版本($chromeVersion)过低, 可能无法正常浏览网页文档\\n\\n建议自行升级版本后重启 GKD 再查看文档, 或点击右上角后在外部浏览器打开查阅\\n\\n若能正常浏览文档请忽略此项提示\"\n                        )\n                    })\n                }\n                var expanded by remember { mutableStateOf(false) }\n                PerfIconButton(imageVector = PerfIcon.MoreVert, onClick = { expanded = true })\n                Box(\n                    modifier = Modifier\n                        .wrapContentSize(Alignment.TopStart)\n                ) {\n                    DropdownMenu(\n                        expanded = expanded,\n                        onDismissRequest = { expanded = false }\n                    ) {\n                        if (webViewState.loadingState !is LoadingState.Loading) {\n                            DropdownMenuItem(\n                                text = {\n                                    Text(text = \"刷新页面\")\n                                },\n                                onClick = {\n                                    expanded = false\n                                    webView.value?.reload()\n                                }\n                            )\n                        }\n                        DropdownMenuItem(\n                            text = {\n                                Text(text = \"复制链接\")\n                            },\n                            onClick = {\n                                expanded = false\n                                copyText(webView.value?.url ?: initUrl)\n                            }\n                        )\n                        DropdownMenuItem(\n                            text = {\n                                Text(text = \"外部打开\")\n                            },\n                            onClick = {\n                                expanded = false\n                                openUri(webView.value?.url ?: initUrl)\n                            }\n                        )\n                    }\n                }\n            }\n        )\n    }) { contentPadding ->\n        WebView(\n            modifier = Modifier\n                .fillMaxSize()\n                .scaffoldPadding(contentPadding),\n            state = webViewState,\n            client = webViewClient,\n            onCreated = {\n                webView.value = it\n                it.addJavascriptInterface(GkdWebViewJsApi, \"gkd\")\n                it.settings.apply {\n                    @SuppressLint(\"SetJavaScriptEnabled\")\n                    javaScriptEnabled = true\n                    domStorageEnabled = true\n                    if (AndroidTarget.TIRAMISU) {\n                        setAlgorithmicDarkeningAllowed(false)\n                    }\n                }\n            }\n        )\n    }\n}\n\n@Suppress(\"unused\")\nprivate object GkdWebViewJsApi {\n    @JavascriptInterface\n    fun getAppId() = META.appId\n\n    @JavascriptInterface\n    fun getAppName() = META.appName\n\n    @JavascriptInterface\n    fun getVersionCode() = META.versionCode\n\n    @JavascriptInterface\n    fun getVersionName() = META.versionName\n\n    @JavascriptInterface\n    fun getChannel() = META.channel\n\n    @JavascriptInterface\n    fun getDebuggable() = META.debuggable\n}\n\nprivate const val MINI_CHROME_VERSION = 107\nprivate val chromeVersion by lazy {\n    WebView.getCurrentWebViewPackage()?.versionName?.run {\n        splitToSequence('.').first().toIntOrNull()\n    } ?: 0\n}\n\nprivate const val DOC_CONFIG_URL =\n    \"https://registry.npmmirror.com/@gkd-kit/docs/latest/files/_config.json\"\n\nprivate const val DEBUG_JS_TEXT = \"\"\"\n<script src=\"https://registry.npmmirror.com/eruda/latest/files\"></script>\n<script>eruda.init();</script>\n\"\"\"\n\n\n@Serializable\nprivate data class DocConfig(\n    val mirrorBaseUrl: String,\n    val htmlUrlMap: Map<String, String>\n)\n\nprivate class GkdWebViewClient() : AccompanistWebViewClient() {\n    override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {\n        super.onPageStarted(view, url, favicon)\n    }\n\n    override fun onPageFinished(view: WebView, url: String?) {\n        super.onPageFinished(view, url)\n    }\n\n    override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {\n        val uri = request?.url\n        if (uri != null && uri.host != \"gkd.li\") {\n            if (uri.scheme == \"gkd\") {\n                (view?.context as? MainActivity)?.mainVm?.handleGkdUri(uri)\n            } else {\n                openUri(uri)\n            }\n            return true\n        }\n        return super.shouldOverrideUrlLoading(view, request)\n    }\n\n    override fun shouldInterceptRequest(\n        view: WebView?,\n        request: WebResourceRequest?\n    ): WebResourceResponse? {\n        try {\n            if (request != null && request.run { isForMainFrame && url.host == \"gkd.li\" && method == \"GET\" }) {\n                LogUtils.d(request.method, request.url)\n                runBlocking(Dispatchers.IO) {\n                    val docConfig = client.get(DOC_CONFIG_URL).body<DocConfig>()\n                    val path = request.url.path.let { if (it.isNullOrEmpty()) \"/\" else it }\n                    val textUrl = docConfig.htmlUrlMap[path]?.let { docConfig.mirrorBaseUrl + it }\n                    if (textUrl != null) {\n                        val textContent = client.get(textUrl).body<String>().let {\n                            if (META.debuggable) {\n                                DEBUG_JS_TEXT + it\n                            } else {\n                                it\n                            }\n                        }\n                        return@runBlocking WebResourceResponse(\n                            \"text/html\",\n                            \"UTF-8\",\n                            textContent.byteInputStream()\n                        )\n                    }\n                    return@runBlocking null\n                }?.let { return it }\n            }\n        } catch (e: Throwable) {\n            e.printStackTrace()\n        }\n        return super.shouldInterceptRequest(view, request)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/AnimatedBooleanContent.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedContentTransitionScope\nimport androidx.compose.animation.ContentTransform\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport li.songe.gkd.util.getUpDownTransform\n\n@Composable\nfun AnimatedBooleanContent(\n    targetState: Boolean,\n    modifier: Modifier = Modifier,\n    transitionSpec: AnimatedContentTransitionScope<Boolean>.() -> ContentTransform = { getUpDownTransform() },\n    contentAlignment: Alignment = Alignment.TopStart,\n    contentTrue: @Composable () -> Unit,\n    contentFalse: @Composable () -> Unit,\n) = AnimatedContent(\n    targetState = targetState,\n    modifier = modifier,\n    transitionSpec = transitionSpec,\n    contentAlignment = contentAlignment,\n) {\n    if (it) {\n        contentTrue()\n    } else {\n        contentFalse()\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/AnimatedIcon.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.annotation.DrawableRes\nimport androidx.compose.animation.graphics.res.animatedVectorResource\nimport androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter\nimport androidx.compose.animation.graphics.vector.AnimatedImageVector\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport li.songe.gkd.R\n\n@Composable\nprivate fun AnimatedIcon(\n    modifier: Modifier,\n    @DrawableRes id: Int,\n    atEnd: Boolean,\n    tint: Color,\n    contentDescription: String?,\n) {\n    val animation = AnimatedImageVector.animatedVectorResource(id)\n    val painter = rememberAnimatedVectorPainter(\n        animation,\n        atEnd,\n    )\n    Icon(\n        modifier = modifier,\n        painter = painter,\n        contentDescription = contentDescription,\n        tint = tint,\n    )\n}\n\n@Composable\nfun AnimatedIconButton(\n    onClick: () -> Unit,\n    @DrawableRes id: Int,\n    modifier: Modifier = Modifier,\n    atEnd: Boolean = false,\n    tint: Color = LocalContentColor.current,\n    contentDescription: String? = getIconDesc(id, atEnd),\n) = TooltipIconButtonBox(\n    contentDescription = contentDescription,\n) {\n    IconButton(\n        onClick = onClick,\n    ) {\n        AnimatedIcon(\n            id = id,\n            atEnd = atEnd,\n            modifier = modifier,\n            tint = tint,\n            contentDescription = contentDescription,\n        )\n    }\n}\n\nprivate fun getIconDesc(@DrawableRes id: Int, atEnd: Boolean): String? = when (id) {\n    R.drawable.ic_anim_search_close -> if (atEnd) \"关闭搜索\" else \"打开搜索\"\n    else -> null\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/Animation.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationVector1D\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.VisibilityThreshold\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.lazy.LazyItemScope\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.IntOffset\n\n@Composable\nfun usePercentAnimatable(\n    visible: Boolean,\n): Animatable<Float, AnimationVector1D> {\n    val percent = remember { Animatable(if (visible) 1f else 0f) }\n    LaunchedEffect(visible) {\n        if (visible && percent.value != 1f) {\n            percent.animateTo(targetValue = 1f, animationSpec = tween())\n        } else if (!visible && percent.value != 0f) {\n            percent.animateTo(targetValue = 0f, animationSpec = tween())\n        }\n    }\n    return percent\n}\n\ncontext(scope: LazyItemScope, )\nfun Modifier.animateListItem(\n    enabled: Boolean = true,\n): Modifier {\n    if (!enabled) {\n        return this\n    }\n    return scope.run {\n        animateItem(\n            fadeInSpec = spring(stiffness = Spring.StiffnessMediumLow),\n            placementSpec = spring(\n                stiffness = Spring.StiffnessMediumLow,\n                visibilityThreshold = IntOffset.VisibilityThreshold\n            ),\n            fadeOutSpec = spring(stiffness = Spring.StiffnessMediumLow)\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/AnimationFloatingActionButton.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis\nimport androidx.compose.animation.core.tween\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.FloatingActionButtonDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.onClick\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.unit.dp\nimport li.songe.gkd.util.throttle\n\nprivate const val elevationDurationMillis = 50\n\n@Composable\nfun AnimationFloatingActionButton(\n    visible: Boolean,\n    onClick: () -> Unit,\n    imageVector: ImageVector,\n    modifier: Modifier = Modifier,\n    onClickLabel: String? = null,\n    contentDescription: String? = getIconDefaultDesc(imageVector),\n) {\n    val density = LocalDensity.current\n    val maxTranslationX = remember(density.density) { density.run { 24.dp.toPx() } }\n    var innerVisible by remember { mutableStateOf(visible) }\n    val percent = remember { Animatable(if (visible) 1f else 0f) }\n    // https://stackoverflow.com/questions/75717579\n    val defaultElevation = remember { Animatable(if (visible) 1f else 0f) }\n    LaunchedEffect(visible) {\n        if (visible != innerVisible) {\n            if (visible) {\n                innerVisible = true\n                percent.animateTo(\n                    targetValue = 1f,\n                    animationSpec = tween(durationMillis = DefaultDurationMillis - elevationDurationMillis)\n                )\n                defaultElevation.animateTo(\n                    targetValue = 1f,\n                    animationSpec = tween(durationMillis = elevationDurationMillis)\n                )\n            } else {\n                defaultElevation.animateTo(\n                    targetValue = 0f,\n                    animationSpec = tween(durationMillis = elevationDurationMillis)\n                )\n                percent.animateTo(\n                    targetValue = 0f,\n                    animationSpec = tween(durationMillis = DefaultDurationMillis - elevationDurationMillis)\n                )\n                innerVisible = false\n            }\n        }\n    }\n    if (innerVisible) {\n        TooltipIconButtonBox(contentDescription) {\n            FloatingActionButton(\n                modifier = modifier\n                    .graphicsLayer(\n                        alpha = percent.value,\n                        translationX = (1f - percent.value) * maxTranslationX\n                    )\n                    .semantics {\n                        if (contentDescription != null) {\n                            this.contentDescription = contentDescription\n                        }\n                        if (onClickLabel != null) {\n                            this.onClick(label = onClickLabel, action = null)\n                        }\n                    },\n                elevation = FloatingActionButtonDefaults.elevation(defaultElevation = (defaultElevation.value * 6f).dp),\n                onClick = throttle(onClick),\n                content = {\n                    PerfIcon(imageVector = imageVector, contentDescription = null)\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/AppBarTextField.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.text.BasicTextField\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TextFieldDefaults.indicatorLine\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.input.VisualTransformation\nimport androidx.compose.ui.unit.dp\n\n/**\n * https://stackoverflow.com/questions/73664765\n */\n@Composable\nfun AppBarTextField(\n    value: String,\n    onValueChange: (String) -> Unit,\n    hint: String,\n    modifier: Modifier = Modifier,\n    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,\n    keyboardActions: KeyboardActions = KeyboardActions.Default,\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n\n    // make sure there is no background color in the decoration box\n    val colors = TextFieldDefaults.colors(\n        focusedContainerColor = Color.Transparent,\n        unfocusedContainerColor = Color.Transparent,\n        disabledContainerColor = Color.Transparent,\n        focusedIndicatorColor = Color.Transparent,\n        unfocusedIndicatorColor = Color.Transparent,\n    )\n\n    val mergedTextStyle = LocalTextStyle.current.merge(MaterialTheme.typography.titleMedium)\n        .merge(color = LocalContentColor.current)\n\n    // set the correct cursor position when this composable is first initialized\n    var textFieldValue by remember {\n        mutableStateOf(TextFieldValue(value, TextRange(value.length)))\n    }\n    textFieldValue = textFieldValue.copy(text = value) // make sure to keep the value updated\n\n    BasicTextField(\n        value = textFieldValue,\n        onValueChange = {\n            textFieldValue = it\n            // remove newlines to avoid strange layout issues, and also because singleLine=true\n            onValueChange(it.text.replace(\"\\n\", \"\"))\n        },\n        modifier = modifier\n            .fillMaxWidth()\n            .heightIn(32.dp)\n            .indicatorLine(\n                enabled = true,\n                isError = false,\n                interactionSource = interactionSource,\n                colors = colors\n            ),\n//                .focusRequester(focusRequester),\n        textStyle = mergedTextStyle,\n        cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),\n        keyboardOptions = keyboardOptions,\n        keyboardActions = keyboardActions,\n        interactionSource = interactionSource,\n        singleLine = true,\n        decorationBox = { innerTextField ->\n            // places text field with placeholder and appropriate bottom padding\n            TextFieldDefaults.DecorationBox(\n                value = value,\n                innerTextField = innerTextField,\n                enabled = true,\n                singleLine = true,\n                visualTransformation = VisualTransformation.None,\n                interactionSource = interactionSource,\n                isError = false,\n                placeholder = { Text(text = hint) },\n                colors = colors,\n                contentPadding = PaddingValues(bottom = 4.dp),\n            )\n        },\n    )\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/AppCheckBoxCard.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.onClick\nimport androidx.compose.ui.semantics.stateDescription\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport li.songe.gkd.data.AppInfo\nimport li.songe.gkd.ui.style.appItemPadding\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun AppCheckBoxCard(\n    appInfo: AppInfo,\n    checked: Boolean,\n    onCheckedChange: (() -> Unit),\n) {\n    Row(\n        modifier = Modifier\n            .clickable(onClick = throttle(onCheckedChange))\n            .clearAndSetSemantics {\n                contentDescription = \"应用：${appInfo.name}\"\n                stateDescription = if (checked) \"已加入名单\" else \"未加入名单\"\n                onClick(\n                    label = if (checked) \"从名单中移除\" else \"加入名单\",\n                    action = null\n                )\n            }\n            .appItemPadding(),\n        horizontalArrangement = Arrangement.spacedBy(12.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        AppIcon(appId = appInfo.id)\n        Column(\n            modifier = Modifier\n                .weight(1f),\n            verticalArrangement = Arrangement.Center\n        ) {\n            AppNameText(appInfo = appInfo)\n            Text(\n                text = appInfo.id,\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                softWrap = false\n            )\n        }\n        PerfCheckbox(\n            key = appInfo.id,\n            checked = checked,\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/AppIcon.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.google.accompanist.drawablepainter.rememberDrawablePainter\nimport li.songe.gkd.util.appIconMapFlow\n\n@Composable\nfun AppIcon(\n    modifier: Modifier = Modifier,\n    appId: String,\n) {\n    val icon = appIconMapFlow.collectAsState().value[appId]\n    val iconModifier = modifier.size(32.dp)\n    if (icon != null) {\n        Image(\n            painter = rememberDrawablePainter(icon),\n            contentDescription = null,\n            modifier = iconModifier\n        )\n    } else {\n        PerfIcon(\n            imageVector = PerfIcon.Android,\n            modifier = iconModifier\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/AppNameText.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.foundation.text.appendInlineContent\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.text.Placeholder\nimport androidx.compose.ui.text.PlaceholderVerticalAlign\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.text.withStyle\nimport li.songe.gkd.data.AppInfo\nimport li.songe.gkd.data.otherUserMapFlow\nimport li.songe.gkd.shizuku.currentUserId\nimport li.songe.gkd.util.appInfoMapFlow\n\n@Composable\nfun AppNameText(\n    modifier: Modifier = Modifier,\n    appId: String? = null,\n    appInfo: AppInfo? = null,\n    fallbackName: String? = null,\n    style: TextStyle = LocalTextStyle.current,\n) {\n    val info = appInfo ?: appInfoMapFlow.collectAsState().value[appId]\n    val showSystemIcon = info?.isSystem == true\n    val appName = (info?.name ?: fallbackName ?: appId ?: error(\"appId is required\"))\n    val userName = info?.userId?.let { userId ->\n        if (userId == currentUserId) {\n            null\n        } else {\n            val userInfo = otherUserMapFlow.collectAsState().value[userId]\n            \"「${userInfo?.name ?: userId}」\"\n        }\n    }\n    val textDecoration = if (info?.enabled == false) TextDecoration.LineThrough else null\n    if (!showSystemIcon && userName == null) {\n        Text(\n            modifier = modifier,\n            text = appName,\n            maxLines = 1,\n            softWrap = false,\n            overflow = TextOverflow.Ellipsis,\n            textDecoration = textDecoration,\n            style = style,\n        )\n    } else {\n        val userNameColor = MaterialTheme.colorScheme.tertiary\n        val annotatedString = remember(showSystemIcon, appName, userName, userNameColor) {\n            buildAnnotatedString {\n                if (showSystemIcon) {\n                    appendInlineContent(\"icon\")\n                }\n                append(appName)\n                if (userName != null) {\n                    append(\" \")\n                    withStyle(\n                        style = SpanStyle(\n                            fontWeight = FontWeight.Bold,\n                            color = userNameColor,\n                        )\n                    ) {\n                        append(userName)\n                    }\n                }\n            }\n        }\n        val inlineContent = if (showSystemIcon) {\n            val contentColor = style.color.takeOrElse { LocalContentColor.current }\n            remember(style, contentColor) {\n                mapOf(\n                    \"icon\" to InlineTextContent(\n                        placeholder = Placeholder(\n                            width = style.fontSize,\n                            height = style.lineHeight,\n                            placeholderVerticalAlign = PlaceholderVerticalAlign.Center\n                        )\n                    ) {\n                        PerfIcon(\n                            imageVector = PerfIcon.VerifiedUser,\n                            modifier = Modifier\n                                .clip(MaterialTheme.shapes.extraSmall)\n                                .fillMaxSize(),\n                            tint = contentColor\n                        )\n                    }\n                )\n            }\n        } else {\n            emptyMap()\n        }\n        Text(\n            modifier = modifier,\n            text = annotatedString,\n            inlineContent = inlineContent,\n            maxLines = 1,\n            softWrap = false,\n            overflow = TextOverflow.Ellipsis,\n            textDecoration = textDecoration,\n            style = style,\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/AuthButtonGroup.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun AuthButtonGroup(\n    buttons: List<Pair<String, () -> Unit>>,\n    modifier: Modifier = Modifier,\n) {\n    FlowRow(\n        modifier = modifier,\n    ) {\n        buttons.forEach { (text, click) ->\n            TextButton(onClick = throttle(click)) {\n                Text(\n                    text = text,\n                    style = MaterialTheme.typography.bodyLarge,\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/AuthCard.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport li.songe.gkd.ui.style.itemPadding\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun AuthCard(\n    title: String,\n    subtitle: String? = null,\n    onAuthClick: () -> Unit,\n) {\n    Row(\n        modifier = Modifier.itemPadding(),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Column(modifier = Modifier.weight(1f)) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.bodyLarge,\n            )\n            if (subtitle!=null) {\n                Text(\n                    text = subtitle,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n        Spacer(modifier = Modifier.width(8.dp))\n        OutlinedButton(onClick = throttle(fn = onAuthClick)) {\n            Text(text = \"授权\")\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/CopyTextCard.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport li.songe.gkd.util.copyText\nimport li.songe.gkd.util.throttle\n\n\n@Composable\nfun CopyTextCard(\n    text: String,\n    modifier: Modifier = Modifier,\n) {\n    Box(\n        modifier = modifier.fillMaxWidth()\n    ) {\n        SelectionContainer(\n            modifier = Modifier\n                .align(Alignment.TopStart)\n                .fillMaxWidth()\n        ) {\n            Text(\n                text = text,\n                modifier = Modifier\n                    .clip(MaterialTheme.shapes.extraSmall)\n                    .background(MaterialTheme.colorScheme.secondaryContainer)\n                    .padding(8.dp),\n                color = MaterialTheme.colorScheme.secondary,\n                style = MaterialTheme.typography.bodyLarge,\n            )\n        }\n        PerfIcon(\n            modifier = Modifier\n                .align(Alignment.TopEnd)\n                .clickable(onClick = throttle {\n                    copyText(text)\n                })\n                .padding(4.dp)\n                .size(24.dp),\n            imageVector = PerfIcon.ContentCopy,\n            tint = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.75f),\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/CustomIconButton.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.annotation.DrawableRes\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.IconButtonColors\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.ripple\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@Composable\nprivate fun CustomIconButton(\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    onClickLabel: String? = null,\n    size: Dp = 40.dp,\n    enabled: Boolean = true,\n    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),\n    interactionSource: MutableInteractionSource? = null,\n    content: @Composable () -> Unit\n) {\n    Box(\n        modifier =\n            modifier\n                .size(size)\n                .clip(CircleShape)\n                .background(color = colors.run { if (enabled) containerColor else disabledContainerColor })\n                .clickable(\n                    onClick = onClick,\n                    onClickLabel = onClickLabel,\n                    enabled = enabled,\n                    role = Role.Button,\n                    interactionSource = interactionSource,\n                    indication = ripple(bounded = false, radius = size / 2)\n                ),\n        contentAlignment = Alignment.Center\n    ) {\n        val contentColor = colors.run { if (enabled) contentColor else disabledContentColor }\n        CompositionLocalProvider(LocalContentColor provides contentColor, content = content)\n    }\n}\n\n@Composable\nfun PerfCustomIconButton(\n    onClick: () -> Unit,\n    size: Dp,\n    iconSize: Dp,\n    onClickLabel: String? = null,\n    @DrawableRes id: Int,\n    contentDescription: String? = null,\n) = TooltipIconButtonBox(\n    contentDescription = contentDescription,\n) {\n    CustomIconButton(\n        size = size,\n        onClickLabel = onClickLabel,\n        onClick = onClick,\n    ) {\n        PerfIcon(\n            modifier = Modifier.size(iconSize),\n            id = id,\n            contentDescription = contentDescription,\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/CustomOutlinedTextField.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsFocusedAsState\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.defaultMinSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.text.BasicTextField\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.foundation.text.selection.LocalTextSelectionColors\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.OutlinedTextFieldDefaults\nimport androidx.compose.material3.TextFieldColors\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.input.VisualTransformation\nimport androidx.compose.ui.unit.sp\n\n// copy from androidx/compose/material3/OutlinedTextField.kt\n\n@Composable\nfun CustomOutlinedTextField(\n    value: String,\n    onValueChange: (String) -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    readOnly: Boolean = false,\n    textStyle: TextStyle = LocalTextStyle.current,\n    label: @Composable (() -> Unit)? = null,\n    placeholder: @Composable (() -> Unit)? = null,\n    leadingIcon: @Composable (() -> Unit)? = null,\n    trailingIcon: @Composable (() -> Unit)? = null,\n    prefix: @Composable (() -> Unit)? = null,\n    suffix: @Composable (() -> Unit)? = null,\n    supportingText: @Composable (() -> Unit)? = null,\n    isError: Boolean = false,\n    visualTransformation: VisualTransformation = VisualTransformation.None,\n    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,\n    keyboardActions: KeyboardActions = KeyboardActions.Default,\n    singleLine: Boolean = false,\n    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,\n    minLines: Int = 1,\n    interactionSource: MutableInteractionSource? = null,\n    shape: Shape = OutlinedTextFieldDefaults.shape,\n    colors: TextFieldColors = OutlinedTextFieldDefaults.colors(),\n    contentPadding: PaddingValues = OutlinedTextFieldDefaults.contentPadding()\n) {\n    @Suppress(\"NAME_SHADOWING\")\n    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }\n    // If color is not provided via the text style, use content color as a default\n    val textColor =\n        textStyle.color.takeOrElse {\n            val focused = interactionSource.collectIsFocusedAsState().value\n            colors.run {\n                when {\n                    !enabled -> disabledTextColor\n                    isError -> errorTextColor\n                    focused -> focusedTextColor\n                    else -> unfocusedTextColor\n                }\n            }\n        }\n    val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))\n\n    val density = LocalDensity.current\n\n    CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {\n        BasicTextField(\n            value = value,\n            modifier =\n                modifier\n                    .then(\n                        if (label != null) {\n                            Modifier\n                                // Merge semantics at the beginning of the modifier chain to ensure\n                                // padding is considered part of the text field.\n                                .semantics(mergeDescendants = true) {}\n                                .padding(top = with(density) { 8.sp.toDp() })\n                        } else {\n                            Modifier\n                        }\n                    )\n//                    .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))\n                    .defaultMinSize(\n                        minWidth = OutlinedTextFieldDefaults.MinWidth,\n                        minHeight = OutlinedTextFieldDefaults.MinHeight\n                    ),\n            onValueChange = onValueChange,\n            enabled = enabled,\n            readOnly = readOnly,\n            textStyle = mergedTextStyle,\n            cursorBrush = SolidColor(colors.run { if (isError) errorCursorColor else cursorColor }),\n            visualTransformation = visualTransformation,\n            keyboardOptions = keyboardOptions,\n            keyboardActions = keyboardActions,\n            interactionSource = interactionSource,\n            singleLine = singleLine,\n            maxLines = maxLines,\n            minLines = minLines,\n            decorationBox =\n                @Composable { innerTextField ->\n                    OutlinedTextFieldDefaults.DecorationBox(\n                        value = value,\n                        visualTransformation = visualTransformation,\n                        innerTextField = innerTextField,\n                        placeholder = placeholder,\n                        label = label,\n                        leadingIcon = leadingIcon,\n                        trailingIcon = trailingIcon,\n                        prefix = prefix,\n                        suffix = suffix,\n                        supportingText = supportingText,\n                        singleLine = singleLine,\n                        enabled = enabled,\n                        isError = isError,\n                        interactionSource = interactionSource,\n                        colors = colors,\n                        container = {\n                            OutlinedTextFieldDefaults.Container(\n                                enabled = enabled,\n                                isError = isError,\n                                interactionSource = interactionSource,\n                                colors = colors,\n                                shape = shape,\n                            )\n                        },\n                        contentPadding = contentPadding,\n                    )\n                }\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/DialogOptions.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.graphics.Color\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.util.stopCoroutine\nimport li.songe.gkd.util.throttle\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\n\ndata class AlertDialogOptions(\n    val title: @Composable (() -> Unit)? = null,\n    val text: @Composable (() -> Unit)? = null,\n    val onDismissRequest: (() -> Unit)? = null,\n    val confirmButton: @Composable () -> Unit,\n    val dismissButton: @Composable (() -> Unit)? = null,\n)\n\nprivate fun buildDialogOptions(\n    title: @Composable (() -> Unit),\n    text: @Composable (() -> Unit),\n    confirmText: String,\n    confirmAction: () -> Unit,\n    dismissText: String? = null,\n    dismissAction: (() -> Unit)? = null,\n    onDismissRequest: (() -> Unit)? = null,\n    error: Boolean = false,\n): AlertDialogOptions {\n    return AlertDialogOptions(\n        title = title,\n        text = text,\n        onDismissRequest = onDismissRequest,\n        confirmButton = {\n            TextButton(\n                onClick = throttle(fn = confirmAction),\n                colors = ButtonDefaults.textButtonColors(\n                    contentColor = if (error) MaterialTheme.colorScheme.error else Color.Unspecified\n                )\n            ) {\n                Text(text = confirmText)\n            }\n        },\n        dismissButton = if (dismissText != null && dismissAction != null) {\n            {\n                TextButton(\n                    onClick = throttle(fn = dismissAction),\n                ) {\n                    Text(text = dismissText)\n                }\n            }\n        } else {\n            null\n        },\n    )\n}\n\n@Composable\nfun BuildDialog(stateFlow: MutableStateFlow<AlertDialogOptions?>) {\n    val options by stateFlow.collectAsState()\n    options?.let {\n        AlertDialog(\n            text = it.text,\n            title = it.title,\n            onDismissRequest = it.onDismissRequest ?: { stateFlow.value = null },\n            confirmButton = it.confirmButton,\n            dismissButton = it.dismissButton,\n        )\n    }\n}\n\nfun MutableStateFlow<AlertDialogOptions?>.updateDialogOptions(\n    title: String,\n    text: String? = null,\n    textContent: (@Composable (() -> Unit))? = null,\n    confirmText: String = DEFAULT_IK_TEXT,\n    confirmAction: (() -> Unit)? = null,\n    dismissText: String? = null,\n    dismissAction: (() -> Unit)? = null,\n    onDismissRequest: (() -> Unit)? = null,\n    error: Boolean = false,\n) {\n    value = buildDialogOptions(\n        title = { Text(text = title) },\n        text = textContent ?: { Text(text = text ?: error(\"miss text\")) },\n        confirmText = confirmText,\n        confirmAction = confirmAction ?: { value = null },\n        dismissText = dismissText,\n        dismissAction = dismissAction ?: { value = null },\n        onDismissRequest = onDismissRequest,\n        error = error,\n    )\n}\n\nprivate const val DEFAULT_IK_TEXT = \"我知道了\"\nprivate const val DEFAULT_CONFIRM_TEXT = \"确定\"\nprivate const val DEFAULT_DISMISS_TEXT = \"取消\"\n\nsuspend fun MutableStateFlow<AlertDialogOptions?>.getResult(\n    title: String,\n    text: String? = null,\n    textContent: (@Composable (() -> Unit))? = null,\n    dismissRequest: Boolean = false,\n    confirmText: String = DEFAULT_CONFIRM_TEXT,\n    dismissText: String = DEFAULT_DISMISS_TEXT,\n    error: Boolean = false,\n): Boolean {\n    return suspendCoroutine { s ->\n        val dismiss = {\n            s.resume(false)\n            this.value = null\n        }\n        updateDialogOptions(\n            title = title,\n            text = text,\n            textContent = textContent,\n            onDismissRequest = if (dismissRequest) dismiss else ({}),\n            confirmText = confirmText,\n            confirmAction = {\n                s.resume(true)\n                this.value = null\n            },\n            dismissText = dismissText,\n            dismissAction = dismiss,\n            error = error,\n        )\n    }\n}\n\nsuspend fun MutableStateFlow<AlertDialogOptions?>.waitResult(\n    title: String,\n    text: String? = null,\n    textContent: (@Composable (() -> Unit))? = null,\n    dismissRequest: Boolean = false,\n    confirmText: String = DEFAULT_CONFIRM_TEXT,\n    dismissText: String = DEFAULT_DISMISS_TEXT,\n    error: Boolean = false,\n) {\n    val r = getResult(\n        title = title,\n        text = text,\n        textContent = textContent,\n        dismissRequest = dismissRequest,\n        confirmText = confirmText,\n        dismissText = dismissText,\n        error = error,\n    )\n    if (!r) {\n        stopCoroutine()\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/EmptyText.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextAlign\n\n@Composable\nfun EmptyText(text: String = \"暂无数据\") {\n    Text(\n        text = text,\n        modifier = Modifier.fillMaxWidth(),\n        style = MaterialTheme.typography.bodyMedium,\n        textAlign = TextAlign.Center,\n        color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),\n    )\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/FixedTimeText.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.rememberTextMeasurer\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.Dp\n\nval LocalNumberCharWidth = compositionLocalOf<Dp> { error(\"not found DestinationsNavigator\") }\n\n@Composable\nfun measureNumberTextWidth(style: TextStyle = LocalTextStyle.current): Dp {\n    val textMeasurer = rememberTextMeasurer()\n    val widthInPixels = \"1234567890\".map { c ->\n        textMeasurer.measure(c.toString(), style).size.width\n    }.average().toFloat()\n    return with(LocalDensity.current) { widthInPixels.toDp() }\n}\n\n@Composable\nfun FixedTimeText(\n    text: String,\n    modifier: Modifier = Modifier,\n    color: Color = Color.Unspecified,\n    style: TextStyle = LocalTextStyle.current,\n    charWidth: Dp = LocalNumberCharWidth.current,\n) {\n    Row(modifier = modifier) {\n        text.forEach { c ->\n            Text(\n                text = c.toString(),\n                style = style,\n                modifier = if (c.isDigit()) {\n                    Modifier.width(charWidth)\n                } else {\n                    Modifier\n                },\n                color = color,\n                softWrap = false,\n                maxLines = 1,\n                textAlign = TextAlign.Center,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/FullscreenDialog.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport android.view.View\nimport android.view.WindowManager\nimport android.widget.FrameLayout\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.platform.LocalView\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.compose.ui.window.DialogWindowProvider\nimport androidx.core.view.WindowInsetsControllerCompat\nimport li.songe.gkd.ui.share.LocalDarkTheme\n\n@Composable\nfun FullscreenDialog(\n    onDismissRequest: () -> Unit,\n    content: @Composable () -> Unit,\n) = Dialog(\n    onDismissRequest = onDismissRequest,\n    properties = DialogProperties(\n        dismissOnClickOutside = false,\n        usePlatformDefaultWidth = false,\n        decorFitsSystemWindows = false,\n        windowTitle = \"全局弹窗\",\n    )\n) {\n    val activity = LocalActivity.current!!\n    val parentView = LocalView.current.parent as View\n    val dialogWindow = (parentView as DialogWindowProvider).window\n    SideEffect {\n        dialogWindow.setDimAmount(0f)\n        dialogWindow.attributes = WindowManager.LayoutParams().apply {\n            copyFrom(activity.window.attributes)\n            type = dialogWindow.attributes.type\n            windowAnimations = android.R.style.Animation_Dialog\n        }\n        parentView.layoutParams = FrameLayout.LayoutParams(\n            activity.window.decorView.width,\n            activity.window.decorView.height\n        )\n        parentView.setBackgroundColor(android.graphics.Color.TRANSPARENT)\n    }\n    val darkTheme = LocalDarkTheme.current\n    val controller = remember(dialogWindow) {\n        WindowInsetsControllerCompat(\n            dialogWindow,\n            dialogWindow.decorView\n        )\n    }\n    LaunchedEffect(darkTheme) {\n        controller.isAppearanceLightStatusBars = !darkTheme\n        controller.isAppearanceLightNavigationBars = !darkTheme\n    }\n    content()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/GroupNameText.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.foundation.text.appendInlineContent\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.text.Placeholder\nimport androidx.compose.ui.text.PlaceholderVerticalAlign\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.style.TextOverflow\nimport li.songe.gkd.ui.icon.SportsBasketball\n\n@Composable\nfun GroupNameText(\n    modifier: Modifier = Modifier,\n    preText: String? = null,\n    isGlobal: Boolean,\n    text: String,\n    color: Color = Color.Unspecified,\n    style: TextStyle = LocalTextStyle.current,\n    overflow: TextOverflow = TextOverflow.Clip,\n    softWrap: Boolean = true,\n    maxLines: Int = Int.MAX_VALUE,\n) {\n    if (isGlobal) {\n        val text = remember(preText, text) {\n            buildAnnotatedString {\n                if (preText != null) {\n                    append(preText)\n                }\n                appendInlineContent(\"icon\")\n                append(text)\n            }\n        }\n        val textColor = color.takeOrElse { style.color.takeOrElse { LocalContentColor.current } }\n        val inlineContent = remember(style, textColor) {\n            mapOf(\n                \"icon\" to InlineTextContent(\n                    placeholder = Placeholder(\n                        width = style.fontSize,\n                        height = style.lineHeight,\n                        placeholderVerticalAlign = PlaceholderVerticalAlign.Center\n                    )\n                ) {\n                    PerfIcon(\n                        imageVector = SportsBasketball,\n                        modifier = Modifier.fillMaxSize(),\n                        tint = textColor\n                    )\n                }\n            )\n        }\n        Text(\n            modifier = modifier,\n            text = text,\n            inlineContent = inlineContent,\n            style = style,\n            color = color,\n            overflow = overflow,\n            softWrap = softWrap,\n            maxLines = maxLines,\n        )\n    } else {\n        Text(\n            text = if (preText.isNullOrEmpty()) {\n                text\n            } else {\n                preText + text\n            },\n            style = style,\n            color = color,\n            overflow = overflow,\n            softWrap = softWrap,\n            maxLines = maxLines,\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/Hooks.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis\nimport androidx.compose.foundation.ScrollState\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.ReadOnlyComposable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.Density\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.StateFlow\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.util.mapState\nimport li.songe.gkd.util.subsMapFlow\n\n@Composable\nfun useSubs(subsId: Long?): RawSubscription? {\n    val scope = rememberCoroutineScope()\n    return remember(subsId) { subsMapFlow.mapState(scope) { it[subsId] } }.collectAsState().value\n}\n\n@Composable\nfun useSubsGroup(\n    subs: RawSubscription?,\n    groupKey: Int?,\n    appId: String?,\n): RawSubscription.RawGroupProps? {\n    return remember(subs, groupKey, appId) {\n        if (subs != null && groupKey != null) {\n            if (appId != null) {\n                subs.apps.find { it.id == appId }?.groups?.find { it.key == groupKey }\n            } else {\n                subs.globalGroups.find { it.key == groupKey }\n            }\n        } else {\n            null\n        }\n    }\n}\n\n@Composable\nfun Modifier.autoFocus(immediateFocus: Boolean = false): Modifier {\n    val focusRequester = remember { FocusRequester() }\n    LaunchedEffect(null) {\n        if (!immediateFocus) {\n            delay(DefaultDurationMillis.toLong())\n        }\n        focusRequester.requestFocus()\n    }\n    return focusRequester(focusRequester)\n}\n\n@Composable\nprivate fun getCompatStateValue(v: Any?): Any? = when (v) {\n    is StateFlow<*> -> v.collectAsState().value\n    is androidx.compose.runtime.State<*> -> v.value\n    else -> v\n}\n\n// key 函数的依赖变化时, compose 将重置 key 函数那行代码之后所有代码的状态, 因此需要需要将 key 作用域限定在 Composable fun 内\n// 所有的 key 参数必须使用 rememberSaveable 或者 viewModel 来保存状态, 以保证正确的 restore 顺序，否则触发 ClassCastException\n@Composable\nfun useListScrollState(\n    v1: Any?,\n    v2: Any? = null,\n    v3: Any? = null,\n    canScroll: () -> Boolean = { true },\n): Pair<TopAppBarScrollBehavior, LazyListState> {\n    return key(\n        getCompatStateValue(v1),\n        getCompatStateValue(v2),\n        getCompatStateValue(v3),\n    ) {\n        TopAppBarDefaults.enterAlwaysScrollBehavior(canScroll = canScroll) to rememberLazyListState()\n    }\n}\n\n@Composable\nfun usePinnedScrollBehaviorState(v1: Any?): Pair<TopAppBarScrollBehavior, LazyListState> {\n    return key(getCompatStateValue(v1)) { TopAppBarDefaults.pinnedScrollBehavior() to rememberLazyListState() }\n}\n\n@Composable\nfun useScrollBehaviorState(v1: Any?): Pair<TopAppBarScrollBehavior, ScrollState> {\n    return key(getCompatStateValue(v1)) { TopAppBarDefaults.enterAlwaysScrollBehavior() to rememberScrollState() }\n}\n\n@Composable\nfun LazyListState.isAtBottom(): androidx.compose.runtime.State<Boolean> = remember(this) {\n    derivedStateOf {\n        val visibleItemsInfo = layoutInfo.visibleItemsInfo\n        if (layoutInfo.totalItemsCount == 0) {\n            false\n        } else {\n            val lastVisibleItem = visibleItemsInfo.last()\n            val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset\n            (lastVisibleItem.index + 1 == layoutInfo.totalItemsCount &&\n                    lastVisibleItem.offset + lastVisibleItem.size <= viewportHeight)\n        }\n    }\n}\n\n\nval TopAppBarScrollBehavior.isFullVisible: Boolean\n    @Composable\n    @ReadOnlyComposable\n    get() = state.collapsedFraction == 0f\n\n@Composable\n@ReadOnlyComposable\nfun Modifier.textSize(\n    style: TextStyle = LocalTextStyle.current,\n    density: Density = LocalDensity.current,\n): Modifier {\n    val fontSizeDp = density.run { style.fontSize.toDp() }\n    val lineHeightDp = density.run { style.lineHeight.toDp() }\n    return height(lineHeightDp).width(fontSizeDp)\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/InnerDisableSwitch.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.material3.minimumInteractiveComponentSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.stateDescription\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun InnerDisableSwitch(\n    modifier: Modifier = Modifier,\n    valid: Boolean = true,\n    isSelectedMode: Boolean = false,\n) {\n    val mainVm = LocalMainViewModel.current\n    val onClick = {\n        if (valid) {\n            mainVm.dialogFlow.updateDialogOptions(\n                title = \"内置禁用\",\n                text = \"此规则组已经在内部配置对当前应用的禁用，就算强制开启规则组也是无意义或不生效的\\n\\n提示: 这种情况一般在此全局规则无法适配/跳过适配/单独适配当前应用时出现\",\n            )\n        } else {\n            mainVm.dialogFlow.updateDialogOptions(\n                title = \"非法规则\",\n                text = \"规则存在错误, 无法启用\",\n            )\n        }\n    }\n    PerfSwitch(\n        checked = false,\n        enabled = false,\n        onCheckedChange = null,\n        modifier = modifier.semantics {\n            stateDescription = \"已禁用\"\n        }\n            .minimumInteractiveComponentSize().run {\n                if (isSelectedMode) {\n                    this\n                } else {\n                    clickable(\n                        interactionSource = remember { MutableInteractionSource() },\n                        indication = null,\n                        role = Role.Switch,\n                        onClick = throttle(onClick),\n                        onClickLabel = \"打开规则禁用说明\",\n                    )\n                }\n            }\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/InputSubsLinkOption.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport android.webkit.URLUtil\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.window.DialogProperties\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.ui.WebViewRoute\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.util.ShortUrlSet\nimport li.songe.gkd.util.subsItemsFlow\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\n\n\nclass InputSubsLinkOption {\n    private val showFlow = MutableStateFlow(false)\n    private val valueFlow = MutableStateFlow(\"\")\n    private val initValueFlow = MutableStateFlow(\"\")\n    private var continuation: Continuation<String?>? = null\n\n    private fun resume(value: String?) {\n        showFlow.value = false\n        valueFlow.value = \"\"\n        initValueFlow.value = \"\"\n        continuation?.resume(value)\n        continuation = null\n    }\n\n    private fun submit() {\n        val value = valueFlow.value\n        if (!URLUtil.isNetworkUrl(value)) {\n            toast(\"非法链接\")\n            return\n        }\n        val initValue = initValueFlow.value\n        if (initValue.isNotEmpty() && initValue == value) {\n            toast(\"未修改\")\n            resume(null)\n            return\n        }\n        if (subsItemsFlow.value.any { it.updateUrl == value }) {\n            toast(\"已有相同链接订阅\")\n            return\n        }\n        resume(value)\n    }\n\n    private fun cancel() = resume(null)\n\n    suspend fun getResult(initValue: String = \"\"): String? {\n        initValueFlow.value = initValue\n        valueFlow.value = initValue\n        showFlow.value = true\n        return suspendCoroutine {\n            continuation = it\n        }\n    }\n\n    @Composable\n    fun ContentDialog() {\n        val show by showFlow.collectAsState()\n        if (show) {\n            val mainVm = LocalMainViewModel.current\n            val value by valueFlow.collectAsState()\n            val initValue by initValueFlow.collectAsState()\n            AlertDialog(\n                properties = DialogProperties(dismissOnClickOutside = false),\n                title = {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        modifier = Modifier.fillMaxWidth(),\n                    ) {\n                        Text(text = if (initValue.isNotEmpty()) \"修改订阅\" else \"添加订阅\")\n                        PerfIconButton(\n                            imageVector = PerfIcon.HelpOutline,\n                            contentDescription = \"订阅帮助\",\n                            onClick = throttle {\n                                cancel()\n                                mainVm.navigatePage(WebViewRoute(initUrl = ShortUrlSet.URL5))\n                            })\n                    }\n                },\n                text = {\n                    OutlinedTextField(\n                        value = value,\n                        onValueChange = {\n                            valueFlow.value = it.trim()\n                        },\n                        maxLines = 8,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .autoFocus(),\n                        placeholder = {\n                            Text(text = \"请输入订阅链接\")\n                        },\n                        isError = value.isNotEmpty() && !URLUtil.isNetworkUrl(value),\n                    )\n                },\n                onDismissRequest = {\n                    cancel()\n                },\n                confirmButton = {\n                    TextButton(\n                        enabled = value.isNotEmpty(),\n                        onClick = throttle(fn = {\n                            submit()\n                        }),\n                    ) {\n                        Text(text = \"确定\")\n                    }\n                },\n                dismissButton = {\n                    TextButton(onClick = ::cancel) {\n                        Text(text = \"取消\")\n                    }\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/ManualAuthDialog.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport li.songe.gkd.ui.WebViewRoute\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.util.ShortUrlSet\nimport li.songe.gkd.util.copyText\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun ManualAuthDialog(\n    commandText: String,\n    show: Boolean,\n    onUpdateShow: (Boolean) -> Unit,\n) {\n    if (show) {\n        val mainVm = LocalMainViewModel.current\n        AlertDialog(\n            onDismissRequest = { onUpdateShow(false) },\n            title = { Text(text = \"命令授权\") },\n            text = {\n                Column(modifier = Modifier.fillMaxWidth()) {\n                    Text(text = \"1. 有一台安装了 adb 的电脑\\n\\n2.手机开启调试模式后连接电脑授权调试\\n\\n3. 在电脑 cmd/pwsh 中运行如下命令\")\n                    Spacer(modifier = Modifier.height(4.dp))\n                    Box(\n                        modifier = Modifier.fillMaxWidth()\n                    ) {\n                        SelectionContainer(\n                            modifier = Modifier\n                                .align(Alignment.TopStart)\n                                .fillMaxWidth()\n                        ) {\n                            Text(\n                                text = commandText,\n                                modifier = Modifier\n                                    .clip(MaterialTheme.shapes.extraSmall)\n                                    .background(MaterialTheme.colorScheme.secondaryContainer)\n                                    .padding(8.dp),\n                                style = MaterialTheme.typography.bodySmall,\n                            )\n                        }\n                        PerfIcon(\n                            modifier = Modifier\n                                .align(Alignment.TopEnd)\n                                .clickable(onClick = throttle {\n                                    copyText(commandText)\n                                })\n                                .padding(4.dp)\n                                .size(20.dp),\n                            imageVector = PerfIcon.ContentCopy,\n                            tint = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.75f),\n                        )\n                    }\n                    Spacer(modifier = Modifier.height(8.dp))\n                    Text(\n                        modifier = Modifier\n                            .clickable(onClick = throttle {\n                                onUpdateShow(false)\n                                mainVm.navigatePage(WebViewRoute(initUrl = ShortUrlSet.URL3))\n                            }),\n                        text = \"运行后授权失败?\",\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.primary,\n                    )\n                }\n            },\n            confirmButton = {\n                TextButton(onClick = {\n                    onUpdateShow(false)\n                }) {\n                    Text(text = \"关闭\")\n                }\n            },\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/MenuExt.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.MenuDefaults\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport li.songe.gkd.util.throttle\n\n@Composable\ninline fun MenuGroupCard(inTop: Boolean = false, title: String, content: @Composable () -> Unit) {\n    Text(\n        text = title,\n        modifier = Modifier\n            .padding(MenuDefaults.DropdownMenuItemContentPadding)\n            .padding(top = if (inTop) 0.dp else 8.dp, bottom = 4.dp),\n        style = MaterialTheme.typography.labelMedium,\n        color = MaterialTheme.colorScheme.primary,\n    )\n    content()\n}\n\n@Composable\nfun MenuItemCheckbox(\n    text: String,\n    checked: Boolean,\n    onClick: () -> Unit,\n    enabled: Boolean = true,\n) {\n    val actualOnClick = throttle(onClick)\n    DropdownMenuItem(\n        text = { Text(text = text) },\n        trailingIcon = {\n            Checkbox(\n                checked = checked,\n                onCheckedChange = { actualOnClick() },\n                enabled = enabled,\n            )\n        },\n        onClick = actualOnClick,\n        enabled = enabled,\n    )\n}\n\n@Composable\nfun MenuItemCheckbox(\n    text: String,\n    stateFlow: MutableStateFlow<Boolean>,\n    enabled: Boolean = true,\n) = MenuItemCheckbox(\n    text = text,\n    checked = stateFlow.collectAsState().value,\n    onClick = { stateFlow.update { !it } },\n    enabled = enabled,\n)\n\n@Composable\nfun MenuItemRadioButton(\n    text: String,\n    selected: Boolean,\n    onClick: () -> Unit,\n    enabled: Boolean = true,\n) {\n    val actualOnClick = throttle(onClick)\n    DropdownMenuItem(\n        text = {\n            Text(text = text)\n        },\n        trailingIcon = {\n            RadioButton(\n                selected = selected,\n                onClick = actualOnClick,\n                enabled = enabled,\n            )\n        },\n        onClick = actualOnClick,\n        enabled = enabled,\n    )\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/ModifierExt.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.ui.Modifier\n\ninline fun Modifier.runIf(\n    enabled: Boolean,\n    block: Modifier.() -> Modifier\n) = run {\n    if (enabled) {\n        block()\n    } else {\n        this\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/MultiTextField.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.MainActivity\n\n@Composable\nfun MultiTextField(\n    modifier: Modifier = Modifier,\n    textFlow: MutableStateFlow<String>,\n    immediateFocus: Boolean = false,\n    indicatorSize: Int? = null,\n    placeholderText: String? = null,\n) {\n    val text by textFlow.collectAsState()\n    Box(modifier = modifier) {\n        val textColors = TextFieldDefaults.colors(\n            focusedIndicatorColor = Color.Transparent,\n            unfocusedIndicatorColor = Color.Transparent,\n            errorIndicatorColor = Color.Transparent,\n            disabledIndicatorColor = Color.Transparent,\n        )\n        CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {\n            val modifier = Modifier\n                .autoFocus(immediateFocus = immediateFocus)\n                .fillMaxSize()\n                .optimizedImePadding()\n            TextField(\n                value = text,\n                onValueChange = { textFlow.value = it },\n                placeholder = if (placeholderText != null) ({ Text(text = placeholderText) }) else null,\n                modifier = modifier,\n                shape = RectangleShape,\n                colors = textColors,\n            )\n        }\n        val actualSize = indicatorSize ?: text.length\n        if (actualSize > 0 && text.isNotEmpty()) {\n            Text(\n                text = actualSize.toString(),\n                modifier = Modifier\n                    .padding(8.dp)\n                    .align(Alignment.TopEnd)\n                    .clip(MaterialTheme.shapes.extraSmall)\n                    .background(MaterialTheme.colorScheme.surfaceContainer)\n                    .padding(horizontal = 2.dp),\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.tertiary,\n            )\n        }\n    }\n}\n\n\nprivate fun Modifier.optimizedImePadding() = composed {\n    val context = LocalActivity.current as MainActivity\n    if (context.imePlayingFlow.collectAsState().value) {\n        this\n    } else {\n        imePadding()\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/PerfCheckbox.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.material3.CheckboxColors\nimport androidx.compose.material3.CheckboxDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\nfun PerfCheckbox(\n    checked: Boolean,\n    modifier: Modifier = Modifier,\n    onCheckedChange: ((Boolean) -> Unit)? = null,\n    key: Any? = null,\n    enabled: Boolean = true,\n    colors: CheckboxColors = CheckboxDefaults.colors(),\n    interactionSource: MutableInteractionSource? = null\n) = androidx.compose.runtime.key(key) {\n    androidx.compose.material3.Checkbox(\n        checked = checked,\n        onCheckedChange = onCheckedChange,\n        modifier = modifier,\n        enabled = enabled,\n        colors = colors,\n        interactionSource = interactionSource\n    )\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/PerfIcon.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.annotation.DrawableRes\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.automirrored.filled.ArrowForward\nimport androidx.compose.material.icons.automirrored.filled.FormatListBulleted\nimport androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight\nimport androidx.compose.material.icons.automirrored.filled.Sort\nimport androidx.compose.material.icons.automirrored.outlined.HelpOutline\nimport androidx.compose.material.icons.automirrored.outlined.OpenInNew\nimport androidx.compose.material.icons.filled.Android\nimport androidx.compose.material.icons.filled.Apps\nimport androidx.compose.material.icons.filled.Autorenew\nimport androidx.compose.material.icons.filled.Block\nimport androidx.compose.material.icons.filled.CenterFocusWeak\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.History\nimport androidx.compose.material.icons.filled.Memory\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material.icons.filled.Share\nimport androidx.compose.material.icons.filled.UnfoldMore\nimport androidx.compose.material.icons.filled.WarningAmber\nimport androidx.compose.material.icons.outlined.Add\nimport androidx.compose.material.icons.outlined.Api\nimport androidx.compose.material.icons.outlined.ArrowDownward\nimport androidx.compose.material.icons.outlined.AutoMode\nimport androidx.compose.material.icons.outlined.Check\nimport androidx.compose.material.icons.outlined.ContentCopy\nimport androidx.compose.material.icons.outlined.DarkMode\nimport androidx.compose.material.icons.outlined.Delete\nimport androidx.compose.material.icons.outlined.Eco\nimport androidx.compose.material.icons.outlined.Edit\nimport androidx.compose.material.icons.outlined.Equalizer\nimport androidx.compose.material.icons.outlined.Home\nimport androidx.compose.material.icons.outlined.Image\nimport androidx.compose.material.icons.outlined.Info\nimport androidx.compose.material.icons.outlined.Layers\nimport androidx.compose.material.icons.outlined.LightMode\nimport androidx.compose.material.icons.outlined.Lock\nimport androidx.compose.material.icons.outlined.Notifications\nimport androidx.compose.material.icons.outlined.RocketLaunch\nimport androidx.compose.material.icons.outlined.Save\nimport androidx.compose.material.icons.outlined.Settings\nimport androidx.compose.material.icons.outlined.TextFields\nimport androidx.compose.material.icons.outlined.Title\nimport androidx.compose.material.icons.outlined.ToggleOff\nimport androidx.compose.material.icons.outlined.ToggleOn\nimport androidx.compose.material.icons.outlined.VerifiedUser\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonColors\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.semantics.onClick\nimport androidx.compose.ui.semantics.semantics\n\n@Composable\nfun PerfIcon(\n    imageVector: ImageVector,\n    modifier: Modifier = Modifier,\n    tint: Color = LocalContentColor.current,\n    contentDescription: String? = getIconDefaultDesc(imageVector),\n) = Icon(\n    imageVector = imageVector,\n    modifier = modifier,\n    contentDescription = contentDescription,\n    tint = tint\n)\n\n@Composable\nfun PerfIconButton(\n    imageVector: ImageVector,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),\n    contentDescription: String? = getIconDefaultDesc(imageVector),\n    onClickLabel: String? = null,\n) = TooltipIconButtonBox(\n    contentDescription = contentDescription,\n) {\n    IconButton(\n        modifier = modifier.semantics {\n            if (onClickLabel != null) {\n                this.onClick(label = onClickLabel, action = null)\n            }\n        },\n        enabled = enabled,\n        onClick = onClick,\n        colors = colors,\n    ) {\n        PerfIcon(\n            imageVector = imageVector,\n            contentDescription = contentDescription,\n        )\n    }\n}\n\n@Composable\nfun PerfIcon(\n    @DrawableRes id: Int,\n    modifier: Modifier = Modifier,\n    tint: Color = LocalContentColor.current,\n    contentDescription: String? = null,\n) = Icon(\n    painter = painterResource(id),\n    modifier = modifier,\n    contentDescription = contentDescription,\n    tint = tint\n)\n\n@Composable\nfun PerfIconButton(\n    @DrawableRes id: Int,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),\n    contentDescription: String? = null,\n    onClickLabel: String? = null,\n) = TooltipIconButtonBox(\n    contentDescription = contentDescription,\n) {\n    IconButton(\n        modifier = modifier.semantics {\n            if (onClickLabel != null) {\n                this.onClick(label = onClickLabel, action = null)\n            }\n        },\n        enabled = enabled,\n        onClick = onClick,\n        colors = colors,\n    ) {\n        PerfIcon(\n            id = id,\n            contentDescription = contentDescription,\n        )\n    }\n}\n\nfun getIconDefaultDesc(imageVector: ImageVector): String? = when (imageVector) {\n    PerfIcon.Add -> \"添加\"\n    PerfIcon.Edit -> \"编辑\"\n    PerfIcon.Save -> \"保存\"\n    PerfIcon.Delete -> \"删除\"\n    PerfIcon.Share -> \"分享\"\n    PerfIcon.Settings -> \"设置\"\n    PerfIcon.Close -> \"关闭\"\n    PerfIcon.ArrowBack -> \"返回\"\n    PerfIcon.HelpOutline -> \"帮助\"\n    PerfIcon.ToggleOff -> \"关闭\"\n    PerfIcon.ToggleOn -> \"开启\"\n    PerfIcon.History -> \"历史记录\"\n    PerfIcon.Sort -> \"排序筛选\"\n    PerfIcon.OpenInNew -> \"新页面打开\"\n    PerfIcon.ContentCopy -> \"复制文本\"\n    PerfIcon.MoreVert -> \"更多操作\"\n    else -> null\n}\n\nobject PerfIcon {\n    val Block get() = Icons.Default.Block\n    val History get() = Icons.Default.History\n    val Sort get() = Icons.AutoMirrored.Filled.Sort\n    val Add get() = Icons.Outlined.Add\n    val KeyboardArrowRight get() = Icons.AutoMirrored.Filled.KeyboardArrowRight\n    val ContentCopy get() = Icons.Outlined.ContentCopy\n    val MoreVert get() = Icons.Default.MoreVert\n    val ArrowBack get() = Icons.AutoMirrored.Filled.ArrowBack\n    val Android get() = Icons.Default.Android\n    val Edit get() = Icons.Outlined.Edit\n    val Save get() = Icons.Outlined.Save\n    val Share get() = Icons.Default.Share\n    val Delete get() = Icons.Outlined.Delete\n    val Eco get() = Icons.Outlined.Eco\n    val Close get() = Icons.Default.Close\n    val OpenInNew get() = Icons.AutoMirrored.Outlined.OpenInNew\n    val Settings get() = Icons.Outlined.Settings\n    val Home get() = Icons.Outlined.Home\n    val FormatListBulleted get() = Icons.AutoMirrored.Filled.FormatListBulleted\n    val Apps get() = Icons.Default.Apps\n    val Info get() = Icons.Outlined.Info\n    val ToggleOff get() = Icons.Outlined.ToggleOff\n    val ToggleOn get() = Icons.Outlined.ToggleOn\n    val HelpOutline get() = Icons.AutoMirrored.Outlined.HelpOutline\n    val ArrowForward get() = Icons.AutoMirrored.Filled.ArrowForward\n    val Image get() = Icons.Outlined.Image\n    val WarningAmber get() = Icons.Default.WarningAmber\n    val RocketLaunch get() = Icons.Outlined.RocketLaunch\n    val CenterFocusWeak get() = Icons.Default.CenterFocusWeak\n    val AutoMode get() = Icons.Outlined.AutoMode\n    val LightMode get() = Icons.Outlined.LightMode\n    val DarkMode get() = Icons.Outlined.DarkMode\n    val VerifiedUser get() = Icons.Outlined.VerifiedUser\n    val Api get() = Icons.Outlined.Api\n    val Autorenew get() = Icons.Default.Autorenew\n    val UnfoldMore get() = Icons.Default.UnfoldMore\n    val Memory get() = Icons.Default.Memory\n    val Notifications get() = Icons.Outlined.Notifications\n    val Layers get() = Icons.Outlined.Layers\n    val Equalizer get() = Icons.Outlined.Equalizer\n    val Lock get() = Icons.Outlined.Lock\n    val Title get() = Icons.Outlined.Title\n    val TextFields get() = Icons.Outlined.TextFields\n    val ArrowDownward get() = Icons.Outlined.ArrowDownward\n    val Check get() = Icons.Outlined.Check\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/PerfSwitch.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchColors\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.stateDescription\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun PerfSwitch(\n    checked: Boolean,\n    onCheckedChange: ((Boolean) -> Unit)?,\n    modifier: Modifier = Modifier,\n    key: Any? = null,\n    thumbContent: (@Composable () -> Unit)? = null,\n    enabled: Boolean = true,\n    colors: SwitchColors = SwitchDefaults.colors(),\n    interactionSource: MutableInteractionSource? = null,\n) = androidx.compose.runtime.key(key) {\n    Switch(\n        checked = checked,\n        onCheckedChange = onCheckedChange?.let { throttle(it) },\n        modifier = modifier.semantics {\n            stateDescription = if (checked) \"已开启\" else \"已关闭\"\n        },\n        thumbContent = thumbContent,\n        enabled = enabled,\n        colors = colors,\n        interactionSource = interactionSource,\n    )\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/PerfTopAppBar.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarColors\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.Dp\nimport li.songe.gkd.MainActivity\n\n@Composable\nfun PerfTopAppBar(\n    title: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n    navigationIcon: @Composable () -> Unit = {},\n    actions: @Composable RowScope.() -> Unit = {},\n    expandedHeight: Dp = TopAppBarDefaults.TopAppBarExpandedHeight,\n    colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),\n    scrollBehavior: TopAppBarScrollBehavior? = null,\n    canScroll: Boolean = true,\n) {\n    val actualScrollBehavior = if (canScroll || scrollBehavior == null) {\n        scrollBehavior\n    } else {\n        remember(scrollBehavior) {\n            object : TopAppBarScrollBehavior by scrollBehavior {\n                // disable inner scroll effect\n                override val isPinned: Boolean\n                    get() = true\n            }\n        }\n    }\n    // SingleRowTopAppBar 内部 containerColor+scrolledContainerColor 合成了一个动画\n    // 应用主题颜色更新时形成叠加动画，导致和周围正常组件视觉变换效果表现割裂\n    key(MaterialTheme.colorScheme.surface) {\n        TopAppBar(\n            title = title,\n            modifier = modifier,\n            navigationIcon = navigationIcon,\n            actions = actions,\n            expandedHeight = expandedHeight,\n            windowInsets = (LocalActivity.current as MainActivity).topBarWindowInsets,\n            colors = colors,\n            scrollBehavior = actualScrollBehavior,\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/QueryPkgTipCard.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.permission.canQueryPkgState\nimport li.songe.gkd.permission.requiredPermission\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.updateAppMutex\n\n@Composable\nfun QueryPkgAuthCard(\n    modifier: Modifier = Modifier,\n) {\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    Column(\n        modifier = modifier.fillMaxWidth(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        PerfIcon(\n            imageVector = PerfIcon.WarningAmber,\n            modifier = Modifier.size(40.dp),\n            tint = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n        Spacer(modifier = Modifier.height(4.dp))\n        Text(\n            text = \"如需显示所有应用\\n请授予「读取应用列表权限」\",\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Center,\n        )\n        TextButton(\n            enabled = !updateAppMutex.state.collectAsState().value,\n            onClick = throttle(fn = mainVm.viewModelScope.launchAsFn {\n                requiredPermission(context, canQueryPkgState)\n            })\n        ) {\n            Text(text = \"申请权限\")\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/RotatingLoadingIcon.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.tween\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport kotlin.math.sin\n\n@Composable\nfun RotatingLoadingIcon(\n    modifier: Modifier = Modifier,\n    loading: Boolean,\n    imageVector: ImageVector = PerfIcon.Autorenew,\n) {\n    val rotation = remember { Animatable(0f) }\n    LaunchedEffect(loading) {\n        if (loading) {\n            rotation.animateTo(\n                targetValue = rotation.value + 180f,\n                animationSpec = tween(\n                    durationMillis = 250,\n                    easing = { x -> sin(Math.PI / 2 * (x - 1f)).toFloat() + 1f }\n                )\n            )\n            rotation.animateTo(\n                targetValue = rotation.value + 360f,\n                animationSpec = infiniteRepeatable(\n                    animation = tween(durationMillis = 500, easing = LinearEasing),\n                    repeatMode = RepeatMode.Restart\n                )\n            )\n        } else if (rotation.value != 0f) {\n            rotation.animateTo(\n                targetValue = rotation.value + 180f,\n                animationSpec = tween(\n                    durationMillis = 250,\n                    easing = { x -> sin(Math.PI / 2 * x).toFloat() }\n                )\n            )\n        }\n    }\n    PerfIcon(\n        imageVector = imageVector,\n        modifier = modifier.graphicsLayer(rotationZ = rotation.value)\n    )\n}\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/RuleGroupCard.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.minimumInteractiveComponentSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.isActive\nimport li.songe.gkd.appScope\nimport li.songe.gkd.data.CategoryConfig\nimport li.songe.gkd.data.ExcludeData\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.data.SubsConfig\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.getGlobalGroupChecked\nimport li.songe.gkd.ui.icon.ResetSettings\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.util.getGroupEnable\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\nimport java.util.Objects\n\n\n@Composable\nfun RuleGroupCard(\n    modifier: Modifier = Modifier,\n    subs: RawSubscription,\n    appId: String?,\n    group: RawSubscription.RawGroupProps,\n    subsConfig: SubsConfig?,\n    category: RawSubscription.RawCategory?,\n    categoryConfig: CategoryConfig?,\n    focusGroupFlow: MutableStateFlow<Triple<Long, String?, Int>?>? = null,\n    isSelectedMode: Boolean = false,\n    isSelected: Boolean = false,\n    onLongClick: () -> Unit = {},\n    onSelectedChange: () -> Unit = {},\n) {\n    val mainVm = LocalMainViewModel.current\n\n    val inGlobalAppPage = appId != null && group is RawSubscription.RawGlobalGroup\n\n    var highlighted by remember { mutableStateOf(false) }\n    if (focusGroupFlow != null) {\n        val focusGroup by focusGroupFlow.collectAsState()\n        if (subs.id == focusGroup?.first && group.key == focusGroup?.third && if (group is RawSubscription.RawAppGroup) appId == focusGroup?.second else focusGroup?.second == null) {\n            LaunchedEffect(isSelectedMode) {\n                if (isSelectedMode) {\n                    highlighted = false\n                    focusGroupFlow.value = null\n                    return@LaunchedEffect\n                }\n                delay(300)\n                var i = 0\n                highlighted = true\n                while (isActive && i < 4) {\n                    delay(400)\n                    highlighted = !highlighted\n                    i++\n                }\n                highlighted = false\n                focusGroupFlow.value = null\n            }\n        }\n    }\n    val excludeData = remember(subsConfig?.exclude) {\n        ExcludeData.parse(subsConfig?.exclude)\n    }\n    val checked = if (inGlobalAppPage) {\n        getGlobalGroupChecked(\n            subs,\n            excludeData,\n            group,\n            appId,\n        )\n    } else {\n        getGroupEnable(\n            group,\n            subsConfig,\n            category,\n            categoryConfig,\n        )\n    }\n    val onCheckedChange = appScope.launchAsFn<Boolean> { newChecked ->\n        val newConfig = if (appId != null) {\n            if (group is RawSubscription.RawGlobalGroup) {\n                // APP 汇总页面 - 全局规则\n                val excludeData = ExcludeData.parse(subsConfig?.exclude)\n                (subsConfig ?: SubsConfig(\n                    type = SubsConfig.GlobalGroupType,\n                    subsId = subs.id,\n                    groupKey = group.key,\n                )).copy(\n                    exclude = excludeData.copy(\n                        appIds = excludeData.appIds.toMutableMap().apply {\n                            set(appId, !newChecked)\n                        }\n                    ).stringify()\n                )\n            } else {\n                // 订阅详情页面 - APP 规则\n                (subsConfig?.copy(enable = newChecked) ?: SubsConfig(\n                    type = SubsConfig.AppGroupType,\n                    subsId = subs.id,\n                    appId = appId,\n                    groupKey = group.key,\n                    enable = newChecked\n                ))\n            }\n        } else {\n            // 订阅详情页面 - 全局规则\n            group as RawSubscription.RawGlobalGroup\n            (subsConfig?.copy(enable = newChecked) ?: SubsConfig(\n                type = SubsConfig.GlobalGroupType,\n                subsId = subs.id,\n                groupKey = group.key,\n                enable = newChecked\n            ))\n        }\n        DbSet.subsConfigDao.insert(newConfig)\n    }\n    val onClick = if (isSelectedMode)\n        (onSelectedChange)\n    else throttle(mainVm.viewModelScope.launchAsFn(Dispatchers.Default) {\n        group.cacheStr // load cache\n        mainVm.ruleGroupState.showGroupFlow.value = ShowGroupState(\n            subsId = subs.id,\n            appId = if (group is RawSubscription.RawAppGroup) appId else null,\n            groupKey = group.key,\n            pageAppId = appId,\n        )\n    })\n    val containerColor = animateColorAsState(\n        if (isSelected || highlighted) {\n            MaterialTheme.colorScheme.primaryContainer\n        } else {\n            MaterialTheme.colorScheme.surfaceContainer\n        },\n        tween()\n    )\n    Card(\n        modifier = modifier\n            .padding(horizontal = 8.dp)\n            .combinedClickable(\n                onClick = onClick,\n                onLongClick = onLongClick,\n                onClickLabel = \"打开规则详情弹窗\",\n                onLongClickLabel = \"进入多选模式\"\n            ),\n        shape = MaterialTheme.shapes.extraSmall,\n        colors = CardDefaults.cardColors(\n            containerColor = containerColor.value\n        ),\n    ) {\n        val canRest = if (inGlobalAppPage) {\n            excludeData.appIds.contains(appId)\n        } else {\n            subsConfig?.enable != null\n        }\n        val hasExcludeActivity = if (inGlobalAppPage) {\n            checked != null && excludeData.activityIds.any { it.first == appId }\n        } else {\n            excludeData.activityIds.isNotEmpty()\n        }\n        Box {\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(8.dp),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n            ) {\n                Column(\n                    modifier = Modifier\n                        .weight(1f)\n                        .fillMaxHeight(),\n                    verticalArrangement = Arrangement.SpaceBetween\n                ) {\n                    GroupNameText(\n                        modifier = Modifier.fillMaxWidth(),\n                        text = group.name,\n                        style = MaterialTheme.typography.bodyLarge,\n                        isGlobal = group is RawSubscription.RawGlobalGroup,\n                        maxLines = 1,\n                        softWrap = false,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n                    if (group.valid) {\n                        if (!group.desc.isNullOrBlank()) {\n                            Text(\n                                text = group.desc!!,\n                                maxLines = 1,\n                                softWrap = false,\n                                overflow = TextOverflow.Ellipsis,\n                                modifier = Modifier.fillMaxWidth(),\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    } else {\n                        Text(\n                            text = group.errorDesc ?: \"未知错误\",\n                            modifier = Modifier.fillMaxWidth(),\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.error\n                        )\n                    }\n                }\n                val percent = usePercentAnimatable(!isSelectedMode)\n                val switchModifier = Modifier.graphicsLayer(\n                    alpha = 0.5f + (1 - 0.5f) * percent.value,\n                )\n                if (!group.valid) {\n                    InnerDisableSwitch(\n                        modifier = switchModifier,\n                        valid = false,\n                        isSelectedMode = isSelectedMode,\n                    )\n                } else if (checked != null) {\n                    PerfSwitch(\n                        key = Objects.hash(subs.id, appId, group.key),\n                        modifier = switchModifier.minimumInteractiveComponentSize(),\n                        checked = checked,\n                        onCheckedChange = if (isSelectedMode) null else onCheckedChange,\n                        thumbContent = if (canRest) ({\n                            PerfIcon(\n                                imageVector = ResetSettings,\n                                modifier = Modifier.size(8.dp)\n                            )\n                        }) else null,\n                    )\n                } else {\n                    InnerDisableSwitch(\n                        modifier = switchModifier,\n                        isSelectedMode = isSelectedMode,\n                    )\n                }\n            }\n            if (hasExcludeActivity) {\n                PerfIcon(\n                    imageVector = PerfIcon.Block,\n                    contentDescription = \"此规则已排除部分页面\",\n                    tint = if (isSelectedMode) {\n                        LocalContentColor.current.copy(alpha = 0.5f)\n                    } else {\n                        LocalContentColor.current\n                    },\n                    modifier = Modifier\n                        .padding(top = 4.dp, end = 4.dp)\n                        .align(Alignment.TopEnd)\n                        .size(8.dp)\n                )\n            }\n        }\n    }\n}\n\n\n@Composable\nfun BatchActionButtonGroup(vm: ViewModel, selectedDataSet: Set<ShowGroupState>) {\n    val mainVm = LocalMainViewModel.current\n    PerfIconButton(\n        imageVector = PerfIcon.ToggleOff,\n        contentDescription = \"批量关闭规则\",\n        onClick = throttle(vm.viewModelScope.launchAsFn(Dispatchers.Default) {\n            mainVm.dialogFlow.waitResult(\n                title = \"操作提示\",\n                text = \"是否将所选规则全部关闭?\\n\\n注: 也可在「订阅-规则类别」操作\"\n            )\n            val list = batchUpdateGroupEnable(selectedDataSet, false)\n            if (list.isNotEmpty()) {\n                toast(\"已关闭 ${list.size} 条规则\")\n            } else {\n                toast(\"无规则被改变\")\n            }\n        })\n    )\n    PerfIconButton(\n        imageVector = PerfIcon.ToggleOn,\n        contentDescription = \"批量打开规则\",\n        onClick = throttle(vm.viewModelScope.launchAsFn(Dispatchers.Default) {\n            mainVm.dialogFlow.waitResult(\n                title = \"操作提示\",\n                text = \"是否将所选规则全部启用?\\n\\n注: 也可在「订阅-规则类别」操作\"\n            )\n            val list = batchUpdateGroupEnable(selectedDataSet, true)\n            if (list.isNotEmpty()) {\n                toast(\"已启用 ${list.size} 条规则\")\n            } else {\n                toast(\"无规则被改变\")\n            }\n        })\n    )\n    PerfIconButton(\n        imageVector = ResetSettings,\n        contentDescription = \"批量重置规则开关\",\n        onClick = throttle(vm.viewModelScope.launchAsFn(Dispatchers.Default) {\n            mainVm.dialogFlow.waitResult(\n                title = \"操作提示\",\n                text = \"是否将所选规则重置开关至初始状态?\\n\\n注: 也可在「订阅-规则类别」操作\"\n            )\n            val list = batchUpdateGroupEnable(selectedDataSet, null)\n            if (list.isNotEmpty()) {\n                toast(\"已重置 ${list.size} 条规则开关至初始状态\")\n            } else {\n                toast(\"无规则被改变\")\n            }\n        })\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/RuleGroupDialog.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.delay\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.ui.ImagePreviewRoute\nimport li.songe.gkd.ui.SubsAppGroupListRoute\nimport li.songe.gkd.ui.SubsGlobalGroupListRoute\nimport li.songe.gkd.ui.icon.ResetSettings\nimport li.songe.gkd.ui.share.LocalDarkTheme\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.getJson5AnnotatedString\nimport li.songe.gkd.util.copyText\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun RuleGroupDialog(\n    subs: RawSubscription,\n    group: RawSubscription.RawGroupProps,\n    appId: String?,\n    onDismissRequest: () -> Unit,\n    onClickEdit: (() -> Unit) = {},\n    onClickEditExclude: () -> Unit,\n    onClickResetSwitch: (() -> Unit)?,\n    onClickDelete: () -> Unit = {}\n) {\n    val mainVm = LocalMainViewModel.current\n    AlertDialog(\n        onDismissRequest = onDismissRequest,\n        title = { Text(text = \"规则组详情\") },\n        text = {\n            Box(\n                modifier = Modifier.fillMaxWidth()\n            ) {\n                val maxHeight = 300.dp\n                Column(\n                    modifier = Modifier\n                        .align(Alignment.TopStart)\n                        .fillMaxWidth()\n                        .heightIn(min = 100.dp, max = maxHeight)\n                        .clip(MaterialTheme.shapes.extraSmall)\n                        .background(MaterialTheme.colorScheme.secondaryContainer)\n                        .verticalScroll(rememberScrollState())\n                        .clearAndSetSemantics {\n                            contentDescription = \"规则组内容\"\n                        }\n                ) {\n                    SelectionContainer {\n                        val textState = remember {\n                            mutableStateOf(\n                                group.cacheStr.run {\n                                    // 优化: 大字符串第一次显示卡顿\n                                    if (length > 1000) substring(0, 1000) else this\n                                }\n                            )\n                        }\n                        LaunchedEffect(group.cacheStr) {\n                            delay(50)\n                            if (group.cacheStr.length != textState.value.length) {\n                                textState.value = group.cacheStr\n                            }\n                        }\n                        val darkTheme = LocalDarkTheme.current\n                        Text(\n                            text = remember(textState.value, darkTheme) {\n                                getJson5AnnotatedString(\n                                    textState.value,\n                                    darkTheme\n                                )\n                            },\n                            modifier = Modifier.padding(4.dp),\n                            color = MaterialTheme.colorScheme.secondary,\n                            style = MaterialTheme.typography.bodySmall,\n                        )\n                    }\n                }\n                PerfIcon(\n                    modifier = Modifier\n                        .align(Alignment.TopEnd)\n                        .clickable(onClick = throttle {\n                            copyText(group.cacheStr)\n                        })\n                        .padding(4.dp)\n                        .size(24.dp),\n                    imageVector = PerfIcon.ContentCopy,\n                    tint = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.75f),\n                )\n                Text(\n                    text = group.cacheStr.length.toString(),\n                    modifier = Modifier\n                        .padding(end = 4.dp, bottom = 4.dp)\n                        .align(Alignment.BottomEnd)\n                        .clip(MaterialTheme.shapes.extraSmall)\n                        .background(MaterialTheme.colorScheme.surfaceContainer)\n                        .padding(horizontal = 2.dp),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.tertiary,\n                )\n            }\n        },\n        confirmButton = {\n            Row {\n                val currentRoute = mainVm.topRoute\n                val targetRoute = remember(subs.id, appId, group.key) {\n                    if (group is RawSubscription.RawGlobalGroup) {\n                        SubsGlobalGroupListRoute(\n                            subsItemId = subs.id,\n                            focusGroupKey = group.key\n                        )\n                    } else {\n                        SubsAppGroupListRoute(\n                            subsItemId = subs.id,\n                            appId = appId.toString(),\n                            focusGroupKey = group.key\n                        )\n                    }\n                }\n                if (targetRoute::class != currentRoute::class) {\n                    PerfIconButton(imageVector = PerfIcon.ArrowForward, onClick = throttle {\n                        onDismissRequest()\n                        mainVm.navigatePage(targetRoute)\n                    })\n                }\n                if (group.allExampleUrls.isNotEmpty()) {\n                    PerfIconButton(imageVector = PerfIcon.Image, onClick = throttle {\n                        onDismissRequest()\n                        mainVm.navigatePage(\n                            ImagePreviewRoute(\n                                title = group.name,\n                                uris = group.allExampleUrls,\n                            )\n                        )\n                    })\n                }\n                if (subs.isLocal) {\n                    PerfIconButton(imageVector = PerfIcon.Edit, onClick = throttle(onClickEdit))\n                }\n                PerfIconButton(\n                    imageVector = PerfIcon.Block,\n                    onClickLabel = \"编辑规则排除名单\",\n                    onClick = throttle(onClickEditExclude),\n                )\n                AnimatedVisibility(\n                    visible = onClickResetSwitch != null,\n                ) {\n                    PerfIconButton(\n                        imageVector = ResetSettings,\n                        onClickLabel = \"重置开关状态至默认值\",\n                        onClick = throttle(onClickResetSwitch ?: {}),\n                    )\n                }\n                if (subs.isLocal) {\n                    PerfIconButton(\n                        imageVector = PerfIcon.Delete,\n                        onClick = throttle(onClickDelete),\n                    )\n                }\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/RuleGroupState.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport li.songe.gkd.MainViewModel\nimport li.songe.gkd.data.CategoryConfig\nimport li.songe.gkd.data.ExcludeData\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.data.SubsConfig\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.ui.SubsGlobalGroupExcludeRoute\nimport li.songe.gkd.ui.UpsertRuleGroupRoute\nimport li.songe.gkd.ui.getGlobalGroupChecked\nimport li.songe.gkd.ui.style.scaffoldPadding\nimport li.songe.gkd.util.getGroupEnable\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.subsMapFlow\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateSubscription\n\ndata class ShowGroupState(\n    val subsId: Long,\n    val appId: String? = null,\n    val groupKey: Int? = null,\n    val pageAppId: String? = null,\n    val addAppRule: Boolean = false,\n) {\n    val groupType: Int\n        get() = if (appId != null) {\n            SubsConfig.AppGroupType\n        } else {\n            SubsConfig.GlobalGroupType\n        }\n\n    suspend fun querySubsConfig(): SubsConfig? {\n        groupKey ?: error(\"require groupKey\")\n        return if (groupType == SubsConfig.AppGroupType) {\n            appId ?: error(\"require appId\")\n            DbSet.subsConfigDao.queryAppGroupTypeConfig(subsId, appId, groupKey).first()\n        } else {\n            DbSet.subsConfigDao.queryGlobalGroupTypeConfig(subsId, groupKey).first()\n        }\n    }\n\n    suspend fun queryCategoryConfig(): CategoryConfig? {\n        groupKey ?: error(\"require groupKey\")\n        val subs = subsMapFlow.value[subsId] ?: error(\"require subs\")\n        val group = if (groupType == SubsConfig.AppGroupType) {\n            subs.apps.find { it.id == appId }?.groups\n        } else {\n            subs.globalGroups\n        }?.find { it.key == groupKey } ?: error(\"require group\")\n        val category = subs.groupToCategoryMap[group] ?: return null\n        return DbSet.categoryConfigDao.queryCategoryConfig(subsId, category.key)\n    }\n}\n\nfun RawSubscription.RawGroupProps.toGroupState(\n    subsId: Long,\n    appId: String? = null,\n) = when (this) {\n    is RawSubscription.RawAppGroup -> ShowGroupState(\n        subsId = subsId,\n        appId = appId ?: error(\"require appId\"),\n        groupKey = key,\n        pageAppId = appId,\n    )\n\n    is RawSubscription.RawGlobalGroup -> ShowGroupState(\n        subsId = subsId,\n        groupKey = key,\n        pageAppId = appId,\n    )\n}\n\nsuspend fun batchUpdateGroupEnable(\n    groups: Collection<ShowGroupState>,\n    enable: Boolean?\n): List<Pair<ShowGroupState, SubsConfig>> {\n    val diffDataList = groups.map { g ->\n        if (g.groupKey == null) return@map null\n        val subscription = subsMapFlow.value[g.subsId] ?: return@map null\n        val targetGroup = subscription.run {\n            if (g.appId != null) {\n                apps.find { a -> a.id == g.appId }?.groups?.find { it.key == g.groupKey }\n            } else {\n                globalGroups.find { it.key == g.groupKey }\n            }\n        }\n        if (targetGroup?.valid != true) {\n            return@map null\n        }\n        val subsConfig = g.querySubsConfig()\n        val categoryConfig = g.queryCategoryConfig()\n        if (enable == null && subsConfig?.enable == null && subsConfig?.exclude.isNullOrEmpty()) {\n            return@map null\n        }\n        val newSubsConfig = if (g.appId != null) {\n            targetGroup as RawSubscription.RawAppGroup\n            val oldEnable = getGroupEnable(\n                targetGroup,\n                subsConfig,\n                subscription.groupToCategoryMap[targetGroup],\n                categoryConfig\n            )\n            // app rule\n            val newSubsConfig = (subsConfig?.copy(enable = enable) ?: SubsConfig(\n                type = SubsConfig.AppGroupType,\n                subsId = g.subsId,\n                appId = g.appId,\n                groupKey = g.groupKey,\n                enable = enable\n            ))\n            val newEnable = getGroupEnable(\n                targetGroup,\n                newSubsConfig,\n                subscription.groupToCategoryMap[targetGroup],\n                categoryConfig\n            )\n            if (enable == newEnable && oldEnable == newEnable) {\n                return@map null\n            }\n            newSubsConfig\n        } else {\n            // global rule\n            if (g.pageAppId != null) {\n                // global rule for some app\n                targetGroup as RawSubscription.RawGlobalGroup\n                val excludeData = ExcludeData.parse(subsConfig?.exclude)\n                getGlobalGroupChecked(subscription, excludeData, targetGroup, g.pageAppId).let {\n                    if (it == null) return@map null\n                }\n                (subsConfig ?: SubsConfig(\n                    type = SubsConfig.GlobalGroupType,\n                    subsId = g.subsId,\n                    groupKey = g.groupKey,\n                )).copy(\n                    exclude = excludeData.copy(\n                        appIds = excludeData.appIds.toMutableMap().apply {\n                            if (enable != null) {\n                                if (!contains(g.pageAppId) && enable) {\n                                    return@map null\n                                }\n                                set(g.pageAppId, !enable)\n                            } else {\n                                if (!contains(g.pageAppId)) {\n                                    return@map null\n                                }\n                                remove(g.pageAppId)\n                            }\n                        }\n                    ).stringify()\n                )\n            } else {\n                // full global rule\n                val newSubsConfig = (subsConfig?.copy(enable = enable) ?: SubsConfig(\n                    type = SubsConfig.GlobalGroupType,\n                    subsId = g.subsId,\n                    groupKey = g.groupKey,\n                    enable = enable\n                ))\n                val oldEnable = getGroupEnable(\n                    targetGroup,\n                    subsConfig,\n                )\n                val newEnable = getGroupEnable(targetGroup, newSubsConfig)\n                if (enable == newEnable && oldEnable == newEnable) {\n                    return@map null\n                }\n                newSubsConfig\n            }\n        }\n\n        if (subsConfig != newSubsConfig) {\n            g to newSubsConfig\n        } else {\n            null\n        }\n    }.filterNotNull()\n    val newSubsConfigs = diffDataList.map { it.second }\n    val canDeleteList = newSubsConfigs.filter {\n        it.type == SubsConfig.AppGroupType && it.enable == null && it.exclude.isEmpty()\n    }\n    DbSet.subsConfigDao.insertAndDelete(\n        newSubsConfigs.filterNot { canDeleteList.contains(it) },\n        canDeleteList\n    )\n    return diffDataList\n}\n\nclass RuleGroupState(\n    private val mainVm: MainViewModel,\n) {\n    fun getSubsConfigFlow(state: MutableStateFlow<ShowGroupState?>): StateFlow<SubsConfig?> {\n        return state.map {\n            if (it?.groupKey != null) {\n                if (it.appId != null) {\n                    DbSet.subsConfigDao.queryAppGroupTypeConfig(it.subsId, it.appId, it.groupKey)\n                } else {\n                    DbSet.subsConfigDao.queryGlobalGroupTypeConfig(it.subsId, it.groupKey)\n                }\n            } else {\n                flow { emit(null) }\n            }\n        }.flatMapLatest { it }.stateIn(mainVm.viewModelScope, SharingStarted.Eagerly, null)\n    }\n\n    val showGroupFlow = MutableStateFlow<ShowGroupState?>(null)\n    private val showSubsConfigFlow = getSubsConfigFlow(showGroupFlow)\n    private val dismissGroupShow = { showGroupFlow.value = null }\n\n    val editExcludeGroupFlow = MutableStateFlow<ShowGroupState?>(null)\n    private val excludeTextFlow = MutableStateFlow(\"\")\n    private val dismissExcludeGroupShow = {\n        editExcludeGroupFlow.value = null\n        excludeTextFlow.value = \"\"\n    }\n    private val excludeSubsConfigFlow = getSubsConfigFlow(editExcludeGroupFlow).apply {\n        mainVm.run {\n            launchOnChange {\n                excludeTextFlow.value = value?.let { config ->\n                    ExcludeData.parse(config.exclude).stringify(config.appId)\n                } ?: \"\"\n            }\n        }\n    }\n    private val changedExcludeData: ExcludeData?\n        get() {\n            val oldValue =\n                ExcludeData.parse(excludeSubsConfigFlow.value?.exclude)\n            val newValue = ExcludeData.parse(\n                excludeTextFlow.value,\n                editExcludeGroupFlow.value?.appId!!\n            )\n            if (oldValue != newValue) {\n                return newValue\n            }\n            return null\n        }\n\n    @Composable\n    fun Render() {\n        val showGroupState = showGroupFlow.collectAsState().value\n        val showSubs = useSubs(showGroupState?.subsId)\n        val showGroup = useSubsGroup(showSubs, showGroupState?.groupKey, showGroupState?.appId)\n        if (showGroupState?.groupKey != null && showSubs != null && showGroup != null) {\n            val subsConfig = showSubsConfigFlow.collectAsState().value\n            val excludeData = remember(subsConfig?.exclude) {\n                ExcludeData.parse(subsConfig?.exclude)\n            }\n            RuleGroupDialog(\n                subs = showSubs,\n                group = showGroup,\n                appId = showGroupState.appId,\n                onDismissRequest = dismissGroupShow,\n                onClickEdit = {\n                    dismissGroupShow()\n                    mainVm.navigatePage(\n                        UpsertRuleGroupRoute(\n                            subsId = showGroupState.subsId,\n                            groupKey = showGroupState.groupKey,\n                            appId = showGroupState.appId,\n                        )\n                    )\n                },\n                onClickEditExclude = {\n                    dismissGroupShow()\n                    if (showGroupState.appId == null) {\n                        mainVm.navigatePage(\n                            SubsGlobalGroupExcludeRoute(\n                                showGroupState.subsId,\n                                showGroupState.groupKey\n                            )\n                        )\n                    } else {\n                        editExcludeGroupFlow.value = showGroupState\n                    }\n                },\n                onClickResetSwitch = subsConfig?.let {\n                    if (showGroup is RawSubscription.RawGlobalGroup) {\n                        if (showGroupState.pageAppId != null) {\n                            if (excludeData.appIds.contains(showGroupState.pageAppId)) {\n                                mainVm.viewModelScope.launchAsFn {\n                                    DbSet.subsConfigDao.update(\n                                        subsConfig.copy(\n                                            exclude = excludeData.clear(\n                                                appId = showGroupState.pageAppId\n                                            ).stringify()\n                                        )\n                                    )\n                                    toast(\"已重置局部开关至初始状态\")\n                                }\n                            } else {\n                                null\n                            }\n                        } else {\n                            subsConfig.enable?.let {\n                                mainVm.viewModelScope.launchAsFn {\n                                    DbSet.subsConfigDao.update(subsConfig.copy(enable = null))\n                                    toast(\"已重置开关至初始状态\")\n                                }\n                            }\n                        }\n                    } else {\n                        subsConfig.enable?.let {\n                            mainVm.viewModelScope.launchAsFn {\n                                DbSet.subsConfigDao.update(subsConfig.copy(enable = null))\n                                toast(\"已重置开关至初始状态\")\n                            }\n                        }\n                    }\n                },\n                onClickDelete = mainVm.viewModelScope.launchAsFn {\n                    dismissGroupShow()\n                    val r = mainVm.dialogFlow.getResult(\n                        title = \"删除规则组\",\n                        text = \"确定删除 ${showGroup.name} ?\",\n                        error = true,\n                    )\n                    if (!r) {\n                        showGroupFlow.value = showGroupState\n                        return@launchAsFn\n                    }\n                    if (showGroup is RawSubscription.RawGlobalGroup) {\n                        updateSubscription(\n                            showSubs.copy(\n                                globalGroups = showSubs.globalGroups.filter { g -> g.key != showGroup.key }\n                            )\n                        )\n                        DbSet.subsConfigDao.deleteGlobalGroupConfig(\n                            showGroupState.subsId,\n                            showGroupState.groupKey\n                        )\n                    } else if (showGroupState.appId != null) {\n                        updateSubscription(\n                            showSubs.copy(\n                                apps = showSubs.apps.map { a ->\n                                    if (a.id == showGroupState.appId) {\n                                        a.copy(groups = a.groups.filter { g -> g.key != showGroup.key })\n                                    } else {\n                                        a\n                                    }\n                                }\n                            )\n                        )\n                        DbSet.subsConfigDao.deleteAppGroupConfig(\n                            showGroupState.subsId,\n                            showGroupState.appId,\n                            showGroupState.groupKey\n                        )\n                    }\n                    toast(\"删除成功\")\n                }\n            )\n        }\n\n        val excludeGroupState = editExcludeGroupFlow.collectAsState().value\n        val excludeSubs = useSubs(excludeGroupState?.subsId)\n        val excludeGroup =\n            useSubsGroup(excludeSubs, excludeGroupState?.groupKey, excludeGroupState?.appId)\n        if (excludeGroupState?.groupKey != null && excludeGroupState.appId != null && excludeSubs != null && excludeGroup is RawSubscription.RawAppGroup) {\n            FullscreenDialog(onDismissRequest = dismissExcludeGroupShow) {\n                val keyboardController = LocalSoftwareKeyboardController.current\n                val onBack = mainVm.viewModelScope.launchAsFn {\n                    keyboardController?.hide()\n                    val newValue = changedExcludeData\n                    if (newValue != null) {\n                        mainVm.dialogFlow.waitResult(\n                            title = \"提示\",\n                            text = \"当前内容未保存，是否放弃编辑？\",\n                        )\n                    }\n                    dismissExcludeGroupShow()\n                }\n                BackHandler(onBack = onBack)\n                Scaffold(\n                    topBar = {\n                        PerfTopAppBar(\n                            navigationIcon = {\n                                PerfIconButton(\n                                    imageVector = PerfIcon.Close,\n                                    onClick = onBack\n                                )\n                            },\n                            title = {\n                                TowLineText(\n                                    title = excludeGroup.name,\n                                    subtitle = \"编辑禁用\",\n                                )\n                            },\n                            actions = {\n                                PerfIconButton(imageVector = PerfIcon.Save, onClick = throttle {\n                                    val newValue = changedExcludeData\n                                    if (newValue == null) {\n                                        toast(\"无修改\")\n                                        dismissExcludeGroupShow()\n                                    } else {\n                                        val newSubsConfig =\n                                            (excludeSubsConfigFlow.value ?: SubsConfig(\n                                                type = SubsConfig.AppGroupType,\n                                                subsId = excludeSubs.id,\n                                                appId = excludeGroupState.appId,\n                                                groupKey = excludeGroupState.groupKey,\n                                            )).copy(\n                                                exclude = newValue.stringify()\n                                            )\n                                        dismissExcludeGroupShow()\n                                        mainVm.viewModelScope.launchTry {\n                                            DbSet.subsConfigDao.insert(newSubsConfig)\n                                            toast(\"更新成功\")\n                                        }\n                                    }\n                                })\n                            }\n                        )\n                    },\n                ) { contentPadding ->\n                    MultiTextField(\n                        modifier = Modifier.scaffoldPadding(contentPadding),\n                        textFlow = excludeTextFlow,\n                        placeholderText = \"请填入需要禁用的 activityId 列表\\n每行一个\",\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/SettingItem.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.unit.dp\nimport li.songe.gkd.ui.style.itemPadding\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun SettingItem(\n    title: String,\n    subtitle: String? = null,\n    suffix: String? = null,\n    suffixUnderline: Boolean = false,\n    onSuffixClick: (() -> Unit)? = null,\n    imageVector: ImageVector? = PerfIcon.KeyboardArrowRight,\n    onClick: (() -> Unit)? = null,\n    onClickLabel: String? = null,\n) {\n    Row(\n        modifier = Modifier\n            .let {\n                if (onClick != null) {\n                    it.clickable(\n                        onClick = throttle(fn = onClick),\n                        onClickLabel = onClickLabel ?: \"进入${title}页面\"\n                    )\n                } else {\n                    it\n                }\n            }\n            .fillMaxWidth()\n            .itemPadding(),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Column(modifier = if (imageVector != null) Modifier.weight(1f) else Modifier.fillMaxWidth()) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.bodyLarge,\n            )\n            if (subtitle != null) {\n                if (suffix != null) {\n                    Row {\n                        Text(\n                            text = subtitle,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                        Spacer(modifier = Modifier.width(4.dp))\n                        Text(\n                            text = suffix,\n                            style = MaterialTheme.typography.bodyMedium.run {\n                                if (suffixUnderline) {\n                                    copy(textDecoration = TextDecoration.Underline)\n                                } else {\n                                    this\n                                }\n                            },\n                            color = MaterialTheme.colorScheme.primary,\n                            modifier = if (onSuffixClick != null) Modifier.clickable(\n                                onClick = throttle(fn = onSuffixClick),\n                            ) else Modifier\n                        )\n                    }\n                } else {\n                    Text(\n                        text = subtitle,\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n            }\n        }\n        if (imageVector != null) {\n            PerfIcon(\n                imageVector = imageVector,\n                contentDescription = null,\n            )\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/ShareDataDialog.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.data.exportData\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.saveFileToDownloads\nimport li.songe.gkd.util.shareFile\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun ShareDataDialog(\n    vm: ViewModel,\n    showShareDataIdsFlow: MutableStateFlow<Set<Long>?>,\n) {\n    val showShareDataIds = showShareDataIdsFlow.collectAsState().value\n    if (showShareDataIds != null) {\n        val context = LocalActivity.current as MainActivity\n        Dialog(onDismissRequest = { showShareDataIdsFlow.value = null }) {\n            Card(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n                shape = RoundedCornerShape(16.dp),\n            ) {\n                val modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp)\n                Text(\n                    text = \"分享到其他应用\", modifier = Modifier\n                        .clickable(onClick = throttle {\n                            showShareDataIdsFlow.value = null\n                            vm.viewModelScope.launchTry(Dispatchers.IO) {\n                                val file = exportData(showShareDataIds)\n                                context.shareFile(file, \"分享数据文件\")\n                            }\n                        })\n                        .then(modifier)\n                )\n                Text(\n                    text = \"保存到下载\",\n                    modifier = Modifier\n                        .clickable(onClick = throttle {\n                            showShareDataIdsFlow.value = null\n                            vm.viewModelScope.launchTry(Dispatchers.IO) {\n                                val file = exportData(showShareDataIds)\n                                context.saveFileToDownloads(file)\n                            }\n                        })\n                        .then(modifier)\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/SubsAppCard.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport li.songe.gkd.store.blockMatchAppListFlow\nimport li.songe.gkd.ui.SubsAppInfoItem\nimport li.songe.gkd.ui.style.appItemPadding\n\n\n@Composable\nfun SubsAppCard(\n    data: SubsAppInfoItem,\n    onClick: (() -> Unit),\n    onValueChange: ((Boolean) -> Unit),\n) {\n    val rawApp = data.rawApp\n    Row(\n        modifier = Modifier\n            .clickable(onClick = onClick)\n            .appItemPadding(),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.spacedBy(12.dp),\n    ) {\n        AppIcon(appId = data.id)\n        Column(\n            modifier = Modifier\n                .weight(1f),\n            verticalArrangement = Arrangement.Center\n        ) {\n            AppNameText(appInfo = data.appInfo, fallbackName = data.rawApp.name)\n            if (rawApp.groups.isNotEmpty()) {\n                val enableDesc = when (data.enableSize) {\n                    0 -> \"${rawApp.groups.size}组规则/${rawApp.groups.size}关闭\"\n                    rawApp.groups.size -> \"${rawApp.groups.size}组规则\"\n                    else -> \"${rawApp.groups.size}组规则/${data.enableSize}启用/${rawApp.groups.size - data.enableSize}关闭\"\n                }\n                Text(\n                    text = enableDesc,\n                    maxLines = 1,\n                    softWrap = false,\n                    overflow = TextOverflow.Ellipsis,\n                    modifier = Modifier.fillMaxWidth(),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n        if (blockMatchAppListFlow.collectAsState().value.contains(data.id)) {\n            PerfIcon(\n                modifier = Modifier\n                    .padding(2.dp)\n                    .size(20.dp),\n                imageVector = PerfIcon.Block,\n                tint = MaterialTheme.colorScheme.secondary,\n            )\n        }\n        PerfSwitch(\n            key = data.id,\n            checked = data.appConfig?.enable ?: (data.appInfo != null),\n            onCheckedChange = onValueChange,\n        )\n    }\n}\n\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/SubsItemCard.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsDraggedAsState\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.minimumInteractiveComponentSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.onClick\nimport androidx.compose.ui.semantics.onLongClick\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.stateDescription\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport li.songe.gkd.META\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.data.SubsItem\nimport li.songe.gkd.ui.home.HomeVm\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.util.formatTimeAgo\nimport li.songe.gkd.util.mapState\nimport li.songe.gkd.util.subsLoadErrorsFlow\nimport li.songe.gkd.util.subsRefreshErrorsFlow\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.updateSubsMutex\n\n\n@Composable\nfun SubsItemCard(\n    modifier: Modifier = Modifier,\n    interactionSource: MutableInteractionSource,\n    subsItem: SubsItem,\n    subscription: RawSubscription?,\n    index: Int,\n    isSelectedMode: Boolean,\n    isSelected: Boolean,\n    onCheckedChange: ((Boolean) -> Unit),\n    onSelectedChange: (() -> Unit)? = null,\n) {\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel<HomeVm>()\n    val subsLoadError by remember(subsItem.id) {\n        subsLoadErrorsFlow.mapState(vm.viewModelScope) { it[subsItem.id] }\n    }.collectAsState()\n    val subsRefreshError by remember(subsItem.id) {\n        subsRefreshErrorsFlow.mapState(vm.viewModelScope) { it[subsItem.id] }\n    }.collectAsState()\n    val subsRefreshing by updateSubsMutex.state.collectAsState()\n    val dragged by interactionSource.collectIsDraggedAsState()\n    val onClick = {\n        if (!dragged) {\n            if (isSelectedMode) {\n                onSelectedChange?.invoke()\n            } else if (!updateSubsMutex.mutex.isLocked) {\n                mainVm.sheetSubsIdFlow.value = subsItem.id\n            }\n        }\n    }\n    val containerColor = animateColorAsState(\n        if (isSelected) {\n            MaterialTheme.colorScheme.primaryContainer\n        } else {\n            MaterialTheme.colorScheme.surfaceContainer\n        },\n        tween()\n    )\n    Card(\n        onClick = onClick,\n        modifier = modifier\n            .padding(16.dp, 4.dp)\n            .semantics {\n                stateDescription = if (isSelectedMode) {\n                    if (isSelected) \"已选中\" else \"未选中\"\n                } else {\n                    if (subsItem.enable) \"已启用\" else \"已禁用\"\n                }\n                this.onClick(label = \"查看订阅详情\", action = null)\n                this.onLongClick(label = \"进入多选模式\", action = null)\n            },\n        shape = MaterialTheme.shapes.small,\n        interactionSource = interactionSource,\n        colors = CardDefaults.cardColors(\n            containerColor = containerColor.value\n        ),\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.padding(8.dp),\n        ) {\n            Column(\n                modifier = Modifier.weight(1f),\n                verticalArrangement = Arrangement.spacedBy(4.dp),\n            ) {\n                if (subscription != null) {\n                    Text(\n                        modifier = Modifier.semantics {\n                            contentDescription = \"订阅顺序：$index, 订阅名称 ${subscription.name}\"\n                        },\n                        text = \"$index. ${subscription.name}\",\n                        maxLines = 1,\n                        softWrap = false,\n                        overflow = TextOverflow.Ellipsis,\n                        style = MaterialTheme.typography.bodyLarge,\n                    )\n                    Text(\n                        text = subscription.numText,\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = if (subscription.groupsSize == 0) {\n                            LocalContentColor.current.copy(alpha = 0.5f)\n                        } else {\n                            LocalContentColor.current\n                        }\n                    )\n                    Row(\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        if (subsItem.id >= 0) {\n                            if (subscription.author != null) {\n                                Text(\n                                    modifier = Modifier.semantics {\n                                        contentDescription = \"作者 ${subscription.author}\"\n                                    },\n                                    text = subscription.author,\n                                    style = MaterialTheme.typography.labelSmall,\n                                )\n                            }\n                            Text(\n                                modifier = Modifier.semantics {\n                                    contentDescription = \"订阅版本号 ${subscription.version}\"\n                                },\n                                text = \"v\" + (subscription.version.toString()),\n                                style = MaterialTheme.typography.labelSmall,\n                            )\n                        } else {\n                            Text(\n                                modifier = Modifier.clearAndSetSemantics {},\n                                text = META.appName,\n                                style = MaterialTheme.typography.labelSmall,\n                                color = MaterialTheme.colorScheme.secondary,\n                            )\n                        }\n                        val timeStr = formatTimeAgo(subsItem.mtime)\n                        Text(\n                            modifier = Modifier.semantics {\n                                contentDescription = \"更新时间 $timeStr\"\n                            },\n                            text = timeStr,\n                            style = MaterialTheme.typography.labelSmall,\n                        )\n                    }\n                } else {\n                    Text(\n                        text = \"id=${subsItem.id}\",\n                        maxLines = 1,\n                        softWrap = false,\n                        overflow = TextOverflow.Ellipsis,\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n                    val color = if (subsLoadError != null) {\n                        MaterialTheme.colorScheme.error\n                    } else {\n                        Color.Unspecified\n                    }\n                    Text(\n                        text = subsLoadError?.message\n                            ?: if (subsRefreshing) \"加载中...\" else \"文件不存在\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = color\n                    )\n                }\n                if (subsRefreshError != null) {\n                    Text(\n                        text = \"更新错误: ${subsRefreshError?.message}\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.error\n                    )\n                }\n            }\n            Spacer(modifier = Modifier.width(4.dp))\n            val percent = usePercentAnimatable(!isSelectedMode)\n            val switchModifier = Modifier.graphicsLayer(\n                alpha = 0.5f + (1 - 0.5f) * percent.value,\n            ).run {\n                if (isSelectedMode) {\n                    minimumInteractiveComponentSize()\n                } else {\n                    this\n                }\n            }\n            PerfSwitch(\n                key = subsItem.id,\n                modifier = switchModifier,\n                checked = subsItem.enable,\n                onCheckedChange = if (isSelectedMode) null else throttle(fn = onCheckedChange),\n            )\n        }\n    }\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/SubsSheet.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.SheetValue\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.META\nimport li.songe.gkd.ui.ActionLogRoute\nimport li.songe.gkd.ui.SubsAppListRoute\nimport li.songe.gkd.ui.SubsCategoryRoute\nimport li.songe.gkd.ui.SubsGlobalGroupListRoute\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.itemHorizontalPadding\nimport li.songe.gkd.util.LOCAL_SUBS_ID\nimport li.songe.gkd.util.checkSubsUpdate\nimport li.songe.gkd.util.deleteSubscription\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.subsItemsFlow\nimport li.songe.gkd.util.subsMapFlow\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateSubsMutex\n\n@Composable\nfun SubsSheet(\n    vm: ViewModel,\n    sheetSubsIdFlow: MutableStateFlow<Long?>\n) {\n    val subsItems by subsItemsFlow.collectAsState()\n    val (subsId, setSubsId) = remember { mutableStateOf(sheetSubsIdFlow.value) }\n    val subsItem = subsItems.find { it.id == subsId }\n    if (subsItem == null) {\n        LaunchedEffect(null) {\n            sheetSubsIdFlow.collect {\n                setSubsId(it)\n            }\n        }\n    } else {\n        val mainVm = LocalMainViewModel.current\n        val subsIdToRaw by subsMapFlow.collectAsState()\n        var swipeEnabled by remember { mutableStateOf(false) }\n        val sheetState = rememberModalBottomSheetState(\n            skipPartiallyExpanded = true,\n            confirmValueChange = { swipeEnabled }\n        )\n        LaunchedEffect(null) {\n            sheetSubsIdFlow.collect {\n                if (it == null && sheetState.isVisible) {\n                    launch {\n                        sheetState.hide()\n                    }.invokeOnCompletion {\n                        if (!sheetState.isVisible) {\n                            setSubsId(null)\n                        }\n                    }\n                } else {\n                    setSubsId(it)\n                }\n            }\n        }\n        val scrollState = rememberScrollState()\n        remember {\n            derivedStateOf {\n                scrollState.value == 0\n            }\n        }.let { a ->\n            LaunchedEffect(a.value) {\n                swipeEnabled = a.value\n            }\n        }\n        ModalBottomSheet(\n            onDismissRequest = {\n                sheetSubsIdFlow.value = null\n            },\n            sheetState = sheetState\n        ) {\n            val subscription = subsIdToRaw[subsItem.id]\n            val showName = subscription?.name ?: \"id=${subsItem.id}\"\n            val childModifier = remember {\n                Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = itemHorizontalPadding, vertical = 8.dp)\n            }\n            Column(\n                modifier = Modifier\n                    .verticalScroll(\n                        state = scrollState,\n                        enabled = sheetState.currentValue == SheetValue.Expanded\n                    )\n                    .fillMaxWidth(),\n            ) {\n                Text(\n                    text = showName,\n                    style = MaterialTheme.typography.titleLarge,\n                    modifier = childModifier\n                )\n                if (subscription != null) {\n                    Column(\n                        modifier = childModifier.clearAndSetSemantics {\n                            contentDescription =\n                                \"作者：${subscription.author ?: \"未知\"}, 版本号：v${subscription.version}, 更新时间：${subsItem.mtimeStr}\"\n                        }\n                    ) {\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.SpaceBetween,\n                        ) {\n                            Text(\n                                text = \"作者\",\n                                style = MaterialTheme.typography.labelLarge,\n                            )\n                            Text(\n                                text = \"v${subscription.version}\",\n                                style = MaterialTheme.typography.labelMedium,\n                                color = MaterialTheme.colorScheme.tertiary,\n                                modifier = Modifier\n                                    .clip(MaterialTheme.shapes.extraSmall)\n                                    .background(MaterialTheme.colorScheme.tertiaryContainer)\n                                    .padding(horizontal = 2.dp),\n                            )\n                        }\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.SpaceBetween,\n                        ) {\n                            if (!subsItem.isLocal) {\n                                Text(\n                                    text = subscription.author ?: \"未知\",\n                                    style = MaterialTheme.typography.labelMedium,\n                                    color = MaterialTheme.colorScheme.onSurfaceVariant.let {\n                                        if (subscription.author == null) {\n                                            it.copy(alpha = 0.5f)\n                                        } else {\n                                            it\n                                        }\n                                    },\n                                    maxLines = 1,\n                                    softWrap = false,\n                                    overflow = TextOverflow.Ellipsis,\n                                )\n                            } else {\n                                Text(\n                                    text = META.appName,\n                                    style = MaterialTheme.typography.labelMedium,\n                                    color = MaterialTheme.colorScheme.secondary,\n                                )\n                            }\n                            Text(\n                                text = subsItem.mtimeStr,\n                                style = MaterialTheme.typography.labelMedium,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    }\n                    if (subscription.globalGroups.isNotEmpty() || subsItem.isLocal) {\n                        Row(\n                            modifier = Modifier\n                                .clickable(onClickLabel = \"查看全局规则组列表\", onClick = throttle {\n                                    setSubsId(null)\n                                    sheetSubsIdFlow.value = null\n                                    mainVm.navigatePage(SubsGlobalGroupListRoute(subsItem.id))\n                                })\n                                .then(childModifier),\n                            horizontalArrangement = Arrangement.SpaceBetween,\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Column(\n                                modifier = Modifier.weight(1f)\n                            ) {\n                                Text(\n                                    text = \"全局规则\",\n                                    style = MaterialTheme.typography.labelLarge,\n                                )\n                                Text(\n                                    text = if (subscription.globalGroups.isNotEmpty()) \"共 ${subscription.globalGroups.size} 全局规则组\" else \"暂无\",\n                                    style = MaterialTheme.typography.labelMedium,\n                                    color = MaterialTheme.colorScheme.onSurfaceVariant.let {\n                                        if (subscription.globalGroups.isEmpty()) {\n                                            it.copy(alpha = 0.5f)\n                                        } else {\n                                            it\n                                        }\n                                    },\n                                )\n                            }\n                            PerfIcon(\n                                imageVector = PerfIcon.KeyboardArrowRight,\n                            )\n                        }\n                    }\n                    if (subscription.appGroups.isNotEmpty() || subsItem.isLocal) {\n                        Row(\n                            modifier = Modifier\n                                .clickable(onClickLabel = \"查看应用规则组列表\", onClick = throttle {\n                                    setSubsId(null)\n                                    sheetSubsIdFlow.value = null\n                                    mainVm.navigatePage(SubsAppListRoute(subsItem.id))\n                                })\n                                .then(childModifier),\n                            horizontalArrangement = Arrangement.SpaceBetween,\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Column(\n                                modifier = Modifier.weight(1f)\n                            ) {\n                                Text(\n                                    text = \"应用规则\",\n                                    style = MaterialTheme.typography.labelLarge,\n                                )\n                                Text(\n                                    text = if (subscription.appGroups.isNotEmpty()) \"共 ${subscription.apps.size} 应用 ${subscription.appGroups.size} 规则组\" else \"暂无\",\n                                    style = MaterialTheme.typography.labelMedium,\n                                    color = MaterialTheme.colorScheme.onSurfaceVariant.let {\n                                        if (subscription.appGroups.isEmpty()) {\n                                            it.copy(alpha = 0.5f)\n                                        } else {\n                                            it\n                                        }\n                                    },\n                                )\n                            }\n                            PerfIcon(\n                                imageVector = PerfIcon.KeyboardArrowRight,\n                            )\n                        }\n\n                    }\n                    if (subscription.categories.isNotEmpty() || subsItem.isLocal) {\n                        Row(\n                            modifier = Modifier\n                                .clickable(onClickLabel = \"查看规则类别列表\", onClick = throttle {\n                                    setSubsId(null)\n                                    sheetSubsIdFlow.value = null\n                                    mainVm.navigatePage(SubsCategoryRoute(subsItem.id))\n                                })\n                                .then(childModifier),\n                            horizontalArrangement = Arrangement.SpaceBetween,\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Column(\n                                modifier = Modifier.weight(1f)\n                            ) {\n                                Text(\n                                    text = \"规则类别\",\n                                    style = MaterialTheme.typography.labelLarge,\n                                )\n                                Text(\n                                    text = if (subscription.categories.isNotEmpty()) \"共 ${subscription.categories.size} 类别\" else \"暂无\",\n                                    style = MaterialTheme.typography.labelMedium,\n                                    color = MaterialTheme.colorScheme.onSurfaceVariant.let {\n                                        if (subscription.categories.isEmpty()) {\n                                            it.copy(alpha = 0.5f)\n                                        } else {\n                                            it\n                                        }\n                                    },\n                                )\n                            }\n                            PerfIcon(\n                                imageVector = PerfIcon.KeyboardArrowRight,\n                            )\n                        }\n                    }\n                    if (!subsItem.isLocal && subsItem.updateUrl != null) {\n                        Row(\n                            modifier = Modifier\n                                .clickable(onClickLabel = \"编辑订阅链接\", onClick = throttle {\n                                    if (updateSubsMutex.mutex.isLocked) {\n                                        toast(\"正在刷新订阅,请稍后操作\")\n                                        return@throttle\n                                    }\n                                    mainVm.viewModelScope.launchTry {\n                                        val url =\n                                            mainVm.inputSubsLinkOption.getResult(initValue = subsItem.updateUrl)\n                                                ?: return@launchTry\n                                        mainVm.addOrModifySubs(url, subsItem)\n                                    }\n                                })\n                                .then(childModifier),\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Column(\n                                modifier = Modifier.weight(1f),\n                            ) {\n                                Text(\n                                    text = \"订阅链接\",\n                                    style = MaterialTheme.typography.labelLarge,\n                                )\n                                Text(\n                                    text = subsItem.updateUrl,\n                                    style = MaterialTheme.typography.labelMedium,\n                                    color = MaterialTheme.colorScheme.secondary,\n                                    softWrap = false,\n                                    overflow = TextOverflow.MiddleEllipsis,\n                                    modifier = Modifier\n                                        .clearAndSetSemantics {}\n                                        .clickable(onClickLabel = \"查看订阅链接\", onClick = {\n                                            mainVm.textFlow.value = subsItem.updateUrl\n                                        })\n                                )\n                            }\n                            Spacer(modifier = Modifier.width(8.dp))\n                            PerfIcon(\n                                imageVector = PerfIcon.Edit,\n                            )\n                        }\n                    }\n                } else {\n                    val loading by updateSubsMutex.state.collectAsState()\n                    Column(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .height(150.dp),\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                    ) {\n                        Spacer(modifier = Modifier.height(EmptyHeight))\n                        if (loading) {\n                            CircularProgressIndicator()\n                        } else {\n                            Text(\n                                text = \"文件加载错误或不存在\",\n                                style = MaterialTheme.typography.labelLarge,\n                                color = MaterialTheme.colorScheme.error,\n                            )\n                            TextButton(onClick = throttle { checkSubsUpdate(showToast = true) }) {\n                                Text(text = \"重新加载\")\n                            }\n                        }\n                    }\n                }\n\n                Row(\n                    modifier = childModifier,\n                    horizontalArrangement = Arrangement.End\n                ) {\n                    if (!subsItem.isLocal && subscription?.supportUri != null) {\n                        PerfIconButton(\n                            imageVector = PerfIcon.HelpOutline,\n                            onClick = throttle {\n                                mainVm.textFlow.value = subscription.supportUri\n                            },\n                        )\n                    }\n                    PerfIconButton(imageVector = PerfIcon.History, onClick = throttle {\n                        setSubsId(null)\n                        sheetSubsIdFlow.value = null\n                        mainVm.navigatePage(ActionLogRoute(subsId = subsItem.id))\n                    })\n                    if (subscription != null || !subsItem.isLocal) {\n                        PerfIconButton(imageVector = PerfIcon.Share, onClick = throttle {\n                            mainVm.showShareDataIdsFlow.value = setOf(subsItem.id)\n                        })\n                    }\n                    if (subsItem.id != LOCAL_SUBS_ID) {\n                        PerfIconButton(\n                            imageVector = PerfIcon.Delete,\n                            onClick = throttle(\n                                vm.viewModelScope.launchAsFn {\n                                    mainVm.dialogFlow.waitResult(\n                                        title = \"删除订阅\",\n                                        text = \"确定删除 ${subscription?.name ?: subsItem.id} ?\",\n                                        error = true,\n                                    )\n                                    sheetSubsIdFlow.value = null\n                                    setSubsId(null)\n                                    deleteSubscription(subsItem.id)\n                                }\n                            ),\n                        )\n                    }\n                }\n                Spacer(modifier = Modifier.height(EmptyHeight / 2))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/TermsAcceptDialog.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.LinkAnnotation\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextLinkStyles\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.withLink\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.util.ShortUrlSet\nimport li.songe.gkd.util.throttle\n\n\n@Composable\nfun TermsAcceptDialog() {\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    val modifier = Modifier.fillMaxWidth()\n    val stepDataList = remember {\n        arrayOf(\n            \"使用声明\" to @Composable {\n                val linkStyles = TextLinkStyles(\n                    style = SpanStyle(\n                        fontWeight = FontWeight.Bold,\n                        color = MaterialTheme.colorScheme.primary,\n                    )\n                )\n                Text(\n                    modifier = modifier,\n                    text = buildAnnotatedString {\n                        append(\"感谢使用 GKD！您需要阅读并同意「\")\n                        withLink(\n                            LinkAnnotation.Url(\n                                ShortUrlSet.URL12,\n                                linkStyles\n                            )\n                        ) {\n                            append(\"用户协议\")\n                        }\n                        append(\"」和「\")\n                        withLink(\n                            LinkAnnotation.Url(\n                                ShortUrlSet.URL11,\n                                linkStyles\n                            )\n                        ) {\n                            append(\"隐私政策\")\n                        }\n                        append(\"」才能继续使用, 请仔细阅读相关内容\")\n                    },\n                )\n            },\n            \"关于无障碍\" to @Composable {\n                Text(\n                    modifier = modifier,\n                    text = \"GKD 请求使用系统「无障碍 API」获取屏幕信息, 以此基于用户自定义订阅规则执行自动化操作\",\n                )\n            }\n        )\n    }\n    var step by rememberSaveable { mutableIntStateOf(0) }\n\n    AlertDialog(\n        onDismissRequest = {},\n        title = {\n            Text(text = stepDataList[step].first)\n        },\n        text = stepDataList[step].second,\n        confirmButton = {\n            TextButton(onClick = throttle {\n                if (step < stepDataList.size - 1) {\n                    step++\n                } else {\n                    mainVm.termsAcceptedFlow.value = true\n                }\n            }) {\n                Text(text = \"同意\")\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = throttle {\n                context.finish()\n            }) {\n                Text(text = \"不同意\")\n            }\n        }\n    )\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/TextDialog.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport android.webkit.URLUtil\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.util.openUri\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun TextDialog(\n    textFlow: MutableStateFlow<String?>\n) {\n    val text = textFlow.collectAsState().value\n    if (text != null) {\n        val isUri = remember(text) { URLUtil.isNetworkUrl(text) }\n        val onDismissRequest = {\n            textFlow.value = null\n        }\n        AlertDialog(\n            onDismissRequest = onDismissRequest,\n            title = {\n                Text(text = if (isUri) \"查看链接\" else \"查看文本\")\n            },\n            text = {\n                CopyTextCard(text = text)\n            },\n            confirmButton = {\n                if (isUri) {\n                    TextButton(onClick = throttle {\n                        onDismissRequest()\n                        openUri(text)\n                    }) {\n                        Text(text = \"打开\")\n                    }\n                } else {\n                    TextButton(onClick = onDismissRequest) {\n                        Text(text = \"关闭\")\n                    }\n                }\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/TextMenu.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport li.songe.gkd.ui.style.itemPadding\nimport li.songe.gkd.util.Option\nimport li.songe.gkd.util.OptionIcon\nimport li.songe.gkd.util.OptionMenuLabel\n\n@Composable\nfun <T> TextMenu(\n    modifier: Modifier = Modifier,\n    title: String,\n    option: Option<T>,\n    onOptionChange: ((Option<T>) -> Unit),\n) {\n    var expanded by remember { mutableStateOf(false) }\n    Row(\n        modifier = Modifier\n            .clickable {\n                expanded = true\n            }\n            .fillMaxWidth().let {\n                if (modifier == Modifier) {\n                    it.itemPadding()\n                } else {\n                    it.then(modifier)\n                }\n            },\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.SpaceBetween\n    ) {\n        Text(\n            text = title,\n            style = MaterialTheme.typography.bodyLarge,\n        )\n        Row(\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Text(\n                text = option.label,\n                style = MaterialTheme.typography.bodyMedium,\n            )\n            PerfIcon(\n                imageVector = PerfIcon.UnfoldMore,\n            )\n            DropdownMenu(\n                expanded = expanded,\n                onDismissRequest = { expanded = false }\n            ) {\n                option.options.forEach { otherOption ->\n                    val selected = remember { otherOption.value == option.value }\n                    DropdownMenuItem(\n                        modifier = if (selected) Modifier.background(MaterialTheme.colorScheme.onSecondary) else Modifier,\n                        leadingIcon = if (otherOption is OptionIcon) ({\n                            PerfIcon(\n                                imageVector = otherOption.icon,\n                            )\n                        }) else null,\n                        text = {\n                            val text = if (otherOption is OptionMenuLabel) {\n                                otherOption.menuLabel\n                            } else {\n                                otherOption.label\n                            }\n                            Text(text = text)\n                        },\n                        onClick = {\n                            expanded = false\n                            if (otherOption != option) {\n                                onOptionChange(otherOption)\n                            }\n                        },\n                    )\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/TextSwitch.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.stateDescription\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.unit.dp\nimport li.songe.gkd.ui.style.itemPadding\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun TextSwitch(\n    modifier: Modifier = Modifier,\n    title: String,\n    paddingDisabled: Boolean = false,\n    subtitle: String? = null,\n    suffix: String? = null,\n    suffixUnderline: Boolean = false,\n    onSuffixClick: (() -> Unit)? = null,\n    suffixIcon: (@Composable () -> Unit)? = null,\n    checked: Boolean = true,\n    enabled: Boolean = true,\n    onCheckedChange: ((Boolean) -> Unit)? = null,\n    onClick: (() -> Unit)? = { onCheckedChange?.invoke(!checked) },\n    onClickLabel: String? = \"切换${title}状态\",\n) {\n    val topModifier = if (onClick != null) {\n        modifier.clickable(onClick = onClick, onClickLabel = onClickLabel)\n    } else {\n        modifier\n    }\n    Row(\n        modifier = if (paddingDisabled) topModifier else topModifier.itemPadding(),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.spacedBy(12.dp)\n    ) {\n        Column(modifier = Modifier.weight(1f)) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.bodyLarge,\n            )\n            if (subtitle != null) {\n                if (suffix != null) {\n                    FlowRow(\n                        modifier = Modifier.fillMaxWidth(),\n                    ) {\n                        Text(\n                            text = subtitle,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                        Spacer(modifier = Modifier.width(4.dp))\n                        Text(\n                            text = suffix, style = MaterialTheme.typography.bodyMedium.run {\n                                if (suffixUnderline) {\n                                    copy(textDecoration = TextDecoration.Underline)\n                                } else {\n                                    this\n                                }\n                            },\n                            color = MaterialTheme.colorScheme.primary,\n                            modifier = if (onSuffixClick != null) Modifier.clickable(\n                                onClick = throttle(fn = onSuffixClick),\n                            ) else Modifier\n                        )\n                    }\n                } else {\n                    Text(\n                        text = subtitle,\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n            }\n        }\n        suffixIcon?.invoke()\n        PerfSwitch(\n            checked = checked,\n            enabled = enabled,\n            onCheckedChange = onCheckedChange?.let { throttle(fn = it) },\n            modifier = Modifier.semantics {\n                this.stateDescription = title + if (checked) \"已开启\" else \"已关闭\"\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/TooltipIconButtonBox.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.material3.PlainTooltip\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TooltipAnchorPosition\nimport androidx.compose.material3.TooltipBox\nimport androidx.compose.material3.TooltipDefaults\nimport androidx.compose.material3.rememberTooltipState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport li.songe.gkd.ui.share.LocalIsTalkbackEnabled\n\n@Composable\nfun TooltipIconButtonBox(contentDescription: String?, content: @Composable () -> Unit) {\n    // 视障用户使用 TalkBack 朗读 contentDescription，不需要 Tooltip\n    if (contentDescription.isNullOrEmpty() || LocalIsTalkbackEnabled.current.collectAsState().value) {\n        content()\n    } else {\n        TooltipBox(\n            tooltip = { PlainTooltip { Text(text = contentDescription) } },\n            state = rememberTooltipState(),\n            positionProvider = TooltipDefaults.rememberTooltipPositionProvider(\n                TooltipAnchorPosition.Start\n            ),\n            content = content,\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/TowLineText.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\n\n@Composable\nfun TowLineText(\n    title: String,\n    subtitle: String,\n    modifier: Modifier = Modifier,\n    showApp: Boolean = false,\n) {\n    Column(\n        modifier = modifier,\n    ) {\n        Text(\n            text = title,\n            maxLines = 1,\n            softWrap = false,\n            overflow = TextOverflow.MiddleEllipsis,\n            style = MaterialTheme.typography.titleMedium,\n        )\n        CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.titleSmall) {\n            if (showApp) {\n                AppNameText(appId = subtitle)\n            } else {\n                Text(\n                    text = subtitle,\n                    maxLines = 1,\n                    overflow = TextOverflow.MiddleEllipsis,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/component/UploadOptions.kt",
    "content": "package li.songe.gkd.ui.component\n\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.gkd.MainViewModel\nimport li.songe.gkd.data.GithubPoliciesAsset\nimport li.songe.gkd.util.GithubCookieException\nimport li.songe.gkd.util.LoadStatus\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.uploadFileToGithub\nimport java.io.File\n\nclass UploadOptions(\n    private val mainVm: MainViewModel,\n) {\n    private val statusFlow = MutableStateFlow<LoadStatus<GithubPoliciesAsset>?>(null)\n    private var job: Job? = null\n    private fun buildTask(\n        cookie: String,\n        getFile: suspend () -> File,\n        onSuccessResult: (suspend (GithubPoliciesAsset) -> Unit)?\n    ) = mainVm.viewModelScope.launchTry(Dispatchers.IO) {\n        statusFlow.value = LoadStatus.Loading()\n        try {\n            val policiesAsset = uploadFileToGithub(cookie, getFile()) {\n                if (statusFlow.value is LoadStatus.Loading) {\n                    statusFlow.value = LoadStatus.Loading(it)\n                }\n            }\n            statusFlow.value = LoadStatus.Success(policiesAsset)\n            onSuccessResult?.invoke(policiesAsset)\n        } catch (e: Exception) {\n            LogUtils.d(e)\n            statusFlow.value = LoadStatus.Failure(e)\n        } finally {\n            job = null\n        }\n    }\n\n\n    private var showHref: (GithubPoliciesAsset) -> String = { it.shortHref }\n    fun startTask(\n        getFile: suspend () -> File,\n        showHref: (GithubPoliciesAsset) -> String = { it.shortHref },\n        onSuccessResult: (suspend (GithubPoliciesAsset) -> Unit)? = null\n    ) {\n        val cookie = mainVm.githubCookieFlow.value\n        if (cookie.isEmpty()) {\n            toast(\"请先设置 cookie 后再上传\")\n            mainVm.showEditCookieDlgFlow.value = true\n            return\n        }\n        if (job != null || statusFlow.value is LoadStatus.Loading) {\n            return\n        }\n        this.showHref = showHref\n        job = buildTask(cookie, getFile, onSuccessResult)\n    }\n\n    private fun stopTask() {\n        if (statusFlow.value is LoadStatus.Loading && job != null) {\n            job?.cancel(\"上传已取消\")\n            job = null\n        }\n    }\n\n\n    @Composable\n    fun ShowDialog() {\n        when (val status = statusFlow.collectAsState().value) {\n            null -> {}\n            is LoadStatus.Loading -> {\n                AlertDialog(\n                    title = { Text(text = \"上传文件中\") },\n                    text = {\n                        val showExactProgress = 0f < status.progress && status.progress < 1f\n                        AnimatedContent(showExactProgress) { showExact ->\n                            if (showExact) {\n                                LinearProgressIndicator(\n                                    progress = { status.progress },\n                                )\n                            } else {\n                                LinearProgressIndicator()\n                            }\n                        }\n                    },\n                    onDismissRequest = { },\n                    confirmButton = {\n                        TextButton(onClick = {\n                            stopTask()\n                        }) {\n                            Text(text = \"终止上传\")\n                        }\n                    },\n                )\n            }\n\n            is LoadStatus.Success -> {\n                val href = showHref(status.result)\n                AlertDialog(\n                    title = { Text(text = \"上传完成\") },\n                    text = { CopyTextCard(text = href) },\n                    onDismissRequest = {},\n                    confirmButton = {\n                        TextButton(onClick = {\n                            statusFlow.value = null\n                        }) {\n                            Text(text = \"关闭\")\n                        }\n                    }\n                )\n            }\n\n            is LoadStatus.Failure -> {\n                AlertDialog(\n                    title = { Text(text = \"上传失败\") },\n                    text = {\n                        Text(text = status.exception.let {\n                            it.message ?: it.toString()\n                        })\n                    },\n                    onDismissRequest = { statusFlow.value = null },\n                    dismissButton = if (status.exception is GithubCookieException) ({\n                        TextButton(onClick = {\n                            statusFlow.value = null\n                            mainVm.showEditCookieDlgFlow.value = true\n                        }) {\n                            Text(text = \"更换 Cookie\")\n                        }\n                    }) else {\n                        null\n                    },\n                    confirmButton = {\n                        TextButton(onClick = {\n                            statusFlow.value = null\n                        }) {\n                            Text(text = \"关闭\")\n                        }\n                    },\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/home/AppListPage.kt",
    "content": "package li.songe.gkd.ui.home\n\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.CheckboxDefaults\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.LocalSaveableStateRegistry\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.onClick\nimport androidx.compose.ui.semantics.stateDescription\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.data.AppInfo\nimport li.songe.gkd.permission.canQueryPkgState\nimport li.songe.gkd.store.blockMatchAppListFlow\nimport li.songe.gkd.ui.AppConfigRoute\nimport li.songe.gkd.ui.EditBlockAppListRoute\nimport li.songe.gkd.ui.component.AnimatedIconButton\nimport li.songe.gkd.ui.component.AnimationFloatingActionButton\nimport li.songe.gkd.ui.component.AppBarTextField\nimport li.songe.gkd.ui.component.AppIcon\nimport li.songe.gkd.ui.component.AppNameText\nimport li.songe.gkd.ui.component.EmptyText\nimport li.songe.gkd.ui.component.MenuGroupCard\nimport li.songe.gkd.ui.component.MenuItemCheckbox\nimport li.songe.gkd.ui.component.MenuItemRadioButton\nimport li.songe.gkd.ui.component.PerfCheckbox\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.QueryPkgAuthCard\nimport li.songe.gkd.ui.component.autoFocus\nimport li.songe.gkd.ui.component.updateDialogOptions\nimport li.songe.gkd.ui.component.useListScrollState\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.asMutableState\nimport li.songe.gkd.ui.share.noRippleClickable\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.appItemPadding\nimport li.songe.gkd.util.AppGroupOption\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.LogUtils\nimport li.songe.gkd.util.appListAuthAbnormalFlow\nimport li.songe.gkd.util.getUpDownTransform\nimport li.songe.gkd.util.ruleSummaryFlow\nimport li.songe.gkd.util.switchItem\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.updateAllAppInfo\nimport li.songe.gkd.util.updateAppMutex\n\n@Composable\nfun useAppListPage(): ScaffoldExt {\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n\n    val vm = viewModel<HomeVm>()\n    val appInfos by vm.appInfosFlow.collectAsState()\n    val searchStr by vm.searchStrFlow.collectAsState()\n    val ruleSummary by ruleSummaryFlow.collectAsState()\n\n    val globalDesc = if (ruleSummary.globalGroups.isNotEmpty()) {\n        \"${ruleSummary.globalGroups.size}全局\"\n    } else {\n        null\n    }\n    val showSearchBar by vm.showSearchBarFlow.collectAsState()\n    val refreshing by updateAppMutex.state.collectAsState()\n    val pullToRefreshState = rememberPullToRefreshState()\n    val editWhiteListMode by vm.editWhiteListModeFlow.collectAsState()\n    val savedStateRegistry = LocalSaveableStateRegistry.current\n    if (savedStateRegistry != null) {\n        LogUtils.d(savedStateRegistry.performSave())\n    }\n    val scrollKey = rememberSaveable { mutableIntStateOf(0) }\n    val (scrollBehavior, listState) = useListScrollState(scrollKey)\n    LaunchedEffect(null) {\n        listOf(\n            canQueryPkgState.stateFlow,\n            vm.appInfosFlow,\n        ).forEach {\n            launch {\n                it.drop(1).collect {\n                    scrollKey.intValue++\n                }\n            }\n        }\n        mainVm.resetPageScrollEvent.collect {\n            if (it == BottomNavItem.AppList) {\n                scrollKey.intValue++\n            }\n        }\n    }\n    return ScaffoldExt(\n        navItem = BottomNavItem.AppList,\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            DisposableEffect(null) {\n                onDispose {\n                    if (vm.searchStrFlow.value.isEmpty()) {\n                        vm.showSearchBarFlow.value = false\n                    }\n                    vm.editWhiteListModeFlow.value = false\n                }\n            }\n            PerfTopAppBar(scrollBehavior = scrollBehavior, title = {\n                val firstShowSearchBar = remember { showSearchBar }\n                if (showSearchBar) {\n                    BackHandler {\n                        if (!context.justHideSoftInput()) {\n                            vm.showSearchBarFlow.value = false\n                        }\n                    }\n                    AppBarTextField(\n                        value = searchStr,\n                        onValueChange = { newValue -> vm.searchStrFlow.value = newValue.trim() },\n                        hint = \"请输入应用名称/ID\",\n                        modifier = if (firstShowSearchBar) Modifier else Modifier.autoFocus(),\n                    )\n                } else {\n                    val titleModifier = Modifier\n                        .noRippleClickable(\n                            onClick = throttle {\n                                scrollKey.intValue++\n                            }\n                        )\n                    if (editWhiteListMode) {\n                        BackHandler {\n                            vm.editWhiteListModeFlow.value = false\n                        }\n                    }\n                    AnimatedContent(\n                        targetState = editWhiteListMode,\n                        transitionSpec = { getUpDownTransform() },\n                    ) { localEditWhiteListMode ->\n                        if (localEditWhiteListMode) {\n                            Text(\n                                modifier = titleModifier,\n                                text = \"应用白名单\",\n                            )\n                        } else {\n                            Text(\n                                modifier = titleModifier,\n                                text = BottomNavItem.AppList.label,\n                            )\n                        }\n                    }\n                }\n            }, actions = {\n                if (appListAuthAbnormalFlow.collectAsState().value) {\n                    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {\n                        PerfIconButton(\n                            imageVector = PerfIcon.WarningAmber,\n                            contentDescription = canQueryPkgState.name + \"异常\",\n                            onClick = throttle {\n                                mainVm.dialogFlow.updateDialogOptions(\n                                    title = \"权限异常\",\n                                    text = \"检测到已授予「${canQueryPkgState.name}」但实际获取应用数量稀少，已使用其它方式获取但可能不全，在应用列表下拉刷新可重新获取，若无法解决可尝试关闭权限后重新授予或重启设备\"\n                                )\n                            },\n                        )\n                    }\n                }\n                PerfIconButton(\n                    imageVector = PerfIcon.Block,\n                    contentDescription = \"切换白名单编辑模式\",\n                    onClickLabel = if (editWhiteListMode) \"退出编辑\" else \"进入编辑\",\n                    colors = IconButtonDefaults.iconButtonColors(\n                        contentColor = if (editWhiteListMode) {\n                            CheckboxDefaults.colors().checkedBoxColor\n                        } else {\n                            LocalContentColor.current\n                        }\n                    ),\n                    onClick = throttle {\n                        vm.editWhiteListModeFlow.update { !it }\n                    },\n                )\n                AnimatedIconButton(\n                    onClick = throttle {\n                        if (showSearchBar) {\n                            if (vm.searchStrFlow.value.isEmpty()) {\n                                vm.showSearchBarFlow.value = false\n                            } else {\n                                vm.searchStrFlow.value = \"\"\n                            }\n                        } else {\n                            vm.showSearchBarFlow.value = true\n                        }\n                    },\n                    id = R.drawable.ic_anim_search_close,\n                    atEnd = showSearchBar,\n                    contentDescription = if (showSearchBar) \"关闭搜索\" else \"搜索应用列表\",\n                )\n                var expanded by remember { mutableStateOf(false) }\n                PerfIconButton(\n                    imageVector = PerfIcon.Sort,\n                    contentDescription = \"排序筛选\",\n                    onClick = {\n                        expanded = true\n                    }\n                )\n                Box(\n                    modifier = Modifier\n                        .wrapContentSize(Alignment.TopStart)\n                ) {\n                    DropdownMenu(\n                        expanded = expanded,\n                        onDismissRequest = { expanded = false }\n                    ) {\n                        MenuGroupCard(inTop = true, title = \"排序\") {\n                            var sortType by vm.sortTypeFlow.asMutableState()\n                            AppSortOption.objects.forEach { option ->\n                                MenuItemRadioButton(\n                                    text = option.label,\n                                    selected = sortType == option,\n                                    onClick = { sortType = option },\n                                )\n                            }\n                        }\n                        MenuGroupCard(title = \"分组\") {\n                            var appGroupType by vm.appGroupTypeFlow.asMutableState()\n                            AppGroupOption.normalObjects.forEach { option ->\n                                val newValue = option.invert(appGroupType)\n                                MenuItemCheckbox(\n                                    enabled = newValue != 0,\n                                    text = option.label,\n                                    checked = option.include(appGroupType),\n                                    onClick = { appGroupType = newValue },\n                                )\n                            }\n                        }\n                        MenuGroupCard(title = \"筛选\") {\n                            MenuItemCheckbox(\n                                text = \"白名单\",\n                                stateFlow = vm.showBlockAppFlow,\n                            )\n                        }\n                    }\n                }\n            })\n        },\n        floatingActionButton = {\n            AnimationFloatingActionButton(\n                visible = editWhiteListMode,\n                contentDescription = \"编辑白名单\",\n                onClick = {\n                    mainVm.navigatePage(EditBlockAppListRoute)\n                },\n                imageVector = PerfIcon.Edit,\n            )\n        }\n    ) { contentPadding ->\n        val canQueryPkg by canQueryPkgState.stateFlow.collectAsState()\n        PullToRefreshBox(\n            modifier = Modifier.padding(contentPadding),\n            state = pullToRefreshState,\n            isRefreshing = refreshing,\n            onRefresh = { updateAllAppInfo() }\n        ) {\n            LazyColumn(\n                modifier = Modifier.fillMaxSize(),\n                state = listState\n            ) {\n                if (!canQueryPkg) {\n                    item(key = 1, contentType = 1) {\n                        QueryPkgAuthCard()\n                    }\n                }\n                items(appInfos, { it.id }) { appInfo ->\n                    val desc = run {\n                        if (editWhiteListMode) return@run null\n                        val appGroups = ruleSummary.appIdToAllGroups[appInfo.id] ?: emptyList()\n                        val appDesc = if (appGroups.isNotEmpty()) {\n                            when (val disabledCount = appGroups.count { g -> !g.enable }) {\n                                0 -> \"${appGroups.size}组规则\"\n                                appGroups.size -> \"${appGroups.size}组规则/${disabledCount}关闭\"\n                                else -> {\n                                    \"${appGroups.size}组规则/${appGroups.size - disabledCount}启用/${disabledCount}关闭\"\n                                }\n                            }\n                        } else {\n                            null\n                        }\n                        if (globalDesc != null) {\n                            if (appDesc != null) {\n                                \"$globalDesc/$appDesc\"\n                            } else {\n                                globalDesc\n                            }\n                        } else {\n                            appDesc\n                        }\n                    }\n                    AppItemCard(\n                        appInfo = appInfo,\n                        desc = desc,\n                    )\n                }\n                item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                    Spacer(modifier = Modifier.height(EmptyHeight))\n                    if (appInfos.isEmpty() && searchStr.isNotEmpty()) {\n                        EmptyText(text = if (vm.appFilter.showAllAppFlow.collectAsState().value) \"暂无搜索结果\" else \"暂无搜索结果，或修改筛选\")\n                        Spacer(modifier = Modifier.height(EmptyHeight / 2))\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun AppItemCard(\n    appInfo: AppInfo,\n    desc: String?,\n) {\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    val vm = viewModel<HomeVm>()\n    val editWhiteListMode = vm.editWhiteListModeFlow.collectAsState().value\n    val inWhiteList = blockMatchAppListFlow.collectAsState().value.contains(appInfo.id)\n    Row(\n        modifier = Modifier\n            .clickable(\n                onClick = throttle {\n                    if (vm.editWhiteListModeFlow.value) {\n                        blockMatchAppListFlow.update { it.switchItem(appInfo.id) }\n                    } else {\n                        context.justHideSoftInput()\n                        mainVm.navigatePage(AppConfigRoute(appInfo.id))\n                    }\n                })\n            .clearAndSetSemantics {\n                contentDescription = if (editWhiteListMode) {\n                    appInfo.name\n                } else {\n                    \"应用：${appInfo.name}，${desc ?: appInfo.id}\"\n                }\n                if (inWhiteList) {\n                    stateDescription = \"已加入白名单\"\n                } else if (editWhiteListMode) {\n                    stateDescription = \"未加入白名单\"\n                }\n                onClick(\n                    label = if (editWhiteListMode) if (inWhiteList) \"从白名单中移除\" else \"加入白名单\" else \"进入规则汇总页面\",\n                    action = null\n                )\n            }\n            .appItemPadding(),\n        horizontalArrangement = Arrangement.spacedBy(12.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        AppIcon(appId = appInfo.id)\n        Column(\n            modifier = Modifier\n                .weight(1f),\n            verticalArrangement = Arrangement.Center\n        ) {\n            AppNameText(appInfo = appInfo)\n            Text(\n                text = desc ?: appInfo.id,\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                softWrap = false\n            )\n        }\n        if (editWhiteListMode) {\n            PerfCheckbox(\n                key = appInfo.id,\n                checked = inWhiteList,\n            )\n        } else if (inWhiteList) {\n            PerfIcon(\n                modifier = Modifier\n                    .padding(2.dp)\n                    .size(20.dp),\n                imageVector = PerfIcon.Block,\n                tint = MaterialTheme.colorScheme.secondary,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt",
    "content": "package li.songe.gkd.ui.home\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.semantics.onClick\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport kotlinx.coroutines.Dispatchers\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.data.SubsConfig\nimport li.songe.gkd.permission.appOpsRestrictedFlow\nimport li.songe.gkd.permission.writeSecureSettingsState\nimport li.songe.gkd.service.A11yService\nimport li.songe.gkd.service.ActivityService\nimport li.songe.gkd.service.StatusService\nimport li.songe.gkd.service.a11yPartDisabledFlow\nimport li.songe.gkd.service.switchAutomatorService\nimport li.songe.gkd.service.topAppIdFlow\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.shizuku.uiAutomationFlow\nimport li.songe.gkd.store.actualA11yScopeAppList\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.ActionLogRoute\nimport li.songe.gkd.ui.ActivityLogRoute\nimport li.songe.gkd.ui.AppConfigRoute\nimport li.songe.gkd.ui.AuthA11yRoute\nimport li.songe.gkd.ui.WebViewRoute\nimport li.songe.gkd.ui.component.GroupNameText\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfSwitch\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.textSize\nimport li.songe.gkd.ui.component.useScrollBehaviorState\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.itemHorizontalPadding\nimport li.songe.gkd.ui.style.itemVerticalPadding\nimport li.songe.gkd.ui.style.surfaceCardColors\nimport li.songe.gkd.util.HOME_PAGE_URL\nimport li.songe.gkd.util.ShortUrlSet\nimport li.songe.gkd.util.latestRecordDescFlow\nimport li.songe.gkd.util.latestRecordFlow\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.throttle\n\n@Composable\nfun useControlPage(): ScaffoldExt {\n    val context = LocalActivity.current as MainActivity\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel<HomeVm>()\n    val scrollKey = rememberSaveable { mutableIntStateOf(0) }\n    val (scrollBehavior, scrollState) = useScrollBehaviorState(scrollKey)\n    LaunchedEffect(null) {\n        mainVm.resetPageScrollEvent.collect {\n            if (it == BottomNavItem.Control) {\n                scrollKey.intValue++\n            }\n        }\n    }\n    return ScaffoldExt(\n        navItem = BottomNavItem.Control,\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(scrollBehavior = scrollBehavior, title = {\n                Text(\n                    text = stringResource(R.string.app_name)\n                )\n            }, actions = {\n                PerfIconButton(\n                    imageVector = PerfIcon.RocketLaunch,\n                    onClickLabel = \"前往工作模式页面\",\n                    contentDescription = \"工作模式\",\n                    onClick = throttle {\n                        mainVm.navigatePage(AuthA11yRoute)\n                    },\n                )\n            })\n        }) { contentPadding ->\n        val store by storeFlow.collectAsState()\n\n        val a11yRunning by A11yService.isRunning.collectAsState()\n        val manageRunning by StatusService.isRunning.collectAsState()\n        val writeSecureSettings by writeSecureSettingsState.stateFlow.collectAsState()\n\n        Column(\n            modifier = Modifier\n                .verticalScroll(scrollState)\n                .padding(contentPadding)\n                .padding(horizontal = itemHorizontalPadding),\n            verticalArrangement = Arrangement.spacedBy(itemHorizontalPadding / 2)\n        ) {\n            if (appOpsRestrictedFlow.collectAsState().value) {\n                Card(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .semantics(mergeDescendants = true) {\n                            this.onClick(label = \"前往解除限制页面\", action = null)\n                        },\n                    shape = MaterialTheme.shapes.large,\n                    colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),\n                    onClick = throttle {\n                        mainVm.navigateWebPage(ShortUrlSet.URL2)\n                    },\n                ) {\n                    Row(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(itemVerticalPadding),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    ) {\n                        PerfIcon(imageVector = PerfIcon.WarningAmber)\n                        Text(\n                            modifier = Modifier.weight(1f),\n                            text = \"检测到权限受限制，请前往解除\",\n                            style = MaterialTheme.typography.bodyLarge,\n                        )\n                        PerfIcon(imageVector = PerfIcon.KeyboardArrowRight)\n                    }\n                }\n            }\n            if (store.useA11y || actualA11yScopeAppList.contains(topAppIdFlow.collectAsState().value)) {\n                PageSwitchItemCard(\n                    imageVector = PerfIcon.Memory,\n                    title = \"服务状态\",\n                    subtitle = if (a11yRunning) {\n                        \"无障碍正在运行\"\n                    } else if (mainVm.a11yServiceEnabledFlow.collectAsState().value) {\n                        \"无障碍发生故障\"\n                    } else if (writeSecureSettings) {\n                        if (store.enableAutomator && a11yPartDisabledFlow.collectAsState().value) {\n                            \"无障碍局部关闭\"\n                        } else {\n                            \"无障碍已关闭\"\n                        }\n                    } else {\n                        \"无障碍未授权\"\n                    },\n                    checked = a11yRunning,\n                    onCheckedChange = { newEnabled ->\n                        if (newEnabled && !writeSecureSettingsState.value) {\n                            mainVm.navigatePage(AuthA11yRoute)\n                        } else {\n                            switchAutomatorService()\n                        }\n                    },\n                )\n            } else {\n                PageSwitchItemCard(\n                    imageVector = PerfIcon.Memory,\n                    title = \"服务状态\",\n                    subtitle = if (uiAutomationFlow.collectAsState().value != null) {\n                        \"自动化正在运行\"\n                    } else if (!shizukuContextFlow.collectAsState().value.ok) {\n                        \"自动化未授权\"\n                    } else {\n                        if (store.enableAutomator && a11yPartDisabledFlow.collectAsState().value) {\n                            \"自动化局部关闭\"\n                        } else {\n                            \"自动化已关闭\"\n                        }\n                    },\n                    checked = uiAutomationFlow.collectAsState().value != null,\n                    onCheckedChange = vm.viewModelScope.launchAsFn(Dispatchers.IO) { newEnabled ->\n                        if (newEnabled) {\n                            mainVm.guardShizukuContext()\n                        }\n                        switchAutomatorService()\n                    },\n                )\n            }\n\n            PageSwitchItemCard(\n                imageVector = PerfIcon.Notifications,\n                title = \"常驻通知\",\n                subtitle = \"显示运行状态及统计数据\",\n                checked = manageRunning && store.enableStatusService,\n                onCheckedChange = vm.viewModelScope.launchAsFn<Boolean> {\n                    if (it) {\n                        StatusService.requestStart(context)\n                    } else {\n                        StatusService.stop()\n                        storeFlow.value = store.copy(\n                            enableStatusService = false\n                        )\n                    }\n                },\n            )\n\n            ServerStatusCard()\n\n            PageItemCard(\n                title = \"触发记录\",\n                subtitle = \"规则误触可定位关闭\",\n                imageVector = PerfIcon.History,\n                onClickLabel = \"打开触发记录页面\",\n                onClick = {\n                    mainVm.navigatePage(ActionLogRoute())\n                })\n\n            if (ActivityService.isRunning.collectAsState().value) {\n                PageItemCard(\n                    title = \"界面日志\",\n                    subtitle = \"记录打开的应用及界面\",\n                    imageVector = PerfIcon.Layers,\n                    onClickLabel = \"打开界面日志页面\",\n                    onClick = {\n                        mainVm.navigatePage(ActivityLogRoute)\n                    })\n            }\n\n            PageItemCard(\n                title = \"了解 GKD\",\n                subtitle = \"查阅规则文档和常见问题\",\n                imageVector = PerfIcon.HelpOutline,\n                onClickLabel = \"打开 GKD 文档页面\",\n                onClick = {\n                    mainVm.navigatePage(WebViewRoute(initUrl = HOME_PAGE_URL))\n                })\n            Spacer(modifier = Modifier.height(EmptyHeight))\n        }\n    }\n}\n\n\n@Composable\nprivate fun PageItemCard(\n    imageVector: ImageVector,\n    title: String,\n    subtitle: String,\n    onClickLabel: String,\n    onClick: () -> Unit,\n) {\n    Card(\n        modifier = Modifier\n            .fillMaxWidth()\n            .semantics {\n                this.onClick(label = onClickLabel, action = null)\n            },\n        shape = MaterialTheme.shapes.large,\n        colors = surfaceCardColors,\n        onClick = throttle(fn = onClick)\n    ) {\n        IconTextCard(\n            imageVector = imageVector,\n        ) {\n            Column(\n                modifier = Modifier.weight(1f)\n            ) {\n                Text(\n                    text = title,\n                    style = MaterialTheme.typography.bodyLarge,\n                )\n                Text(\n                    text = subtitle,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun PageSwitchItemCard(\n    imageVector: ImageVector,\n    title: String,\n    subtitle: String,\n    checked: Boolean,\n    onCheckedChange: (Boolean) -> Unit,\n) {\n    val onClick = throttle { onCheckedChange(!checked) }\n    Card(\n        modifier = Modifier\n            .fillMaxWidth()\n            .semantics(mergeDescendants = true) {\n                this.onClick(label = \"切换$title\", action = null)\n            },\n        shape = MaterialTheme.shapes.large,\n        colors = surfaceCardColors,\n        onClick = onClick,\n    ) {\n        IconTextCard(\n            imageVector = imageVector,\n        ) {\n            Column(\n                modifier = Modifier.weight(1f)\n            ) {\n                Text(\n                    text = title,\n                    style = MaterialTheme.typography.bodyLarge,\n                )\n                Text(\n                    text = subtitle,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n            Spacer(Modifier.width(8.dp))\n            PerfSwitch(\n                checked = checked,\n                onCheckedChange = null,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun IconTextCard(\n    imageVector: ImageVector, content: @Composable () -> Unit\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(itemVerticalPadding),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        PerfIcon(\n            imageVector = imageVector,\n            modifier = Modifier\n                .clip(CircleShape)\n                .background(MaterialTheme.colorScheme.primaryContainer)\n                .padding(8.dp)\n                .size(24.dp),\n            tint = MaterialTheme.colorScheme.primary,\n            contentDescription = null,\n        )\n        Spacer(modifier = Modifier.width(itemHorizontalPadding))\n        content()\n    }\n}\n\n@Composable\nprivate fun ServerStatusCard() {\n    val mainVm = LocalMainViewModel.current\n    val vm = viewModel<HomeVm>()\n    Card(\n        modifier = Modifier\n            .fillMaxWidth()\n            .semantics {\n                onClick(label = \"不执行操作\", action = null)\n            }, shape = RoundedCornerShape(20.dp), colors = surfaceCardColors, onClick = {}) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(\n                    start = itemVerticalPadding,\n                    end = itemVerticalPadding,\n                    top = itemVerticalPadding,\n                    bottom = itemVerticalPadding / 2\n                ), verticalAlignment = Alignment.CenterVertically\n        ) {\n            PerfIcon(\n                imageVector = PerfIcon.Equalizer,\n                modifier = Modifier\n                    .clip(CircleShape)\n                    .background(MaterialTheme.colorScheme.primaryContainer)\n                    .padding(8.dp)\n                    .size(24.dp),\n                tint = MaterialTheme.colorScheme.primary\n            )\n            Spacer(modifier = Modifier.width(itemHorizontalPadding))\n            Column(\n                modifier = Modifier.weight(1f)\n            ) {\n                Text(\n                    text = \"数据概览\",\n                    style = MaterialTheme.typography.bodyLarge,\n                )\n                val usedSubsItemCount by vm.usedSubsItemCountFlow.collectAsState()\n                AnimatedVisibility(usedSubsItemCount > 0) {\n                    Text(\n                        text = \"已开启 $usedSubsItemCount 条订阅\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n            }\n        }\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = itemVerticalPadding)\n        ) {\n            val subsStatus by vm.subsStatusFlow.collectAsState()\n            AnimatedVisibility(subsStatus.isNotEmpty()) {\n                Text(\n                    modifier = Modifier.padding(horizontal = 8.dp),\n                    text = subsStatus,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n\n            val latestRecordDesc by latestRecordDescFlow.collectAsState()\n            if (latestRecordDesc != null) {\n                Row(\n                    modifier = Modifier\n                        .padding(horizontal = 4.dp)\n                        .clip(MaterialTheme.shapes.extraSmall)\n                        .clickable(onClickLabel = \"前往应用的规则汇总页面\", onClick = throttle {\n                            latestRecordFlow.value?.let {\n                                mainVm.navigatePage(\n                                    AppConfigRoute(\n                                        appId = it.appId, focusLog = it\n                                    )\n                                )\n                            }\n                        })\n                        .fillMaxWidth()\n                        .padding(horizontal = 4.dp)\n                ) {\n                    Column(\n                        modifier = Modifier.weight(1f),\n                    ) {\n                        GroupNameText(\n                            modifier = Modifier.fillMaxWidth(),\n                            preText = \"最近触发: \",\n                            isGlobal = latestRecordFlow.collectAsState().value?.groupType == SubsConfig.GlobalGroupType,\n                            text = latestRecordDesc ?: \"\",\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n                    }\n                    PerfIcon(\n                        imageVector = PerfIcon.KeyboardArrowRight,\n                        modifier = Modifier.textSize(style = MaterialTheme.typography.bodyMedium),\n                        tint = MaterialTheme.colorScheme.primary,\n                    )\n                }\n            }\n            Spacer(modifier = Modifier.height(itemVerticalPadding))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/home/HomePage.kt",
    "content": "package li.songe.gkd.ui.home\n\nimport androidx.compose.material3.NavigationBar\nimport androidx.compose.material3.NavigationBarItem\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.share.LocalMainViewModel\n\nsealed class BottomNavItem(\n    val key: Int,\n    val label: String,\n    val icon: ImageVector,\n) {\n    object Control : BottomNavItem(\n        key = 0,\n        label = \"首页\",\n        icon = PerfIcon.Home,\n    )\n\n    object SubsManage : BottomNavItem(\n        key = 1,\n        label = \"订阅\",\n        icon = PerfIcon.FormatListBulleted,\n    )\n\n    object AppList : BottomNavItem(\n        key = 2,\n        label = \"应用\",\n        icon = PerfIcon.Apps,\n    )\n\n    object Settings : BottomNavItem(\n        key = 3,\n        label = \"设置\",\n        icon = PerfIcon.Settings,\n    )\n\n    companion object {\n        val allSubObjects by lazy { arrayOf(Control, SubsManage, AppList, Settings) }\n    }\n}\n\n@Serializable\ndata object HomeRoute : NavKey\n\n@Composable\nfun HomePage() {\n    val mainVm = LocalMainViewModel.current\n    viewModel<HomeVm>() // init state\n    val tab by mainVm.tabFlow.collectAsState()\n    val pages = arrayOf(useControlPage(), useSubsManagePage(), useAppListPage(), useSettingsPage())\n    val page = pages.find { p -> p.navItem.key == tab } ?: pages.first()\n\n    Scaffold(\n        modifier = page.modifier,\n        topBar = page.topBar,\n        floatingActionButton = page.floatingActionButton,\n        bottomBar = {\n            NavigationBar {\n                pages.forEach { page ->\n                    NavigationBarItem(\n                        selected = page.navItem.key == tab,\n                        modifier = Modifier,\n                        onClick = { mainVm.handleClickTab(page.navItem) },\n                        icon = {\n                            PerfIcon(\n                                imageVector = page.navItem.icon,\n                                contentDescription = null,\n                            )\n                        },\n                        label = {\n                            Text(text = page.navItem.label)\n                        })\n                }\n            }\n        },\n        content = page.content\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt",
    "content": "package li.songe.gkd.ui.home\n\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.combine\nimport li.songe.gkd.store.actionCountFlow\nimport li.songe.gkd.store.blockMatchAppListFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.share.BaseViewModel\nimport li.songe.gkd.ui.share.asMutableStateFlow\nimport li.songe.gkd.ui.share.useAppFilter\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.EMPTY_RULE_TIP\nimport li.songe.gkd.util.findOption\nimport li.songe.gkd.util.getSubsStatus\nimport li.songe.gkd.util.ruleSummaryFlow\nimport li.songe.gkd.util.usedSubsEntriesFlow\n\nclass HomeVm : BaseViewModel() {\n\n    val subsStatusFlow by lazy {\n        combine(ruleSummaryFlow, actionCountFlow) { ruleSummary, count ->\n            getSubsStatus(ruleSummary, count)\n        }.stateInit(EMPTY_RULE_TIP)\n    }\n\n    val usedSubsItemCountFlow = usedSubsEntriesFlow.mapNew { it.size }\n\n    val sortTypeFlow = storeFlow.asMutableStateFlow(\n        getter = { AppSortOption.objects.findOption(it.appSort) },\n        setter = {\n            storeFlow.value.copy(appSort = it.value)\n        }\n    )\n    val showBlockAppFlow = storeFlow.asMutableStateFlow(\n        getter = { it.showBlockApp },\n        setter = {\n            storeFlow.value.copy(showBlockApp = it)\n        }\n    )\n    val appGroupTypeFlow = storeFlow.asMutableStateFlow(\n        getter = { it.appGroupType },\n        setter = {\n            storeFlow.value.copy(appGroupType = it)\n        }\n    )\n\n    val editWhiteListModeFlow = MutableStateFlow(false)\n    val blockAppListFlow = MutableStateFlow(blockMatchAppListFlow.value).also { stateFlow ->\n        combine(blockMatchAppListFlow, editWhiteListModeFlow) { it }.launchCollect {\n            if (!editWhiteListModeFlow.value) {\n                stateFlow.value = blockMatchAppListFlow.value\n            }\n        }\n    }\n\n    val appFilter = useAppFilter(\n        appGroupTypeFlow = appGroupTypeFlow,\n        sortTypeFlow = sortTypeFlow,\n        showBlockAppFlow = showBlockAppFlow,\n        blockAppListFlow = blockAppListFlow,\n    )\n    val searchStrFlow = appFilter.searchStrFlow\n\n    val showSearchBarFlow = MutableStateFlow(false).apply {\n        launchCollect {\n            if (!it) {\n                searchStrFlow.value = \"\"\n            }\n        }\n    }\n    val appInfosFlow = appFilter.appListFlow\n\n    val showToastInputDlgFlow = MutableStateFlow(false)\n    val showNotifTextInputDlgFlow = MutableStateFlow(false)\n    val showToastSettingsDlgFlow = MutableStateFlow(false)\n    val showA11yBlockDlgFlow = MutableStateFlow(false)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/home/ScaffoldExt.kt",
    "content": "package li.songe.gkd.ui.home\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport li.songe.gkd.ui.component.PerfTopAppBar\n\ndata class ScaffoldExt(\n    val navItem: BottomNavItem,\n    val modifier: Modifier = Modifier,\n    val topBar: @Composable () -> Unit = {\n        PerfTopAppBar(title = {\n            Text(\n                text = navItem.label,\n            )\n        })\n    },\n    val floatingActionButton: @Composable () -> Unit = {},\n    val content: @Composable (PaddingValues) -> Unit\n)\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/home/SettingsPage.kt",
    "content": "package li.songe.gkd.ui.home\n\nimport android.view.KeyEvent\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.BottomAppBar\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.app\nimport li.songe.gkd.permission.ignoreBatteryOptimizationsState\nimport li.songe.gkd.permission.requiredPermission\nimport li.songe.gkd.permission.writeSecureSettingsState\nimport li.songe.gkd.service.A11yService\nimport li.songe.gkd.service.StatusService\nimport li.songe.gkd.service.fixRestartAutomatorService\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.AboutRoute\nimport li.songe.gkd.ui.AdvancedPageRoute\nimport li.songe.gkd.ui.BlockA11yAppListRoute\nimport li.songe.gkd.ui.component.CustomOutlinedTextField\nimport li.songe.gkd.ui.component.FullscreenDialog\nimport li.songe.gkd.ui.component.PerfCustomIconButton\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.SettingItem\nimport li.songe.gkd.ui.component.TextMenu\nimport li.songe.gkd.ui.component.TextSwitch\nimport li.songe.gkd.ui.component.autoFocus\nimport li.songe.gkd.ui.component.updateDialogOptions\nimport li.songe.gkd.ui.component.useScrollBehaviorState\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.share.asMutableState\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.iconTextSize\nimport li.songe.gkd.ui.style.itemHorizontalPadding\nimport li.songe.gkd.ui.style.titleItemPadding\nimport li.songe.gkd.util.AndroidTarget\nimport li.songe.gkd.util.DarkThemeOption\nimport li.songe.gkd.util.findOption\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.mapState\nimport li.songe.gkd.util.openA11ySettings\nimport li.songe.gkd.util.openAppDetailsSettings\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\n\n@Composable\nfun useSettingsPage(): ScaffoldExt {\n    val mainVm = LocalMainViewModel.current\n    val context = LocalActivity.current as MainActivity\n    val store by storeFlow.collectAsState()\n    val vm = viewModel<HomeVm>()\n\n    var showToastInputDlg by vm.showToastInputDlgFlow.asMutableState()\n\n    if (showToastInputDlg) {\n        var value by remember {\n            mutableStateOf(store.actionToast)\n        }\n        val maxCharLen = 64\n        AlertDialog(\n            properties = DialogProperties(dismissOnClickOutside = false),\n            title = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    Text(text = \"触发提示\")\n                    PerfIconButton(\n                        imageVector = PerfIcon.HelpOutline,\n                        contentDescription = \"文案规则\",\n                        onClickLabel = \"打开文案规则弹窗\",\n                        onClick = throttle {\n                            showToastInputDlg = false\n                            val confirmAction = {\n                                mainVm.dialogFlow.value = null\n                                showToastInputDlg = true\n                            }\n                            mainVm.dialogFlow.updateDialogOptions(\n                                title = \"文案规则\",\n                                text = $$\"触发文案支持变量替换，规则如下\\n${1} 子规则名称\\n${2} 规则组名称\\n${3} 触发次数\\n\\n示例模板\\n${1}/${2}/${3}\\n\\n替换结果\\n子规则a/规则组A/3\",\n                                confirmAction = confirmAction,\n                                onDismissRequest = confirmAction,\n                            )\n                        },\n                    )\n                }\n            },\n            text = {\n                OutlinedTextField(\n                    value = value,\n                    placeholder = {\n                        Text(text = \"请输入提示内容\")\n                    },\n                    onValueChange = {\n                        value = it.take(maxCharLen)\n                    },\n                    supportingText = {\n                        Text(\n                            text = \"${value.length} / $maxCharLen\",\n                            modifier = Modifier.fillMaxWidth(),\n                            textAlign = TextAlign.End,\n                        )\n                    },\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .autoFocus()\n                )\n            },\n            onDismissRequest = { showToastInputDlg = false },\n            confirmButton = {\n                TextButton(enabled = value.isNotEmpty(), onClick = {\n                    if (value != storeFlow.value.actionToast) {\n                        storeFlow.update { it.copy(actionToast = value) }\n                        toast(\"更新成功\")\n                    }\n                    showToastInputDlg = false\n                }) {\n                    Text(text = \"确认\")\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = { showToastInputDlg = false }) {\n                    Text(text = \"取消\")\n                }\n            }\n        )\n    }\n\n    var showNotifTextInputDlg by vm.showNotifTextInputDlgFlow.asMutableState()\n    if (showNotifTextInputDlg) {\n        var titleValue by remember { mutableStateOf(store.customNotifTitle) }\n        var textValue by remember { mutableStateOf(store.customNotifText) }\n        AlertDialog(\n            properties = DialogProperties(dismissOnClickOutside = false),\n            title = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    Text(text = \"通知文案\")\n                    PerfIconButton(\n                        imageVector = PerfIcon.HelpOutline,\n                        contentDescription = \"文案规则\",\n                        onClickLabel = \"打开文案规则弹窗\",\n                        onClick = throttle {\n                            showNotifTextInputDlg = false\n                            val confirmAction = {\n                                mainVm.dialogFlow.value = null\n                                showNotifTextInputDlg = true\n                            }\n                            mainVm.dialogFlow.updateDialogOptions(\n                                title = \"文案规则\",\n                                text = $$\"通知文案支持变量替换，规则如下\\n${i} 全局规则数\\n${k} 应用数\\n${u} 应用规则组数\\n${n} 触发次数\\n\\n示例模板\\n${i}全局/${k}应用/${u}规则组/${n}触发\\n\\n替换结果\\n0全局/1应用/2规则组/3触发\",\n                                confirmAction = confirmAction,\n                                onDismissRequest = confirmAction,\n                            )\n                        },\n                    )\n                }\n            },\n            text = {\n                val titleMaxLen = 32\n                val textMaxLen = 64\n                Column(\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    CustomOutlinedTextField(\n                        label = { Text(\"主标题\") },\n                        value = titleValue,\n                        placeholder = { Text(text = \"请输入内容，支持变量替换\") },\n                        onValueChange = {\n                            titleValue = (if (it.length > titleMaxLen) it.take(titleMaxLen) else it)\n                                .filter { c -> c !in \"\\n\\r\" }\n                        },\n                        supportingText = {\n                            Text(\n                                text = \"${titleValue.length} / $titleMaxLen\",\n                                modifier = Modifier.fillMaxWidth(),\n                                textAlign = TextAlign.End,\n                            )\n                        },\n                        singleLine = true,\n                        modifier = Modifier.fillMaxWidth(),\n                        contentPadding = PaddingValues(12.dp),\n                    )\n                    Spacer(modifier = Modifier.height(4.dp))\n                    CustomOutlinedTextField(\n                        label = { Text(\"副标题\") },\n                        value = textValue,\n                        placeholder = { Text(text = \"请输入内容，支持变量替换\") },\n                        onValueChange = {\n                            textValue = if (it.length > textMaxLen) it.take(textMaxLen) else it\n                        },\n                        supportingText = {\n                            Text(\n                                text = \"${textValue.length} / $textMaxLen\",\n                                modifier = Modifier.fillMaxWidth(),\n                                textAlign = TextAlign.End,\n                            )\n                        },\n                        maxLines = 4,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .autoFocus(),\n                        contentPadding = PaddingValues(12.dp),\n                    )\n                }\n            },\n            onDismissRequest = {\n                showNotifTextInputDlg = false\n            },\n            confirmButton = {\n                TextButton(onClick = {\n                    context.justHideSoftInput()\n                    if (store.customNotifTitle != textValue || store.customNotifText != textValue) {\n                        storeFlow.update {\n                            it.copy(\n                                customNotifTitle = titleValue,\n                                customNotifText = textValue\n                            )\n                        }\n                        toast(\"更新成功\")\n                    }\n                    showNotifTextInputDlg = false\n                }) {\n                    Text(\n                        text = \"确认\",\n                    )\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = { showNotifTextInputDlg = false }) {\n                    Text(\n                        text = \"取消\",\n                    )\n                }\n            })\n    }\n\n    var showToastSettingsDlg by vm.showToastSettingsDlgFlow.asMutableState()\n    if (showToastSettingsDlg) {\n        AlertDialog(\n            onDismissRequest = { showToastSettingsDlg = false },\n            title = { Text(\"提示设置\") },\n            text = {\n                TextSwitch(\n                    paddingDisabled = true,\n                    title = \"系统提示\",\n                    subtitle = \"系统样式触发提示\",\n                    suffix = \"查看限制\",\n                    onSuffixClick = {\n                        showToastSettingsDlg = false\n                        val confirmAction = {\n                            mainVm.dialogFlow.value = null\n                            showToastSettingsDlg = true\n                        }\n                        mainVm.dialogFlow.updateDialogOptions(\n                            title = \"限制说明\",\n                            text = \"系统 Toast 存在频率限制, 触发过于频繁会被系统强制不显示\\n\\n如果只使用开屏一类低频率规则可使用系统提示, 否则建议关闭此项使用自定义样式提示\",\n                            confirmAction = confirmAction,\n                            onDismissRequest = confirmAction,\n                        )\n                    },\n                    checked = store.useSystemToast,\n                    onCheckedChange = {\n                        storeFlow.value = store.copy(\n                            useSystemToast = it\n                        )\n                    })\n            },\n            confirmButton = {\n                TextButton(onClick = { showToastSettingsDlg = false }) {\n                    Text(\"关闭\")\n                }\n            }\n        )\n    }\n\n    var showA11yBlockDlg by vm.showA11yBlockDlgFlow.asMutableState()\n    if (showA11yBlockDlg) {\n        BlockA11yDialog(onDismissRequest = { showA11yBlockDlg = false })\n    }\n\n    val scrollKey = rememberSaveable { mutableIntStateOf(0) }\n    val (scrollBehavior, scrollState) = useScrollBehaviorState(scrollKey)\n    LaunchedEffect(null) {\n        mainVm.resetPageScrollEvent.collect {\n            if (it == BottomNavItem.Settings) {\n                scrollKey.intValue++\n            }\n        }\n    }\n    return ScaffoldExt(\n        navItem = BottomNavItem.Settings,\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(\n                scrollBehavior = scrollBehavior,\n                title = {\n                    Text(\n                        text = BottomNavItem.Settings.label,\n                    )\n                },\n            )\n        },\n    ) { contentPadding ->\n        Column(\n            modifier = Modifier\n                .verticalScroll(scrollState)\n                .padding(contentPadding)\n        ) {\n\n            Text(\n                text = \"常规\",\n                modifier = Modifier.titleItemPadding(showTop = false),\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.primary,\n            )\n\n            TextSwitch(\n                title = \"触发提示\",\n                subtitle = store.actionToast,\n                checked = store.toastWhenClick,\n                onClickLabel = \"打开触发提示弹窗\",\n                onClick = {\n                    showToastInputDlg = true\n                },\n                suffixIcon = {\n                    PerfCustomIconButton(\n                        size = 32.dp,\n                        iconSize = 20.dp,\n                        onClickLabel = \"打开提示设置弹窗\",\n                        onClick = throttle { showToastSettingsDlg = true },\n                        id = R.drawable.ic_page_info,\n                        contentDescription = \"提示设置\",\n                    )\n                },\n                onCheckedChange = {\n                    storeFlow.value = store.copy(\n                        toastWhenClick = it\n                    )\n                })\n\n            val subsStatus by vm.subsStatusFlow.collectAsState()\n            TextSwitch(\n                title = \"通知文案\",\n                subtitle = if (store.useCustomNotifText) {\n                    store.customNotifTitle + \" / \" + store.customNotifText\n                } else {\n                    subsStatus\n                },\n                checked = store.useCustomNotifText,\n                onClickLabel = \"打开修改通知文案弹窗\",\n                onClick = { showNotifTextInputDlg = true },\n                onCheckedChange = {\n                    storeFlow.value = store.copy(\n                        useCustomNotifText = it\n                    )\n                })\n\n            TextSwitch(\n                title = \"后台隐藏\",\n                subtitle = \"在「最近任务」隐藏卡片\",\n                checked = store.excludeFromRecents,\n                onCheckedChange = vm.viewModelScope.launchAsFn<Boolean> {\n                    if (it) {\n                        mainVm.dialogFlow.waitResult(\n                            title = \"后台隐藏\",\n                            text = \"隐藏卡片后可能导致部分设备无法给任务卡片加锁后台，建议先加锁后再隐藏，若已加锁或没有锁后台机制请继续\",\n                            confirmText = \"继续\",\n                        )\n                    }\n                    storeFlow.value = store.copy(\n                        excludeFromRecents = !store.excludeFromRecents\n                    )\n                })\n\n            val scope = rememberCoroutineScope()\n            val lazyOn = remember {\n                storeFlow.mapState(scope) { it.enableBlockA11yAppList }.debounce(300)\n                    .stateIn(scope, SharingStarted.Eagerly, store.enableBlockA11yAppList)\n            }.collectAsState()\n            AnimatedVisibility(visible = lazyOn.value) {\n                Text(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .titleItemPadding(),\n                    text = \"无障碍\",\n                    style = MaterialTheme.typography.titleSmall,\n                    color = MaterialTheme.colorScheme.primary,\n                )\n            }\n            TextSwitch(\n                title = \"局部关闭\",\n                subtitle = \"白名单内关闭服务\",\n                checked = store.enableBlockA11yAppList && shizukuContextFlow.collectAsState().value.ok,\n                onCheckedChange = vm.viewModelScope.launchAsFn<Boolean> {\n                    if (it) {\n                        showA11yBlockDlg = true\n                    } else {\n                        storeFlow.value = store.copy(enableBlockA11yAppList = false)\n                        fixRestartAutomatorService()\n                    }\n                },\n            )\n            AnimatedVisibility(visible = lazyOn.value) {\n                SettingItem(title = \"白名单\", onClickLabel = \"进入无障碍白名单页面\", onClick = {\n                    mainVm.navigatePage(BlockA11yAppListRoute)\n                })\n            }\n\n            Text(\n                text = \"外观\",\n                modifier = Modifier.titleItemPadding(),\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.primary,\n            )\n\n            TextMenu(\n                title = \"深色模式\",\n                option = DarkThemeOption.objects.findOption(store.enableDarkTheme),\n                onOptionChange = {\n                    storeFlow.update { s -> s.copy(enableDarkTheme = it.value) }\n                }\n            )\n\n            if (AndroidTarget.S) {\n                TextSwitch(\n                    title = \"动态配色\",\n                    checked = store.enableDynamicColor,\n                    onCheckedChange = {\n                        storeFlow.update { s -> s.copy(enableDynamicColor = it) }\n                    }\n                )\n            }\n\n            Text(\n                text = \"其他\",\n                modifier = Modifier.titleItemPadding(),\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.primary,\n            )\n\n            SettingItem(title = \"高级设置\", onClick = {\n                mainVm.navigatePage(AdvancedPageRoute)\n            })\n\n            SettingItem(title = \"关于\", onClick = {\n                mainVm.navigatePage(AboutRoute)\n            })\n\n            Spacer(modifier = Modifier.height(EmptyHeight))\n        }\n    }\n}\n\n@Composable\nprivate fun BlockA11yDialog(onDismissRequest: () -> Unit) = FullscreenDialog(onDismissRequest) {\n    val mainVm = LocalMainViewModel.current\n    val statusRunning by StatusService.isRunning.collectAsState()\n    val shizukuContext by shizukuContextFlow.collectAsState()\n    val ignoreBatteryOptimizations by ignoreBatteryOptimizationsState.stateFlow.collectAsState()\n    val hasOtherA11y by mainVm.hasOtherA11yFlow.collectAsState()\n    val context = LocalActivity.current as MainActivity\n    Scaffold(\n        topBar = {\n            PerfTopAppBar(\n                navigationIcon = {\n                    PerfIconButton(\n                        imageVector = PerfIcon.Close,\n                        onClickLabel = \"关闭弹窗\",\n                        onClick = onDismissRequest,\n                    )\n                },\n                title = {\n                    Text(text = \"局部关闭\")\n                },\n            )\n        },\n        bottomBar = {\n            BottomAppBar {\n                Spacer(modifier = Modifier.weight(1f))\n                TextButton(\n                    enabled = shizukuContext.ok && statusRunning && ignoreBatteryOptimizations && !hasOtherA11y,\n                    onClick = mainVm.viewModelScope.launchAsFn {\n                        onDismissRequest()\n                        delay(200)\n                        storeFlow.update { it.copy(enableBlockA11yAppList = true) }\n                    }\n                ) {\n                    Text(text = \"继续\")\n                }\n                Spacer(modifier = Modifier.width(itemHorizontalPadding))\n            }\n        },\n    ) { contentPadding ->\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .verticalScroll(rememberScrollState())\n                .padding(contentPadding)\n                .padding(horizontal = itemHorizontalPadding)\n        ) {\n            CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyMedium) {\n                Text(text = \"「局部关闭」可在白名单应用内关闭服务，来解决界面异常，游戏掉帧或无障碍检测的问题\")\n                Spacer(modifier = Modifier.height(16.dp))\n                Text(text = \"使用须知\", style = MaterialTheme.typography.titleMedium)\n                Column(\n                    verticalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    RequiredTextItem(text = \"切换服务会造成短暂触摸卡顿，请自行测试后再编辑白名单\")\n                    RequiredTextItem(text = \"使用其它无障碍应用会导致优化无效，因为无障碍不会被完全关闭\")\n                    RequiredTextItem(text = \"必须确保服务关闭后的持续后台运行，否则会被系统暂停或结束运行导致重启失败\")\n                }\n                Spacer(modifier = Modifier.height(16.dp))\n                Text(text = \"使用条件\", style = MaterialTheme.typography.titleMedium)\n                Column(\n                    verticalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    RequiredTextItem(\n                        text = \"Shizuku 授权\",\n                        enabled = !shizukuContext.ok,\n                        imageVector = if (shizukuContext.ok) PerfIcon.Check else PerfIcon.ArrowForward,\n                        onClick = mainVm.viewModelScope.launchAsFn(Dispatchers.IO) {\n                            mainVm.guardShizukuContext()\n                        },\n                    )\n                    RequiredTextItem(\n                        text = \"开启「常驻通知」\",\n                        enabled = !statusRunning,\n                        imageVector = if (statusRunning) PerfIcon.Check else PerfIcon.ArrowForward,\n                        onClick = mainVm.viewModelScope.launchAsFn {\n                            StatusService.requestStart(context)\n                        },\n                    )\n                    RequiredTextItem(\n                        text = \"省电策略设置为无限制\",\n                        enabled = !ignoreBatteryOptimizations,\n                        imageVector = if (ignoreBatteryOptimizations) PerfIcon.Check else PerfIcon.ArrowForward,\n                        onClickLabel = \"打开忽略电池优化设置页面\",\n                        onClick = mainVm.viewModelScope.launchAsFn {\n                            requiredPermission(context, ignoreBatteryOptimizationsState)\n                        },\n                    )\n                    RequiredTextItem(\n                        text = \"关闭其它应用的无障碍\",\n                        enabled = hasOtherA11y,\n                        imageVector = if (!hasOtherA11y) PerfIcon.Check else PerfIcon.ArrowForward,\n                        onClick = {\n                            if (writeSecureSettingsState.updateAndGet()) {\n                                if (A11yService.isRunning.value) {\n                                    setOf(A11yService.a11yCn)\n                                } else {\n                                    emptySet()\n                                }.let {\n                                    app.putSecureA11yServices(it)\n                                }\n                                toast(\"关闭成功\")\n                            } else {\n                                openA11ySettings()\n                            }\n                        },\n                    )\n                    RequiredTextItem(\n                        text = \"(可选) 允许自启动\",\n                        enabled = true,\n                        imageVector = PerfIcon.OpenInNew,\n                        onClickLabel = \"打开应用详情页面\",\n                        onClick = {\n                            openAppDetailsSettings()\n                        },\n                    )\n                    RequiredTextItem(\n                        text = \"(可选) 在「最近任务」锁定\",\n                        enabled = true,\n                        imageVector = PerfIcon.OpenInNew,\n                        onClickLabel = \"打开应用详情页面\",\n                        onClick = {\n                            val m = shizukuContextFlow.value.inputManager\n                            if (m != null) {\n                                m.key(KeyEvent.KEYCODE_APP_SWITCH)\n                            } else {\n                                toast(\"请先授权 Shizuku\")\n                            }\n                        },\n                    )\n                }\n                Spacer(modifier = Modifier.height(16.dp))\n                Text(text = \"某些场景下服务刚启动时概率不工作，如多次遇到此情况则不建议使用此功能\")\n            }\n            Spacer(modifier = Modifier.height(EmptyHeight))\n        }\n    }\n}\n\n@Composable\nprivate fun RequiredTextItem(\n    text: String,\n    imageVector: ImageVector? = null,\n    enabled: Boolean = false,\n    onClick: (() -> Unit)? = null,\n    onClickLabel: String? = null,\n) {\n    Row(\n        modifier = Modifier\n            .clip(MaterialTheme.shapes.extraSmall)\n            .run {\n                if (onClick != null) {\n                    clickable(\n                        enabled = enabled,\n                        onClick = throttle(onClick),\n                        onClickLabel = onClickLabel\n                    )\n                } else {\n                    this\n                }\n            }\n            .padding(horizontal = 4.dp),\n    ) {\n        val lineHeightDp = LocalDensity.current.run { LocalTextStyle.current.lineHeight.toDp() }\n        Spacer(\n            modifier = Modifier\n                .padding(vertical = (lineHeightDp - 4.dp) / 2)\n                .clip(CircleShape)\n                .background(MaterialTheme.colorScheme.tertiary)\n                .size(4.dp)\n        )\n        Spacer(modifier = Modifier.width(8.dp))\n        Text(text = text)\n        if (imageVector != null) {\n            PerfIcon(\n                imageVector = imageVector,\n                modifier = Modifier.iconTextSize(),\n            )\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/home/SubsManagePage.kt",
    "content": "package li.songe.gkd.ui.home\n\nimport android.content.Intent\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.CheckboxDefaults\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.semantics.onClick\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.stateDescription\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.viewModelScope\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport com.dylanc.activityresult.launcher.launchForResult\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.update\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.R\nimport li.songe.gkd.data.Value\nimport li.songe.gkd.data.importData\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.store.switchStoreEnableMatch\nimport li.songe.gkd.ui.SlowGroupRoute\nimport li.songe.gkd.ui.UpsertRuleGroupRoute\nimport li.songe.gkd.ui.WebViewRoute\nimport li.songe.gkd.ui.component.AnimationFloatingActionButton\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.PerfTopAppBar\nimport li.songe.gkd.ui.component.SubsItemCard\nimport li.songe.gkd.ui.component.TextMenu\nimport li.songe.gkd.ui.component.usePinnedScrollBehaviorState\nimport li.songe.gkd.ui.component.waitResult\nimport li.songe.gkd.ui.share.ListPlaceholder\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.gkd.ui.style.EmptyHeight\nimport li.songe.gkd.ui.style.itemVerticalPadding\nimport li.songe.gkd.util.LOCAL_SUBS_ID\nimport li.songe.gkd.util.ShortUrlSet\nimport li.songe.gkd.util.UpdateTimeOption\nimport li.songe.gkd.util.checkSubsUpdate\nimport li.songe.gkd.util.deleteSubscription\nimport li.songe.gkd.util.findOption\nimport li.songe.gkd.util.getUpDownTransform\nimport li.songe.gkd.util.launchAsFn\nimport li.songe.gkd.util.launchTry\nimport li.songe.gkd.util.mapState\nimport li.songe.gkd.util.ruleSummaryFlow\nimport li.songe.gkd.util.subsItemsFlow\nimport li.songe.gkd.util.subsMapFlow\nimport li.songe.gkd.util.throttle\nimport li.songe.gkd.util.toast\nimport li.songe.gkd.util.updateSubsMutex\nimport li.songe.gkd.util.usedSubsEntriesFlow\nimport sh.calvin.reorderable.ReorderableItem\nimport sh.calvin.reorderable.rememberReorderableLazyListState\n\n@Composable\nfun useSubsManagePage(): ScaffoldExt {\n    val context = LocalActivity.current as MainActivity\n    val mainVm = LocalMainViewModel.current\n\n    val vm = viewModel<HomeVm>()\n    val subItems by subsItemsFlow.collectAsState()\n    val subsIdToRaw by subsMapFlow.collectAsState()\n\n    var orderSubItems by remember {\n        mutableStateOf(subItems)\n    }\n    LaunchedEffect(subItems) {\n        orderSubItems = subItems\n    }\n\n    val refreshing by updateSubsMutex.state.collectAsState()\n    val pullToRefreshState = rememberPullToRefreshState()\n    var isSelectedMode by remember { mutableStateOf(false) }\n    var selectedIds by remember { mutableStateOf(emptySet<Long>()) }\n    val draggedFlag = remember { Value(false) }\n    LaunchedEffect(key1 = isSelectedMode) {\n        if (!isSelectedMode && selectedIds.isNotEmpty()) {\n            selectedIds = emptySet()\n        }\n    }\n    BackHandler(isSelectedMode) {\n        isSelectedMode = false\n    }\n    LaunchedEffect(key1 = subItems.size) {\n        if (subItems.size <= 1) {\n            isSelectedMode = false\n        }\n    }\n\n    var showSettingsDlg by remember { mutableStateOf(false) }\n    if (showSettingsDlg) {\n        AlertDialog(\n            onDismissRequest = { showSettingsDlg = false },\n            title = { Text(\"订阅设置\") },\n            text = {\n                val store by storeFlow.collectAsState()\n                Column {\n                    TextMenu(\n                        modifier = Modifier.padding(0.dp, itemVerticalPadding),\n                        title = \"更新订阅\",\n                        option = UpdateTimeOption.objects.findOption(store.updateSubsInterval)\n                    ) {\n                        storeFlow.update { s -> s.copy(updateSubsInterval = it.value) }\n                    }\n                    val updateValue = throttle {\n                        storeFlow.update { it.copy(subsPowerWarn = !it.subsPowerWarn) }\n                    }\n                    Row(\n                        modifier = Modifier\n                            .padding(0.dp, itemVerticalPadding)\n                            .clickable(\n                                onClickLabel = if (store.subsPowerWarn) \"关闭警告\" else \"开启警告\",\n                                onClick = updateValue\n                            )\n                            .semantics(mergeDescendants = true) {\n                                stateDescription = if (store.subsPowerWarn) \"已开启\" else \"已关闭\"\n                            },\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.SpaceBetween\n                    ) {\n                        Column(\n                            modifier = Modifier.weight(1f)\n                        ) {\n                            Text(\n                                text = \"耗电警告\",\n                                style = MaterialTheme.typography.bodyLarge,\n                            )\n                            Text(\n                                text = \"启用多条订阅时弹窗确认\",\n                                style = MaterialTheme.typography.bodySmall,\n                            )\n                        }\n                        Checkbox(\n                            checked = store.subsPowerWarn,\n                            onCheckedChange = null,\n                        )\n                    }\n                }\n            },\n            confirmButton = {\n                TextButton(onClick = { showSettingsDlg = false }, modifier = Modifier.semantics {\n                    onClick(label = \"关闭弹窗\", action = null)\n                }) {\n                    Text(\"关闭\")\n                }\n            }\n        )\n    }\n\n    val scrollKey = rememberSaveable { mutableIntStateOf(0) }\n    val (scrollBehavior, lazyListState) = usePinnedScrollBehaviorState(scrollKey)\n    LaunchedEffect(null) {\n        mainVm.resetPageScrollEvent.collect {\n            if (it == BottomNavItem.SubsManage) {\n                scrollKey.intValue++\n            }\n        }\n    }\n    return ScaffoldExt(\n        navItem = BottomNavItem.SubsManage,\n        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n        topBar = {\n            PerfTopAppBar(scrollBehavior = scrollBehavior, navigationIcon = {\n                if (isSelectedMode) {\n                    PerfIconButton(\n                        imageVector = PerfIcon.Close,\n                        contentDescription = \"取消选择\",\n                        onClick = { isSelectedMode = false },\n                    )\n                }\n            }, title = {\n                if (isSelectedMode) {\n                    Text(\n                        text = if (selectedIds.isNotEmpty()) selectedIds.size.toString() else \"\",\n                    )\n                } else {\n                    Text(\n                        text = BottomNavItem.SubsManage.label,\n                    )\n                }\n            }, actions = {\n                var expanded by remember { mutableStateOf(false) }\n                AnimatedContent(\n                    targetState = isSelectedMode,\n                    transitionSpec = { getUpDownTransform() },\n                    contentAlignment = Alignment.TopEnd,\n                ) {\n                    Row {\n                        if (it) {\n                            PerfIconButton(\n                                imageVector = PerfIcon.Share,\n                                contentDescription = \"分享选中订阅\",\n                                onClick = {\n                                    mainVm.showShareDataIdsFlow.value = selectedIds\n                                })\n                            val canDeleteIds = if (selectedIds.contains(LOCAL_SUBS_ID)) {\n                                selectedIds - LOCAL_SUBS_ID\n                            } else {\n                                selectedIds\n                            }\n                            if (canDeleteIds.isNotEmpty()) {\n                                val text = \"确定删除所选 ${canDeleteIds.size} 个订阅?\".let { s ->\n                                    if (selectedIds.contains(LOCAL_SUBS_ID)) \"$s\\n\\n注: 不包含本地订阅\" else s\n                                }\n                                PerfIconButton(\n                                    imageVector = PerfIcon.Delete,\n                                    contentDescription = \"删除选中订阅\",\n                                    onClick = vm.viewModelScope.launchAsFn {\n                                        mainVm.dialogFlow.waitResult(\n                                            title = \"删除订阅\",\n                                            text = text,\n                                            error = true,\n                                        )\n                                        deleteSubscription(*canDeleteIds.toLongArray())\n                                        selectedIds = selectedIds - canDeleteIds\n                                        if (selectedIds.size == canDeleteIds.size) {\n                                            isSelectedMode = false\n                                        }\n                                    },\n                                )\n                            }\n                        } else {\n                            val ruleSummary by ruleSummaryFlow.collectAsState()\n                            AnimatedVisibility(\n                                visible = ruleSummary.slowGroupCount > 0,\n                                enter = scaleIn(),\n                                exit = scaleOut(),\n                            ) {\n                                PerfIconButton(\n                                    imageVector = PerfIcon.Eco,\n                                    contentDescription = \"缓慢查询规则列表\",\n                                    onClickLabel = \"查看列表\",\n                                    onClick = throttle {\n                                        mainVm.navigatePage(SlowGroupRoute)\n                                    })\n                            }\n                            val scope = rememberCoroutineScope()\n                            val enableMatch by remember {\n                                storeFlow.mapState(scope) { s -> s.enableMatch }\n                            }.collectAsState()\n                            PerfIconButton(\n                                id = if (enableMatch) R.drawable.ic_flash_on else R.drawable.ic_flash_off,\n                                colors = IconButtonDefaults.iconButtonColors(\n                                    contentColor = if (!enableMatch) {\n                                        CheckboxDefaults.colors().checkedBoxColor\n                                    } else {\n                                        LocalContentColor.current\n                                    }\n                                ),\n                                contentDescription = \"规则匹配\" + if (enableMatch) \"已启用\" else \"已禁用\",\n                                onClickLabel = \"切换开关\",\n                                onClick = throttle { switchStoreEnableMatch() },\n                            )\n                            PerfIconButton(\n                                id = R.drawable.ic_page_info,\n                                contentDescription = \"订阅设置\",\n                                onClickLabel = \"打开设置弹窗\",\n                                onClick = {\n                                    showSettingsDlg = true\n                                })\n                        }\n                    }\n                }\n                PerfIconButton(\n                    imageVector = PerfIcon.MoreVert,\n                    contentDescription = \"更多操作\",\n                    onClick = {\n                        if (updateSubsMutex.mutex.isLocked) {\n                            toast(\"正在刷新订阅，请稍后操作\")\n                        } else {\n                            expanded = true\n                        }\n                    })\n                Box(\n                    modifier = Modifier.wrapContentSize(Alignment.TopStart)\n                ) {\n                    key(isSelectedMode) {\n                        DropdownMenu(\n                            expanded = expanded,\n                            onDismissRequest = { expanded = false }\n                        ) {\n                            if (isSelectedMode) {\n                                DropdownMenuItem(\n                                    text = {\n                                        Text(text = \"全选\")\n                                    },\n                                    onClick = {\n                                        expanded = false\n                                        selectedIds = subItems.map { it.id }.toSet()\n                                    }\n                                )\n                                DropdownMenuItem(\n                                    text = {\n                                        Text(text = \"反选\")\n                                    },\n                                    onClick = {\n                                        expanded = false\n                                        val newSelectedIds =\n                                            subItems.map { it.id }.toSet() - selectedIds\n                                        if (newSelectedIds.isEmpty()) {\n                                            isSelectedMode = false\n                                        }\n                                        selectedIds = newSelectedIds\n                                    }\n                                )\n                            } else {\n                                DropdownMenuItem(\n                                    text = {\n                                        Text(text = \"导入本地数据\")\n                                    },\n                                    onClick = vm.viewModelScope.launchAsFn(Dispatchers.IO) {\n                                        expanded = false\n                                        val result =\n                                            context.launcher.launchForResult(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {\n                                                addCategory(Intent.CATEGORY_OPENABLE)\n                                                type = \"application/zip\"\n                                            })\n                                        val uri = result.data?.data\n                                        if (uri == null) {\n                                            toast(\"未选择文件\")\n                                            return@launchAsFn\n                                        }\n                                        importData(uri)\n                                    },\n                                )\n                                DropdownMenuItem(\n                                    text = { Text(text = \"添加应用规则\") },\n                                    onClick = throttle {\n                                        expanded = false\n                                        mainVm.navigatePage(\n                                            UpsertRuleGroupRoute(\n                                                subsId = LOCAL_SUBS_ID,\n                                                groupKey = null,\n                                                appId = \"\",\n                                                forward = true,\n                                            )\n                                        )\n                                    },\n                                )\n                                DropdownMenuItem(\n                                    text = { Text(text = \"添加全局规则\") },\n                                    onClick = throttle {\n                                        expanded = false\n                                        mainVm.navigatePage(\n                                            UpsertRuleGroupRoute(\n                                                subsId = LOCAL_SUBS_ID,\n                                                groupKey = null,\n                                                appId = null,\n                                                forward = true,\n                                            )\n                                        )\n                                    },\n                                )\n                            }\n                        }\n                    }\n                }\n            })\n        },\n        floatingActionButton = {\n            AnimationFloatingActionButton(\n                contentDescription = \"添加订阅\",\n                onClickLabel = \"打开添加订阅弹窗\",\n                visible = !isSelectedMode,\n                onClick = {\n                    if (updateSubsMutex.mutex.isLocked) {\n                        toast(\"正在刷新订阅,请稍后操作\")\n                    } else {\n                        mainVm.viewModelScope.launchTry {\n                            val url = mainVm.inputSubsLinkOption.getResult() ?: return@launchTry\n                            mainVm.addOrModifySubs(url)\n                        }\n                    }\n                },\n                imageVector = PerfIcon.Add,\n            )\n        },\n    ) { contentPadding ->\n        val reorderableLazyColumnState =\n            rememberReorderableLazyListState(lazyListState) { from, to ->\n                orderSubItems = orderSubItems.toMutableList().apply {\n                    add(to.index, removeAt(from.index))\n                    forEachIndexed { index, subsItem ->\n                        if (subsItem.order != index) {\n                            this[index] = subsItem.copy(order = index)\n                        }\n                    }\n                }\n                draggedFlag.value = true\n            }\n        PullToRefreshBox(\n            modifier = Modifier.padding(contentPadding),\n            state = pullToRefreshState,\n            isRefreshing = refreshing,\n            onRefresh = { checkSubsUpdate(true) }\n        ) {\n            LazyColumn(\n                state = lazyListState,\n                modifier = Modifier.fillMaxSize(),\n            ) {\n                itemsIndexed(orderSubItems, { _, subItem -> subItem.id }) { index, subItem ->\n                    val canDrag = !refreshing && orderSubItems.size > 1\n                    ReorderableItem(\n                        state = reorderableLazyColumnState,\n                        key = subItem.id,\n                        enabled = canDrag,\n                    ) {\n                        val interactionSource = remember { MutableInteractionSource() }\n                        SubsItemCard(\n                            modifier = Modifier.longPressDraggableHandle(\n                                enabled = canDrag,\n                                interactionSource = interactionSource,\n                                onDragStarted = {\n                                    if (orderSubItems.size > 1 && !isSelectedMode) {\n                                        isSelectedMode = true\n                                        selectedIds = setOf(subItem.id)\n                                    }\n                                },\n                                onDragStopped = {\n                                    if (draggedFlag.value) {\n                                        draggedFlag.value = false\n                                        isSelectedMode = false\n                                        selectedIds = emptySet()\n                                    }\n                                    val changeItems = orderSubItems.filter { newItem ->\n                                        subItems.find { oldItem -> oldItem.id == newItem.id }?.order != newItem.order\n                                    }\n                                    if (changeItems.isNotEmpty()) {\n                                        vm.viewModelScope.launchTry {\n                                            DbSet.subsItemDao.batchUpdateOrder(changeItems)\n                                        }\n                                    }\n                                },\n                            ),\n                            interactionSource = interactionSource,\n                            subsItem = subItem,\n                            subscription = subsIdToRaw[subItem.id],\n                            index = index + 1,\n                            isSelectedMode = isSelectedMode,\n                            isSelected = selectedIds.contains(subItem.id),\n                            onCheckedChange = mainVm.viewModelScope.launchAsFn { checked ->\n                                if (checked && storeFlow.value.subsPowerWarn && !subItem.isLocal && usedSubsEntriesFlow.value.any { !it.subsItem.isLocal }) {\n                                    mainVm.dialogFlow.waitResult(\n                                        title = \"耗电警告\",\n                                        textContent = {\n                                            Column {\n                                                Text(text = \"启用多个远程订阅可能导致执行大量重复规则, 这可能造成规则执行卡顿以及多余耗电\\n\\n请认真考虑后再确认开启！！！\\n\")\n                                                Text(\n                                                    text = \"查看耗电说明\",\n                                                    modifier = Modifier.clickable(onClick = throttle {\n                                                        mainVm.dialogFlow.value = null\n                                                        mainVm.navigatePage(\n                                                            WebViewRoute(\n                                                                initUrl = ShortUrlSet.URL6\n                                                            )\n                                                        )\n                                                    }),\n                                                    textDecoration = TextDecoration.Underline,\n                                                    color = MaterialTheme.colorScheme.primary,\n                                                )\n                                            }\n                                        },\n                                        confirmText = \"仍然启用\",\n                                        error = true\n                                    )\n                                }\n                                DbSet.subsItemDao.updateEnable(subItem.id, checked)\n                            },\n                            onSelectedChange = {\n                                val newSelectedIds = if (selectedIds.contains(subItem.id)) {\n                                    selectedIds.toMutableSet().apply {\n                                        remove(subItem.id)\n                                    }\n                                } else {\n                                    selectedIds + subItem.id\n                                }\n                                selectedIds = newSelectedIds\n                                if (newSelectedIds.isEmpty()) {\n                                    isSelectedMode = false\n                                }\n                            },\n                        )\n                    }\n                }\n                item(ListPlaceholder.KEY, ListPlaceholder.TYPE) {\n                    Spacer(modifier = Modifier.height(EmptyHeight))\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/icon/BackCloseIcon.kt",
    "content": "package li.songe.gkd.ui.icon\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationVector1D\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.role\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport li.songe.gkd.ui.component.TooltipIconButtonBox\n\nprivate fun Animatable<Float, AnimationVector1D>.calc(start: Float, end: Float): Float {\n    return start + (end - start) * value\n}\n\n@Composable\nfun BackCloseIcon(\n    backOrClose: Boolean,\n    modifier: Modifier = Modifier,\n    contentDescription: String = if (backOrClose) \"返回\" else \"关闭\",\n    tint: Color = LocalContentColor.current\n) = TooltipIconButtonBox(\n    contentDescription = contentDescription,\n) {\n    InnerBackCloseIcon(\n        backOrClose = backOrClose,\n        modifier = modifier,\n        contentDescription = contentDescription,\n        tint = tint,\n    )\n}\n\n@Composable\nfun InnerBackCloseIcon(\n    backOrClose: Boolean,\n    modifier: Modifier,\n    contentDescription: String,\n    tint: Color,\n) {\n    // https://codepen.io/lisonge/pen/WbNEoPR\n    val percent = remember { Animatable(if (backOrClose) 1f else 0f) }\n    LaunchedEffect(backOrClose) {\n        if (backOrClose && percent.value != 1f) {\n            percent.animateTo(targetValue = 1f, animationSpec = tween())\n        } else if (!backOrClose && percent.value != 0f) {\n            percent.animateTo(targetValue = 0f, animationSpec = tween())\n        }\n    }\n    Canvas(\n        modifier = modifier\n            .size(24.dp)\n            .semantics {\n                this.contentDescription = contentDescription\n                this.role = Role.Image\n            },\n    ) {\n        val x = size.width\n        val halfX = x * 0.5f\n        val closeOffset = 0.2375f * x\n        val otherCloseOffset = x - closeOffset\n        val closeStrokeWidth = x * 0.08202f\n        val backStrokeWidth = x * 0.08316f\n        val strokeWidth = percent.calc(closeStrokeWidth, backStrokeWidth)\n        if (backOrClose) {\n            drawLine(\n                color = tint,\n                start = Offset(\n                    percent.calc(closeOffset, x * 0.208f),\n                    percent.calc(closeOffset, halfX)\n                ),\n                end = Offset(\n                    percent.calc(otherCloseOffset, x * 0.8335f),\n                    percent.calc(otherCloseOffset, halfX)\n                ),\n                strokeWidth = strokeWidth,\n            )\n            drawLine(\n                color = tint,\n                start = Offset(\n                    percent.calc(otherCloseOffset, x * 0.529675f),\n                    percent.calc(closeOffset, x * 0.1861f)\n                ),\n                end = Offset(\n                    percent.calc(halfX, x * 0.196f),\n                    percent.calc(halfX, x * 0.5295f)\n                ),\n                strokeWidth = strokeWidth,\n            )\n            drawLine(\n                color = tint,\n                start = Offset(\n                    percent.calc(closeOffset, x * 0.5295f),\n                    percent.calc(otherCloseOffset, x * 0.804f)\n                ),\n                end = Offset(\n                    percent.calc(halfX, x * 0.196f),\n                    percent.calc(halfX, x * 0.4705f)\n                ),\n                strokeWidth = strokeWidth,\n            )\n        } else {\n            drawLine(\n                color = tint,\n                start = Offset(\n                    percent.calc(closeOffset, x * 0.208f),\n                    percent.calc(otherCloseOffset, halfX)\n                ),\n                end = Offset(\n                    percent.calc(otherCloseOffset, x * 0.8335f),\n                    percent.calc(closeOffset, halfX)\n                ),\n                strokeWidth = strokeWidth,\n            )\n            drawLine(\n                color = tint,\n                start = Offset(\n                    percent.calc(closeOffset, x * 0.529675f),\n                    percent.calc(closeOffset, x * 0.1861f)\n                ),\n                end = Offset(\n                    percent.calc(halfX, x * 0.196f),\n                    percent.calc(halfX, x * 0.5295f)\n                ),\n                strokeWidth = strokeWidth,\n            )\n            drawLine(\n                color = tint,\n                start = Offset(\n                    percent.calc(otherCloseOffset, x * 0.5295f),\n                    percent.calc(otherCloseOffset, x * 0.804f)\n                ),\n                end = Offset(\n                    percent.calc(halfX, x * 0.196f),\n                    percent.calc(halfX, x * 0.4705f)\n                ),\n                strokeWidth = strokeWidth,\n            )\n        }\n    }\n}\n\n@Preview(\n    showBackground = true,\n    heightDp = 48,\n    widthDp = 48\n)\n@Composable\nfun PreviewBackCloseIcon() {\n    var backOrClose by remember { mutableStateOf(true) }\n    LaunchedEffect(null) {\n        delay(100)\n        while (isActive) {\n            backOrClose = !backOrClose\n            delay(1000)\n        }\n    }\n    BackCloseIcon(\n        backOrClose = backOrClose,\n        modifier = Modifier.fillMaxSize(),\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/icon/DragPan.kt",
    "content": "package li.songe.gkd.ui.icon\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval DragPan: ImageVector\n    get() {\n        if (_IconName != null) {\n            return _IconName!!\n        }\n        _IconName = ImageVector.Builder(\n            name = \"IconName\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 960f,\n            viewportHeight = 960f\n        ).apply {\n            path(fill = SolidColor(Color(0xFF5F6368))) {\n                moveTo(480f, 880f)\n                lineTo(310f, 710f)\n                lineToRelative(57f, -57f)\n                lineToRelative(73f, 73f)\n                verticalLineToRelative(-206f)\n                lineTo(235f, 520f)\n                lineToRelative(73f, 72f)\n                lineToRelative(-58f, 58f)\n                lineTo(80f, 480f)\n                lineToRelative(169f, -169f)\n                lineToRelative(57f, 57f)\n                lineToRelative(-72f, 72f)\n                horizontalLineToRelative(206f)\n                verticalLineToRelative(-206f)\n                lineToRelative(-73f, 73f)\n                lineToRelative(-57f, -57f)\n                lineToRelative(170f, -170f)\n                lineToRelative(170f, 170f)\n                lineToRelative(-57f, 57f)\n                lineToRelative(-73f, -73f)\n                verticalLineToRelative(206f)\n                horizontalLineToRelative(205f)\n                lineToRelative(-73f, -72f)\n                lineToRelative(58f, -58f)\n                lineToRelative(170f, 170f)\n                lineToRelative(-170f, 170f)\n                lineToRelative(-57f, -57f)\n                lineToRelative(73f, -73f)\n                lineTo(520f, 520f)\n                verticalLineToRelative(205f)\n                lineToRelative(72f, -73f)\n                lineToRelative(58f, 58f)\n                lineTo(480f, 880f)\n                close()\n            }\n        }.build()\n\n        return _IconName!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _IconName: ImageVector? = null\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/icon/LockOpenRight.kt",
    "content": "package li.songe.gkd.ui.icon\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval LockOpenRight: ImageVector\n    get() {\n        if (_LockOpenRight != null) {\n            return _LockOpenRight!!\n        }\n        _LockOpenRight = ImageVector.Builder(\n            name = \"LockOpenRight\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 960f,\n            viewportHeight = 960f\n        ).apply {\n            path(fill = SolidColor(Color(0xFF5F6368))) {\n                moveTo(240f, 800f)\n                horizontalLineToRelative(480f)\n                verticalLineToRelative(-400f)\n                lineTo(240f, 400f)\n                verticalLineToRelative(400f)\n                close()\n                moveTo(480f, 680f)\n                quadToRelative(33f, 0f, 56.5f, -23.5f)\n                reflectiveQuadTo(560f, 600f)\n                quadToRelative(0f, -33f, -23.5f, -56.5f)\n                reflectiveQuadTo(480f, 520f)\n                quadToRelative(-33f, 0f, -56.5f, 23.5f)\n                reflectiveQuadTo(400f, 600f)\n                quadToRelative(0f, 33f, 23.5f, 56.5f)\n                reflectiveQuadTo(480f, 680f)\n                close()\n                moveTo(240f, 800f)\n                verticalLineToRelative(-400f)\n                verticalLineToRelative(400f)\n                close()\n                moveTo(240f, 880f)\n                quadToRelative(-33f, 0f, -56.5f, -23.5f)\n                reflectiveQuadTo(160f, 800f)\n                verticalLineToRelative(-400f)\n                quadToRelative(0f, -33f, 23.5f, -56.5f)\n                reflectiveQuadTo(240f, 320f)\n                horizontalLineToRelative(280f)\n                verticalLineToRelative(-80f)\n                quadToRelative(0f, -83f, 58.5f, -141.5f)\n                reflectiveQuadTo(720f, 40f)\n                quadToRelative(83f, 0f, 141.5f, 58.5f)\n                reflectiveQuadTo(920f, 240f)\n                horizontalLineToRelative(-80f)\n                quadToRelative(0f, -50f, -35f, -85f)\n                reflectiveQuadToRelative(-85f, -35f)\n                quadToRelative(-50f, 0f, -85f, 35f)\n                reflectiveQuadToRelative(-35f, 85f)\n                verticalLineToRelative(80f)\n                horizontalLineToRelative(120f)\n                quadToRelative(33f, 0f, 56.5f, 23.5f)\n                reflectiveQuadTo(800f, 400f)\n                verticalLineToRelative(400f)\n                quadToRelative(0f, 33f, -23.5f, 56.5f)\n                reflectiveQuadTo(720f, 880f)\n                lineTo(240f, 880f)\n                close()\n            }\n        }.build()\n\n        return _LockOpenRight!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _LockOpenRight: ImageVector? = null\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/icon/ResetSettings.kt",
    "content": "package li.songe.gkd.ui.icon\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval ResetSettings: ImageVector\n    get() {\n        if (_IconName != null) {\n            return _IconName!!\n        }\n        _IconName = ImageVector.Builder(\n            name = \"IconName\",\n            defaultWidth = 24.dp,\n            defaultHeight = 24.dp,\n            viewportWidth = 960f,\n            viewportHeight = 960f\n        ).apply {\n            path(fill = SolidColor(Color(0xFF5F6368))) {\n                moveTo(520f, 630f)\n                verticalLineToRelative(-60f)\n                horizontalLineToRelative(160f)\n                verticalLineToRelative(60f)\n                lineTo(520f, 630f)\n                close()\n                moveTo(580f, 840f)\n                verticalLineToRelative(-50f)\n                horizontalLineToRelative(-60f)\n                verticalLineToRelative(-60f)\n                horizontalLineToRelative(60f)\n                verticalLineToRelative(-50f)\n                horizontalLineToRelative(60f)\n                verticalLineToRelative(160f)\n                horizontalLineToRelative(-60f)\n                close()\n                moveTo(680f, 790f)\n                verticalLineToRelative(-60f)\n                horizontalLineToRelative(160f)\n                verticalLineToRelative(60f)\n                lineTo(680f, 790f)\n                close()\n                moveTo(720f, 680f)\n                verticalLineToRelative(-160f)\n                horizontalLineToRelative(60f)\n                verticalLineToRelative(50f)\n                horizontalLineToRelative(60f)\n                verticalLineToRelative(60f)\n                horizontalLineToRelative(-60f)\n                verticalLineToRelative(50f)\n                horizontalLineToRelative(-60f)\n                close()\n                moveTo(831f, 400f)\n                horizontalLineToRelative(-83f)\n                quadToRelative(-26f, -88f, -99f, -144f)\n                reflectiveQuadToRelative(-169f, -56f)\n                quadToRelative(-117f, 0f, -198.5f, 81.5f)\n                reflectiveQuadTo(200f, 480f)\n                quadToRelative(0f, 72f, 32.5f, 132f)\n                reflectiveQuadToRelative(87.5f, 98f)\n                verticalLineToRelative(-110f)\n                horizontalLineToRelative(80f)\n                verticalLineToRelative(240f)\n                lineTo(160f, 840f)\n                verticalLineToRelative(-80f)\n                horizontalLineToRelative(94f)\n                quadToRelative(-62f, -50f, -98f, -122.5f)\n                reflectiveQuadTo(120f, 480f)\n                quadToRelative(0f, -75f, 28.5f, -140.5f)\n                reflectiveQuadToRelative(77f, -114f)\n                quadToRelative(48.5f, -48.5f, 114f, -77f)\n                reflectiveQuadTo(480f, 120f)\n                quadToRelative(129f, 0f, 226.5f, 79.5f)\n                reflectiveQuadTo(831f, 400f)\n                close()\n            }\n        }.build()\n\n        return _IconName!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _IconName: ImageVector? = null\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/icon/SportsBasketball.kt",
    "content": "package li.songe.gkd.ui.icon\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.PathFillType\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.path\nimport androidx.compose.ui.unit.dp\n\nval SportsBasketball: ImageVector\n    get() {\n        if (_SportsBasketball != null) {\n            return _SportsBasketball!!\n        }\n        _SportsBasketball = ImageVector.Builder(\n            name = \"SportsBasketball\",\n            defaultWidth = 32.dp,\n            defaultHeight = 32.dp,\n            viewportWidth = 24f,\n            viewportHeight = 24f\n        ).apply {\n            path(\n                fill = SolidColor(Color(0xFF000000)),\n                pathFillType = PathFillType.EvenOdd\n            ) {\n                moveTo(8.367f, 3.492f)\n                curveToRelative(0.499f, 0.396f, 1.172f, 0.95f, 1.905f, 1.607f)\n                curveToRelative(0.702f, 0.63f, 1.473f, 1.366f, 2.203f, 2.161f)\n                curveToRelative(1.403f, -1.191f, 2.486f, -2.535f, 3.044f, -3.815f)\n                arcToRelative(9.3f, 9.3f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2.414f, -0.63f)\n                arcToRelative(9.2f, 9.2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -4.738f, 0.677f)\n                moveToRelative(8.491f, 0.634f)\n                curveToRelative(-0.678f, 1.509f, -1.901f, 2.993f, -3.405f, 4.271f)\n                arcToRelative(14f, 14f, 0f, isMoreThanHalf = false, isPositiveArc = true, 1.197f, 1.728f)\n                curveToRelative(0.41f, 0.71f, 0.774f, 1.533f, 1.095f, 2.396f)\n                curveToRelative(1.812f, -0.683f, 3.717f, -1.021f, 5.502f, -0.805f)\n                arcToRelative(9.2f, 9.2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -1.236f, -4.341f)\n                arcToRelative(9.2f, 9.2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -3.153f, -3.249f)\n                moveToRelative(4.312f, 9.093f)\n                curveToRelative(-1.536f, -0.209f, -3.255f, 0.08f, -4.944f, 0.724f)\n                curveToRelative(0.29f, 0.937f, 0.535f, 1.876f, 0.733f, 2.729f)\n                arcToRelative(52f, 52f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0.563f, 2.75f)\n                arcToRelative(9.2f, 9.2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2.984f, -3.788f)\n                curveToRelative(0.33f, -0.771f, 0.553f, -1.585f, 0.664f, -2.415f)\n                moveToRelative(-5.026f, 7.05f)\n                arcToRelative(51f, 51f, 0f, isMoreThanHalf = false, isPositiveArc = false, -0.645f, -3.257f)\n                arcToRelative(39f, 39f, 0f, isMoreThanHalf = false, isPositiveArc = false, -0.655f, -2.461f)\n                quadToRelative(-0.614f, 0.307f, -1.207f, 0.667f)\n                curveToRelative(-2.412f, 1.468f, -4.342f, 3.47f, -5.156f, 5.337f)\n                arcToRelative(9.24f, 9.24f, 0f, isMoreThanHalf = false, isPositiveArc = false, 7.663f, -0.285f)\n                moveToRelative(-9.002f, -0.395f)\n                curveToRelative(1.001f, -2.227f, 3.19f, -4.401f, 5.715f, -5.937f)\n                quadToRelative(0.73f, -0.445f, 1.508f, -0.822f)\n                curveToRelative(-0.302f, -0.823f, -0.64f, -1.593f, -1.014f, -2.24f)\n                arcTo(13f, 13f, 0f, isMoreThanHalf = false, isPositiveArc = false, 12.27f, 9.32f)\n                arcToRelative(18f, 18f, 0f, isMoreThanHalf = false, isPositiveArc = true, -1.064f, 0.706f)\n                curveToRelative(-2.57f, 1.578f, -5.658f, 2.597f, -8.454f, 2.259f)\n                arcToRelative(9.2f, 9.2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 1.237f, 4.34f)\n                arcToRelative(9.2f, 9.2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 3.153f, 3.25f)\n                moveTo(2.83f, 10.78f)\n                arcToRelative(9.24f, 9.24f, 0f, isMoreThanHalf = false, isPositiveArc = true, 4.092f, -6.513f)\n                quadToRelative(0.188f, 0.143f, 0.466f, 0.363f)\n                curveToRelative(0.486f, 0.383f, 1.154f, 0.931f, 1.883f, 1.585f)\n                curveToRelative(0.66f, 0.592f, 1.358f, 1.26f, 2.012f, 1.965f)\n                quadToRelative(-0.42f, 0.293f, -0.862f, 0.566f)\n                curveTo(7.973f, 10.25f, 5.187f, 11.1f, 2.83f, 10.78f)\n                moveToRelative(3.795f, -8.09f)\n                arcToRelative(10.7f, 10.7f, 0f, isMoreThanHalf = false, isPositiveArc = true, 6.66f, -1.364f)\n                arcToRelative(10.74f, 10.74f, 0f, isMoreThanHalf = false, isPositiveArc = true, 8.025f, 5.299f)\n                arcToRelative(10.74f, 10.74f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0.576f, 9.598f)\n                arcToRelative(10.7f, 10.7f, 0f, isMoreThanHalf = false, isPositiveArc = true, -4.511f, 5.087f)\n                curveToRelative(-5.142f, 2.968f, -11.716f, 1.206f, -14.685f, -3.935f)\n                curveTo(-0.278f, 12.233f, 1.483f, 5.658f, 6.625f, 2.69f)\n            }\n        }.build()\n\n        return _SportsBasketball!!\n    }\n\n@Suppress(\"ObjectPropertyName\")\nprivate var _SportsBasketball: ImageVector? = null\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/share/AppFilter.kt",
    "content": "package li.songe.gkd.ui.share\n\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.map\nimport li.songe.gkd.MainViewModel\nimport li.songe.gkd.data.AppInfo\nimport li.songe.gkd.store.blockMatchAppListFlow\nimport li.songe.gkd.util.AppGroupOption\nimport li.songe.gkd.util.AppSortOption\nimport li.songe.gkd.util.visibleAppInfosFlow\n\nfun BaseViewModel.useAppFilter(\n    appGroupTypeFlow: StateFlow<Int>,\n    sortTypeFlow: StateFlow<AppSortOption>,\n    appOrderListFlow: StateFlow<List<String>> = MainViewModel.instance.appOrderListFlow,\n    showBlockAppFlow: StateFlow<Boolean>? = null,\n    blockAppListFlow: StateFlow<Set<String>> = blockMatchAppListFlow,\n): AppFilter {\n\n    var tempListFlow: Flow<List<AppInfo>> = visibleAppInfosFlow\n\n    if (showBlockAppFlow != null) {\n        tempListFlow = combine(\n            tempListFlow,\n            showBlockAppFlow,\n            blockAppListFlow,\n        ) { appInfos, showBlockApp, blockAppList ->\n            if (showBlockApp) {\n                appInfos\n            } else {\n                appInfos.filterNot { it.id in blockAppList }\n            }\n        }\n    }\n\n    tempListFlow = combine(\n        tempListFlow,\n        appGroupTypeFlow,\n    ) { list, type ->\n        if (type == 0) {\n            return@combine emptyList()\n        }\n        if (AppGroupOption.normalObjects.all { it.include(type) }) {\n            return@combine list\n        }\n        var resultList = list\n        if (!AppGroupOption.SystemGroup.include(type)) {\n            resultList = resultList.filterNot { it.isSystem }\n        }\n        if (!AppGroupOption.UserGroup.include(type)) {\n            resultList = resultList.filterNot { !it.isSystem }\n        }\n        resultList\n    }\n\n    val showAllAppFlow = combine(\n        tempListFlow,\n        visibleAppInfosFlow,\n    ) { a, b ->\n        a.size == b.size\n    }.stateInit(true)\n\n    val searchStrFlow = MutableStateFlow(\"\")\n    val debounceSearchStrFlow = searchStrFlow.debounce(200)\n        .stateInit(searchStrFlow.value)\n    val appActionOrderMapFlow = appOrderListFlow.map {\n        it.mapIndexed { i, appId -> appId to i }.toMap()\n    }\n    tempListFlow = combine(\n        tempListFlow,\n        sortTypeFlow,\n        appActionOrderMapFlow,\n        MainViewModel.instance.appVisitOrderMapFlow,\n    ) { apps, sortType, appActionOrderMap, appVisitOrderMap ->\n        when (sortType) {\n            AppSortOption.ByActionTime -> {\n                apps.sortedBy { a -> appActionOrderMap[a.id] ?: Int.MAX_VALUE }\n            }\n\n            AppSortOption.ByAppName -> {\n                apps\n            }\n\n            AppSortOption.ByUsedTime -> {\n                apps.sortedBy { a -> appVisitOrderMap[a.id] ?: Int.MAX_VALUE }\n            }\n        }\n    }\n    tempListFlow = tempListFlow.combine(debounceSearchStrFlow) { apps, str ->\n        if (str.isBlank()) {\n            apps\n        } else {\n            (apps.filter { a -> a.name.contains(str, true) } + apps.filter { a ->\n                a.id.contains(\n                    str,\n                    true\n                )\n            }).distinct()\n        }\n    }.stateInit(emptyList())\n    return AppFilter(\n        searchStrFlow = searchStrFlow,\n        appListFlow = tempListFlow,\n        showAllAppFlow = showAllAppFlow,\n    )\n}\n\n\nclass AppFilter(\n    val searchStrFlow: MutableStateFlow<String>,\n    val appListFlow: StateFlow<List<AppInfo>>,\n    val showAllAppFlow: StateFlow<Boolean>,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/share/BaseViewModel.kt",
    "content": "package li.songe.gkd.ui.share\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.FlowCollector\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.util.subsMapFlow\n\n\nabstract class BaseViewModel : ViewModel() {\n    private val countFlow by lazy { MutableStateFlow(0) }\n    val firstLoadingFlow by lazy { countFlow.mapNew { it > 0 } }\n    fun <T> Flow<T>.attachLoad(): Flow<T> {\n        countFlow.update { it + 1 }\n        var currentUsed = false\n        return onEach {\n            if (!currentUsed) {\n                countFlow.update {\n                    if (!currentUsed) {\n                        currentUsed = true\n                        it - 1\n                    } else {\n                        it\n                    }\n                }\n            }\n        }\n    }\n\n    fun <T> Flow<T>.stateInit(initialValue: T): StateFlow<T> {\n        return stateIn(viewModelScope, SharingStarted.Eagerly, initialValue)\n    }\n\n    fun <T> Flow<T>.launchCollect(collector: FlowCollector<T>) {\n        viewModelScope.launch { collect(collector) }\n    }\n\n    fun <T> StateFlow<T>.launchOnChange(collector: FlowCollector<T>) {\n        viewModelScope.launch { drop(1).collect(collector) }\n    }\n\n    fun <T, M> StateFlow<T>.mapNew(\n        mapper: (value: T) -> M,\n    ): StateFlow<M> = map { mapper(it) }.stateIn(\n        viewModelScope, SharingStarted.Eagerly, mapper(value)\n    )\n\n    fun mapSafeSubs(id: Long): StateFlow<RawSubscription> {\n        return subsMapFlow.mapNew {\n            it[id] ?: RawSubscription(\n                id = id,\n                version = 0,\n                name = id.toString()\n            )\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/share/FixedWindowInsets.kt",
    "content": "package li.songe.gkd.ui.share\n\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.ui.unit.Density\n\n// 解决 val obj = TopAppBarDefaults.windowInsets 在不同时机返回不一致的问题\nclass FixedWindowInsets(\n    val insets: WindowInsets\n) : WindowInsets by insets {\n    var top: Int? = null\n    override fun getTop(density: Density) = top ?: insets.getTop(density).also { top = it }\n\n    var bottom: Int? = null\n    override fun getBottom(density: Density) =\n        bottom ?: insets.getBottom(density).also { bottom = it }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/share/ListPlaceholder.kt",
    "content": "package li.songe.gkd.ui.share\n\nimport kotlin.math.E\nimport kotlin.math.PI\n\nobject ListPlaceholder {\n    const val KEY = PI\n    const val TYPE = E\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/share/LocalExt.kt",
    "content": "package li.songe.gkd.ui.share\n\nimport androidx.compose.runtime.compositionLocalOf\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport li.songe.gkd.MainViewModel\n\nval LocalMainViewModel = compositionLocalOf<MainViewModel> {\n    error(\"not found MainViewModel\")\n}\n\nval LocalDarkTheme = compositionLocalOf { false }\n\nval LocalIsTalkbackEnabled = compositionLocalOf<StateFlow<Boolean>> {\n    MutableStateFlow(false)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/share/ModifierExt.kt",
    "content": "package li.songe.gkd.ui.share\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.Role\n\n@Composable\nfun Modifier.noRippleClickable(\n    enabled: Boolean = true,\n    onClickLabel: String? = null,\n    role: Role? = null,\n    onClick: () -> Unit,\n): Modifier = clickable(\n    interactionSource = remember { MutableInteractionSource() },\n    indication = null,\n    enabled = enabled,\n    onClickLabel = onClickLabel,\n    role = role,\n    onClick = onClick,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/share/StateExt.kt",
    "content": "package li.songe.gkd.ui.share\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi\nimport kotlinx.coroutines.flow.FlowCollector\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\n\n\n@Composable\nfun <T> MutableStateFlow<T>.asMutableState(): MutableState<T> {\n    val state = collectAsState()\n    return remember(this) {\n        val stateFlow = this\n        object : MutableState<T> {\n            val setter: (T) -> Unit = { stateFlow.value = it }\n            override var value: T\n                get() = state.value\n                set(newValue) = setter(newValue)\n\n            override fun component1() = value\n            override fun component2() = setter\n        }\n    }\n}\n\n@OptIn(ExperimentalForInheritanceCoroutinesApi::class)\nfun <T, S> MutableStateFlow<T>.asMutableStateFlow(\n    getter: (T) -> S,\n    setter: (S) -> T\n) = object : MutableStateFlow<S> {\n    val source = this@asMutableStateFlow\n    override var value: S\n        get() = getter(source.value)\n        set(newValue) = source.update { setter(newValue) }\n\n    override fun compareAndSet(expect: S, update: S) = source.compareAndSet(\n        setter(expect),\n        setter(update),\n    )\n\n    override suspend fun collect(collector: FlowCollector<S>): Nothing {\n        var oldValue = value\n        collector.emit(oldValue)\n        source.collect {\n            val newValue = getter(it)\n            if (oldValue != newValue) {\n                oldValue = newValue\n                collector.emit(oldValue)\n            }\n        }\n    }\n\n    override val replayCache get() = source.replayCache.map(getter)\n    override val subscriptionCount get() = source.subscriptionCount\n    override suspend fun emit(value: S) = source.emit(setter(value))\n    override fun tryEmit(value: S) = source.tryEmit(setter(value))\n    override fun resetReplayCache() = source.resetReplayCache()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/style/Color.kt",
    "content": "package li.songe.gkd.ui.style\n\nimport androidx.compose.material3.CardColors\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport li.songe.json5.Json5\nimport li.songe.json5.Json5Token\n\n\nval surfaceCardColors: CardColors\n    @Composable\n    get() = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer)\n\nprivate fun getDarkJson5TokenColor(json5Token: Json5Token?): Color = when (json5Token) {\n    null -> Color(0xFFFF00FF) // unknown token color\n    Json5Token.Comment -> Color(0xFF75715E)\n    Json5Token.LeftBrace, Json5Token.RightBrace -> Color(0xFFFFA07A)\n    Json5Token.LeftBracket, Json5Token.RightBracket -> Color(0xFFFFA07A)\n    Json5Token.Colon -> Color(0xFFE1E4E8)\n    Json5Token.Comma -> Color(0xFFE1E4E8)\n    Json5Token.BooleanLiteral -> Color(0xFF79B8FF)\n    Json5Token.NullLiteral -> Color(0xFFB22222)\n    Json5Token.NumberLiteral -> Color(0xFF2E8B57)\n    Json5Token.StringLiteral -> Color(0xFFE6DB74)\n    Json5Token.Property -> Color(0xFFBCBEC4)\n    Json5Token.Whitespace -> Color.Transparent\n}\n\nprivate fun getLightJson5TokenColor(json5Token: Json5Token?): Color = when (json5Token) {\n    null -> Color(0xFFFF0000)\n    Json5Token.Comment -> Color(0xFF6A9955)\n    Json5Token.LeftBrace, Json5Token.RightBrace -> Color(0xFFAF00DB)\n    Json5Token.LeftBracket, Json5Token.RightBracket -> Color(0xFFAF00DB)\n    Json5Token.Colon -> Color(0xFF000000)\n    Json5Token.Comma -> Color(0xFF000000)\n    Json5Token.BooleanLiteral -> Color(0xFF0000FF)\n    Json5Token.NullLiteral -> Color(0xFFA31515)\n    Json5Token.NumberLiteral -> Color(0xFF098658)\n    Json5Token.StringLiteral -> Color(0xFF669900)\n    Json5Token.Property -> Color(0xFF001080)\n    Json5Token.Whitespace -> Color.Transparent\n}\n\nprivate val json5LightStyleCache = HashMap<Json5Token?, SpanStyle>()\nprivate val json5DarkStyleCache = HashMap<Json5Token?, SpanStyle>()\n\nfun getJson5AnnotatedString(source: String, dark: Boolean): AnnotatedString = buildAnnotatedString {\n    append(source)\n    val styleCache = if (dark) {\n        json5DarkStyleCache\n    } else {\n        json5LightStyleCache\n    }\n    Json5.parseToJson5LooseRanges(source).forEach { range ->\n        if (range.token is Json5Token.Whitespace) {\n            return@forEach\n        }\n        val style = styleCache[range.token] ?: SpanStyle(\n            color = if (dark) {\n                getDarkJson5TokenColor(range.token)\n            } else {\n                getLightJson5TokenColor(range.token)\n            },\n        ).apply {\n            styleCache[range.token] = this\n        }\n        addStyle(\n            style = style,\n            range.start,\n            range.end\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/style/Padding.kt",
    "content": "package li.songe.gkd.ui.style\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.dp\n\nval itemHorizontalPadding = 16.dp\nval itemVerticalPadding = 12.dp\nval EmptyHeight = 80.dp\nval cardHorizontalPadding = 12.dp\n\nfun Modifier.itemPadding() = this.padding(itemHorizontalPadding, itemVerticalPadding)\n\nfun Modifier.titleItemPadding(showTop: Boolean = true) = this.padding(\n    itemHorizontalPadding,\n    if (showTop) itemVerticalPadding + itemVerticalPadding / 2 else 0.dp,\n    itemHorizontalPadding,\n    itemVerticalPadding - itemVerticalPadding / 2\n)\n\nfun Modifier.appItemPadding() = this.padding(itemHorizontalPadding, itemVerticalPadding)\n\nfun Modifier.scaffoldPadding(values: PaddingValues): Modifier {\n    return padding(\n        top = values.calculateTopPadding(),\n        // 被 LazyXXX 使用时, 移除 bottom padding, 否则 底部导航栏 无法实现透明背景\n    )\n}\n\n@Composable\nfun Modifier.iconTextSize(\n    textStyle: TextStyle = LocalTextStyle.current,\n    square: Boolean = true,\n): Modifier {\n    val density = LocalDensity.current\n    val lineHeightDp = density.run { textStyle.lineHeight.toDp() }\n    val fontSizeDp = density.run { textStyle.fontSize.toDp() }\n    return if (square) {\n        padding((lineHeightDp - fontSizeDp) / 2).size(fontSizeDp)\n    } else {\n        size(height = lineHeightDp, width = fontSizeDp)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/style/TextTransformation.kt",
    "content": "package li.songe.gkd.ui.style\n\nimport androidx.collection.LruCache\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.input.OffsetMapping\nimport androidx.compose.ui.text.input.TransformedText\nimport androidx.compose.ui.text.input.VisualTransformation\n\nprivate const val HIGHLIGHT_JSON5_MAX_LENGTH = 10000\n\nprivate class Json5VisualTransformation(val dark: Boolean) : VisualTransformation {\n    val cache = LruCache<String, TransformedText>(0xF)\n    override fun filter(text: AnnotatedString): TransformedText {\n        if (text.text.isBlank() || text.text.length > HIGHLIGHT_JSON5_MAX_LENGTH) {\n            return VisualTransformation.None.filter(text)\n        }\n        cache[text.text]?.let { return it }\n        return TransformedText(\n            text = getJson5AnnotatedString(text.text, dark),\n            offsetMapping = OffsetMapping.Identity,\n        ).apply {\n            cache.put(text.text, this)\n        }\n    }\n}\n\nprivate val darkVisualTransformation = Json5VisualTransformation(true)\nprivate val lightVisualTransformation = Json5VisualTransformation(false)\n\nfun getJson5Transformation(dark: Boolean): VisualTransformation = if (dark) {\n    darkVisualTransformation\n} else {\n    lightVisualTransformation\n}\n\nfun clearJson5TransformationCache() {\n    darkVisualTransformation.cache.evictAll()\n    lightVisualTransformation.cache.evictAll()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/ui/style/Theme.kt",
    "content": "package li.songe.gkd.ui.style\n\nimport android.view.accessibility.AccessibilityManager\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.core.view.WindowInsetsControllerCompat\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport li.songe.gkd.app\nimport li.songe.gkd.store.storeFlow\nimport li.songe.gkd.ui.share.LocalDarkTheme\nimport li.songe.gkd.ui.share.LocalIsTalkbackEnabled\nimport li.songe.gkd.util.AndroidTarget\n\nprivate val LightColorScheme = lightColorScheme()\nprivate val DarkColorScheme = darkColorScheme()\n\n@Composable\nfun AppTheme(\n    invertedTheme: Boolean = false,\n    content: @Composable () -> Unit,\n) {\n    val scope = rememberCoroutineScope()\n    val enableDarkThemeFlow = remember {\n        storeFlow.map { it.enableDarkTheme }.debounce(300).stateIn(\n            scope, SharingStarted.Eagerly, storeFlow.value.enableDarkTheme\n        )\n    }\n    val enableDynamicColorFlow = remember {\n        storeFlow.map { it.enableDynamicColor }.debounce(300).stateIn(\n            scope, SharingStarted.Eagerly, storeFlow.value.enableDynamicColor\n        )\n    }\n    val enableDarkTheme by enableDarkThemeFlow.collectAsState()\n    val enableDynamicColor by enableDynamicColorFlow.collectAsState()\n    val systemInDarkTheme = isSystemInDarkTheme()\n    val darkTheme = (enableDarkTheme ?: systemInDarkTheme).let {\n        if (invertedTheme) !it else it\n    }\n    val colorScheme = when {\n        AndroidTarget.S && enableDynamicColor && darkTheme -> dynamicDarkColorScheme(app)\n        AndroidTarget.S && enableDynamicColor && !darkTheme -> dynamicLightColorScheme(app)\n        darkTheme -> DarkColorScheme\n        else -> LightColorScheme\n    }\n\n    val activity = LocalActivity.current\n    if (activity != null) {\n        LaunchedEffect(darkTheme) {\n            // https://github.com/gkd-kit/gkd/pull/421\n            WindowInsetsControllerCompat(activity.window, activity.window.decorView).apply {\n                isAppearanceLightStatusBars = !darkTheme\n            }\n        }\n        val bg = colorScheme.background.toArgb()\n        LaunchedEffect(darkTheme, bg) {\n            activity.window.decorView.setBackgroundColor(bg)\n        }\n    }\n\n    val isTalkbackEnabledFlow = remember {\n        MutableStateFlow(app.a11yManager.isTouchExplorationEnabled)\n    }\n    DisposableEffect(null) {\n        val listener = AccessibilityManager.TouchExplorationStateChangeListener {\n            isTalkbackEnabledFlow.value = it\n        }\n        app.a11yManager.addTouchExplorationStateChangeListener(listener)\n        onDispose {\n            app.a11yManager.removeTouchExplorationStateChangeListener(listener)\n        }\n    }\n    CompositionLocalProvider(\n        LocalDarkTheme provides darkTheme,\n        LocalIsTalkbackEnabled provides isTalkbackEnabledFlow\n    ) {\n        MaterialTheme(\n            colorScheme = colorScheme.animation(),\n            content = content,\n        )\n    }\n}\n\n@Composable\nprivate fun Color.animation() = animateColorAsState(\n    targetValue = this,\n    animationSpec = tween(durationMillis = 500),\n    label = \"animation\"\n).value\n\n@Composable\nprivate fun ColorScheme.animation(): ColorScheme {\n    return copy(\n        primary = primary.animation(),\n        onPrimary = onPrimary.animation(),\n        primaryContainer = primaryContainer.animation(),\n        onPrimaryContainer = onPrimaryContainer.animation(),\n        inversePrimary = inversePrimary.animation(),\n        secondary = secondary.animation(),\n        onSecondary = onSecondary.animation(),\n        secondaryContainer = secondaryContainer.animation(),\n        onSecondaryContainer = onSecondaryContainer.animation(),\n        tertiary = tertiary.animation(),\n        onTertiary = onTertiary.animation(),\n        tertiaryContainer = tertiaryContainer.animation(),\n        onTertiaryContainer = onTertiaryContainer.animation(),\n        background = background.animation(),\n        onBackground = onBackground.animation(),\n        surface = surface.animation(),\n        onSurface = onSurface.animation(),\n        surfaceVariant = surfaceVariant.animation(),\n        onSurfaceVariant = onSurfaceVariant.animation(),\n        surfaceTint = surfaceTint.animation(),\n        inverseSurface = inverseSurface.animation(),\n        inverseOnSurface = inverseOnSurface.animation(),\n        error = error.animation(),\n        onError = onError.animation(),\n        errorContainer = errorContainer.animation(),\n        onErrorContainer = onErrorContainer.animation(),\n        outline = outline.animation(),\n        outlineVariant = outlineVariant.animation(),\n        scrim = scrim.animation(),\n        surfaceBright = surfaceBright.animation(),\n        surfaceDim = surfaceDim.animation(),\n        surfaceContainer = surfaceContainer.animation(),\n        surfaceContainerHigh = surfaceContainerHigh.animation(),\n        surfaceContainerHighest = surfaceContainerHighest.animation(),\n        surfaceContainerLow = surfaceContainerLow.animation(),\n        surfaceContainerLowest = surfaceContainerLowest.animation(),\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/AndroidTarget.kt",
    "content": "package li.songe.gkd.util\n\nimport android.os.Build\nimport androidx.annotation.ChecksSdkIntAtLeast\n\nobject AndroidTarget {\n    /** Android 9+ */\n    @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)\n    val P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P\n\n    /** Android 10+ */\n    @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)\n    val Q = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q\n\n    /** Android 11+ */\n    @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)\n    val R = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R\n\n    /** Android 12+ */\n    @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)\n    val S = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S\n\n    /** Android 13+ */\n    @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)\n    val TIRAMISU = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU\n\n    /** Android 14+ */\n    @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    val UPSIDE_DOWN_CAKE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE\n\n    /** Android 16+ */\n    @get:ChecksSdkIntAtLeast(api = Build.VERSION_CODES.BAKLAVA)\n    val BAKLAVA = Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/AppInfoState.kt",
    "content": "package li.songe.gkd.util\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.content.pm.PackageManager\nimport android.graphics.drawable.Drawable\nimport androidx.core.content.ContextCompat\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.filter\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport li.songe.gkd.App\nimport li.songe.gkd.app\nimport li.songe.gkd.appScope\nimport li.songe.gkd.data.AppInfo\nimport li.songe.gkd.data.otherUserMapFlow\nimport li.songe.gkd.data.toAppInfo\nimport li.songe.gkd.data.toAppInfoAndIcon\nimport li.songe.gkd.permission.canQueryPkgState\nimport li.songe.gkd.shizuku.currentUserId\nimport li.songe.gkd.shizuku.shizukuContextFlow\n\nval userAppInfoMapFlow = MutableStateFlow(emptyMap<String, AppInfo>())\nval userAppIconMapFlow = MutableStateFlow(emptyMap<String, Drawable>())\nval otherUserAppInfoMapFlow = MutableStateFlow(emptyMap<String, AppInfo>())\nval otherUserAppIconMapFlow = MutableStateFlow(emptyMap<String, Drawable>())\n\nval appInfoMapFlow by lazy {\n    combine(otherUserAppInfoMapFlow, userAppInfoMapFlow) { a, b -> a + b }\n        .stateIn(appScope, SharingStarted.Eagerly, emptyMap())\n}\n\nval appIconMapFlow by lazy {\n    combine(otherUserAppIconMapFlow, userAppIconMapFlow) { a, b -> a + b }\n        .stateIn(appScope, SharingStarted.Eagerly, emptyMap())\n}\n\nval systemAppInfoCacheFlow by lazy {\n    appInfoMapFlow.mapState(appScope) { c ->\n        c.filter { a -> a.value.isSystem }\n    }\n}\n\nval systemAppsFlow by lazy { systemAppInfoCacheFlow.mapState(appScope) { c -> c.keys } }\n\nval visibleAppInfosFlow by lazy {\n    appInfoMapFlow.mapState(appScope) { c ->\n        c.values.filterNot { it.hidden }.sortedWith { a, b ->\n            collator.compare(a.name, b.name)\n        }\n    }\n}\n\nprivate val willUpdateAppIds by lazy { MutableStateFlow(emptySet<String>()) }\n\nprivate val packageReceiver by lazy {\n    object : BroadcastReceiver() {\n        val actions = arrayOf(\n            Intent.ACTION_PACKAGE_ADDED,\n            Intent.ACTION_PACKAGE_REPLACED,\n            Intent.ACTION_PACKAGE_REMOVED\n        )\n\n        override fun onReceive(context: Context?, intent: Intent?) {\n            // PACKAGE_REMOVED->PACKAGE_ADDED->PACKAGE_REPLACED\n            val appId = intent?.data?.schemeSpecificPart ?: return\n            willUpdateAppIds.update { it + appId }\n        }\n    }.apply {\n        val intentFilter = IntentFilter().apply {\n            actions.forEach { addAction(it) }\n            addDataScheme(\"package\")\n        }\n        ContextCompat.registerReceiver(\n            app,\n            this,\n            intentFilter,\n            ContextCompat.RECEIVER_EXPORTED\n        )\n    }\n}\n\nconst val PKG_FLAGS = PackageManager.MATCH_UNINSTALLED_PACKAGES\n\nval updateAppMutex = MutexState()\n\nprivate fun updateOtherUserAppInfo(userAppInfoMap: Map<String, AppInfo>? = null) {\n    val pkgManager = shizukuContextFlow.value.packageManager\n    val userManager = shizukuContextFlow.value.userManager\n    val actualUserAppInfoMap = userAppInfoMap ?: userAppInfoMapFlow.value\n    if (pkgManager == null || userManager == null || actualUserAppInfoMap.isEmpty()) {\n        otherUserMapFlow.value = emptyMap()\n        otherUserAppIconMapFlow.value = emptyMap()\n        otherUserAppInfoMapFlow.value = emptyMap()\n        return\n    }\n    val otherUsers = userManager.getUsers().filter { it.id != currentUserId }.sortedBy { it.id }\n    val userPackageInfoMap = otherUsers.associate { user ->\n        user.id to pkgManager.getInstalledPackages(\n            PKG_FLAGS,\n            user.id\n        ).filterNot { actualUserAppInfoMap.contains(it.packageName) }\n    }\n    val newIconMap = HashMap<String, Drawable>()\n    val newAppMap = HashMap<String, AppInfo>()\n    userPackageInfoMap.forEach { (userId, pkgInfoList) ->\n        pkgInfoList.forEach { pkgInfo ->\n            if (!newAppMap.contains(pkgInfo.packageName)) {\n                val (appInfo, appIcon) = pkgInfo.toAppInfoAndIcon(userId)\n                newAppMap[pkgInfo.packageName] = appInfo\n                if (appIcon != null) {\n                    newIconMap[pkgInfo.packageName] = appIcon\n                }\n            }\n        }\n    }\n    otherUserMapFlow.value = otherUsers.associateBy { it.id }\n    otherUserAppInfoMapFlow.value = newAppMap\n    otherUserAppIconMapFlow.value = newIconMap\n}\n\nprivate fun updatePartAppInfo(\n    appIds: Set<String>,\n) = updateAppMutex.launchTry(appScope, Dispatchers.IO) {\n    willUpdateAppIds.update { it - appIds }\n    val newAppMap = HashMap(userAppInfoMapFlow.value)\n    val newIconMap = HashMap(userAppIconMapFlow.value)\n    appIds.forEach { appId ->\n        val info = app.getPkgInfo(appId)\n        if (info != null) {\n            newAppMap[appId] = info.toAppInfo()\n        } else {\n            newAppMap.remove(appId)\n        }\n        val icon = info?.pkgIcon\n        if (icon != null) {\n            newIconMap[appId] = icon\n        } else {\n            newIconMap.remove(appId)\n        }\n    }\n    updateOtherUserAppInfo(newAppMap)\n    userAppInfoMapFlow.value = newAppMap\n    userAppIconMapFlow.value = newIconMap\n}\n\nval appListAuthAbnormalFlow = MutableStateFlow(false)\n\nfun updateAllAppInfo(): Unit = updateAppMutex.launchTry(appScope, Dispatchers.IO) {\n    val newAppMap = HashMap<String, AppInfo>()\n    val newIconMap = HashMap<String, Drawable>()\n    // see #1169 DeadObjectException BadParcelableException\n    val pkgList = app.packageManager.getInstalledPackages(PKG_FLAGS)\n    pkgList.forEach { pkgInfo ->\n        val (appInfo, appIcon) = pkgInfo.toAppInfoAndIcon()\n        newAppMap[pkgInfo.packageName] = appInfo\n        if (appIcon != null) {\n            newIconMap[pkgInfo.packageName] = appIcon\n        }\n    }\n    val mayAuthDenied = newAppMap.count { !it.value.isSystem } <= 4\n    canQueryPkgState.updateAndGet()\n    appListAuthAbnormalFlow.value = canQueryPkgState.value && mayAuthDenied\n    if (!canQueryPkgState.value || mayAuthDenied) {\n        LogUtils.d(\n            \"updateAllAppInfo\",\n            \"mayAuthDenied=$mayAuthDenied, newAppMap.size=${newAppMap.size}\"\n        )\n        val pkgList2 = shizukuContextFlow.value.packageManager?.getInstalledPackages(PKG_FLAGS)\n        if (!pkgList2.isNullOrEmpty()) {\n            pkgList2.forEach { pkgInfo ->\n                val (appInfo, appIcon) = pkgInfo.toAppInfoAndIcon()\n                newAppMap[pkgInfo.packageName] = appInfo\n                if (appIcon != null) {\n                    newIconMap[pkgInfo.packageName] = appIcon\n                }\n            }\n        } else {\n            val visiblePkgList = arrayOf(Intent.ACTION_MAIN, Intent.ACTION_VIEW).map { action ->\n                try {\n                    // DeadObjectException BadParcelableException\n                    app.packageManager.queryIntentActivities(\n                        Intent(action),\n                        PackageManager.MATCH_DISABLED_COMPONENTS\n                    )\n                } catch (_: Throwable) {\n                    emptyList()\n                }\n            }.flatten()\n                .map { it.activityInfo.packageName }.toSet()\n                .filter { !newAppMap.contains(it) }.mapNotNull { app.getPkgInfo(it) }\n            visiblePkgList.forEach { pkgInfo ->\n                val (appInfo, appIcon) = pkgInfo.toAppInfoAndIcon(hidden = false)\n                newAppMap[pkgInfo.packageName] = appInfo\n                if (appIcon != null) {\n                    newIconMap[pkgInfo.packageName] = appIcon\n                }\n            }\n        }\n    }\n    updateOtherUserAppInfo(newAppMap)\n    userAppInfoMapFlow.value = newAppMap\n    userAppIconMapFlow.value = newIconMap\n    if (!app.justStarted) {\n        toast(\"应用列表更新成功\")\n    }\n    if (canQueryPkgState.value && mayAuthDenied && app.justStarted) {\n        // 概率出现：即使有「读取应用列表权限」在刚启动时也只能获取到少量应用，延迟几秒再试一次\n        appScope.launch {\n            delay(App.START_WAIT_TIME)\n            updateAllAppInfo()\n        }\n    }\n}\n\nfun initAppState() {\n    packageReceiver\n    updateAllAppInfo()\n    appScope.launchTry {\n        shizukuContextFlow.drop(1).collect {\n            updateAppMutex.launchTry(appScope, Dispatchers.IO) {\n                updateOtherUserAppInfo()\n            }\n        }\n    }\n    appScope.launchTry {\n        willUpdateAppIds.debounce(3000)\n            .filter { it.isNotEmpty() }\n            .collect { updatePartAppInfo(it) }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/BarUtils.kt",
    "content": "package li.songe.gkd.util\n\nimport android.annotation.SuppressLint\nimport android.content.res.Resources\nimport android.graphics.Rect\nimport android.view.WindowInsets\nimport android.view.accessibility.AccessibilityWindowInfo\nimport androidx.annotation.WorkerThread\nimport li.songe.gkd.a11y.A11yRuleEngine\nimport li.songe.gkd.app\n\n@SuppressLint(\"DiscouragedApi\", \"InternalInsetResource\")\nobject BarUtils {\n    fun getNavBarHeight(): Int {\n        val res = Resources.getSystem()\n        val resourceId = res.getIdentifier(\"navigation_bar_height\", \"dimen\", \"android\")\n        return if (resourceId != 0) {\n            res.getDimensionPixelSize(resourceId)\n        } else {\n            0\n        }\n    }\n\n    fun getStatusBarHeight(): Int {\n        val resources = Resources.getSystem()\n        val resourceId = resources.getIdentifier(\"status_bar_height\", \"dimen\", \"android\")\n        return resources.getDimensionPixelSize(resourceId)\n    }\n\n    @WorkerThread\n    fun checkStatusBarVisible(): Boolean? {\n        val r = if (AndroidTarget.R) {\n            // 后台/小窗模式下依然可判断\n            app.windowManager.currentWindowMetrics.windowInsets.getInsets(WindowInsets.Type.statusBars()).top > 0\n        } else {\n            null\n        }\n        if (r == false) return r\n        val windows = A11yRuleEngine.compatWindows()\n        val rect = Rect() // Rect(0, 0 - 1280, 152)\n        if (windows.isNotEmpty()) {\n            return windows.any { w ->\n                w.getBoundsInScreen(rect)\n                w.type == AccessibilityWindowInfo.TYPE_SYSTEM\n                        && !w.isFocused && !w.isActive\n                        && rect.top == 0 && rect.left == 0 && rect.right == ScreenUtils.getScreenWidth()\n                        && rect.bottom <= getStatusBarHeight() * 2\n            }\n        }\n        return r\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/CollectionExt.kt",
    "content": "package li.songe.gkd.util\n\nfun <T> Set<T>.switchItem(t: T): Set<T> {\n    return if (contains(t)) {\n        minus(t)\n    } else {\n        plus(t)\n    }\n}\n\ninline fun <T> Iterable<T>.filterIfNotAll(predicate: (T) -> Boolean): List<T> {\n    return if (count() > 0 && !all(predicate)) {\n        filter(predicate)\n    } else {\n        this as? List ?: toList()\n    }\n}\n\ninline fun <T, K> Iterable<T>.distinctByIfAny(selector: (T) -> K): List<T> {\n    return if (count() > 1 && any { v1 -> any { v2 -> v1 !== v2 && selector(v1) == selector(v2) } }) {\n        distinctBy(selector)\n    } else {\n        this as? List ?: toList()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/Constants.kt",
    "content": "package li.songe.gkd.util\n\nconst val FILE_SHORT_URL = \"https://f.gkd.li/\"\nconst val IMPORT_SHORT_URL = \"https://i.gkd.li/i/\"\n\nconst val SERVER_SCRIPT_URL =\n    \"https://registry.npmmirror.com/@gkd-kit/config/latest/files/dist/server.js\"\n\nconst val REPOSITORY_URL = \"https://github.com/gkd-kit/gkd\"\nconst val ISSUES_URL = \"${REPOSITORY_URL}/issues\"\n\nconst val HOME_PAGE_URL = \"https://gkd.li\"\n\nconst val LOCAL_SUBS_ID = -2L\nconst val LOCAL_HTTP_SUBS_ID = -1L\nval LOCAL_SUBS_IDS = arrayOf(LOCAL_SUBS_ID, LOCAL_HTTP_SUBS_ID)\n\nconst val EMPTY_RULE_TIP = \"暂无规则\"\n\nobject ShortUrlSet {\n    const val URL1 = \"https://gkd.li?r=1\"\n    const val URL2 = \"https://gkd.li?r=2\"\n    const val URL3 = \"https://gkd.li?r=3\"\n    const val URL4 = \"https://gkd.li?r=4\"\n    const val URL5 = \"https://gkd.li?r=5\"\n    const val URL6 = \"https://gkd.li?r=6\"\n    const val URL10 = \"https://gkd.li?r=10\"\n    const val URL11 = \"https://gkd.li?r=11\"\n    const val URL12 = \"https://gkd.li?r=12\"\n    const val URL13 = \"https://gkd.li?r=13\"\n    const val URL14 = \"https://gkd.li?r=14\"\n    const val URL15 = \"https://gkd.li?r=15\"\n}\n\nconst val shizukuAppId = \"moe.shizuku.privileged.api\"\n\nconst val PLAY_STORE_URL = \"https://play.google.com/store/apps/details?id=li.songe.gkd\"\n\nconst val systemUiAppId = \"com.android.systemui\"\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/CoroutineExt.kt",
    "content": "package li.songe.gkd.util\n\nimport androidx.compose.runtime.Composable\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.CoroutineStart\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.yield\nimport li.songe.gkd.data.RpcError\nimport kotlin.coroutines.CoroutineContext\nimport kotlin.coroutines.EmptyCoroutineContext\n\nfun CoroutineScope.launchTry(\n    context: CoroutineContext = EmptyCoroutineContext,\n    start: CoroutineStart = CoroutineStart.DEFAULT,\n    silent: Boolean = false,\n    block: suspend CoroutineScope.() -> Unit,\n) = launch(context, start) {\n    try {\n        block()\n    } catch (e: CancellationException) {\n        e.printStackTrace()\n    } catch (_: InterruptRuleMatchException) {\n    } catch (e: Throwable) {\n        LogUtils.d(e)\n        if (!silent) {\n            toast(e.message ?: e.stackTraceToString(), loc = \"\", forced = e is RpcError)\n        }\n    }\n}\n\n@Composable\nfun CoroutineScope.launchAsFn(\n    context: CoroutineContext = EmptyCoroutineContext,\n    start: CoroutineStart = CoroutineStart.DEFAULT,\n    block: suspend CoroutineScope.() -> Unit,\n): () -> Unit = {\n    launch(context, start) {\n        try {\n            block()\n        } catch (e: CancellationException) {\n            e.printStackTrace()\n        } catch (e: Throwable) {\n            LogUtils.d(e)\n            toast(e.message ?: e.stackTraceToString(), loc = \"\")\n        }\n    }\n}\n\n@Composable\nfun <T> CoroutineScope.launchAsFn(\n    context: CoroutineContext = EmptyCoroutineContext,\n    start: CoroutineStart = CoroutineStart.DEFAULT,\n    block: suspend CoroutineScope.(T) -> Unit,\n): (T) -> Unit = {\n    launch(context, start) {\n        try {\n            block(it)\n        } catch (e: CancellationException) {\n            e.printStackTrace()\n        } catch (e: Throwable) {\n            LogUtils.d(e)\n            toast(e.message ?: e.stackTraceToString(), loc = \"\")\n        }\n    }\n}\n\nsuspend fun stopCoroutine(): Nothing {\n    currentCoroutineContext()[Job]?.cancel()\n    yield()\n    // the following code will not be run\n    throw CancellationException(\"Coroutine stopped\")\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/FlowExt.kt",
    "content": "package li.songe.gkd.util\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\n\n\nfun <T, M> StateFlow<T>.mapState(\n    coroutineScope: CoroutineScope,\n    mapper: (value: T) -> M,\n): StateFlow<M> = map { mapper(it) }.stateIn(\n    coroutineScope, SharingStarted.Eagerly, mapper(value)\n)\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/FolderExt.kt",
    "content": "package li.songe.gkd.util\n\nimport android.text.format.DateUtils\nimport androidx.annotation.WorkerThread\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.META\nimport li.songe.gkd.app\nimport li.songe.gkd.data.AppInfo\nimport li.songe.gkd.data.UserInfo\nimport li.songe.gkd.data.otherUserMapFlow\nimport li.songe.gkd.permission.allPermissionStates\nimport li.songe.gkd.shizuku.currentUserId\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport java.io.File\n\nfun File.autoMk(): File {\n    if (!exists()) {\n        mkdirs()\n    }\n    return this\n}\n\nprivate val filesDir: File by lazy {\n    app.getExternalFilesDir(null) ?: error(\"failed getExternalFilesDir\")\n}\n\nval dbFolder: File\n    get() = filesDir.resolve(\"db\").autoMk()\nval shFolder: File\n    get() = filesDir.resolve(\"sh\").autoMk()\nval storeFolder: File\n    get() = filesDir.resolve(\"store\").autoMk()\nval subsFolder: File\n    get() = filesDir.resolve(\"subscription\").autoMk()\nval snapshotFolder: File\n    get() = filesDir.resolve(\"snapshot\").autoMk()\nval logFolder: File\n    get() = filesDir.resolve(\"log\").autoMk()\n\nval privateStoreFolder: File\n    get() = app.filesDir.resolve(\"store\").autoMk()\n\nprivate val cacheDir by lazy { app.externalCacheDir ?: app.cacheDir }\nval coilCacheDir: File\n    get() = cacheDir.resolve(\"coil\").autoMk()\nval sharedDir: File\n    get() = cacheDir.resolve(\"shared\").autoMk()\nprivate val tempDir: File\n    get() = cacheDir.resolve(\"temp\").autoMk()\n\nfun createGkdTempDir(): File {\n    return tempDir\n        .resolve(System.currentTimeMillis().toString())\n        .apply { mkdirs() }\n}\n\nprivate fun removeExpired(dir: File) {\n    dir.listFiles()?.forEach { f ->\n        if (System.currentTimeMillis() - f.lastModified() > DateUtils.HOUR_IN_MILLIS) {\n            if (f.isDirectory) {\n                f.deleteRecursively()\n            } else if (f.isFile) {\n                f.delete()\n            }\n        }\n    }\n}\n\nfun clearCache() {\n    removeExpired(sharedDir)\n    removeExpired(tempDir)\n}\n\n@Serializable\nprivate data class AppJsonData(\n    val userId: Int = currentUserId,\n    val apps: List<AppInfo> = userAppInfoMapFlow.value.values.toList(),\n    val otherUsers: List<UserInfo> = otherUserMapFlow.value.values.toList(),\n    val othersApps: List<AppInfo> = otherUserAppInfoMapFlow.value.values.toList(),\n)\n\n@WorkerThread\nfun buildLogFile(): File {\n    val tempDir = createGkdTempDir()\n    val files = mutableListOf(dbFolder, storeFolder, subsFolder, logFolder)\n    tempDir.resolve(\"meta.json\").also {\n        it.writeText(toJson5String(META))\n        files.add(it)\n    }\n    tempDir.resolve(\"apps.json\").also {\n        it.writeText(json.encodeToString(AppJsonData()))\n        files.add(it)\n    }\n    tempDir.resolve(\"shizuku.txt\").also {\n        it.writeText(shizukuContextFlow.value.states.joinToString(\"\\n\") { state ->\n            state.first + \": \" + state.second.toString()\n        })\n        files.add(it)\n    }\n    tempDir.resolve(\"permission.txt\").also {\n        it.writeText(allPermissionStates.joinToString(\"\\n\") { state ->\n            state.name + \": \" + state.stateFlow.value.toString()\n        })\n        it.appendText(\"\\nappListAuthAbnormalFlow: ${appListAuthAbnormalFlow.value}\")\n        files.add(it)\n    }\n    tempDir.resolve(\"gkd-${META.versionCode}-v${META.versionName}.json\").also {\n        it.writeText(json.encodeToString(META))\n        files.add(it)\n    }\n    val logZipFile = sharedDir.resolve(\"log-${System.currentTimeMillis()}.zip\")\n    ZipUtils.zipFiles(files, logZipFile)\n    tempDir.deleteRecursively()\n    return logZipFile\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/Github.kt",
    "content": "package li.songe.gkd.util\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.window.DialogProperties\nimport io.ktor.client.call.body\nimport io.ktor.client.plugins.onUpload\nimport io.ktor.client.request.forms.MultiPartFormDataContent\nimport io.ktor.client.request.forms.formData\nimport io.ktor.client.request.header\nimport io.ktor.client.request.post\nimport io.ktor.client.request.put\nimport io.ktor.client.request.setBody\nimport io.ktor.client.statement.HttpResponse\nimport io.ktor.client.statement.bodyAsText\nimport io.ktor.http.Headers\nimport io.ktor.http.HttpHeaders\nimport io.ktor.http.HttpMessageBuilder\nimport io.ktor.http.HttpStatusCode\nimport kotlinx.coroutines.delay\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\nimport kotlinx.serialization.json.jsonObject\nimport li.songe.gkd.data.GithubPoliciesAsset\nimport li.songe.gkd.ui.WebViewRoute\nimport li.songe.gkd.ui.component.PerfIcon\nimport li.songe.gkd.ui.component.PerfIconButton\nimport li.songe.gkd.ui.component.autoFocus\nimport li.songe.gkd.ui.share.LocalMainViewModel\nimport li.songe.json5.Json5\nimport java.io.File\n\nprivate fun HttpMessageBuilder.setCommonHeaders(cookie: String) {\n    header(\"Cookie\", cookie)\n    header(\"Referer\", \"https://github.com/gkd-kit/inspect/issues/46\")\n    header(\"Origin\", \"https://github.com\")\n    header(\n        \"User-Agent\",\n        \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0\"\n    )\n}\n\nprivate fun String.json5ToJsonString(): String {\n    return json.encodeToString(Json5.parseToJson5Element(this))\n}\n\n@Suppress(\"PropertyName\")\n@Serializable\nprivate data class UploadPoliciesAssetsResponse(\n    val upload_url: String,\n    val asset_upload_url: String,\n    val asset_upload_authenticity_token: String,\n    val asset: GithubPoliciesAsset,\n    val form: Map<String, String>,\n)\n\ndata class GithubCookieException(override val message: String) : Exception(message)\n\nprivate suspend fun graphqlFetch(\n    cookie: String,\n    data: String,\n): HttpResponse {\n    return client.post(\"https://github.com/_graphql\") {\n        setCommonHeaders(cookie)\n        header(\"Accept\", \"application/json\")\n        header(\"Content-Type\", \"text/plain;charset=UTF-8\")\n        header(\"GitHub-Verified-Fetch\", \"true\")\n        setBody(data)\n    }\n}\n\n// https://github.com/lisonge/user-attachments\nsuspend fun uploadFileToGithub(\n    cookie: String,\n    file: File,\n    listener: ((progress: Float) -> Unit)\n): GithubPoliciesAsset {\n    // prepare upload asset\n    val policiesRawResp = client.post(\"https://github.com/upload/policies/assets\") {\n        setCommonHeaders(cookie)\n        header(\"GitHub-Verified-Fetch\", \"true\")\n        header(\"X-Requested-With\", \"XMLHttpRequest\")\n        setBody(MultiPartFormDataContent(formData {\n            append(\"repository_id\", \"661952005\")\n            append(\"name\", \"file.zip\")\n            append(\"size\", file.length().toString())\n            append(\"content_type\", \"application/x-zip-compressed\")\n        }))\n    }\n    if (policiesRawResp.status == HttpStatusCode.Unauthorized) {\n        throw GithubCookieException(\"检测到 cookie 失效, 请更换\")\n    }\n    val policiesResp = policiesRawResp.body<UploadPoliciesAssetsResponse>()\n\n    // upload to s3\n    val byteArray = file.readBytes()\n    client.post(policiesResp.upload_url) {\n        setCommonHeaders(cookie)\n        setBody(MultiPartFormDataContent(formData {\n            policiesResp.form.forEach { (key, value) ->\n                append(key, value)\n            }\n            append(\"file\", byteArray, Headers.build {\n                append(HttpHeaders.ContentType, \"application/x-zip-compressed\")\n                append(HttpHeaders.ContentDisposition, \"filename=\\\"file.zip\\\"\")\n            })\n        }))\n        onUpload { bytesSentTotal, contentLength ->\n            listener(bytesSentTotal / (contentLength ?: byteArray.size).toFloat())\n        }\n    }\n\n    // check assets\n    client.put(\"https://github.com\" + policiesResp.asset_upload_url) {\n        setCommonHeaders(cookie)\n        header(\"Accept\", \"application/json\")\n        setBody(MultiPartFormDataContent(formData {\n            append(\"authenticity_token\", policiesResp.asset_upload_authenticity_token)\n        }))\n    }\n\n    // send file url text to github comment\n    val commentResultResp = graphqlFetch(\n        cookie,\n        \"\"\"\n        {\n            query: '50e7774b5a519b88858e02e46e0348da',\n            variables: {\n              connections: [\n                'client:I_kwDOJ3SWBc6viUWN:__Issue__backTimelineItems_connection(visibleEventsOnly:true)',\n              ],\n              input: {\n                body: '${policiesResp.asset.href}',\n                subjectId: 'I_kwDOJ3SWBc6viUWN',\n              },\n            },\n        }\n        \"\"\".json5ToJsonString()\n    )\n    val commentResult = json.decodeFromString<JsonElement>(commentResultResp.bodyAsText())\n    val commentId = (commentResult.jsonObject[\"data\"]\n        ?.jsonObject[\"addComment\"]\n        ?.jsonObject[\"timelineEdge\"]\n        ?.jsonObject[\"node\"]\n        ?.jsonObject[\"id\"]?.toString() ?: error(\"commentId not found\"))\n\n    // delay is needed\n    delay(1000)\n\n    // unsubscribe the comment\n    graphqlFetch(\n        cookie,\n        \"\"\"\n        {\n            query: 'd0752b2e49295017f67c84f21bfe41a3',\n            variables: {\n                input: { state: 'UNSUBSCRIBED', subscribableId: 'I_kwDOJ3SWBc6viUWN' },\n            },\n        }\n        \"\"\".json5ToJsonString()\n    )\n\n    // delete the comment\n    graphqlFetch(\n        cookie,\n        \"\"\"\n        {\n            query: 'b0f125991160e607a64d9407db9c01b3',\n            variables: {\n                connections: [],\n                input: { id: $commentId },\n            },\n        }\n        \"\"\".json5ToJsonString()\n    )\n    return policiesResp.asset\n}\n\n@Composable\nfun EditGithubCookieDlg() {\n    val mainVm = LocalMainViewModel.current\n    val showEditCookieDlg by mainVm.showEditCookieDlgFlow.collectAsState()\n    if (showEditCookieDlg) {\n        var value by remember {\n            mutableStateOf(mainVm.githubCookieFlow.value)\n        }\n        AlertDialog(\n            properties = DialogProperties(dismissOnClickOutside = false),\n            onDismissRequest = {\n                mainVm.showEditCookieDlgFlow.value = false\n            },\n            title = {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    Text(text = \"Github Cookie\")\n                    PerfIconButton(\n                        imageVector = PerfIcon.HelpOutline,\n                        onClick = throttle {\n                            mainVm.showEditCookieDlgFlow.value = false\n                            mainVm.navigatePage(WebViewRoute(initUrl = ShortUrlSet.URL1))\n                        })\n                }\n            },\n            text = {\n                OutlinedTextField(\n                    value = value,\n                    onValueChange = {\n                        value = it.filter { c -> c != '\\n' && c != '\\r' }\n                    },\n                    placeholder = { Text(text = \"请输入 Github Cookie\") },\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .autoFocus(),\n                    maxLines = 10,\n                )\n            },\n            confirmButton = {\n                TextButton(onClick = {\n                    mainVm.showEditCookieDlgFlow.value = false\n                    mainVm.githubCookieFlow.value = value.trim()\n                    toast(\"更新成功\")\n                }) {\n                    Text(text = \"确认\")\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = { mainVm.showEditCookieDlgFlow.value = false }) {\n                    Text(text = \"取消\")\n                }\n            }\n        )\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/ImageUtils.kt",
    "content": "package li.songe.gkd.util\n\nimport android.content.ContentValues\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Environment\nimport android.provider.MediaStore\nimport androidx.core.net.toUri\nimport li.songe.gkd.app\nimport li.songe.gkd.permission.canWriteExternalStorage\nimport java.io.BufferedOutputStream\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.io.IOException\nimport java.io.OutputStream\n\n\nobject ImageUtils {\n    fun save2Album(\n        src: Bitmap,\n        quality: Int = 100,\n        format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,\n        recycle: Boolean = true,\n    ): Boolean {\n        val safeDirName = app.packageName\n        val suffix: String? = if (Bitmap.CompressFormat.JPEG == format) \"JPG\" else format.name\n        val fileName = System.currentTimeMillis().toString() + \"_\" + quality + \".\" + suffix\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {\n            if (!canWriteExternalStorage.updateAndGet()) {\n                return false\n            }\n            val picDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)\n            val destFile = File(picDir, \"$safeDirName/$fileName\")\n            BufferedOutputStream(FileOutputStream(destFile)).use {\n                val ret = src.compress(format, quality, it)\n                if (!ret) return false\n            }\n            if (recycle && !src.isRecycled) {\n                src.recycle()\n            }\n            @Suppress(\"DEPRECATION\")\n            val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)\n            intent.setData((\"file://\" + destFile.absolutePath).toUri())\n            app.sendBroadcast(intent)\n            return true\n        } else {\n            val contentValues = ContentValues()\n            contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, fileName)\n            contentValues.put(MediaStore.Images.Media.MIME_TYPE, \"image/*\")\n            val contentUri: Uri\n            if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {\n                contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI\n            } else {\n                contentUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI\n            }\n            contentValues.put(\n                MediaStore.Images.Media.RELATIVE_PATH,\n                Environment.DIRECTORY_DCIM + \"/\" + safeDirName\n            )\n            contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1)\n            val uri: Uri? = app.contentResolver.insert(contentUri, contentValues)\n            if (uri == null) {\n                return false\n            }\n            var os: OutputStream? = null\n            try {\n                os = app.contentResolver.openOutputStream(uri)\n                src.compress(format, quality, os!!)\n                contentValues.clear()\n                contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)\n                app.contentResolver.update(uri, contentValues, null, null)\n                return true\n            } catch (e: Exception) {\n                app.contentResolver.delete(uri, null, null)\n                e.printStackTrace()\n                return false\n            } finally {\n                try {\n                    os?.close()\n                } catch (e: IOException) {\n                    e.printStackTrace()\n                }\n            }\n        }\n\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/IntentExt.kt",
    "content": "package li.songe.gkd.util\n\nimport android.app.Service\nimport android.content.ComponentName\nimport android.content.ContentValues\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Environment\nimport android.provider.MediaStore\nimport android.provider.Settings\nimport android.webkit.MimeTypeMap\nimport androidx.core.content.FileProvider\nimport androidx.core.net.toUri\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport li.songe.gkd.META\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.app\nimport li.songe.gkd.isActivityVisible\nimport li.songe.gkd.permission.canWriteExternalStorage\nimport li.songe.gkd.permission.foregroundServiceSpecialUseState\nimport li.songe.gkd.permission.notificationState\nimport li.songe.gkd.permission.requiredPermission\nimport java.io.File\nimport kotlin.reflect.KClass\n\nfun MainActivity.shareFile(file: File, title: String) {\n    val uri = FileProvider.getUriForFile(\n        app, \"${app.packageName}.provider\", file\n    )\n    val intent = Intent().apply {\n        action = Intent.ACTION_SEND\n        putExtra(Intent.EXTRA_STREAM, uri)\n        type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)\n        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n    }\n    tryStartActivity(\n        Intent.createChooser(\n            intent, title\n        )\n    )\n}\n\nsuspend fun MainActivity.saveFileToDownloads(file: File) {\n    if (AndroidTarget.Q) {\n        val values = ContentValues().apply {\n            put(MediaStore.MediaColumns.DISPLAY_NAME, file.name)\n            put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)\n        }\n        withContext(Dispatchers.IO) {\n            val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)\n                ?: error(\"创建URI失败\")\n            contentResolver.openOutputStream(uri)?.use { outputStream ->\n                outputStream.write(file.readBytes())\n                outputStream.flush()\n            }\n        }\n    } else {\n        requiredPermission(this, canWriteExternalStorage)\n        val targetFile = File(\n            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),\n            file.name\n        )\n        targetFile.writeBytes(file.readBytes())\n    }\n    toast(\"已保存 ${file.name} 到下载\")\n}\n\nfun Context.tryStartActivity(intent: Intent) {\n    try {\n        startActivity(intent)\n    } catch (e: Exception) {\n        e.printStackTrace()\n        LogUtils.d(\"tryStartActivity\", e)\n        toast(\"跳转失败\\n\" + (e.message ?: e.stackTraceToString()))\n    }\n}\n\nfun openWeChatScaner() {\n    val intent = app.packageManager.getLaunchIntentForPackage(\"com.tencent.mm\")?.apply {\n        putExtra(\"LauncherUI.From.Scaner.Shortcut\", true)\n    }\n    if (intent == null) {\n        toast(\"请检查微信是否安装或禁用\")\n        return\n    }\n    app.tryStartActivity(intent)\n}\n\nfun openA11ySettings() {\n    val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)\n    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK\n    app.tryStartActivity(intent)\n}\n\nfun openAppDetailsSettings() {\n    val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {\n        data = \"package:${app.packageName}\".toUri()\n        flags = Intent.FLAG_ACTIVITY_NEW_TASK\n    }\n    app.tryStartActivity(intent)\n}\n\nfun openUri(uri: String) {\n    val u = try {\n        uri.toUri()\n    } catch (e: Exception) {\n        e.printStackTrace()\n        toast(\"非法链接\")\n        return\n    }\n    openUri(u)\n}\n\nfun openUri(uri: Uri) {\n    val intent = Intent(Intent.ACTION_VIEW, uri)\n    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n    app.tryStartActivity(intent)\n}\n\nfun openApp(appId: String) {\n    val intent = app.packageManager.getLaunchIntentForPackage(appId)\n    if (intent != null) {\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        app.tryStartActivity(intent)\n    } else {\n        toast(\"请检查此应用是否安装或禁用\")\n    }\n}\n\nfun <T : Service> stopServiceByClass(clazz: KClass<T>) {\n    val intent = Intent(app, clazz.java)\n    app.stopService(intent)\n}\n\nfun <T : Service> startForegroundServiceByClass(clazz: KClass<T>) {\n    if (!notificationState.checkOrToast()) return\n    if (!foregroundServiceSpecialUseState.checkOrToast()) return\n    val intent = Intent(app, clazz.java)\n    try {\n        app.startForegroundService(intent)\n    } catch (e: Throwable) {\n        LogUtils.d(e)\n        val prefix = if (isActivityVisible) \"\" else \"${META.appName}: \"\n        toast(\"${prefix}启动服务失败: ${e.message}\", forced = true)\n    }\n}\n\nval Intent.extraCptName: ComponentName?\n    get() = if (AndroidTarget.TIRAMISU) {\n        getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName::class.java)\n    } else {\n        @Suppress(\"DEPRECATION\")\n        getParcelableExtra(Intent.EXTRA_COMPONENT_NAME) as? ComponentName?\n    }\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/KeyboardUtils.kt",
    "content": "package li.songe.gkd.util\n\nimport android.app.Activity\nimport android.graphics.Rect\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.ViewTreeObserver.OnGlobalLayoutListener\nimport android.view.Window\nimport android.view.WindowManager\nimport android.widget.EditText\nimport li.songe.gkd.app\nimport kotlin.math.abs\n\nobject KeyboardUtils {\n    private const val TAG_ON_GLOBAL_LAYOUT_LISTENER = -8\n    private var sDecorViewDelta = 0\n    private fun getDecorViewInvisibleHeight(window: Window): Int {\n        val decorView = window.decorView\n        val outRect = Rect()\n        decorView.getWindowVisibleDisplayFrame(outRect)\n        val delta = abs(decorView.bottom - outRect.bottom)\n        if (delta <= BarUtils.getNavBarHeight() + BarUtils.getStatusBarHeight()) {\n            sDecorViewDelta = delta\n            return 0\n        }\n        return delta - sDecorViewDelta\n    }\n\n    fun registerSoftInputChangedListener(window: Window, onSoftInputChanged: (Int) -> Unit) {\n        val flags = window.attributes.flags\n        if ((flags and WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) != 0) {\n            window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)\n        }\n        val contentView = window.findViewById<View>(android.R.id.content)\n        val decorViewInvisibleHeightPre = intArrayOf(getDecorViewInvisibleHeight(window))\n        val onGlobalLayoutListener = OnGlobalLayoutListener {\n            val height = getDecorViewInvisibleHeight(window)\n            if (decorViewInvisibleHeightPre[0] != height) {\n                onSoftInputChanged(height)\n                decorViewInvisibleHeightPre[0] = height\n            }\n        }\n        contentView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener)\n        contentView.setTag(TAG_ON_GLOBAL_LAYOUT_LISTENER, onGlobalLayoutListener)\n    }\n\n\n    fun hideSoftInput(activity: Activity) {\n        hideSoftInput(activity.window)\n    }\n\n    fun hideSoftInput(window: Window) {\n        val tempTag = \"keyboardTagView\"\n        var view = window.currentFocus\n        if (view == null) {\n            val decorView = window.decorView\n            val focusView = decorView.findViewWithTag<View?>(tempTag)\n            if (focusView == null) {\n                view = EditText(window.context)\n                view.tag = tempTag\n                (decorView as ViewGroup).addView(view, 0, 0)\n            } else {\n                view = focusView\n            }\n            view.requestFocus()\n        }\n        hideSoftInput(view)\n    }\n\n    fun hideSoftInput(view: View) {\n        app.inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/LifecycleCallbacks.kt",
    "content": "package li.songe.gkd.util\n\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport li.songe.loc.Loc\nimport java.util.WeakHashMap\n\nprivate val cbMap = WeakHashMap<Any, HashMap<Int, MutableList<Any>>>()\n\ntypealias CbFn = () -> Unit\n\n@Suppress(\"UNCHECKED_CAST\")\nprivate fun <T> OnSimpleLife.cbs(method: Int): MutableList<T> = synchronized(cbMap) {\n    return cbMap.getOrPut(this) { hashMapOf() }\n        .getOrPut(method) { mutableListOf() } as MutableList<T>\n}\n\ninterface OnSimpleLife {\n    fun onCreated(f: CbFn) = cbs<CbFn>(1).add(f)\n    fun onCreated() = cbs<CbFn>(1).forEach { it() }\n\n    fun onDestroyed(f: CbFn) = cbs<CbFn>(2).add(f)\n    fun onDestroyed() = cbs<CbFn>(2).forEach { it() }\n\n    @Loc\n    fun useLogLifecycle(@Loc loc: String = \"\") {\n        onCreated { LogUtils.d(\"onCreated -> \" + this::class.simpleName, loc = loc) }\n        onDestroyed { LogUtils.d(\"onDestroyed -> \" + this::class.simpleName, loc = loc) }\n        if (this is OnA11yLife) {\n            onA11yConnected {\n                LogUtils.d(\n                    \"onA11yConnected -> \" + this::class.simpleName,\n                    loc = loc,\n                )\n            }\n        }\n        if (this is OnTileLife) {\n            onTileClicked { LogUtils.d(\"onTileClicked -> \" + this::class.simpleName, loc = loc) }\n        }\n    }\n\n    val scope: CoroutineScope\n    fun useScope(): CoroutineScope = MainScope().apply { onDestroyed { cancel() } }\n\n    fun useAliveFlow(stateFlow: MutableStateFlow<Boolean>) {\n        onCreated { stateFlow.value = true }\n        onDestroyed { stateFlow.value = false }\n    }\n\n    @Loc\n    fun useAliveToast(\n        name: String,\n        delayMillis: Long = 0L,\n        @Loc loc: String = \"\",\n    ) {\n        onCreated {\n            toast(\"${name}已启动\", loc = loc, delayMillis = delayMillis)\n        }\n        onDestroyed {\n            toast(\"${name}已关闭\", loc = loc)\n        }\n    }\n}\n\ninterface OnA11yLife : OnSimpleLife {\n    fun onA11yConnected(f: CbFn) = cbs<CbFn>(3).add(f)\n    fun onA11yConnected() = cbs<CbFn>(3).forEach { it() }\n}\n\ninterface OnTileLife : OnSimpleLife {\n    fun onStartListened(f: CbFn) = cbs<CbFn>(4).add(f)\n    fun onStartListened() = cbs<CbFn>(4).forEach { it() }\n\n    fun onStopListened(f: CbFn) = cbs<CbFn>(5).add(f)\n    fun onStopListened() = cbs<CbFn>(5).forEach { it() }\n\n    fun onTileClicked(f: CbFn) = cbs<CbFn>(6).add(f)\n    fun onTileClicked() = cbs<CbFn>(6).forEach { it() }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/LinkLoad.kt",
    "content": "package li.songe.gkd.util\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/LoadStatus.kt",
    "content": "package li.songe.gkd.util\n\nsealed class LoadStatus<out T> {\n    data class Loading(val progress: Float = 0f) : LoadStatus<Nothing>()\n    data class Failure(val exception: Exception) : LoadStatus<Nothing>()\n    data class Success<T>(val result: T) : LoadStatus<T>()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/LogUtils.kt",
    "content": "package li.songe.gkd.util\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.util.Log\nimport com.hjq.device.compat.DeviceBrand\nimport com.hjq.device.compat.DeviceMarketName\nimport com.hjq.device.compat.DeviceOs\nimport li.songe.gkd.META\nimport li.songe.gkd.app\nimport li.songe.loc.Loc\nimport java.util.concurrent.Executors\nimport kotlin.time.Duration.Companion.days\n\nobject LogUtils {\n    @Loc\n    fun d(\n        vararg args: Any?,\n        @Loc loc: String = \"\",\n        @Loc(\"{fileName}\") fileName: String = \"\",\n        tag: String = fileName.substringBeforeLast('.'),\n    ) {\n        val name = Thread.currentThread().name\n        val actualLoc = loc.substring(\"li.songe.gkd.\".length)\n        val texts = args.map { stringify(it) }\n        if (META.debuggable) {\n            val msg = buildString {\n                append(\"$name, $actualLoc\")\n                texts.forEachIndexed { i, text ->\n                    if (texts.size == 1) {\n                        append(\"\\n\")\n                    } else {\n                        append(\"\\n[$i]: \")\n                    }\n                    append(text)\n                }\n            }\n            Log.d(tag, msg)\n        }\n        val t = System.currentTimeMillis()\n        logFileExecutor.run {\n            logToFile(tag, name, actualLoc, texts, t)\n        }\n    }\n}\n\nprivate val logFileExecutor = Executors.newSingleThreadExecutor()\nprivate const val MAX_LOG_KEEP_DAYS = 7\nprivate val deviceInfoText by lazy {\n    val deviceInfos = listOf(\n        android.os.Build.MANUFACTURER,\n        android.os.Build.MODEL,\n        DeviceBrand.getBrandName(),\n        DeviceOs.getOsName() + DeviceOs.getOsVersionName() + DeviceOs.getOsBigVersionCode(),\n        DeviceMarketName.getMarketName(app)\n    )\n    buildString {\n        append(\"Android: ${android.os.Build.VERSION.RELEASE} (${android.os.Build.VERSION.SDK_INT})\\n\")\n        append(\"Device: ${deviceInfos.joinToString(\"/\")}\\n\")\n        append(\"App: ${META.versionName} (${META.versionCode})\\n\")\n    }\n}\n\nprivate fun logToFile(tag: String, name: String, loc: String, texts: List<String>, t: Long) {\n    val file = logFolder.resolve(\"gkd-${t.format(\"yyyyMMdd\")}.log\")\n    val sb = StringBuilder()\n    if (!file.exists()) {\n        val files = logFolder.listFiles()\n        if (files != null && files.size >= MAX_LOG_KEEP_DAYS) {\n            files.forEach {\n                if (t - it.lastModified() > MAX_LOG_KEEP_DAYS.days.inWholeMilliseconds) {\n                    it.delete()\n                }\n            }\n        }\n        sb.append(\"=== Log ===\\n\")\n        sb.append(\"Date: ${t.format(\"yyyy-MM-dd HH:mm:ss.SSS\")}\\n\")\n        sb.append(deviceInfoText)\n        sb.append(\"=== Log ===\\n\\n\")\n    }\n    sb.append(t.format(\"HH:mm:ss.SSS\"))\n    sb.append(\" $tag, $name, $loc\")\n    if (texts.size == 1) {\n        sb.append('\\n')\n        sb.append(texts[0])\n    } else {\n        texts.forEachIndexed { i, text ->\n            sb.append(\"\\n[$i]: \")\n            sb.append(text)\n        }\n    }\n    sb.append(\"\\n\\n\")\n    file.appendText(sb.toString())\n}\n\nprivate fun stringify(arg: Any?): String = when (arg) {\n    is Bundle -> {\n        val sb = StringBuilder()\n        sb.append(\"Bundle{\")\n        val keys = arg.keySet()\n        keys.forEachIndexed { index, key ->\n            @Suppress(\"DEPRECATION\")\n            val value = arg.get(key)\n            sb.append(\"$key=${stringify(value)}\")\n            if (index < keys.size - 1) {\n                sb.append(\",\")\n            }\n        }\n        sb.append(\"}\")\n        sb.toString()\n    }\n\n    is Intent -> {\n        val sb = StringBuilder()\n        sb.append(\"Intent{\")\n        arg.action?.let { sb.append(\"action=$it,\") }\n        arg.data?.let { sb.append(\"data=$it,\") }\n        arg.type?.let { sb.append(\"type=$it,\") }\n        arg.component?.let { sb.append(\"component=$it,\") }\n        arg.categories?.let { sb.append(\"categories=$it,\") }\n        arg.extras?.let { sb.append(\"extras=${stringify(it)}\") }\n        sb.append(\"}\")\n        sb.toString()\n    }\n\n    is Throwable -> Log.getStackTraceString(arg)\n\n    else -> arg.toString()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/MutexState.kt",
    "content": "package li.songe.gkd.util\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi\nimport kotlinx.coroutines.flow.FlowCollector\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlin.coroutines.CoroutineContext\n\nclass MutexState() {\n    val mutex: Mutex = Mutex()\n    val intState = MutableStateFlow(0)\n\n    @OptIn(ExperimentalForInheritanceCoroutinesApi::class)\n    val state = object : StateFlow<Boolean> {\n        override val value: Boolean\n            get() = intState.value > 0\n        override val replayCache: List<Boolean>\n            get() = listOf(value)\n\n        override suspend fun collect(collector: FlowCollector<Boolean>): Nothing {\n            var currentValue = value\n            collector.emit(currentValue)\n            intState.collect {\n                val newValue = it > 0\n                if (newValue != currentValue) {\n                    currentValue = newValue\n                    collector.emit(currentValue)\n                }\n            }\n        }\n    }\n\n    suspend inline fun withStateLock(block: () -> Unit): Unit = mutex.withLock {\n        intState.update { it + 1 }\n        try {\n            block()\n        } finally {\n            intState.update { it - 1 }\n        }\n    }\n\n    suspend inline fun whenUnLock(block: () -> Unit) {\n        if (mutex.isLocked) return\n        withStateLock(block)\n    }\n\n    fun launchTry(\n        scope: CoroutineScope,\n        context: CoroutineContext,\n        block: suspend () -> Unit,\n    ) = scope.launchTry(context = context) {\n        withStateLock {\n            block()\n        }\n    }.let { }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/NetworkExt.kt",
    "content": "package li.songe.gkd.util\n\nimport java.net.NetworkInterface\nimport java.net.ServerSocket\n\nfun getIpAddressInLocalNetwork(): List<String> {\n    val networkInterfaces = try {\n        NetworkInterface.getNetworkInterfaces().asSequence()\n    } catch (e: Exception) {\n        // android.system.ErrnoException: getifaddrs failed: EACCES (Permission denied)\n        toast(\"获取HOST失败:\" + e.message)\n        return emptyList()\n    }\n    val localAddresses = networkInterfaces.flatMap {\n        it.inetAddresses.asSequence().filter { inetAddress ->\n            inetAddress.isSiteLocalAddress && !(inetAddress.hostAddress?.contains(\":\")\n                ?: false) && inetAddress.hostAddress != \"127.0.0.1\"\n        }.map { inetAddress -> inetAddress.hostAddress }\n    }\n    return localAddresses.toList()\n}\n\n\nfun isPortAvailable(port: Int): Boolean {\n    var serverSocket: ServerSocket? = null\n    return try {\n        serverSocket = ServerSocket(port)\n        serverSocket.reuseAddress = true\n        true\n    } catch (_: Exception) {\n        false\n    } finally {\n        serverSocket?.close()\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/NetworkUtils.kt",
    "content": "package li.songe.gkd.util\n\nimport java.net.InetAddress\n\nobject NetworkUtils {\n    fun isAvailable(): Boolean = try {\n        InetAddress.getByName(\"www.baidu.com\") != null\n    } catch (_: Throwable) {\n        false\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/Option.kt",
    "content": "package li.songe.gkd.util\n\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.state.ToggleableState\nimport li.songe.gkd.ui.component.PerfIcon\n\nsealed interface Option<T> {\n    val value: T\n    val label: String\n    val options: List<Option<T>>\n}\n\nsealed interface OptionIcon {\n    val icon: ImageVector\n}\n\nsealed interface OptionMenuLabel {\n    val menuLabel: String\n}\n\nfun <V, T : Option<V>> Iterable<T>.findOption(value: V): T {\n    return find { it.value == value } ?: first()\n}\n\nfun Option<Boolean?>.toToggleableState() = when (value) {\n    true -> ToggleableState.On\n    false -> ToggleableState.Off\n    null -> ToggleableState.Indeterminate\n}\n\nsealed class AppSortOption(override val value: Int, override val label: String) : Option<Int> {\n    override val options get() = objects\n\n    data object ByAppName : AppSortOption(0, \"按应用名称\")\n    data object ByActionTime : AppSortOption(2, \"按最近触发\")\n    data object ByUsedTime : AppSortOption(3, \"按最近使用\")\n\n    companion object {\n        val objects by lazy { listOf(ByAppName, ByUsedTime, ByActionTime) }\n    }\n}\n\nsealed class UpdateTimeOption(\n    override val value: Long,\n    override val label: String\n) : Option<Long> {\n    override val options get() = objects\n\n    data object Pause : UpdateTimeOption(-1, \"暂停\")\n    data object Everyday : UpdateTimeOption(24 * 60 * 60_000, \"每天\")\n    data object Every3Days : UpdateTimeOption(24 * 60 * 60_000 * 3, \"每3天\")\n    data object Every7Days : UpdateTimeOption(24 * 60 * 60_000 * 7, \"每7天\")\n\n    companion object {\n        val objects by lazy { listOf(Pause, Everyday, Every3Days, Every7Days) }\n    }\n}\n\nsealed class DarkThemeOption(\n    override val value: Boolean?,\n    override val label: String,\n    override val menuLabel: String,\n    override val icon: ImageVector\n) : Option<Boolean?>, OptionIcon, OptionMenuLabel {\n    override val options get() = objects\n\n    data object FollowSystem : DarkThemeOption(null, \"自动\", \"自动\", PerfIcon.AutoMode)\n    data object AlwaysEnable : DarkThemeOption(true, \"启用\", \"深色\", PerfIcon.DarkMode)\n    data object AlwaysDisable : DarkThemeOption(false, \"关闭\", \"浅色\", PerfIcon.LightMode)\n\n    companion object {\n        val objects by lazy { listOf(FollowSystem, AlwaysEnable, AlwaysDisable) }\n    }\n}\n\nsealed class EnableGroupOption(\n    override val value: Boolean?,\n    override val label: String\n) : Option<Boolean?> {\n    override val options get() = objects\n\n    data object FollowSubs : EnableGroupOption(null, \"跟随订阅\")\n    data object AllEnable : EnableGroupOption(true, \"全部启用\")\n    data object AllDisable : EnableGroupOption(false, \"全部关闭\")\n\n    companion object {\n        val objects by lazy { listOf(FollowSubs, AllEnable, AllDisable) }\n    }\n}\n\nsealed class RuleSortOption(override val value: Int, override val label: String) : Option<Int> {\n    override val options get() = objects\n\n    data object ByDefault : RuleSortOption(0, \"按默认顺序\")\n    data object ByActionTime : RuleSortOption(1, \"按最近触发\")\n    data object ByRuleName : RuleSortOption(2, \"按规则名称\")\n\n    companion object {\n        val objects by lazy { listOf(ByDefault, ByActionTime, ByRuleName) }\n    }\n}\n\nsealed class UpdateChannelOption(\n    override val value: Int,\n    override val label: String,\n    val url: String\n) : Option<Int> {\n    override val options get() = objects\n\n    data object Stable : UpdateChannelOption(\n        0,\n        \"稳定版\",\n        \"https://registry.npmmirror.com/@gkd-kit/app/latest/files/index.json\"\n    )\n\n    data object Beta : UpdateChannelOption(\n        1,\n        \"测试版\",\n        \"https://registry.npmmirror.com/@gkd-kit/app-beta/latest/files/index.json\"\n    )\n\n    companion object {\n        val objects by lazy { listOf(Stable, Beta) }\n    }\n}\n\nsealed interface BinaryOption : Option<Int> {\n    fun include(flag: Int): Boolean = (value and flag) != 0\n    fun invert(flag: Int): Int = value xor flag\n\n    companion object {\n        fun combine(options: Collection<BinaryOption>): Int {\n            return options.fold(0) { a, b -> a or b.value }\n        }\n    }\n}\n\n\nsealed class AppGroupOption(\n    override val value: Int,\n    override val label: String\n) : BinaryOption {\n    override val options get() = allObjects\n\n    data object SystemGroup : AppGroupOption(1 shl 0, \"系统应用\")\n    data object UserGroup : AppGroupOption(1 shl 1, \"用户应用\")\n    data object UnInstalledGroup : AppGroupOption(1 shl 2, \"未安装应用\")\n\n    companion object {\n        val normalObjects by lazy { listOf(SystemGroup, UserGroup) }\n        val allObjects by lazy { listOf(SystemGroup, UserGroup, UnInstalledGroup) }\n    }\n}\n\nsealed class AutomatorModeOption(\n    override val value: Int,\n    override val label: String,\n) : Option<Int> {\n    override val options get() = objects\n\n    data object A11yMode : AutomatorModeOption(1, \"无障碍\")\n    data object AutomationMode : AutomatorModeOption(2, \"自动化\")\n\n    companion object {\n        val objects by lazy { listOf(A11yMode, AutomationMode) }\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/Others.kt",
    "content": "package li.songe.gkd.util\n\nimport android.app.Activity\nimport android.content.ComponentName\nimport android.content.Intent\nimport android.content.pm.PackageInfo\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.Paint\nimport android.graphics.drawable.Drawable\nimport android.os.Handler\nimport android.os.Looper\nimport android.provider.AlarmClock\nimport android.provider.MediaStore\nimport android.provider.Settings\nimport androidx.compose.animation.AnimatedContentTransitionScope\nimport androidx.compose.animation.ContentTransform\nimport androidx.compose.animation.SizeTransform\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.ui.unit.sp\nimport androidx.core.graphics.get\nimport kotlinx.serialization.json.JsonElement\nimport li.songe.gkd.META\nimport li.songe.gkd.MainActivity\nimport li.songe.gkd.app\nimport li.songe.json5.Json5\nimport li.songe.json5.Json5EncoderConfig\nimport li.songe.json5.encodeToJson5String\nimport java.io.File\nimport kotlin.reflect.KClass\nimport kotlin.reflect.jvm.jvmName\n\nprivate val componentNameCache by lazy { HashMap<String, ComponentName>() }\n\nval KClass<*>.componentName\n    get() = componentNameCache.getOrPut(jvmName) { ComponentName(META.appId, jvmName) }\n\nfun Bitmap.isFullTransparent(): Boolean {\n    repeat(width) { x ->\n        repeat(height) { y ->\n            if (this[x, y] != Color.TRANSPARENT) {\n                return false\n            }\n        }\n    }\n    return true\n}\n\nclass InterruptRuleMatchException() : Exception()\n\nfun getShowActivityId(appId: String, activityId: String?): String? {\n    return if (activityId != null) {\n        if (activityId.startsWith(appId) && activityId.getOrNull(appId.length) == '.') {\n            activityId.substring(appId.length)\n        } else {\n            activityId\n        }\n    } else {\n        null\n    }\n}\n\nfun MainActivity.fixSomeProblems() {\n    fixTransparentNavigationBar()\n}\n\nprivate fun Activity.fixTransparentNavigationBar() {\n    // 修复在浅色主题下导航栏背景不透明的问题\n    if (AndroidTarget.Q) {\n        window.isNavigationBarContrastEnforced = false\n    } else {\n        @Suppress(\"DEPRECATION\")\n        window.navigationBarColor = Color.TRANSPARENT\n    }\n}\n\n\nfun <S : Comparable<S>> AnimatedContentTransitionScope<S>.getUpDownTransform(): ContentTransform {\n    return if (targetState > initialState) {\n        slideInVertically { height -> height } + fadeIn() togetherWith\n                slideOutVertically { height -> -height } + fadeOut()\n    } else {\n        slideInVertically { height -> -height } + fadeIn() togetherWith\n                slideOutVertically { height -> height } + fadeOut()\n    }.using(\n        SizeTransform(clip = false)\n    )\n}\n\nval defaultJson5Config = Json5EncoderConfig(indent = \"\\u0020\\u0020\", trailingComma = true)\ninline fun <reified T> toJson5String(value: T): String {\n    if (value is JsonElement) {\n        return Json5.encodeToString(value, defaultJson5Config)\n    }\n    return json.encodeToJson5String(value, defaultJson5Config)\n}\n\nfun drawTextToBitmap(text: String, bitmap: Bitmap) {\n    val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {\n        textSize = 32.sp.px\n        color = Color.BLUE\n        textAlign = Paint.Align.CENTER\n    }\n    val canvas = Canvas(bitmap)\n    val strList = text.split('\\n')\n    strList.forEachIndexed { i, str ->\n        canvas.drawText(\n            str,\n            bitmap.width / 2f,\n            (bitmap.height / 2f) + (i - strList.size / 2f) * (paint.textSize + 4.sp.px),\n            paint\n        )\n    }\n}\n\n// https://github.com/gkd-kit/gkd/issues/924\nprivate val Drawable.safeDrawable: Drawable?\n    get() = if (intrinsicHeight > 0 && intrinsicWidth > 0) {\n        this\n    } else {\n        null\n    }\n\nval PackageInfo.pkgIcon: Drawable?\n    get() = applicationInfo?.loadIcon(app.packageManager)?.safeDrawable\n\nprivate fun Char.isAsciiLetter(): Boolean {\n    return this in 'a'..'z' || this in 'A'..'Z'\n}\n\nprivate fun Char.isAsciiVar(): Boolean {\n    return this.isAsciiLetter() || this in '0'..'9' || this == '_'\n}\n\nprivate fun Char.isAsciiClassVar(): Boolean {\n    return this.isAsciiVar() || this == '$'\n}\n\n// https://developer.android.com/build/configure-app-module?hl=zh-cn\nfun String.isValidAppId(): Boolean {\n    if (!contains('.')) return false\n    if (!first().isAsciiLetter()) return false\n    var i = 0\n    while (i < length) {\n        val c = get(i)\n        if (c == '.') {\n            i++\n            if (getOrNull(i)?.isAsciiLetter() != true) {\n                return false\n            }\n        } else if (!c.isAsciiVar()) {\n            return false\n        }\n        i++\n    }\n    return true\n}\n\nfun String.isValidActivityId(): Boolean {\n    if (isEmpty()) return false\n    var i = 0\n    while (i < length) {\n        val c = get(i)\n        if (c == '.') {\n            i++\n            if (getOrNull(i)?.isAsciiClassVar() == false) {\n                return false\n            }\n        } else if (!c.isAsciiClassVar()) {\n            return false\n        }\n        i++\n    }\n    return true\n}\n\nobject AppListString {\n    fun decode(text: String): Set<String> {\n        return text.split('\\n').filter { a -> a.isValidAppId() }.toHashSet()\n    }\n\n    fun encode(set: Set<String>, append: Boolean = false): String {\n        val list = set.sorted()\n        if (append) {\n            return list.sortedBy { id -> if (id in appInfoMapFlow.value) 0 else 1 }\n                .joinToString(separator = \"\\n\\n\", postfix = \"\\n\\n\") {\n                    val name = appInfoMapFlow.value[it]?.name\n                    if (name != null) {\n                        \"$it\\n# $name\"\n                    } else {\n                        it\n                    }\n                }\n        }\n        return list.joinToString(\"\\n\")\n    }\n\n    fun getDefaultBlockList(): Set<String> {\n        val set = hashSetOf(META.appId, systemUiAppId)\n        listOf(\n            Intent.ACTION_MAIN to Intent.CATEGORY_HOME,\n            Intent.ACTION_MAIN to Intent.CATEGORY_APP_GALLERY,\n            Intent.ACTION_MAIN to Intent.CATEGORY_APP_CONTACTS,\n            Intent.ACTION_MAIN to Intent.CATEGORY_APP_CALENDAR,\n            Intent.ACTION_MAIN to Intent.CATEGORY_APP_MESSAGING,\n            Intent.ACTION_MAIN to Intent.CATEGORY_APP_CALCULATOR,\n            Intent.ACTION_OPEN_DOCUMENT to Intent.CATEGORY_OPENABLE,\n            AlarmClock.ACTION_SHOW_ALARMS to null,\n            MediaStore.ACTION_IMAGE_CAPTURE to null,\n            Settings.ACTION_SETTINGS to null,\n        ).forEach {\n            app.resolveAppId(it.first, it.second)?.let(set::add)\n        }\n        return set\n    }\n}\n\nval isMainThread: Boolean get() = Looper.getMainLooper() == Looper.myLooper()\n\nfun runMainPost(delayMillis: Long = 0L, r: Runnable) {\n    if (delayMillis == 0L && isMainThread) {\n        r.run()\n        return\n    }\n    Handler(Looper.getMainLooper()).postDelayed(r, delayMillis)\n}\n\nfun getShareApkFile(): File {\n    return sharedDir.resolve(\"gkd-v${META.versionName}.apk\").apply {\n        File(app.packageCodePath).copyTo(this, overwrite = true)\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/ScreenUtils.kt",
    "content": "package li.songe.gkd.util\n\nimport android.content.res.Configuration\nimport android.content.res.Resources\nimport android.graphics.Point\nimport li.songe.gkd.app\n\n@Suppress(\"DEPRECATION\")\nobject ScreenUtils {\n    fun getScreenWidth(): Int = Point().apply {\n        app.windowManager.defaultDisplay.getRealSize(this)\n    }.x\n\n    fun getScreenHeight(): Int = Point().apply {\n        app.windowManager.defaultDisplay.getRealSize(this)\n    }.y\n\n    fun getScreenDensityDpi(): Int = Resources.getSystem().displayMetrics.densityDpi\n\n    fun isLandscape(): Boolean {\n        return app.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE\n    }\n\n    fun isScreenLock(): Boolean = app.keyguardManager.inKeyguardRestrictedInputMode()\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/ScreenshotUtil.kt",
    "content": "package li.songe.gkd.util\n\nimport android.app.Activity\nimport android.app.Activity.RESULT_OK\nimport android.content.Context\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.graphics.PixelFormat\nimport android.hardware.display.DisplayManager\nimport android.hardware.display.VirtualDisplay\nimport android.media.Image\nimport android.media.ImageReader\nimport android.media.projection.MediaProjection\nimport android.media.projection.MediaProjectionManager\nimport android.os.Handler\nimport android.os.Looper\nimport androidx.core.graphics.createBitmap\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\nimport kotlin.coroutines.suspendCoroutine\n\n// https://github.com/npes87184/ScreenShareTile/blob/master/app/src/main/java/com/npes87184/screenshottile/ScreenshotService.kt\n\nclass ScreenshotUtil(\n    private val context: Context,\n    private val screenshotIntent: Intent\n) {\n\n    private val handler by lazy { Handler(Looper.getMainLooper()) }\n    private var virtualDisplay: VirtualDisplay? = null\n    private var imageReader: ImageReader? = null\n    private var mediaProjection: MediaProjection? = null\n\n\n    private val mediaProjectionManager by lazy {\n        context.getSystemService(\n            Activity.MEDIA_PROJECTION_SERVICE\n        ) as MediaProjectionManager\n    }\n\n    private val width: Int\n        get() = ScreenUtils.getScreenWidth()\n    private val height: Int\n        get() = ScreenUtils.getScreenHeight()\n    private val dpi: Int\n        get() = ScreenUtils.getScreenDensityDpi()\n\n    fun destroy() {\n        imageReader?.setOnImageAvailableListener(null, null)\n        virtualDisplay?.release()\n        imageReader?.close()\n        mediaProjection?.stop()\n    }\n\n    //    TODO android13 上一半概率获取到全透明图片, android12 暂无此问题\n    suspend fun execute() = suspendCoroutine { block ->\n        imageReader = ImageReader.newInstance(\n            width, height,\n            PixelFormat.RGBA_8888, 2\n        )\n        if (mediaProjection == null) {\n            mediaProjection = mediaProjectionManager.getMediaProjection(\n                RESULT_OK,\n                screenshotIntent\n            )\n        }\n        virtualDisplay = mediaProjection!!.createVirtualDisplay(\n            \"screenshot\",\n            width,\n            height,\n            dpi,\n            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,\n            imageReader!!.surface,\n            null,\n            handler\n        )\n        var resumed = false\n        imageReader!!.setOnImageAvailableListener({ reader ->\n            if (resumed) return@setOnImageAvailableListener\n            var image: Image? = null\n            var bitmapWithStride: Bitmap? = null\n            val bitmap: Bitmap?\n            try {\n                image = reader.acquireLatestImage()\n                if (image != null) {\n                    val planes = image.planes\n                    val buffer = planes[0].buffer\n                    val pixelStride = planes[0].pixelStride\n                    val rowStride = planes[0].rowStride\n                    bitmapWithStride = createBitmap(rowStride / pixelStride, height)\n                    bitmapWithStride.copyPixelsFromBuffer(buffer)\n                    bitmap = Bitmap.createBitmap(bitmapWithStride, 0, 0, width, height)\n                    if (!bitmap.isFullTransparent()) {\n                        imageReader?.setOnImageAvailableListener(null, null)\n                        block.resume(bitmap)\n                        resumed = true\n                    }\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n                imageReader?.setOnImageAvailableListener(null, null)\n                block.resumeWithException(e)\n            } finally {\n                bitmapWithStride?.recycle()\n                image?.close()\n            }\n        }, handler)\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/Singleton.kt",
    "content": "package li.songe.gkd.util\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.http.ContentType\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.json.Json\nimport java.text.Collator\nimport java.util.Locale\n\n\nval json by lazy {\n    Json {\n        ignoreUnknownKeys = true\n        explicitNulls = false\n        encodeDefaults = true\n    }\n}\n\nval keepNullJson by lazy {\n    Json(from = json) {\n        explicitNulls = true\n    }\n}\n\nval client by lazy {\n    HttpClient(OkHttp) {\n        install(ContentNegotiation) {\n            json(json, ContentType.Any)\n        }\n        engine {\n            clientCacheSize = 0\n        }\n    }\n}\n\nval collator by lazy { Collator.getInstance(Locale.CHINESE)!! }\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/SnapshotExt.kt",
    "content": "package li.songe.gkd.util\n\nimport android.graphics.Bitmap\nimport androidx.core.graphics.createBitmap\nimport androidx.core.graphics.set\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.json.JsonArray\nimport kotlinx.serialization.json.JsonObject\nimport li.songe.gkd.a11y.A11yRuleEngine\nimport li.songe.gkd.a11y.TopActivity\nimport li.songe.gkd.a11y.topActivityFlow\nimport li.songe.gkd.data.ComplexSnapshot\nimport li.songe.gkd.data.RpcError\nimport li.songe.gkd.data.info2nodeList\nimport li.songe.gkd.db.DbSet\nimport li.songe.gkd.notif.snapshotNotif\nimport li.songe.gkd.service.ScreenshotService\nimport li.songe.gkd.shizuku.shizukuContextFlow\nimport li.songe.gkd.store.storeFlow\nimport java.io.File\nimport kotlin.math.min\n\nobject SnapshotExt {\n\n    private fun snapshotParentPath(id: Long) = snapshotFolder.resolve(id.toString())\n    fun snapshotFile(id: Long) = snapshotParentPath(id).resolve(\"${id}.json\")\n    private fun minSnapshotFile(id: Long): File {\n        return snapshotParentPath(id).resolve(\"${id}.min.json\")\n    }\n\n    suspend fun getMinSnapshot(id: Long): JsonObject {\n        val f = minSnapshotFile(id)\n        if (!f.exists()) {\n            val text = withContext(Dispatchers.IO) { snapshotFile(id).readText() }\n            val snapshotJson = withContext(Dispatchers.Default) {\n                // #1185\n                json.decodeFromString<JsonObject>(text)\n            }\n            val minSnapshot = JsonObject(snapshotJson.toMutableMap().apply {\n                this[\"nodes\"] = JsonArray(emptyList())\n            })\n            withContext(Dispatchers.IO) {\n                f.writeText(keepNullJson.encodeToString(minSnapshot))\n            }\n            return minSnapshot\n        }\n        val text = withContext(Dispatchers.IO) { f.readText() }\n        return withContext(Dispatchers.Default) {\n            json.decodeFromString<JsonObject>(text)\n        }\n    }\n\n    fun screenshotFile(id: Long) = snapshotParentPath(id).resolve(\"${id}.png\")\n\n    suspend fun snapshotZipFile(\n        snapshotId: Long,\n        appId: String? = null,\n        activityId: String? = null\n    ): File {\n        val filename = if (appId != null) {\n            val name =\n                appInfoMapFlow.value[appId]?.name?.filterNot { c -> c in \"\\\\/:*?\\\"<>|\" || c <= ' ' }\n            if (activityId != null) {\n                \"${(name ?: appId).take(20)}_${\n                    activityId.split('.').last().take(40)\n                }-${snapshotId}.zip\"\n            } else {\n                \"${(name ?: appId).take(20)}-${snapshotId}.zip\"\n            }\n        } else {\n            \"${snapshotId}.zip\"\n        }\n        val file = sharedDir.resolve(filename)\n        if (file.exists()) {\n            file.delete()\n        }\n        withContext(Dispatchers.IO) {\n            ZipUtils.zipFiles(\n                listOf(\n                    snapshotFile(snapshotId),\n                    screenshotFile(snapshotId)\n                ),\n                file\n            )\n        }\n        return file\n    }\n\n    fun removeSnapshot(id: Long) {\n        snapshotParentPath(id).apply {\n            if (exists()) {\n                deleteRecursively()\n            }\n        }\n    }\n\n    private fun emptyScreenBitmap(text: String): Bitmap {\n        return createBitmap(ScreenUtils.getScreenWidth(), ScreenUtils.getScreenHeight()).apply {\n            drawTextToBitmap(text, this)\n        }\n    }\n\n    private fun cropBitmapStatusBar(bitmap: Bitmap): Bitmap {\n        val tempBp = bitmap.run {\n            if (!isMutable || config == Bitmap.Config.HARDWARE) {\n                copy(Bitmap.Config.ARGB_8888, true)\n            } else {\n                this\n            }\n        }\n        val barHeight = min(BarUtils.getStatusBarHeight(), tempBp.height)\n        for (x in 0 until tempBp.width) {\n            for (y in 0 until barHeight) {\n                tempBp[x, y] = 0\n            }\n        }\n        return tempBp\n    }\n\n    private val captureLoading = MutableStateFlow(false)\n    suspend fun captureSnapshot(\n        skipScreenshot: Boolean = false,\n        forcedCropStatusBar: Boolean = false,\n    ): ComplexSnapshot {\n        if (A11yRuleEngine.instance == null) {\n            throw RpcError(\"服务不可用，请先授权\")\n        }\n        if (captureLoading.value) {\n            throw RpcError(\"正在保存快照，不可重复操作\")\n        }\n        captureLoading.value = true\n        try {\n            val rootNode =\n                A11yRuleEngine.instance?.safeActiveWindow\n                    ?: throw RpcError(\"当前应用没有无障碍信息，捕获失败\")\n            if (storeFlow.value.showSaveSnapshotToast) {\n                toast(\"正在保存快照...\", forced = true)\n            }\n            val (snapshot, bitmap) = coroutineScope {\n                val d1 = async(Dispatchers.IO) {\n                    val appId = rootNode.packageName.toString()\n                    var activityId = shizukuContextFlow.value.topCpn()?.className\n                    if (activityId == null) {\n                        var topActivity = topActivityFlow.value\n                        var i = 0L\n                        while (topActivity.appId != appId) {\n                            delay(100)\n                            topActivity = topActivityFlow.value\n                            i += 100\n                            if (i >= 2000) {\n                                topActivity = TopActivity(appId = appId)\n                                break\n                            }\n                        }\n                        activityId = topActivity.activityId\n                    }\n                    ComplexSnapshot(\n                        id = System.currentTimeMillis(),\n                        appId = appId,\n                        activityId = activityId,\n                        screenHeight = ScreenUtils.getScreenHeight(),\n                        screenWidth = ScreenUtils.getScreenWidth(),\n                        isLandscape = ScreenUtils.isLandscape(),\n                        nodes = info2nodeList(rootNode)\n                    )\n                }\n                val d2 = async(Dispatchers.IO) {\n                    if (skipScreenshot) {\n                        emptyScreenBitmap(\"跳过截图\\n请自行替换\")\n                    } else {\n                        A11yRuleEngine.screenshot()\n                            ?: ScreenshotService.screenshot()\n                            ?: emptyScreenBitmap(\"无截图权限\\n请自行替换\")\n                    }.let {\n                        if (storeFlow.value.hideSnapshotStatusBar && (forcedCropStatusBar || BarUtils.checkStatusBarVisible() == true)) {\n                            cropBitmapStatusBar(it)\n                        } else {\n                            it\n                        }\n                    }\n                }\n                d1.await() to d2.await()\n            }\n            withContext(Dispatchers.IO) {\n                snapshotParentPath(snapshot.id).autoMk()\n                screenshotFile(snapshot.id).outputStream().use { stream ->\n                    bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)\n                }\n                snapshotFile(snapshot.id).writeText(keepNullJson.encodeToString(snapshot))\n                minSnapshotFile(snapshot.id).writeText(\n                    keepNullJson.encodeToString(\n                        snapshot.copy(\n                            nodes = emptyList()\n                        )\n                    )\n                )\n                DbSet.snapshotDao.insert(snapshot.toSnapshot())\n            }\n            toast(\"快照成功\", forced = true)\n            val desc = snapshot.appInfo?.name ?: snapshot.appId\n            snapshotNotif.copy(text = \"快照「$desc」已保存至记录\").notifySelf()\n            return snapshot\n        } finally {\n            captureLoading.value = false\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/SubsState.kt",
    "content": "package li.songe.gkd.util\n\nimport io.ktor.client.request.get\nimport io.ktor.client.statement.bodyAsText\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withContext\nimport li.songe.gkd.appScope\nimport li.songe.gkd.data.AppRule\nimport li.songe.gkd.data.CategoryConfig\nimport li.songe.gkd.data.GlobalRule\nimport li.songe.gkd.data.RawSubscription\nimport li.songe.gkd.data.ResolvedAppGroup\nimport li.songe.gkd.data.ResolvedGlobalGroup\nimport li.songe.gkd.data.SubsConfig\nimport li.songe.gkd.data.SubsItem\nimport li.songe.gkd.data.SubsVersion\nimport li.songe.gkd.db.DbSet\nimport li.songe.json5.decodeFromJson5String\nimport java.net.URI\n\nval subsItemsFlow by lazy {\n    DbSet.subsItemDao.query().stateIn(appScope, SharingStarted.Eagerly, emptyList())\n}\n\nprivate fun getCheckUpdateUrl(\n    subsItem: SubsItem,\n    subscription: RawSubscription?,\n): String? {\n    val checkUpdateUrl = subscription?.checkUpdateUrl ?: return null\n    val updateUrl = subscription.updateUrl ?: subsItem.updateUrl ?: return checkUpdateUrl\n    try {\n        return URI(updateUrl).resolve(checkUpdateUrl).toString()\n    } catch (e: Exception) {\n        e.printStackTrace()\n    }\n    return null\n}\n\nsealed class SubsEntryType {\n    abstract val subsItem: SubsItem\n    abstract val subscription: RawSubscription?\n    val checkUpdateUrl by lazy { getCheckUpdateUrl(subsItem, subscription) }\n}\n\ndata class SubsEntry(\n    override val subsItem: SubsItem,\n    override val subscription: RawSubscription?,\n) : SubsEntryType()\n\ndata class UsedSubsEntry(\n    override val subsItem: SubsItem,\n    override val subscription: RawSubscription,\n) : SubsEntryType()\n\nval subsLoadErrorsFlow = MutableStateFlow<Map<Long, Exception>>(emptyMap())\nval subsRefreshErrorsFlow = MutableStateFlow<Map<Long, Exception>>(emptyMap())\nval subsMapFlow = MutableStateFlow<Map<Long, RawSubscription>>(emptyMap())\n\nval latestRecordFlow by lazy {\n    DbSet.actionLogDao.queryLatest().stateIn(appScope, SharingStarted.Eagerly, null)\n}\nval latestRecordDescFlow by lazy {\n    combine(\n        latestRecordFlow,\n        subsMapFlow,\n        appInfoMapFlow,\n    ) { record, subsMap, appMap ->\n        if (record == null) return@combine null\n        val isAppRule = record.groupType == SubsConfig.AppGroupType\n        val groupName = if (isAppRule) {\n            subsMap[record.subsId]?.apps?.find { a -> a.id == record.appId }?.groups?.find { g -> g.key == record.groupKey }?.name\n        } else {\n            subsMap[record.subsId]?.globalGroups?.find { g -> g.key == record.groupKey }?.name\n        }\n        val appName = appMap[record.appId]?.name\n        val appShowName = appName ?: record.appId\n        if (groupName != null) {\n            if (groupName.startsWith(appShowName)) {\n                groupName\n            } else {\n                if (isAppRule) {\n                    \"$appShowName/$groupName\"\n                } else {\n                    \"$groupName/$appShowName\"\n                }\n            }\n        } else {\n            appShowName\n        }\n    }.stateIn(appScope, SharingStarted.Eagerly, null)\n}\n\nval subsEntriesFlow by lazy {\n    combine(\n        subsItemsFlow,\n        subsMapFlow,\n    ) { subsItems, subsIdToRaw ->\n        subsItems.map { s ->\n            SubsEntry(\n                subsItem = s,\n                subscription = subsIdToRaw[s.id],\n            )\n        }\n    }.stateIn(appScope, SharingStarted.Eagerly, emptyList())\n}\n\nval usedSubsEntriesFlow by lazy {\n    subsEntriesFlow.map { list ->\n        list.filter { s -> s.subsItem.enable && s.subscription?.hasRule == true }\n            .map { UsedSubsEntry(it.subsItem, it.subscription!!) }\n    }.stateIn(appScope, SharingStarted.Eagerly, emptyList())\n}\n\nfun updateSubscription(subscription: RawSubscription) {\n    appScope.launchTry {\n        updateSubsMutex.withStateLock {\n            val subsId = subscription.id\n            val subsName = subscription.name\n            val newMap = subsMapFlow.value.toMutableMap()\n            val nextSubsRaw: RawSubscription\n            if (subsId < 0 && newMap[subsId]?.version == subscription.version) {\n                nextSubsRaw = subscription.run {\n                    copy(\n                        version = version + 1,\n                        apps = apps.filterIfNotAll { it.groups.isNotEmpty() }\n                            .distinctByIfAny { it.id },\n                    )\n                }\n            } else {\n                nextSubsRaw = subscription\n            }\n            newMap[subsId] = nextSubsRaw\n            subsMapFlow.value = newMap\n            if (subsLoadErrorsFlow.value.contains(subsId)) {\n                subsLoadErrorsFlow.update {\n                    it.toMutableMap().apply {\n                        remove(subsId)\n                    }\n                }\n            }\n            withContext(Dispatchers.IO) {\n                cleanupSubsConfig(subsId, nextSubsRaw)\n                DbSet.subsItemDao.updateMtime(subsId, System.currentTimeMillis())\n                subsFolder.resolve(\"${subsId}.json\")\n                    .writeText(json.encodeToString(nextSubsRaw))\n            }\n            LogUtils.d(\"更新订阅文件:id=${subsId},name=${subsName}\")\n        }\n    }\n}\n\nfun deleteSubscription(vararg subsIds: Long) {\n    appScope.launchTry(Dispatchers.IO) {\n        updateSubsMutex.mutex.withLock {\n            val deleteSize = DbSet.subsItemDao.deleteById(*subsIds)\n            if (deleteSize > 0) {\n                DbSet.subsConfigDao.deleteBySubsId(*subsIds)\n                DbSet.actionLogDao.deleteBySubsId(*subsIds)\n                DbSet.categoryConfigDao.deleteBySubsId(*subsIds)\n                val newMap = subsMapFlow.value.toMutableMap()\n                subsIds.forEach { id ->\n                    newMap.remove(id)\n                    subsFolder.resolve(\"$id.json\").apply {\n                        if (exists()) {\n                            delete()\n                        }\n                    }\n                }\n                subsMapFlow.value = newMap\n                toast(\"删除成功\")\n                LogUtils.d(\"deleteSubscription\", subsIds)\n            }\n        }\n    }\n}\n\nfun getCategoryEnable(\n    category: RawSubscription.RawCategory?,\n    categoryConfig: CategoryConfig?,\n): Boolean? = if (categoryConfig != null) {\n    // 批量配置\n    categoryConfig.enable\n} else {\n    // 批量默认\n    category?.enable\n}\n\nfun getGroupEnable(\n    group: RawSubscription.RawGroupProps,\n    subsConfig: SubsConfig?,\n    category: RawSubscription.RawCategory? = null,\n    categoryConfig: CategoryConfig? = null,\n): Boolean = group.valid && when (group) {\n    // 优先级: 规则用户配置 > 批量配置 > 批量默认 > 规则默认\n    is RawSubscription.RawAppGroup -> {\n        subsConfig?.enable ?: getCategoryEnable(category, categoryConfig) ?: group.enable ?: true\n    }\n\n    is RawSubscription.RawGlobalGroup -> {\n        subsConfig?.enable ?: group.enable ?: true\n    }\n}\n\ndata class RuleSummary(\n    val globalRules: List<GlobalRule> = emptyList(),\n    val globalGroups: List<ResolvedGlobalGroup> = emptyList(),\n    val appIdToRules: Map<String, List<AppRule>> = emptyMap(),\n    val appIdToGroups: Map<String, List<RawSubscription.RawAppGroup>> = emptyMap(),\n    val appIdToAllGroups: Map<String, List<ResolvedAppGroup>> = emptyMap(),\n) {\n    val appSize = appIdToRules.keys.size\n    val appGroupSize = appIdToGroups.values.sumOf { s -> s.size }\n\n    val numText = if (globalGroups.size + appGroupSize > 0) {\n        if (globalGroups.isNotEmpty()) {\n            \"${globalGroups.size}全局\" + if (appGroupSize > 0) {\n                \"/\"\n            } else {\n                \"\"\n            }\n        } else {\n            \"\"\n        } + if (appGroupSize > 0) {\n            \"${appSize}应用/${appGroupSize}规则组\"\n        } else {\n            \"\"\n        }\n    } else {\n        EMPTY_RULE_TIP\n    }\n\n    val slowGlobalGroups =\n        globalRules.filter { r -> r.isSlow }.distinctBy { r -> r.group }\n            .map { r -> r.group to r }\n    val slowAppGroups =\n        appIdToRules.values.flatten().filter { r -> r.isSlow }.distinctBy { r -> r.group }\n            .map { r -> r.group to r }\n    val slowGroupCount = slowGlobalGroups.size + slowAppGroups.size\n}\n\nval ruleSummaryFlow by lazy {\n    combine(\n        usedSubsEntriesFlow,\n        appInfoMapFlow,\n        DbSet.appConfigDao.queryUsedList(),\n        DbSet.subsConfigDao.queryUsedList(),\n        DbSet.categoryConfigDao.queryUsedList(),\n    ) { subsEntries, appInfoCache, appConfigs, subsConfigs, categoryConfigs ->\n        val globalSubsConfigs = subsConfigs.filter { c -> c.type == SubsConfig.GlobalGroupType }\n        val groupSubsConfigs = subsConfigs.filter { c -> c.type == SubsConfig.AppGroupType }\n        val appRules = HashMap<String, MutableList<AppRule>>()\n        val appGroups = HashMap<String, List<RawSubscription.RawAppGroup>>()\n        val appAllGroups =\n            HashMap<String, List<ResolvedAppGroup>>()\n        val globalRules = mutableListOf<GlobalRule>()\n        val globalGroups = mutableListOf<ResolvedGlobalGroup>()\n        subsEntries.forEach { (subsItem, rawSubs) ->\n            // global scope\n            val subGlobalSubsConfigs = globalSubsConfigs.filter { c -> c.subsId == subsItem.id }\n            val subGlobalGroupToRules =\n                mutableMapOf<RawSubscription.RawGlobalGroup, List<GlobalRule>>()\n            rawSubs.globalGroups.filter { g ->\n                (subGlobalSubsConfigs.find { c -> c.groupKey == g.key }?.enable\n                    ?: g.enable ?: true) && g.valid\n            }.forEach { groupRaw ->\n                val config = subGlobalSubsConfigs.find { c -> c.groupKey == groupRaw.key }\n                val g = ResolvedGlobalGroup(\n                    group = groupRaw,\n                    subscription = rawSubs,\n                    subsItem = subsItem,\n                    config = config\n                )\n                globalGroups.add(g)\n                val subRules = groupRaw.rules.map { ruleRaw ->\n                    GlobalRule(\n                        rule = ruleRaw,\n                        g = g,\n                        appInfoCache = appInfoCache,\n                    )\n                }\n                subGlobalGroupToRules[groupRaw] = subRules\n                globalRules.addAll(subRules)\n            }\n            subGlobalGroupToRules.values.forEach {\n                it.forEach { r ->\n                    r.groupToRules = subGlobalGroupToRules\n                }\n            }\n            subGlobalGroupToRules.clear()\n\n            // app scope\n            val subAppConfigs = appConfigs.filter { c -> c.subsId == subsItem.id }\n            val subGroupSubsConfigs = groupSubsConfigs.filter { c -> c.subsId == subsItem.id }\n            val subCategoryConfigs = categoryConfigs.filter { c -> c.subsId == subsItem.id }\n            rawSubs.apps.filter { appRaw ->\n                // 筛选 当前启用的 app 订阅规则\n                appRaw.groups.isNotEmpty() && (subAppConfigs.find { c -> c.appId == appRaw.id }?.enable\n                    ?: (appInfoCache[appRaw.id] != null))\n            }.forEach { appRaw ->\n                val subAppGroups = mutableListOf<RawSubscription.RawAppGroup>()\n                val appGroupConfigs = subGroupSubsConfigs.filter { c -> c.appId == appRaw.id }\n                val subAppGroupToRules = mutableMapOf<RawSubscription.RawAppGroup, List<AppRule>>()\n                val groupAndEnables = appRaw.groups.map { group ->\n                    val config = appGroupConfigs.find { c -> c.groupKey == group.key }\n                    val category = rawSubs.groupToCategoryMap[group]\n                    val categoryConfig =\n                        subCategoryConfigs.find { c -> c.categoryKey == category?.key }\n                    val enable = getGroupEnable(\n                        group,\n                        config,\n                        category,\n                        categoryConfig\n                    ) && group.valid\n                    ResolvedAppGroup(\n                        group = group,\n                        subscription = rawSubs,\n                        subsItem = subsItem,\n                        config = config,\n                        app = appRaw,\n                        enable = enable,\n                    )\n                }\n                appAllGroups[appRaw.id] = (appAllGroups[appRaw.id] ?: emptyList()) + groupAndEnables\n                groupAndEnables.forEach { g ->\n                    if (g.enable) {\n                        subAppGroups.add(g.group)\n                        val subRules = g.group.rules.map { ruleRaw ->\n                            AppRule(\n                                rule = ruleRaw,\n                                g = g,\n                                appInfo = appInfoCache[appRaw.id]\n                            )\n                        }.filter { r -> r.enable }\n                        subAppGroupToRules[g.group] = subRules\n                        if (subRules.isNotEmpty()) {\n                            val rules = appRules[appRaw.id] ?: mutableListOf()\n                            appRules[appRaw.id] = rules\n                            rules.addAll(subRules)\n                        }\n                    }\n                }\n                if (subAppGroups.isNotEmpty()) {\n                    appGroups[appRaw.id] = subAppGroups\n                }\n                subAppGroupToRules.values.forEach {\n                    it.forEach { r ->\n                        r.groupToRules = subAppGroupToRules\n                    }\n                }\n            }\n        }\n        RuleSummary(\n            globalRules = globalRules,\n            globalGroups = globalGroups,\n            appIdToRules = appRules,\n            appIdToGroups = appGroups,\n            appIdToAllGroups = appAllGroups\n        )\n    }.flowOn(Dispatchers.Default).stateIn(appScope, SharingStarted.Eagerly, RuleSummary())\n}\n\nfun getSubsStatus(ruleSummary: RuleSummary, count: Long): String {\n    return if (count > 0) {\n        \"${ruleSummary.numText}/${count}触发\"\n    } else {\n        ruleSummary.numText\n    }\n}\n\nprivate fun loadSubs(id: Long): RawSubscription {\n    val file = subsFolder.resolve(\"${id}.json\")\n    if (!file.exists()) {\n        // 某些设备出现这种情况\n        if (id == LOCAL_SUBS_ID) {\n            return RawSubscription(\n                id = LOCAL_SUBS_ID,\n                name = \"本地订阅\",\n                version = 0\n            )\n        }\n        if (id == LOCAL_HTTP_SUBS_ID) {\n            return RawSubscription(\n                id = LOCAL_HTTP_SUBS_ID,\n                name = \"内存订阅\",\n                version = 0\n            )\n        }\n        error(\"订阅文件不存在\")\n    }\n    val subscription = try {\n        RawSubscription.parse(file.readText(), json5 = false)\n    } catch (e: Exception) {\n        throw Exception(\"订阅文件解析失败\", e)\n    }\n    if (subscription.id != id) {\n        error(\"订阅文件id不一致\")\n    }\n    return subscription\n}\n\nprivate fun refreshRawSubsList(items: List<SubsItem>): Boolean {\n    if (items.isEmpty()) return false\n    val subscriptions = subsMapFlow.value.toMutableMap()\n    val errors = subsLoadErrorsFlow.value.toMutableMap()\n    var changed = false\n    items.forEach { s ->\n        try {\n            subscriptions[s.id] = loadSubs(s.id)\n            errors.remove(s.id)\n            changed = true\n        } catch (e: Exception) {\n            errors[s.id] = e\n        }\n    }\n    subsMapFlow.value = subscriptions\n    subsLoadErrorsFlow.value = errors\n    return changed\n}\n\nfun initSubsState() {\n    subsItemsFlow.value\n    appScope.launchTry(Dispatchers.IO) {\n        updateSubsMutex.withStateLock {\n            val items = DbSet.subsItemDao.queryAll()\n            refreshRawSubsList(items)\n        }\n    }\n}\n\nprivate suspend fun cleanupSubsConfig(subsId: Long, subsRaw: RawSubscription): Int {\n    val globalGroupKeys = subsRaw.globalGroups.map { it.key }.toHashSet()\n    val appIdToGroupKeys = subsRaw.apps.associate { a ->\n        a.id to a.groups.map { g -> g.key }.toHashSet()\n    }\n    val configs = DbSet.subsConfigDao.querySubsItemConfig(listOf(subsId))\n    val deleteList = configs.filter { c ->\n        when (c.type) {\n            SubsConfig.AppGroupType -> {\n                val groupKeys = appIdToGroupKeys[c.appId]\n                groupKeys == null || !groupKeys.contains(c.groupKey)\n            }\n\n            SubsConfig.GlobalGroupType -> !globalGroupKeys.contains(c.groupKey)\n            else -> false\n        }\n    }\n    if (deleteList.isEmpty()) return 0\n    DbSet.subsConfigDao.delete(*deleteList.toTypedArray())\n    LogUtils.d(\"清理已移除规则配置\", \"subsId=$subsId, delete=${deleteList.size}\")\n    return deleteList.size\n}\n\nval updateSubsMutex = MutexState()\n\nprivate suspend fun updateSubs(subsEntry: SubsEntry): RawSubscription? {\n    val subsItem = subsEntry.subsItem\n    val subsRaw = subsEntry.subscription\n    if (subsItem.updateUrl == null || subsItem.id < 0) return null\n    val checkUpdateUrl = subsEntry.checkUpdateUrl\n    if (checkUpdateUrl != null && subsRaw != null) {\n        try {\n            val subsVersion = json.decodeFromJson5String<SubsVersion>(\n                client.get(checkUpdateUrl).bodyAsText()\n            )\n            if (subsVersion.id == subsRaw.id && subsVersion.version <= subsRaw.version) {\n                return null\n            }\n        } catch (e: Exception) {\n            LogUtils.d(\"快速检测更新失败\", subsItem, e.message)\n        }\n    }\n    val updateUrl = subsRaw?.updateUrl ?: subsItem.updateUrl\n    val text = try {\n        client.get(updateUrl).bodyAsText()\n    } catch (e: Exception) {\n        throw Exception(\"请求更新链接失败\", e)\n    }\n    val newSubsRaw = try {\n        RawSubscription.parse(text)\n    } catch (e: Exception) {\n        throw Exception(\"解析文本失败\", e)\n    }\n    if (newSubsRaw.id != subsItem.id) {\n        error(\"新id=${newSubsRaw.id}不匹配旧id=${subsItem.id}\")\n    }\n    if (subsRaw != null && newSubsRaw.version <= subsRaw.version) {\n        LogUtils.d(\n            \"版本号不满足条件:id=${subsItem.id}\",\n            \"${subsRaw.version} -> ${newSubsRaw.version}\"\n        )\n        return null\n    }\n    return newSubsRaw\n}\n\nfun checkSubsUpdate(showToast: Boolean = false) = appScope.launchTry(Dispatchers.IO) {\n    if (updateSubsMutex.mutex.isLocked) {\n        return@launchTry\n    }\n    updateSubsMutex.withStateLock {\n        if (subsEntriesFlow.value.any { !it.subsItem.isLocal } && !NetworkUtils.isAvailable()) {\n            if (showToast) {\n                toast(\"网络不可用\")\n            }\n            return@withStateLock\n        }\n        LogUtils.d(\"开始检测更新\")\n        // 文件不存在, 重新加载\n        val changed = refreshRawSubsList(subsEntriesFlow.value.filter { it.subscription == null }\n            .map { it.subsItem })\n        if (changed) {\n            delay(500)\n        }\n        var successNum = 0\n        subsEntriesFlow.value.filter { !it.subsItem.isLocal }.forEach { subsEntry ->\n            try {\n                val newSubsRaw = updateSubs(subsEntry)\n                if (newSubsRaw != null) {\n                    updateSubscription(newSubsRaw)\n                    successNum++\n                }\n                if (subsRefreshErrorsFlow.value.contains(subsEntry.subsItem.id)) {\n                    subsRefreshErrorsFlow.update {\n                        it.toMutableMap().apply {\n                            remove(subsEntry.subsItem.id)\n                        }\n                    }\n                }\n            } catch (e: Exception) {\n                subsRefreshErrorsFlow.update {\n                    it.toMutableMap().apply {\n                        set(subsEntry.subsItem.id, e)\n                    }\n                }\n                LogUtils.d(\"检测更新失败\", e.message)\n            }\n        }\n        if (showToast) {\n            if (successNum > 0) {\n                toast(\"更新 $successNum 条订阅\")\n            } else {\n                toast(\"暂无更新\")\n            }\n        }\n        LogUtils.d(\"结束检测更新\")\n        delay(500)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/TimeExt.kt",
    "content": "package li.songe.gkd.util\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport java.text.SimpleDateFormat\nimport java.util.Locale\nimport java.util.concurrent.TimeUnit\n\nfun formatTimeAgo(timestamp: Long): String {\n    val currentTime = System.currentTimeMillis()\n    val timeDifference = currentTime - timestamp\n\n    val minutes = TimeUnit.MILLISECONDS.toMinutes(timeDifference)\n    val hours = TimeUnit.MILLISECONDS.toHours(timeDifference)\n    val days = TimeUnit.MILLISECONDS.toDays(timeDifference)\n    val weeks = days / 7\n    val months = (days / 30)\n    val years = (days / 365)\n    return when {\n        years > 0 -> \"${years}年前\"\n        months > 0 -> \"${months}月前\"\n        weeks > 0 -> \"${weeks}周前\"\n        days > 0 -> \"${days}天前\"\n        hours > 0 -> \"${hours}小时前\"\n        minutes > 0 -> \"${minutes}分钟前\"\n        else -> \"刚刚\"\n    }\n}\n\nprivate val formatDateMap by lazy { hashMapOf<String, SimpleDateFormat>() }\n\nfun Long.format(formatStr: String): String {\n    var df = formatDateMap[formatStr]\n    if (df == null) {\n        df = SimpleDateFormat(formatStr, Locale.getDefault())\n        formatDateMap[formatStr] = df\n    }\n    return df.format(this)\n}\n\ndata class ThrottleTimer(\n    private val interval: Long = 500L,\n) {\n    private var lastAccessTime: Long = 0L\n    fun expired(): Boolean {\n        val t = System.currentTimeMillis()\n        if (t - lastAccessTime > interval) {\n            lastAccessTime = t\n            return true\n        }\n        return false\n    }\n}\n\n@Composable\nfun throttle(\n    fn: (() -> Unit),\n): (() -> Unit) {\n    val timer = remember { ThrottleTimer() }\n    return remember(fn) {\n        {\n            if (timer.expired()) {\n                fn.invoke()\n            }\n        }\n    }\n}\n\n@Composable\nfun <T> throttle(\n    fn: ((T) -> Unit),\n): ((T) -> Unit) {\n    val timer = remember { ThrottleTimer() }\n    return remember(fn) {\n        {\n            if (timer.expired()) {\n                fn.invoke(it)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/Toast.kt",
    "content": "package li.songe.gkd.util\n\nimport android.content.ClipData\nimport android.content.Context\nimport android.content.res.Configuration\nimport android.graphics.Color\nimport android.graphics.Outline\nimport android.graphics.PixelFormat\nimport android.graphics.drawable.GradientDrawable\nimport android.graphics.text.LineBreaker\nimport android.util.TypedValue\nimport android.view.Gravity\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.ViewOutlineProvider\nimport android.view.WindowManager\nimport android.widget.TextView\nimport android.widget.Toast\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.core.graphics.toColorInt\nimport com.hjq.toast.Toaster\nimport com.hjq.toast.style.WhiteToastStyle\nimport li.songe.gkd.app\nimport li.songe.gkd.data.ResolvedRule\nimport li.songe.gkd.isActivityVisible\nimport li.songe.gkd.permission.canDrawOverlaysState\nimport li.songe.gkd.service.A11yService\nimport li.songe.gkd.service.OverlayWindowService\nimport li.songe.gkd.store.actionCountFlow\nimport li.songe.gkd.store.storeFlow\nimport li.songe.loc.Loc\n\n@Loc\nfun toast(\n    text: CharSequence,\n    forced: Boolean = false,\n    delayMillis: Long = 0L,\n    @Loc loc: String = \"\",\n) {\n    if (delayMillis > 0) {\n        runMainPost(delayMillis) {\n            toast(text = text, forced = forced, loc = loc)\n        }\n        return\n    }\n    if (forced || isActivityVisible || OverlayWindowService.isAnyAlive) {\n        Toaster.show(text)\n    }\n    if (loc.isNotEmpty()) {\n        LogUtils.d(text, loc = loc)\n    }\n}\n\nprivate val darkTheme: Boolean\n    get() = storeFlow.value.enableDarkTheme ?: app.resources.configuration.let {\n        it.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES\n    }\n\nprivate val toastYOffset: Int\n    get() = (ScreenUtils.getScreenHeight() * 0.12f).toInt()\n\nprivate val circleOutlineProvider by lazy {\n    object : ViewOutlineProvider() {\n        override fun getOutline(view: View?, outline: Outline?) {\n            if (view != null && outline != null) {\n                // 20.sp : line height, 12.dp : top/bottom padding\n                outline.setRoundRect(\n                    0,\n                    0,\n                    view.width,\n                    view.height,\n                    (12.dp.px * 2 + 20.sp.px) / 2f\n                )\n            }\n        }\n    }\n}\n\nprivate fun View.updateToastView() {\n    setPaddingRelative(\n        16.dp.px.toInt(),\n        12.dp.px.toInt(),\n        16.dp.px.toInt(),\n        12.dp.px.toInt(),\n    )\n    layoutParams = ViewGroup.LayoutParams(\n        ViewGroup.LayoutParams.WRAP_CONTENT,\n        ViewGroup.LayoutParams.WRAP_CONTENT\n    )\n    if (this is TextView) {\n        setTextSize(TypedValue.COMPLEX_UNIT_PX, 14.sp.px)\n        setTextColor(if (darkTheme) Color.WHITE else Color.BLACK)\n        if (AndroidTarget.Q) {\n            breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE\n        }\n    }\n    background = GradientDrawable().apply {\n        setColor((if (darkTheme) \"#303030\" else \"#fafafa\").toColorInt())\n    }\n    outlineProvider = circleOutlineProvider\n    clipToOutline = true\n    elevation = 2.dp.px\n    outlineProvider = circleOutlineProvider\n    clipToOutline = true\n}\n\nprivate fun setReactiveToastStyle() {\n    Toaster.setStyle(object : WhiteToastStyle() {\n        override fun getGravity() = Gravity.BOTTOM\n        override fun getYOffset() = toastYOffset\n        override fun getTranslationZ(context: Context?) = 0f\n        override fun createView(context: Context?): View {\n            return super.createView(context).apply {\n                updateToastView()\n            }\n        }\n    })\n}\n\nprivate var triggerTime = 0L\nprivate const val triggerInterval = 2000L\nfun showActionToast(rule: ResolvedRule) {\n    if (!storeFlow.value.toastWhenClick) return\n    runMainPost {\n        val t = System.currentTimeMillis()\n        if (t - triggerTime > triggerInterval + 100) { // 100ms 保证二次显示的时候上一次已经完全消失\n            triggerTime = t\n            val text = storeFlow.value.actionToast\n                .replace($$\"${1}\", rule.rule.name.toString())\n                .replace($$\"${2}\", rule.g.group.name)\n                .replace($$\"${3}\", actionCountFlow.value.toString())\n            if (storeFlow.value.useSystemToast) {\n                showSystemToast(text)\n            } else {\n                showA11yToast(text)\n            }\n        }\n    }\n}\n\nprivate var cacheToast: Toast? = null\nprivate fun showSystemToast(message: CharSequence) {\n    cacheToast?.cancel()\n    cacheToast = Toast.makeText(app, message, Toast.LENGTH_SHORT).apply {\n        show()\n    }\n    runMainPost(Toast.LENGTH_SHORT.toLong()) { cacheToast = null }\n}\n\n// 1.使用 WeakReference<View> 在某些机型上导致无法取消\n// 2.使用协程 delay + cacheView 也可能导致无法取消\n// https://github.com/gkd-kit/gkd/issues/697\n// https://github.com/gkd-kit/gkd/issues/698\nprivate fun showA11yToast(message: CharSequence) {\n    val wm = A11yService.instance?.wm\n        ?: if (canDrawOverlaysState.updateAndGet()) app.windowManager else null\n    if (wm == null) {\n        showSystemToast(message)\n        return\n    }\n    val textView = TextView(app).apply {\n        text = message\n        id = android.R.id.message\n        gravity = Gravity.CENTER\n        updateToastView()\n    }\n    val layoutParams = WindowManager.LayoutParams().apply {\n        type = if (wm == app.windowManager) {\n            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY\n        } else {\n            WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY\n        }\n        format = PixelFormat.TRANSLUCENT\n        flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or\n                WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or\n                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or\n                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON\n        packageName = app.packageName\n        width = WindowManager.LayoutParams.WRAP_CONTENT\n        height = WindowManager.LayoutParams.WRAP_CONTENT\n        gravity = Gravity.BOTTOM\n        y = toastYOffset\n        windowAnimations = android.R.style.Animation_Toast\n    }\n    wm.addView(textView, layoutParams)\n    runMainPost(triggerInterval) {\n        try {\n            wm.removeViewImmediate(textView)\n        } catch (_: Exception) {\n        }\n    }\n}\n\nfun copyText(text: String) {\n    app.clipboardManager.setPrimaryClip(ClipData.newPlainText(app.packageName, text))\n    toast(\"复制成功\")\n}\n\nfun initToast() {\n    Toaster.init(app)\n    Toaster.setDebugMode(false)\n    Toaster.setInterceptor { false } // 覆盖默认拦截器\n    setReactiveToastStyle()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/Unit.kt",
    "content": "package li.songe.gkd.util\n\nimport android.util.TypedValue\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport li.songe.gkd.app\n\n/**\n * px -> dp\n */\nval Dp.px: Float\n    get() = value * app.resources.displayMetrics.density\n\n/**\n * sp -> px\n */\nval TextUnit.px: Float\n    get() = TypedValue.applyDimension(\n        TypedValue.COMPLEX_UNIT_SP,\n        value, app.resources.displayMetrics\n    )\n\n///**\n// * px -> dp\n// */\n//val Int.calcDp: Float\n//    get() = this / app.resources.displayMetrics.density\n\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/Upgrade.kt",
    "content": "package li.songe.gkd.util\n\nimport android.content.Intent\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.core.content.FileProvider\nimport io.ktor.client.call.body\nimport io.ktor.client.plugins.onDownload\nimport io.ktor.client.request.get\nimport io.ktor.client.statement.bodyAsChannel\nimport io.ktor.util.cio.writeChannel\nimport io.ktor.utils.io.copyAndClose\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport li.songe.gkd.META\nimport li.songe.gkd.app\nimport li.songe.gkd.store.createAnyFlow\nimport li.songe.gkd.store.storeFlow\nimport java.io.File\nimport java.net.URI\n\n\nprivate val UPDATE_URL: String\n    get() = UpdateChannelOption.objects.findOption(storeFlow.value.updateChannel).url\n\n@Serializable\ndata class NewVersion(\n    val versionCode: Int,\n    val versionName: String,\n    val changelog: String,\n    val downloadUrl: String,\n    val fileSize: Long,\n    val versionLogs: List<VersionLog> = emptyList(),\n)\n\n@Serializable\ndata class VersionLog(\n    val name: String,\n    val code: Int,\n    val desc: String,\n)\n\nclass UpdateStatus(val scope: CoroutineScope) {\n    private val checkUpdatingMutex = MutexState()\n    val checkUpdatingFlow\n        get() = checkUpdatingMutex.state\n    private val newVersionFlow = MutableStateFlow<NewVersion?>(null)\n    private val downloadStatusFlow = MutableStateFlow<LoadStatus<File>?>(null)\n    private var downloadJob: Job? = null\n\n    private val ignoreVersionListFlow by lazy {\n        createAnyFlow(\n            key = \"ignore_version_list\",\n            default = { emptySet<Int>() },\n            scope = scope,\n        )\n    }\n    private var lastManual = false\n\n    fun checkUpdate(manual: Boolean = false) = scope.launchTry(Dispatchers.IO, silent = manual) {\n        lastManual = manual\n        checkUpdatingMutex.whenUnLock {\n            if (!NetworkUtils.isAvailable()) {\n                error(\"网络不可用\")\n            }\n            val newVersion = client.get(UPDATE_URL).body<NewVersion>()\n            if (newVersion.versionCode <= META.versionCode) {\n                if (manual) toast(\"暂无更新\")\n                return@launchTry\n            }\n            if (!manual && ignoreVersionListFlow.value.contains(newVersion.versionCode)) return@launchTry\n            newVersionFlow.value = newVersion\n        }\n    }.let { }\n\n    private fun startDownload(newVersion: NewVersion) {\n        if (downloadStatusFlow.value is LoadStatus.Loading) return\n        downloadStatusFlow.value = LoadStatus.Loading(0f)\n        val apkFile = sharedDir.resolve(\"gkd-v${newVersion.versionCode}.apk\").apply {\n            if (exists()) {\n                delete()\n            }\n        }\n        downloadJob = scope.launch(Dispatchers.IO) {\n            try {\n                val channel =\n                    client.get(URI(UPDATE_URL).resolve(newVersion.downloadUrl).toString()) {\n                        onDownload { bytesSentTotal, _ ->\n                            val downloadStatus = downloadStatusFlow.value\n                            if (downloadStatus is LoadStatus.Loading) {\n                                downloadStatusFlow.value = LoadStatus.Loading(\n                                    bytesSentTotal.toFloat() / (newVersion.fileSize)\n                                )\n                            } else if (downloadStatus is LoadStatus.Failure) {\n                                // 提前终止下载\n                                downloadJob?.cancel()\n                            }\n                        }\n                    }.bodyAsChannel()\n                if (downloadStatusFlow.value is LoadStatus.Loading) {\n                    channel.copyAndClose(apkFile.writeChannel())\n                    downloadStatusFlow.value = LoadStatus.Success(apkFile)\n                }\n            } catch (e: Exception) {\n                if (downloadStatusFlow.value is LoadStatus.Loading) {\n                    downloadStatusFlow.value = LoadStatus.Failure(e)\n                }\n            } finally {\n                downloadJob = null\n            }\n        }\n    }\n\n    @Composable\n    fun UpgradeDialog() {\n        newVersionFlow.collectAsState().value?.let { newVersionVal ->\n            val text = remember {\n                val logs = newVersionVal.versionLogs.takeWhile { v ->\n                    v.code > META.versionCode\n                }\n                \"v${META.versionName} -> v${newVersionVal.versionName}\\n\\n${\n                    if (logs.size > 1) {\n                        logs.joinToString(\"\\n\\n\") { v -> \"v${v.name}\\n${v.desc}\" }\n                    } else if (logs.isNotEmpty()) {\n                        logs.first().desc\n                    } else {\n                        \"\"\n                    }\n                }\".trimEnd()\n            }\n            AlertDialog(\n                title = {\n                    Text(text = \"新版本\")\n                },\n                text = {\n                    Text(\n                        text = text,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .heightIn(max = 400.dp)\n                            .verticalScroll(rememberScrollState())\n                    )\n                },\n                onDismissRequest = { },\n                confirmButton = {\n                    TextButton(onClick = {\n                        newVersionFlow.value = null\n                        startDownload(newVersionVal)\n                    }) {\n                        Text(text = \"下载更新\")\n                    }\n                },\n                dismissButton = {\n                    TextButton(onClick = { newVersionFlow.value = null }) {\n                        Text(text = \"取消\")\n                    }\n                    if (!lastManual) {\n                        TextButton(onClick = {\n                            newVersionFlow.value = null\n                            ignoreVersionListFlow.update {\n                                it + newVersionVal.versionCode\n                            }\n                            toast(\"已忽略此版本\")\n                        }) {\n                            Text(text = \"忽略\")\n                        }\n                    }\n                },\n            )\n        }\n\n        downloadStatusFlow.collectAsState().value?.let { downloadStatusVal ->\n            when (downloadStatusVal) {\n                is LoadStatus.Loading -> {\n                    AlertDialog(\n                        title = { Text(text = \"下载中\") },\n                        text = {\n                            LinearProgressIndicator(\n                                progress = { downloadStatusVal.progress },\n                            )\n                        },\n                        onDismissRequest = {},\n                        confirmButton = {\n                            TextButton(onClick = {\n                                downloadStatusFlow.value = LoadStatus.Failure(\n                                    Exception(\"终止下载\")\n                                )\n                            }) {\n                                Text(text = \"终止下载\")\n                            }\n                        },\n                    )\n                }\n\n                is LoadStatus.Failure -> {\n                    AlertDialog(\n                        title = { Text(text = \"下载失败\") },\n                        text = {\n                            Text(text = downloadStatusVal.exception.let {\n                                it.message ?: it.toString()\n                            })\n                        },\n                        onDismissRequest = { downloadStatusFlow.value = null },\n                        confirmButton = {\n                            TextButton(onClick = {\n                                downloadStatusFlow.value = null\n                            }) {\n                                Text(text = \"关闭\")\n                            }\n                        },\n                    )\n                }\n\n                is LoadStatus.Success -> {\n                    AlertDialog(\n                        title = { Text(text = \"下载完毕\") },\n                        text = {\n                            Text(text = \"可继续选择安装新版本\")\n                        },\n                        onDismissRequest = {},\n                        dismissButton = {\n                            TextButton(onClick = {\n                                downloadStatusFlow.value = null\n                            }) {\n                                Text(text = \"关闭\")\n                            }\n                        },\n                        confirmButton = {\n                            TextButton(onClick = throttle {\n                                installApk(downloadStatusVal.result)\n                            }) {\n                                Text(text = \"安装\")\n                            }\n                        })\n                }\n            }\n        }\n    }\n}\n\n\nprivate fun installApk(file: File) {\n    val uri = FileProvider.getUriForFile(\n        app,\n        \"${app.packageName}.provider\",\n        file\n    )\n    val intent = Intent(Intent.ACTION_VIEW).apply {\n        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        setDataAndType(uri, \"application/vnd.android.package-archive\")\n    }\n    app.tryStartActivity(intent)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/UriUtils.kt",
    "content": "package li.songe.gkd.util\n\nimport android.net.Uri\nimport li.songe.gkd.app\n\nobject UriUtils {\n    fun uri2Bytes(uri: Uri): ByteArray {\n        app.contentResolver.openInputStream(uri)?.use {\n            return it.readBytes()\n        }\n        return ByteArray(0)\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/li/songe/gkd/util/ZipUtils.kt",
    "content": "package li.songe.gkd.util\n\nimport java.io.BufferedInputStream\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\nimport java.io.InputStream\nimport java.util.zip.ZipEntry\nimport java.util.zip.ZipFile\nimport java.util.zip.ZipOutputStream\n\nobject ZipUtils {\n    private const val BUFFER_LEN = 8192\n    private fun zipFile(\n        srcFile: File,\n        rawRootPath: String,\n        zos: ZipOutputStream,\n        comment: String?,\n    ): Boolean {\n        val rootPath =\n            rawRootPath + (if (rawRootPath.isBlank()) \"\" else File.separator) + srcFile.getName()\n        if (srcFile.isDirectory()) {\n            val fileList = srcFile.listFiles()\n            if (fileList == null || fileList.size <= 0) {\n                val entry = ZipEntry(\"$rootPath/\")\n                entry.setComment(comment)\n                zos.putNextEntry(entry)\n                zos.closeEntry()\n            } else {\n                for (file in fileList) {\n                    if (!zipFile(file, rootPath, zos, comment)) return false\n                }\n            }\n        } else {\n            var stream: InputStream? = null\n            try {\n                stream = BufferedInputStream(FileInputStream(srcFile))\n                val entry = ZipEntry(rootPath)\n                entry.setComment(comment)\n                zos.putNextEntry(entry)\n                val buffer: ByteArray? = ByteArray(BUFFER_LEN)\n                var len: Int\n                while ((stream.read(buffer, 0, BUFFER_LEN).also { len = it }) != -1) {\n                    zos.write(buffer, 0, len)\n                }\n                zos.closeEntry()\n            } finally {\n                stream?.close()\n            }\n        }\n        return true\n    }\n\n    fun zipFiles(srcFiles: Collection<File>, zipFile: File): Boolean {\n        var zos: ZipOutputStream? = null\n        try {\n            zos = ZipOutputStream(FileOutputStream(zipFile))\n            for (srcFile in srcFiles) {\n                if (!zipFile(srcFile, \"\", zos, null)) return false\n            }\n            return true\n        } finally {\n            if (zos != null) {\n                zos.finish()\n                zos.close()\n            }\n        }\n    }\n\n    fun unzipFile(\n        zipFile: File,\n        destDir: File,\n    ) {\n        ZipFile(zipFile).use { zip ->\n            zip.entries().asSequence().forEach { entry ->\n                val outFile = destDir.resolve(entry.name)\n                if (entry.isDirectory) {\n                    outFile.mkdirs()\n                } else {\n                    outFile.parentFile?.mkdirs()\n                    zip.getInputStream(entry).use { input ->\n                        FileOutputStream(outFile).use { output ->\n                            input.copyTo(output)\n                        }\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/res/drawable/ic_anim_logo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animated-vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\">\n    <aapt:attr name=\"android:drawable\">\n        <vector\n            android:width=\"64dp\"\n            android:height=\"64dp\"\n            android:viewportWidth=\"64\"\n            android:viewportHeight=\"64\">\n            <group android:name=\"path1\">\n                <path\n                    android:fillColor=\"#FFF\"\n                    android:pathData=\"M19,17.485l8.485,-8.485l2.816,2.841l-8.485,8.485z\" />\n            </group>\n            <group android:name=\"path2\">\n                <path\n                    android:fillColor=\"#FFF\"\n                    android:pathData=\"M35.816,9l8.485,8.485l-2.816,2.841l-8.485,-8.485z\" />\n            </group>\n            <group android:name=\"path3\">\n                <path\n                    android:fillColor=\"#FFF\"\n                    android:pathData=\"M23.08,21.999l8.466,-8.504l8.466,8.504l-8.466,8.504z\" />\n            </group>\n            <group android:name=\"path4\">\n                <path\n                    android:fillColor=\"#FFF\"\n                    android:pathData=\"M8,55C8.726,50.581 10.104,46.395 12.775,42.977C14.285,41.046 16.133,39.452 17.847,37.729C18.107,37.468 18.465,37.332 19,37C20.272,43.004 20.091,48.871 19.751,54.875C15.872,55 11.994,55 8,55Z\" />\n            </group>\n            <path\n                android:name=\"path5\"\n                android:fillColor=\"#FFF\"\n                android:pathData=\"M21.912,55C21.867,52.521 22.113,50.034 21.993,47.565C21.833,44.288 21.429,41.019 21.039,37.758C20.922,36.775 21.013,36.135 22.054,35.748C27.61,33.682 33.218,33.423 38.918,35.103C39.826,35.37 40.103,35.763 39.967,36.755C39.717,38.583 39.642,40.44 39.586,42.288C39.461,46.44 39.392,50.593 39.297,54.873C33.543,55 27.792,55 21.912,55Z\" />\n            <group android:name=\"path6\">\n                <path\n                    android:fillColor=\"#FFF\"\n                    android:pathData=\"M42,55C42.06,48.791 42.24,42.582 42.432,36C50.149,40.036 53.985,46.652 56,54.895C51.379,55 46.75,55 42,55Z\" />\n            </group>\n        </vector>\n    </aapt:attr>\n    <target android:name=\"path1\">\n        <aapt:attr name=\"android:animation\">\n            <set>\n                <objectAnimator\n                    android:duration=\"1000\"\n                    android:interpolator=\"@android:interpolator/accelerate_decelerate\"\n                    android:propertyName=\"translateX\"\n                    android:repeatCount=\"infinite\"\n                    android:repeatMode=\"reverse\"\n                    android:valueFrom=\"0\"\n                    android:valueTo=\"-0.7071\"\n                    android:valueType=\"floatType\" />\n                <objectAnimator\n                    android:duration=\"1000\"\n                    android:interpolator=\"@android:interpolator/accelerate_decelerate\"\n                    android:propertyName=\"translateY\"\n                    android:repeatCount=\"infinite\"\n                    android:repeatMode=\"reverse\"\n                    android:valueFrom=\"0\"\n                    android:valueTo=\"-0.7071\"\n                    android:valueType=\"floatType\" />\n            </set>\n        </aapt:attr>\n    </target>\n    <target android:name=\"path2\">\n        <aapt:attr name=\"android:animation\">\n            <set>\n                <objectAnimator\n                    android:duration=\"1000\"\n                    android:interpolator=\"@android:interpolator/accelerate_decelerate\"\n                    android:propertyName=\"translateX\"\n                    android:repeatCount=\"infinite\"\n                    android:repeatMode=\"reverse\"\n                    android:valueFrom=\"0\"\n                    android:valueTo=\"0.7071\"\n                    android:valueType=\"floatType\" />\n                <objectAnimator\n                    android:duration=\"1000\"\n                    android:interpolator=\"@android:interpolator/accelerate_decelerate\"\n                    android:propertyName=\"translateY\"\n                    android:repeatCount=\"infinite\"\n                    android:repeatMode=\"reverse\"\n                    android:valueFrom=\"0\"\n                    android:valueTo=\"-0.7071\"\n                    android:valueType=\"floatType\" />\n            </set>\n        </aapt:attr>\n    </target>\n    <target android:name=\"path3\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:duration=\"1000\"\n                android:interpolator=\"@android:interpolator/accelerate_decelerate\"\n                android:propertyName=\"translateY\"\n                android:repeatCount=\"infinite\"\n                android:repeatMode=\"reverse\"\n                android:valueFrom=\"0\"\n                android:valueTo=\"-1.13137\"\n                android:valueType=\"floatType\" />\n        </aapt:attr>\n    </target>\n    <target android:name=\"path4\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:duration=\"1000\"\n                android:interpolator=\"@android:interpolator/accelerate_decelerate\"\n                android:propertyName=\"translateX\"\n                android:repeatCount=\"infinite\"\n                android:repeatMode=\"reverse\"\n                android:valueFrom=\"0\"\n                android:valueTo=\"1.5\"\n                android:valueType=\"floatType\" />\n        </aapt:attr>\n    </target>\n    <target android:name=\"path5\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:duration=\"1000\"\n                android:interpolator=\"@android:interpolator/accelerate_decelerate\"\n                android:propertyName=\"fillAlpha\"\n                android:repeatCount=\"infinite\"\n                android:repeatMode=\"reverse\"\n                android:valueFrom=\"0.8\"\n                android:valueTo=\"1\"\n                android:valueType=\"floatType\" />\n        </aapt:attr>\n    </target>\n    <target android:name=\"path6\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:duration=\"1000\"\n                android:interpolator=\"@android:interpolator/accelerate_decelerate\"\n                android:propertyName=\"translateX\"\n                android:repeatCount=\"infinite\"\n                android:repeatMode=\"reverse\"\n                android:valueFrom=\"-0.7\"\n                android:valueTo=\"-2.2\"\n                android:valueType=\"floatType\" />\n        </aapt:attr>\n    </target>\n</animated-vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_anim_search_close.xml",
    "content": "<animated-vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\">\n    <aapt:attr name=\"android:drawable\">\n        <vector\n            android:width=\"24dp\"\n            android:height=\"24dp\"\n            android:viewportWidth=\"24\"\n            android:viewportHeight=\"24\">\n            <group android:name=\"oval_container\">\n                <path\n                    android:name=\"oval\"\n                    android:pathData=\"M 13.389 13.389 C 15.537 11.241 15.537 7.759 13.389 5.611 C 11.241 3.463 7.759 3.463 5.611 5.611 C 3.463 7.759 3.463 11.241 5.611 13.389 C 7.759 15.537 11.241 15.537 13.389 13.389 Z\"\n                    android:strokeWidth=\"1.8\"\n                    android:strokeColor=\"#FFF\" />\n            </group>\n            <path\n                android:name=\"ne_stem\"\n                android:pathData=\"M 18 6 L 6 18\"\n                android:strokeWidth=\"1.8\"\n                android:strokeColor=\"#FFF\"\n                android:trimPathStart=\"1\" />\n            <path\n                android:name=\"nw_stem\"\n                android:pathData=\"M 6 6 L 20 20\"\n                android:strokeWidth=\"1.8\"\n                android:strokeColor=\"#FFF\"\n                android:trimPathStart=\"0.48\" />\n        </vector>\n    </aapt:attr>\n    <target android:name=\"nw_stem\">\n        <aapt:attr name=\"android:animation\">\n            <set>\n                <objectAnimator\n                    android:duration=\"179\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"\n                    android:propertyName=\"trimPathStart\"\n                    android:startOffset=\"108\"\n                    android:valueFrom=\"0.48\"\n                    android:valueTo=\"0\"\n                    android:valueType=\"floatType\" />\n                <objectAnimator\n                    android:duration=\"179\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"\n                    android:propertyName=\"trimPathEnd\"\n                    android:startOffset=\"108\"\n                    android:valueFrom=\"1\"\n                    android:valueTo=\"0.86\"\n                    android:valueType=\"floatType\" />\n            </set>\n        </aapt:attr>\n    </target>\n    <target android:name=\"ne_stem\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:duration=\"113\"\n                android:interpolator=\"@android:interpolator/fast_out_slow_in\"\n                android:propertyName=\"trimPathStart\"\n                android:startOffset=\"187\"\n                android:valueFrom=\"1\"\n                android:valueTo=\"0\"\n                android:valueType=\"floatType\" />\n        </aapt:attr>\n    </target>\n    <target android:name=\"oval\">\n        <aapt:attr name=\"android:animation\">\n            <objectAnimator\n                android:duration=\"149\"\n                android:interpolator=\"@android:anim/accelerate_decelerate_interpolator\"\n                android:propertyName=\"trimPathStart\"\n                android:startOffset=\"48\"\n                android:valueFrom=\"0\"\n                android:valueTo=\"1\"\n                android:valueType=\"floatType\" />\n        </aapt:attr>\n    </target>\n    <target android:name=\"oval_container\">\n        <aapt:attr name=\"android:animation\">\n            <set>\n                <objectAnimator\n                    android:duration=\"179\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"\n                    android:propertyName=\"translateX\"\n                    android:startOffset=\"108\"\n                    android:valueFrom=\"0\"\n                    android:valueTo=\"-6.7\"\n                    android:valueType=\"floatType\" />\n                <objectAnimator\n                    android:duration=\"179\"\n                    android:interpolator=\"@android:interpolator/fast_out_slow_in\"\n                    android:propertyName=\"translateY\"\n                    android:startOffset=\"108\"\n                    android:valueFrom=\"0\"\n                    android:valueTo=\"-6.7\"\n                    android:valueType=\"floatType\" />\n            </set>\n        </aapt:attr>\n    </target>\n</animated-vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_capture.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"20dp\"\n    android:height=\"20dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M128,384a42.7,42.7 0,0 0,42.7 -42.7L170.7,213.3a42.7,42.7 0,0 1,42.7 -42.7h128a42.7,42.7 0,0 0,0 -85.3L213.3,85.3a128,128 0,0 0,-128 128v128a42.7,42.7 0,0 0,42.7 42.7zM341.3,853.3L213.3,853.3a42.7,42.7 0,0 1,-42.7 -42.7v-128a42.7,42.7 0,0 0,-85.3 0v128a128,128 0,0 0,128 128h128a42.7,42.7 0,0 0,0 -85.3zM512,341.3a170.7,170.7 0,1 0,170.7 170.7,170.7 170.7,0 0,0 -170.7,-170.7zM512,597.3a85.3,85.3 0,1 1,85.3 -85.3,85.3 85.3,0 0,1 -85.3,85.3zM810.7,85.3h-128a42.7,42.7 0,0 0,0 85.3h128a42.7,42.7 0,0 1,42.7 42.7v128a42.7,42.7 0,0 0,85.3 0L938.7,213.3a128,128 0,0 0,-128 -128zM896,640a42.7,42.7 0,0 0,-42.7 42.7v128a42.7,42.7 0,0 1,-42.7 42.7h-128a42.7,42.7 0,0 0,0 85.3h128a128,128 0,0 0,128 -128v-128a42.7,42.7 0,0 0,-42.7 -42.7z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_event_list.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n    android:width=\"24dp\"\r\n    android:height=\"24dp\"\r\n    android:viewportWidth=\"960\"\r\n    android:viewportHeight=\"960\">\r\n    <path\r\n        android:fillColor=\"#FFF\"\r\n        android:pathData=\"M640,840q-33,0 -56.5,-23.5T560,760v-160q0,-33 23.5,-56.5T640,520h160q33,0 56.5,23.5T880,600v160q0,33 -23.5,56.5T800,840L640,840ZM640,760h160v-160L640,600v160ZM80,720v-80h360v80L80,720ZM640,440q-33,0 -56.5,-23.5T560,360v-160q0,-33 23.5,-56.5T640,120h160q33,0 56.5,23.5T880,200v160q0,33 -23.5,56.5T800,440L640,440ZM640,360h160v-160L640,200v160ZM80,320v-80h360v80L80,320ZM720,680ZM720,280Z\" />\r\n</vector>\r\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_flash_off.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M280,80h400l-80,280h160L643,529l-57,-57 22,-32h-54l-47,-47 67,-233L360,160v86l-80,-80v-86ZM400,880v-320L280,560v-166L55,169l57,-57 736,736 -57,57 -241,-241L400,880ZM473,359Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_flash_on.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"m480,624 l128,-184L494,440l80,-280L360,160v320h120v144ZM400,880v-320L280,560v-480h400l-80,280h160L400,880ZM480,480L360,480h120Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_http.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M40,600v-240h60v80h80v-80h60v240h-60v-100h-80v100L40,600ZM340,600v-180h-60v-60h180v60h-60v180h-60ZM560,600v-180h-60v-60h180v60h-60v180h-60ZM720,600v-240h140q24,0 42,18t18,42v40q0,24 -18,42t-42,18h-80v80h-60ZM780,460h80v-40h-80v40Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"32dp\"\n    android:height=\"32dp\"\n    android:viewportWidth=\"32\"\n    android:viewportHeight=\"32\">\n    <path\n        android:fillColor=\"@color/ic_launcher_background_tint\"\n        android:pathData=\"M0,0h32v32h-32z\" />\n</vector>"
  },
  {
    "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    <path\n        android:fillColor=\"@color/ic_launcher_foreground_tint\"\n        android:pathData=\"M43.91,75C43.87,72.52 44.11,70.03 43.99,67.57C43.83,64.29 43.43,61.02 43.04,57.76C42.92,56.77 43.01,56.13 44.05,55.75C49.61,53.68 55.22,53.42 60.92,55.1C61.83,55.37 62.1,55.76 61.97,56.76C61.72,58.58 61.64,60.44 61.59,62.29C61.46,66.44 61.39,70.59 61.3,74.87C55.54,75 49.79,75 43.91,75Z\" />\n    <path\n        android:fillColor=\"@color/ic_launcher_foreground_tint\"\n        android:pathData=\"M64,75C64.06,68.79 64.24,62.58 64.43,56C72.15,60.04 75.99,66.65 78,74.9C73.38,75 68.75,75 64,75Z\" />\n    <path\n        android:fillColor=\"@color/ic_launcher_foreground_tint\"\n        android:pathData=\"M30,75C30.73,70.58 32.1,66.39 34.78,62.98C36.28,61.05 38.13,59.45 39.85,57.73C40.11,57.47 40.47,57.33 41,57C42.27,63 42.09,68.87 41.75,74.87C37.87,75 33.99,75 30,75Z\" />\n    <path\n        android:fillColor=\"@color/ic_launcher_foreground_tint\"\n        android:pathData=\"M45.08,42l8.47,-8.5l8.47,8.5l-8.47,8.5z\" />\n    <path\n        android:fillColor=\"@color/ic_launcher_foreground_tint\"\n        android:pathData=\"M41,37.49l8.49,-8.49l2.82,2.84l-8.49,8.49z\" />\n    <path\n        android:fillColor=\"@color/ic_launcher_foreground_tint\"\n        android:pathData=\"M57.82,29l8.49,8.49l-2.82,2.84l-8.49,-8.49z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_layers.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n    android:width=\"24dp\"\r\n    android:height=\"24dp\"\r\n    android:viewportWidth=\"960\"\r\n    android:viewportHeight=\"960\">\r\n    <path\r\n        android:fillColor=\"#FFF\"\r\n        android:pathData=\"M480,842 L120,562l66,-50 294,228 294,-228 66,50 -360,280ZM480,640L120,360l360,-280 360,280 -360,280ZM480,360ZM480,538 L710,360 480,182 250,360 480,538Z\" />\r\n</vector>\r\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_page_info.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M710,810Q647,810 603.5,766.5Q560,723 560,660Q560,597 603.5,553.5Q647,510 710,510Q773,510 816.5,553.5Q860,597 860,660Q860,723 816.5,766.5Q773,810 710,810ZM710,730Q739,730 759.5,709.5Q780,689 780,660Q780,631 759.5,610.5Q739,590 710,590Q681,590 660.5,610.5Q640,631 640,660Q640,689 660.5,709.5Q681,730 710,730ZM160,700L160,620L480,620L480,700L160,700ZM250,450Q187,450 143.5,406.5Q100,363 100,300Q100,237 143.5,193.5Q187,150 250,150Q313,150 356.5,193.5Q400,237 400,300Q400,363 356.5,406.5Q313,450 250,450ZM250,370Q279,370 299.5,349.5Q320,329 320,300Q320,271 299.5,250.5Q279,230 250,230Q221,230 200.5,250.5Q180,271 180,300Q180,329 200.5,349.5Q221,370 250,370ZM480,340L480,260L800,260L800,340L480,340ZM710,660Q710,660 710,660Q710,660 710,660Q710,660 710,660Q710,660 710,660Q710,660 710,660Q710,660 710,660Q710,660 710,660Q710,660 710,660ZM250,300Q250,300 250,300Q250,300 250,300Q250,300 250,300Q250,300 250,300Q250,300 250,300Q250,300 250,300Q250,300 250,300Q250,300 250,300Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_radio_button.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"#fff\"\n        android:pathData=\"M480,680q83,0 141.5,-58.5T680,480q0,-83 -58.5,-141.5T480,280q-83,0 -141.5,58.5T280,480q0,83 58.5,141.5T480,680ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_status.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"64\"\n    android:viewportHeight=\"64\">\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M21.912,55C21.867,52.521 22.113,50.034 21.993,47.565C21.833,44.288 21.429,41.019 21.039,37.758C20.922,36.775 21.013,36.135 22.054,35.748C27.61,33.682 33.218,33.423 38.918,35.103C39.826,35.37 40.103,35.763 39.967,36.755C39.717,38.583 39.642,40.44 39.586,42.288C39.461,46.44 39.392,50.593 39.297,54.873C33.543,55 27.792,55 21.912,55Z\" />\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M42,55C42.06,48.791 42.24,42.582 42.432,36C50.149,40.036 53.985,46.652 56,54.895C51.379,55 46.75,55 42,55Z\" />\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M8,55C8.726,50.581 10.104,46.395 12.775,42.977C14.285,41.046 16.133,39.452 17.847,37.729C18.107,37.468 18.465,37.332 19,37C20.272,43.004 20.091,48.871 19.751,54.875C15.872,55 11.994,55 8,55Z\" />\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M23.08,21.999l8.466,-8.504l8.466,8.504l-8.466,8.504z\" />\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M19,17.485l8.485,-8.485l2.816,2.841l-8.485,8.485z\" />\n    <path\n        android:fillColor=\"#FFF\"\n        android:pathData=\"M35.816,9l8.485,8.485l-2.816,2.841l-8.485,-8.485z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"better_white\">#FCFCFC</color>\n    <color name=\"better_black\">#111111</color>\n    <color name=\"ic_launcher_background_tint\">@color/better_white</color>\n    <color name=\"ic_launcher_foreground_tint\">@color/better_black</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\" debug_suffix=\"true\">GKD</string>\n    <string name=\"a11y_desc\">基于高级选择器和订阅规则的屏幕自定义点击服务\\n\\n通过自定义选择器和订阅规则，能帮助你实现点击任意位置控件，自定义快捷操作等高级功能</string>\n    <string name=\"import_data\" debug_suffix=\"true\">导入数据</string>\n    <string name=\"capture_snapshot\" debug_suffix=\"true\">捕获快照</string>\n    <string name=\"http_server\" debug_suffix=\"true\">HTTP服务</string>\n    <string name=\"snapshot_button\" debug_suffix=\"true\">快照按钮</string>\n    <string name=\"rule_match\" debug_suffix=\"true\">规则匹配</string>\n    <string name=\"record_activity\" debug_suffix=\"true\">界面服务</string>\n    <string name=\"record_a11y_event\" debug_suffix=\"true\">事件服务</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "content": "<resources>\n\n    <style name=\"AppLightTheme\" parent=\"android:Theme.Material.Light.NoActionBar\">\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n    </style>\n\n    <style name=\"AppNightTheme\" parent=\"android:Theme.Material.NoActionBar\">\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n    </style>\n\n    <style name=\"SplashScreenLightTheme\" parent=\"Theme.SplashScreen\">\n        <item name=\"windowSplashScreenAnimatedIcon\">@mipmap/gkd_black</item>\n        <item name=\"windowSplashScreenAnimationDuration\">1000</item>\n        <item name=\"postSplashScreenTheme\">@style/AppLightTheme</item>\n    </style>\n\n    <style name=\"SplashScreenNightTheme\" parent=\"Theme.SplashScreen\">\n        <item name=\"windowSplashScreenAnimatedIcon\">@mipmap/gkd_white</item>\n        <item name=\"windowSplashScreenAnimationDuration\">1000</item>\n        <item name=\"postSplashScreenTheme\">@style/AppNightTheme</item>\n    </style>\n\n    <style name=\"TransparentTheme\">\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:windowIsTranslucent\">true</item>\n        <item name=\"android:colorBackgroundCacheHint\">@null</item>\n    </style>\n\n    <style name=\"AppTheme\" parent=\"AppLightTheme\" />\n\n    <style name=\"SplashScreenTheme\" parent=\"SplashScreenLightTheme\" />\n\n</resources>"
  },
  {
    "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_tint\">@color/better_black</color>\n    <color name=\"ic_launcher_foreground_tint\">@color/better_white</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night/themes.xml",
    "content": "<resources>\n\n    <style name=\"AppTheme\" parent=\"AppNightTheme\" />\n\n    <style name=\"SplashScreenTheme\" parent=\"SplashScreenNightTheme\" />\n\n</resources>"
  },
  {
    "path": "app/src/main/res/xml/ab_desc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<accessibility-service xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:accessibilityEventTypes=\"typeWindowContentChanged|typeWindowStateChanged\"\n    android:accessibilityFeedbackType=\"feedbackAllMask\"\n    android:accessibilityFlags=\"flagReportViewIds|flagIncludeNotImportantViews|flagDefault|flagRetrieveInteractiveWindows\"\n    android:canPerformGestures=\"true\"\n    android:canRetrieveWindowContent=\"true\"\n    android:canTakeScreenshot=\"true\"\n    android:isAccessibilityTool=\"@bool/is_accessibility_tool\"\n    android:description=\"@string/a11y_desc\"\n    android:notificationTimeout=\"100\"\n    android:settingsActivity=\"li.songe.gkd.MainActivity\"\n    tools:ignore=\"UnusedAttribute\" />"
  },
  {
    "path": "app/src/main/res/xml/file_paths.xml",
    "content": "<paths>\n    <files-path\n        name=\"files_path\"\n        path=\".\" />\n\n    <cache-path\n        name=\"cache_path\"\n        path=\".\" />\n\n    <external-path\n        name=\"external_path\"\n        path=\".\" />\n\n    <external-files-path\n        name=\"external_files_path\"\n        path=\".\" />\n\n    <external-cache-path\n        name=\"external_cache_path\"\n        path=\".\" />\n\n    <external-media-path\n        name=\"external_media_path\"\n        path=\".\" />\n\n    <root-path\n        name=\"root\"\n        path=\".\" />\n</paths>\n\n"
  },
  {
    "path": "app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config xmlns:tools=\"http://schemas.android.com/tools\">\n    <base-config\n        cleartextTrafficPermitted=\"true\"\n        tools:ignore=\"InsecureBaseConfiguration\" />\n</network-security-config>\n"
  },
  {
    "path": "app/src/test/kotlin/li/songe/gkd/ExampleUnitTest.kt",
    "content": "package li.songe.gkd\n\nimport org.junit.Test\n\nclass ExampleUnitTest {\n\n    @Test\n    fun test() {\n    }\n\n}"
  },
  {
    "path": "build.gradle.kts",
    "content": "import nl.littlerobots.vcu.plugin.versionSelector\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\next {\n    set(\"android.namespace\", \"li.songe.gkd\")\n    set(\"android.buildToolsVersion\", \"36.1.0\")\n    set(\"android.compileSdk\", 36)\n    set(\"android.targetSdk\", 36)\n    set(\"android.minSdk\", 26)\n    set(\"android.javaVersion\", JavaVersion.VERSION_11)\n    set(\"kotlin.jvmTarget\", JvmTarget.JVM_11)\n}\n\nplugins {\n    alias(libs.plugins.google.ksp) apply false\n    alias(libs.plugins.android.library) apply false\n    alias(libs.plugins.android.application) apply false\n    alias(libs.plugins.androidx.room) apply false\n    alias(libs.plugins.kotlin.serialization) apply false\n    alias(libs.plugins.kotlin.multiplatform) apply false\n    alias(libs.plugins.kotlin.parcelize) apply false\n    alias(libs.plugins.kotlin.compose) apply false\n    alias(libs.plugins.kotlinx.atomicfu) apply false\n    alias(libs.plugins.rikka.refine) apply false\n    alias(libs.plugins.loc) apply false\n    alias(libs.plugins.littlerobots.version)\n}\n\n// ./gradlew versionCatalogUpdate --interactive\nversionCatalogUpdate {\n    versionSelector {\n        val a = it.currentVersion\n        val b = it.candidate.version\n        isSameTypeVersion(a, b) && isNewerVersion(a, b)\n    }\n}\nprojectDir.resolve(\"./gradle/libs.versions.updates.toml\").apply {\n    if (exists()) {\n        delete()\n    }\n}\n\nval versionReg = \"^[0-9\\\\.]+\".toRegex()\nfun isSameTypeVersion(currentVersion: String, newVersion: String): Boolean {\n    if (versionReg.matches(currentVersion)) {\n        return versionReg.matches(newVersion)\n    }\n    arrayOf(\"alpha\", \"beta\", \"dev\", \"rc\").forEach { v ->\n        if (currentVersion.contains(v, true)) {\n            return newVersion.contains(v, true)\n        }\n    }\n    throw IllegalArgumentException(\"Unknown version type: $currentVersion -> $newVersion\")\n}\n\nval numberReg = \"\\\\d+\".toRegex()\nfun isNewerVersion(currentVersion: String, newVersion: String): Boolean {\n    val currentParts = numberReg.findAll(currentVersion).map { it.value.toInt() }.toList()\n    val newParts = numberReg.findAll(newVersion).map { it.value.toInt() }.toList()\n    val length = maxOf(currentParts.size, newParts.size)\n    for (i in 0 until length) {\n        val currentPart = currentParts.getOrNull(i) ?: 0\n        val newPart = newParts.getOrNull(i) ?: 0\n        if (currentPart < newPart) {\n            return true\n        } else if (currentPart > newPart) {\n            return false\n        }\n    }\n    return false\n}\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nkotlin = \"2.3.20\"\nserialization = \"1.10.0\"\nksp = \"2.3.6\"\nagp = \"9.1.0\"\ncompose = \"1.10.5\"\nnav3 = \"1.0.1\"\nroom = \"2.8.4\"\npaging = \"3.4.2\"\nktor = \"3.4.1\"\natomicfu = \"0.31.0\"\ncoil = \"3.4.0\"\nrefine = \"4.4.0\"\nshizuku = \"13.1.5\"\nloc = \"0.5.4\"\n\n[libraries]\nkotlin-stdlib = { module = \"org.jetbrains.kotlin:kotlin-stdlib\", version.ref = \"kotlin\" }\nkotlin-test = { module = \"org.jetbrains.kotlin:kotlin-test\", version.ref = \"kotlin\" }\nkotlinx-serialization-core = { module = \"org.jetbrains.kotlinx:kotlinx-serialization-core\", version.ref = \"serialization\" }\nkotlinx-serialization-json = { module = \"org.jetbrains.kotlinx:kotlinx-serialization-json\", version.ref = \"serialization\" }\nkotlinx-atomicfu = { module = \"org.jetbrains.kotlinx:atomicfu\", version.ref = \"atomicfu\" }\nktor-server-core = { module = \"io.ktor:ktor-server-core\", version.ref = \"ktor\" }\nktor-server-cio = { module = \"io.ktor:ktor-server-cio\", version.ref = \"ktor\" }\nktor-server-content-negotiation = { module = \"io.ktor:ktor-server-content-negotiation\", version.ref = \"ktor\" }\nktor-client-core = { module = \"io.ktor:ktor-client-core\", version.ref = \"ktor\" }\nktor-client-okhttp = { module = \"io.ktor:ktor-client-okhttp\", version.ref = \"ktor\" }\nktor-client-content-negotiation = { module = \"io.ktor:ktor-client-content-negotiation\", version.ref = \"ktor\" }\nktor-serialization-kotlinx-json = { module = \"io.ktor:ktor-serialization-kotlinx-json\", version.ref = \"ktor\" }\ncompose-ui = { module = \"androidx.compose.ui:ui\", version.ref = \"compose\" }\ncompose-ui-graphics = { module = \"androidx.compose.ui:ui-graphics\", version.ref = \"compose\" }\ncompose-animation = { module = \"androidx.compose.animation:animation\", version.ref = \"compose\" }\ncompose-animation-graphics = { module = \"androidx.compose.animation:animation-graphics\", version.ref = \"compose\" }\ncompose-preview = { module = \"androidx.compose.ui:ui-tooling-preview\", version.ref = \"compose\" }\ncompose-tooling = { module = \"androidx.compose.ui:ui-tooling\", version.ref = \"compose\" }\ncompose-junit4 = { module = \"androidx.compose.ui:ui-test-junit4\", version.ref = \"compose\" }\ncompose-icons = \"androidx.compose.material:material-icons-extended:1.7.8\"\ncompose-material3 = \"androidx.compose.material3:material3:1.4.0\"\ncompose-activity = \"androidx.activity:activity-compose:1.13.0\"\nandroidx-appcompat = \"androidx.appcompat:appcompat:1.7.1\"\nandroidx-core-ktx = \"androidx.core:core-ktx:1.18.0\"\nandroidx-lifecycle-runtime-ktx = \"androidx.lifecycle:lifecycle-runtime-ktx:2.10.0\"\nandroidx-lifecycle-service = \"androidx.lifecycle:lifecycle-service:2.10.0\"\nandroidx-navigation3-runtime = { module = \"androidx.navigation3:navigation3-runtime\", version.ref = \"nav3\" }\nandroidx-navigation3-ui = { module = \"androidx.navigation3:navigation3-ui\", version.ref = \"nav3\" }\nandroidx-material3-adaptive-navigation3 = \"androidx.compose.material3.adaptive:adaptive-navigation3:1.3.0-alpha09\"\nandroidx-lifecycle-viewmodel-navigation3 = \"androidx.lifecycle:lifecycle-viewmodel-navigation3:2.10.0\"\nandroidx-junit = \"androidx.test.ext:junit:1.3.0\"\nandroidx-annotation = \"androidx.annotation:annotation:1.9.1\"\nandroidx-espresso = \"androidx.test.espresso:espresso-core:3.7.0\"\nandroidx-room-runtime = { module = \"androidx.room:room-runtime\", version.ref = \"room\" }\nandroidx-room-compiler = { module = \"androidx.room:room-compiler\", version.ref = \"room\" }\nandroidx-room-ktx = { module = \"androidx.room:room-ktx\", version.ref = \"room\" }\nandroidx-room-paging = { module = \"androidx.room:room-paging\", version.ref = \"room\" }\nandroidx-splashscreen = \"androidx.core:core-splashscreen:1.2.0\"\nandroidx-paging-runtime = { module = \"androidx.paging:paging-runtime\", version.ref = \"paging\" }\nandroidx-paging-compose = { module = \"androidx.paging:paging-compose\", version.ref = \"paging\" }\ngoogle-accompanist-drawablepainter = \"com.google.accompanist:accompanist-drawablepainter:0.37.3\"\njunit = \"junit:junit:4.13.2\"\nrikka-refine-processor = { module = \"dev.rikka.tools.refine:annotation-processor\", version.ref = \"refine\" }\nrikka-refine-annotation = { module = \"dev.rikka.tools.refine:annotation\", version.ref = \"refine\" }\nrikka-shizuku-api = { module = \"dev.rikka.shizuku:api\", version.ref = \"shizuku\" }\nrikka-shizuku-provider = { module = \"dev.rikka.shizuku:provider\", version.ref = \"shizuku\" }\nlsposed-hiddenapibypass = \"org.lsposed.hiddenapibypass:hiddenapibypass:6.1\"\ncoil-compose = { module = \"io.coil-kt.coil3:coil-compose\", version.ref = \"coil\" }\ncoil-network = { module = \"io.coil-kt.coil3:coil-network-okhttp\", version.ref = \"coil\" }\ncoil-gif = { module = \"io.coil-kt.coil3:coil-gif\", version.ref = \"coil\" }\nloc-annotation = { module = \"li.songe.loc:loc-annotation\", version.ref = \"loc\" }\nreorderable = \"sh.calvin.reorderable:reorderable:3.0.0\"\nexp4j = \"net.objecthunter:exp4j:0.4.8\"\ntoaster = \"com.github.getActivity:Toaster:13.8\"\npermissions = \"com.github.getActivity:XXPermissions:28.0\"\ndevice = \"com.github.getActivity:DeviceCompat:2.6\"\njson5 = \"li.songe:json5:0.5.0\"\nactivityResultLauncher = \"com.github.DylanCaiCoding:ActivityResultLauncher:1.1.2\"\nkevinnzouWebview = \"io.github.kevinnzou:compose-webview:0.33.6\"\n\n[plugins]\nkotlin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\nkotlin-multiplatform = { id = \"org.jetbrains.kotlin.multiplatform\", version.ref = \"kotlin\" }\nkotlin-parcelize = { id = \"org.jetbrains.kotlin.plugin.parcelize\", version.ref = \"kotlin\" }\nkotlin-compose = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\nkotlinx-atomicfu = { id = \"org.jetbrains.kotlinx.atomicfu\", version.ref = \"atomicfu\" }\nandroid-library = { id = \"com.android.library\", version.ref = \"agp\" }\nandroid-application = { id = \"com.android.application\", version.ref = \"agp\" }\nandroidx-room = { id = \"androidx.room\", version.ref = \"room\" }\ngoogle-ksp = { id = \"com.google.devtools.ksp\", version.ref = \"ksp\" }\nrikka-refine = { id = \"dev.rikka.tools.refine\", version.ref = \"refine\" }\nloc = { id = \"li.songe.loc\", version.ref = \"loc\" }\nlittlerobots-version = \"nl.littlerobots.version-catalog-update:1.1.0\"\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.4.0-bin.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g\nkotlin.code.style=official\nandroid.useAndroidX=true\nandroid.debug.obsoleteApi=true\nandroid.nonTransitiveRClass=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "hidden_api/.gitignore",
    "content": "/build"
  },
  {
    "path": "hidden_api/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.android.library)\n}\n\nandroid {\n    namespace = rootProject.ext[\"android.namespace\"].toString() + \".\" + project.name\n    compileSdk = rootProject.ext[\"android.compileSdk\"] as Int\n    buildToolsVersion = rootProject.ext[\"android.buildToolsVersion\"].toString()\n    defaultConfig {\n        minSdk = rootProject.ext[\"android.minSdk\"] as Int\n    }\n}\n\ndependencies {\n    compileOnly(libs.androidx.annotation)\n    compileOnly(libs.rikka.refine.annotation)\n    annotationProcessor(libs.rikka.refine.processor)\n}"
  },
  {
    "path": "hidden_api/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest />\n"
  },
  {
    "path": "hidden_api/src/main/java/android/accessibilityservice/AccessibilityServiceInfoHidden.java",
    "content": "package android.accessibilityservice;\n\nimport android.os.Build;\n\nimport androidx.annotation.RequiresApi;\n\nimport dev.rikka.tools.refine.RefineAs;\n\n/**\n * @noinspection unused\n */\n@RefineAs(AccessibilityServiceInfo.class)\npublic class AccessibilityServiceInfoHidden {\n    public static int FLAG_FORCE_DIRECT_BOOT_AWARE;\n\n    public void setCapabilities(int capabilities) {\n        throw new RuntimeException();\n    }\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    public void setAccessibilityTool(boolean isAccessibilityTool) {\n        throw new RuntimeException();\n    }\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/accessibilityservice/IAccessibilityServiceClient.java",
    "content": "package android.accessibilityservice;\n\nimport android.os.Binder;\nimport android.os.IBinder;\nimport android.os.IInterface;\n\n/**\n * @noinspection unused\n */\npublic interface IAccessibilityServiceClient extends IInterface {\n    abstract class Stub extends Binder implements IAccessibilityServiceClient {\n        public static IAccessibilityServiceClient asInterface(IBinder obj) {\n            throw new RuntimeException();\n        }\n    }\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/app/AppOpsManagerHidden.java",
    "content": "package android.app;\n\n\nimport android.os.Build;\n\nimport androidx.annotation.RequiresApi;\n\nimport dev.rikka.tools.refine.RefineAs;\n\n/**\n * @noinspection unused\n */\n@RefineAs(AppOpsManager.class)\npublic class AppOpsManagerHidden {\n    public static int OP_POST_NOTIFICATION;\n    @RequiresApi(Build.VERSION_CODES.P)\n    public static String OPSTR_POST_NOTIFICATION;\n\n    public static int OP_SYSTEM_ALERT_WINDOW;\n    public static String OPSTR_SYSTEM_ALERT_WINDOW;\n\n    @RequiresApi(Build.VERSION_CODES.Q)\n    public static int OP_ACCESS_ACCESSIBILITY;\n\n    @RequiresApi(Build.VERSION_CODES.Q)\n    public static String OPSTR_ACCESS_ACCESSIBILITY;\n\n    // https://diff.songe.li/?ref=AppOpsManager.OP_CREATE_ACCESSIBILITY_OVERLAY\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    public static int OP_CREATE_ACCESSIBILITY_OVERLAY;\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    public static String OPSTR_CREATE_ACCESSIBILITY_OVERLAY;\n\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    public static int OP_ACCESS_RESTRICTED_SETTINGS;\n\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    public static String OPSTR_ACCESS_RESTRICTED_SETTINGS;\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    public static int OP_FOREGROUND_SERVICE_SPECIAL_USE;\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    public static String OPSTR_FOREGROUND_SERVICE_SPECIAL_USE;\n\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    public static boolean opRestrictsRead(int op) {\n        throw new RuntimeException();\n    }\n\n    /**\n     * @return X_Y_Z\n     */\n    public static String opToName(int op) {\n        throw new RuntimeException();\n    }\n\n    /**\n     * @return android:x_y_z\n     */\n    @RequiresApi(Build.VERSION_CODES.Q)\n    public static String opToPublicName(int op) {\n        throw new RuntimeException();\n    }\n\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/app/IActivityManager.java",
    "content": "package android.app;\n\nimport android.content.ComponentName;\nimport android.content.Intent;\nimport android.os.Binder;\nimport android.os.Build;\nimport android.os.IBinder;\nimport android.os.IInterface;\n\nimport androidx.annotation.DeprecatedSinceApi;\nimport androidx.annotation.RequiresApi;\n\nimport java.util.List;\n\n/**\n * @noinspection unused\n */\npublic interface IActivityManager extends IInterface {\n    abstract class Stub extends Binder implements IActivityManager {\n        public static IActivityManager asInterface(IBinder obj) {\n            throw new RuntimeException();\n        }\n    }\n\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.P)\n    List<ActivityManager.RunningTaskInfo> getTasks(int maxNum, int flags);\n\n    @RequiresApi(Build.VERSION_CODES.P)\n    List<ActivityManager.RunningTaskInfo> getTasks(int maxNum);\n\n    void registerTaskStackListener(ITaskStackListener listener);\n\n    void unregisterTaskStackListener(ITaskStackListener listener);\n\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.R)\n    ComponentName startService(IApplicationThread caller, Intent service, String resolvedType, boolean requireForeground, String callingPackage, int userId);\n\n    @RequiresApi(Build.VERSION_CODES.R)\n    ComponentName startService(IApplicationThread caller, Intent service, String resolvedType, boolean requireForeground, String callingPackage, String callingFeatureId, int userId);\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/app/IActivityTaskManager.java",
    "content": "package android.app;\n\nimport android.os.Binder;\nimport android.os.IBinder;\nimport android.os.IInterface;\n\nimport java.util.List;\n\n/**\n * @noinspection unused\n */\npublic interface IActivityTaskManager extends IInterface {\n    // android10+\n    abstract class Stub extends Binder implements IActivityTaskManager {\n        public static IActivityTaskManager asInterface(IBinder obj) {\n            throw new RuntimeException();\n        }\n    }\n\n    // https://diff.songe.li/i/IActivityTaskManager.getTasks\n    List<ActivityManager.RunningTaskInfo> getTasks(int maxNum);\n\n    List<ActivityManager.RunningTaskInfo> getTasks(int maxNum, boolean filterOnlyVisibleRecents, boolean keepIntentExtra);\n\n    List<ActivityManager.RunningTaskInfo> getTasks(int maxNum, boolean filterOnlyVisibleRecents, boolean keepIntentExtra, int displayId);\n\n    void registerTaskStackListener(ITaskStackListener listener);\n\n    void unregisterTaskStackListener(ITaskStackListener listener);\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/app/IApplicationThread.java",
    "content": "package android.app;\n\n/**\n * @noinspection unused\n */\npublic interface IApplicationThread {\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/app/ITaskStackListener.java",
    "content": "package android.app;\n\nimport android.os.Binder;\nimport android.os.IBinder;\n\n/**\n * @noinspection unused\n */\npublic interface ITaskStackListener {\n    abstract class Stub extends Binder implements ITaskStackListener {\n        public static ITaskStackListener asInterface(IBinder obj) {\n            throw new RuntimeException();\n        }\n    }\n\n    // 应用->桌面不会回调，分屏下切换窗口不会回调，但从最近任务界面移除窗口会回调\n    void onTaskStackChanged();\n\n    // https://diff.songe.li/?ref=ITaskStackListener.onTaskMovedToFront\n    // android8 - android9\n    void onTaskMovedToFront(int taskId);\n\n    // android10+\n    void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo);\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/app/IUiAutomationConnection.java",
    "content": "package android.app;\n\nimport android.accessibilityservice.IAccessibilityServiceClient;\nimport android.graphics.Bitmap;\nimport android.graphics.Rect;\nimport android.os.Binder;\nimport android.os.Build;\nimport android.os.IBinder;\nimport android.window.ScreenCapture;\n\nimport androidx.annotation.DeprecatedSinceApi;\nimport androidx.annotation.RequiresApi;\n\n/**\n * @noinspection unused\n */\npublic interface IUiAutomationConnection {\n    abstract class Stub extends Binder implements IUiAutomationConnection {\n        public static IUiAutomationConnection asInterface(IBinder obj) {\n            throw new RuntimeException();\n        }\n    }\n\n    void connect(IAccessibilityServiceClient client, int flags);\n\n    void disconnect();\n\n    void shutdown();\n\n    // https://diff.songe.li/?ref=IUiAutomationConnection.takeScreenshot\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.P)\n    Bitmap takeScreenshot(int width, int height);\n\n    @RequiresApi(Build.VERSION_CODES.P)\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.S)\n    Bitmap takeScreenshot(Rect crop, int rotation);\n\n    @RequiresApi(Build.VERSION_CODES.S)\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)\n    Bitmap takeScreenshot(Rect crop);\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.BAKLAVA)\n    boolean takeScreenshot(Rect crop, ScreenCapture.ScreenCaptureListener listener);\n\n    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)\n    boolean takeScreenshot(Rect crop, ScreenCapture.ScreenCaptureListener listener, int displayId);\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/app/UiAutomationHidden.java",
    "content": "package android.app;\n\nimport android.os.Build;\nimport android.os.Looper;\n\nimport androidx.annotation.RequiresApi;\n\nimport dev.rikka.tools.refine.RefineAs;\n\n/**\n * @noinspection unused\n */\n@RefineAs(UiAutomation.class)\npublic class UiAutomationHidden {\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    public static int FLAG_NOT_ACCESSIBILITY_TOOL;\n\n    public UiAutomationHidden(Looper looper, IUiAutomationConnection connection) {\n        throw new RuntimeException();\n    }\n\n    public void connect() {\n        throw new RuntimeException();\n    }\n\n    public void connect(int flag) {\n        throw new RuntimeException();\n    }\n\n    public void disconnect() {\n        throw new RuntimeException();\n    }\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/content/ContextHidden.java",
    "content": "package android.content;\n\nimport android.os.Build;\n\nimport androidx.annotation.RequiresApi;\n\nimport dev.rikka.tools.refine.RefineAs;\n\n/**\n * @noinspection unused\n */\n@RefineAs(Context.class)\npublic class ContextHidden {\n    @RequiresApi(Build.VERSION_CODES.Q)\n    public static String ACTIVITY_TASK_SERVICE;\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/content/pm/IPackageManager.java",
    "content": "package android.content.pm;\n\nimport android.content.IntentFilter;\nimport android.os.Binder;\nimport android.os.Build;\nimport android.os.IBinder;\nimport android.os.IInterface;\n\nimport androidx.annotation.DeprecatedSinceApi;\nimport androidx.annotation.RequiresApi;\n\n/**\n * @noinspection unused\n */\npublic interface IPackageManager extends IInterface {\n    abstract class Stub extends Binder implements IPackageManager {\n        public static IPackageManager asInterface(IBinder binder) {\n            throw new IllegalArgumentException(\"Stub!\");\n        }\n    }\n\n    boolean isSafeMode();\n\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.TIRAMISU)\n    ParceledListSlice<PackageInfo> getInstalledPackages(int flags, int userId);\n\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);\n\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.TIRAMISU)\n    PackageInfo getPackageInfo(String packageName, int flags, int userId);\n\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    PackageInfo getPackageInfo(String packageName, long flags, int userId);\n\n    ParceledListSlice<IntentFilter> getAllIntentFilters(String packageName);\n\n    int checkPermission(String permName, String pkgName, int userId);\n\n    void grantRuntimePermission(String packageName, String permissionName, int userId);\n\n    int getApplicationEnabledSetting(String packageName, int userId);\n\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/content/pm/PackageInfoHidden.java",
    "content": "package android.content.pm;\n\nimport dev.rikka.tools.refine.RefineAs;\n\n/**\n * @noinspection unused\n */\n@RefineAs(PackageInfo.class)\npublic class PackageInfoHidden {\n    public String overlayTarget;\n}"
  },
  {
    "path": "hidden_api/src/main/java/android/content/pm/ParceledListSlice.java",
    "content": "package android.content.pm;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport java.util.List;\n\n/**\n * @noinspection rawtypes, unused\n */\npublic class ParceledListSlice<T extends Parcelable> {\n    public static final Parcelable.ClassLoaderCreator<ParceledListSlice> CREATOR = new Parcelable.ClassLoaderCreator<ParceledListSlice>() {\n        public ParceledListSlice createFromParcel(Parcel var1) {\n            throw new UnsupportedOperationException();\n        }\n\n        public ParceledListSlice createFromParcel(Parcel var1, ClassLoader var2) {\n            throw new UnsupportedOperationException();\n        }\n\n        public ParceledListSlice[] newArray(int var1) {\n            throw new UnsupportedOperationException();\n        }\n    };\n\n    public List<T> getList() {\n        throw new UnsupportedOperationException();\n    }\n}"
  },
  {
    "path": "hidden_api/src/main/java/android/content/pm/UserInfo.java",
    "content": "package android.content.pm;\n\n/**\n * @noinspection unused\n */\npublic class UserInfo {\n    public int id;\n    public String name;\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/hardware/input/IInputManager.java",
    "content": "package android.hardware.input;\n\nimport android.os.Binder;\nimport android.os.Build;\nimport android.os.IBinder;\nimport android.os.IInterface;\nimport android.view.InputEvent;\n\nimport androidx.annotation.RequiresApi;\n\n/**\n * @noinspection unused\n */\npublic interface IInputManager extends IInterface {\n    abstract class Stub extends Binder implements IInputManager {\n        public static IInputManager asInterface(IBinder binder) {\n            throw new IllegalArgumentException(\"Stub!\");\n        }\n    }\n\n    boolean injectInputEvent(InputEvent ev, int mode);\n\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    boolean injectInputEventToTarget(InputEvent ev, int mode, int targetUid);\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/hardware/input/InputManagerHidden.java",
    "content": "package android.hardware.input;\n\n\nimport dev.rikka.tools.refine.RefineAs;\n\n/**\n * @noinspection unused\n */\n@RefineAs(InputManager.class)\npublic class InputManagerHidden {\n    public static int INJECT_INPUT_EVENT_MODE_ASYNC;\n    public static int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;\n    public static int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT;\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/os/IUserManager.java",
    "content": "package android.os;\n\nimport android.content.pm.UserInfo;\n\nimport java.util.List;\n\n/**\n * @noinspection unused\n */\npublic interface IUserManager extends IInterface {\n    abstract class Stub extends Binder implements IUserManager {\n        public static IUserManager asInterface(IBinder obj) {\n            throw new RuntimeException();\n        }\n    }\n\n    // https://diff.songe.li/i/IUserManager.getUsers\n    List<UserInfo> getUsers(boolean excludeDying);\n\n    List<UserInfo> getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated);\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/view/IWindowManager.java",
    "content": "package android.view;\n\nimport android.os.Binder;\nimport android.os.Build;\nimport android.os.IBinder;\nimport android.os.IInterface;\nimport android.window.ScreenCapture;\n\nimport androidx.annotation.DeprecatedSinceApi;\nimport androidx.annotation.RequiresApi;\n\n/**\n * @noinspection unused\n */\npublic interface IWindowManager extends IInterface {\n    abstract class Stub extends Binder implements IWindowManager {\n        public static IWindowManager asInterface(IBinder obj) {\n            throw new RuntimeException();\n        }\n    }\n\n    boolean isRotationFrozen();\n\n    int getDefaultDisplayRotation();\n\n    // https://diff.songe.li/?ref=IWindowManager.freezeRotation\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)\n    void freezeRotation(int rotation);\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    void freezeRotation(int rotation, String caller);\n\n    // https://diff.songe.li/?ref=IWindowManager.thawRotation\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.VANILLA_ICE_CREAM)\n    void thawRotation();\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    void thawRotation(String caller);\n\n    // https://diff.songe.li/?ref=IWindowManager.captureDisplay\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    void captureDisplay(int displayId, ScreenCapture.CaptureArgs captureArgs, ScreenCapture.ScreenCaptureListener listener);\n\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/view/KeyEventHidden.java",
    "content": "package android.view;\n\nimport android.os.Build;\n\nimport androidx.annotation.RequiresApi;\n\nimport dev.rikka.tools.refine.RefineAs;\n\n/**\n * @noinspection unused\n */\n@RefineAs(KeyEvent.class)\npublic class KeyEventHidden {\n    @RequiresApi(Build.VERSION_CODES.Q)\n    public void setDisplayId(int displayId) {\n        throw new RuntimeException();\n    }\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/view/MotionEventHidden.java",
    "content": "package android.view;\n\nimport android.os.Build;\n\nimport androidx.annotation.RequiresApi;\n\nimport dev.rikka.tools.refine.RefineAs;\n\n/**\n * @noinspection unused\n */\n@RefineAs(MotionEvent.class)\npublic class MotionEventHidden {\n    @RequiresApi(Build.VERSION_CODES.Q)\n    public static MotionEvent obtain(long downTime, long eventTime, int action, int pointerCount, MotionEvent.PointerProperties[] pointerProperties, MotionEvent.PointerCoords[] pointerCoords, int metaState, int buttonState, float xPrecision, float yPrecision, int deviceId, int edgeFlags, int source, int displayId, int flags) {\n        throw new RuntimeException();\n    }\n\n    @RequiresApi(Build.VERSION_CODES.Q)\n    public void setDisplayId(int displayId) {\n        throw new RuntimeException();\n    }\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/view/SurfaceControlHidden.java",
    "content": "package android.view;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Rect;\nimport android.os.IBinder;\n\nimport dev.rikka.tools.refine.RefineAs;\n\n/**\n * @noinspection unused\n */\n@RefineAs(SurfaceControl.class)\npublic class SurfaceControlHidden {\n    public static IBinder getInternalDisplayToken() {\n        throw new RuntimeException();\n    }\n\n    public static ScreenshotHardwareBuffer captureDisplay(DisplayCaptureArgs captureArgs) {\n        throw new RuntimeException();\n    }\n\n    public static Bitmap screenshot(Rect sourceCrop, int width, int height, int rotation) {\n        throw new RuntimeException();\n    }\n\n    public static Bitmap screenshot(int width, int height) {\n        throw new RuntimeException();\n    }\n\n    public interface ScreenCaptureListener {\n    }\n\n    public static class ScreenshotHardwareBuffer {\n        public Bitmap asBitmap() {\n            throw new RuntimeException();\n        }\n    }\n\n\n    private abstract static class CaptureArgs {\n        abstract static class Builder<T extends Builder<T>> {\n            public T setSourceCrop(Rect sourceCrop) {\n                throw new RuntimeException();\n            }\n        }\n    }\n\n    public static class DisplayCaptureArgs extends CaptureArgs {\n        public static class Builder extends CaptureArgs.Builder<Builder> {\n            public Builder(IBinder displayToken) {\n                throw new RuntimeException();\n            }\n\n            public Builder setSize(int width, int height) {\n                throw new RuntimeException();\n            }\n\n            public DisplayCaptureArgs build() {\n                throw new RuntimeException();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/view/accessibility/AccessibilityNodeInfoHidden.java",
    "content": "package android.view.accessibility;\n\nimport android.graphics.Rect;\n\nimport dev.rikka.tools.refine.RefineAs;\n\n/**\n * @noinspection unused\n */\n@RefineAs(AccessibilityNodeInfo.class)\npublic class AccessibilityNodeInfoHidden {\n    public Rect getBoundsInScreen() {\n        throw new RuntimeException();\n    }\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/view/accessibility/IAccessibilityManager.java",
    "content": "package android.view.accessibility;\n\nimport android.accessibilityservice.AccessibilityServiceInfo;\nimport android.accessibilityservice.IAccessibilityServiceClient;\nimport android.os.Binder;\nimport android.os.Build;\nimport android.os.IBinder;\n\nimport androidx.annotation.DeprecatedSinceApi;\nimport androidx.annotation.RequiresApi;\n\n/**\n * @noinspection unused\n */\npublic interface IAccessibilityManager {\n    abstract class Stub extends Binder implements IAccessibilityManager {\n        public static IAccessibilityManager asInterface(IBinder obj) {\n            throw new RuntimeException();\n        }\n    }\n\n    @DeprecatedSinceApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    void registerUiTestAutomationService(IBinder owner, IAccessibilityServiceClient client, AccessibilityServiceInfo info, int flags);\n\n    @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\n    void registerUiTestAutomationService(IBinder owner, IAccessibilityServiceClient client, AccessibilityServiceInfo info, int userId, int flags);\n\n    void unregisterUiTestAutomationService(IAccessibilityServiceClient client);\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/android/window/ScreenCapture.java",
    "content": "package android.window;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Rect;\nimport android.os.Build;\n\nimport androidx.annotation.RequiresApi;\n\n/**\n * @noinspection unused\n */\n@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)\npublic class ScreenCapture {\n    public static SynchronousScreenCaptureListener createSyncCaptureListener() {\n        throw new RuntimeException();\n    }\n\n    public static class CaptureArgs {\n        public static class Builder<T extends CaptureArgs.Builder<T>> {\n            public CaptureArgs build() {\n                throw new RuntimeException();\n            }\n\n            public T setSourceCrop(Rect sourceCrop) {\n                throw new RuntimeException();\n            }\n        }\n    }\n\n    public static class ScreenCaptureListener {\n    }\n\n    public static class ScreenshotHardwareBuffer {\n        public Bitmap asBitmap() {\n            throw new RuntimeException();\n        }\n    }\n\n    public abstract static class SynchronousScreenCaptureListener extends ScreenCaptureListener {\n        public abstract ScreenshotHardwareBuffer getBuffer();\n    }\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/com/android/internal/R.java",
    "content": "package com.android.internal;\n\nimport android.os.Build;\n\nimport androidx.annotation.RequiresApi;\n\n/**\n * @noinspection unused\n */\npublic class R {\n    public static final class string {\n        @RequiresApi(api = Build.VERSION_CODES.P)\n        public static int config_recentsComponentName;\n    }\n}\n"
  },
  {
    "path": "hidden_api/src/main/java/com/android/internal/app/IAppOpsService.java",
    "content": "package com.android.internal.app;\n\nimport android.os.Binder;\nimport android.os.IBinder;\nimport android.os.IInterface;\n\n/**\n * @noinspection unused\n */\npublic interface IAppOpsService extends IInterface {\n    abstract class Stub extends Binder implements IAppOpsService {\n        public static IAppOpsService asInterface(IBinder obj) {\n            throw new RuntimeException();\n        }\n    }\n\n    int checkOperation(int code, int uid, String packageName);\n\n    void setMode(int code, int uid, String packageName, int mode);\n}\n"
  },
  {
    "path": "selector/.gitignore",
    "content": "/build"
  },
  {
    "path": "selector/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n    alias(libs.plugins.kotlin.multiplatform)\n    alias(libs.plugins.kotlin.serialization)\n}\n\nkotlin {\n    jvm {\n        compilerOptions {\n            jvmTarget.set(rootProject.ext[\"kotlin.jvmTarget\"] as JvmTarget)\n        }\n    }\n    js {\n        compilerOptions {\n            target.set(\"es2015\")\n        }\n        binaries.executable()\n        useEsModules()\n        generateTypeScriptDefinitions()\n        browser {}\n    }\n    sourceSets {\n        all {\n            languageSettings.optIn(\"kotlin.js.ExperimentalJsExport\")\n        }\n        commonMain {\n            dependencies {\n                implementation(libs.kotlin.stdlib)\n            }\n        }\n        jvmTest {\n            dependencies {\n                implementation(libs.kotlinx.serialization.json)\n                implementation(libs.kotlin.test)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/Exception.kt",
    "content": "package li.songe.selector\n\nimport li.songe.selector.property.BinaryExpression\nimport li.songe.selector.property.ValueExpression\nimport kotlin.js.JsExport\n\n@JsExport\nsealed class GkdException(override val message: String) : Exception(message) {\n    // for kotlin js\n    @Suppress(\"unused\")\n    val outMessage: String\n        get() = message\n}\n\n@JsExport\ndata class SyntaxException(\n    override val message: String,\n    val expectedValue: String,\n    val index: Int\n) : GkdException(message)\n\n@JsExport\nsealed class TypeException(override val message: String) : GkdException(message)\n\n@JsExport\ndata class UnknownIdentifierException(\n    val value: ValueExpression.Identifier,\n) : TypeException(\"Unknown Identifier: ${value.stringify()}\")\n\n@JsExport\ndata class UnknownMemberException(\n    val value: ValueExpression.MemberExpression,\n) : TypeException(\"Unknown Member: ${value.stringify()}\")\n\n@JsExport\ndata class UnknownIdentifierMethodException(\n    val value: ValueExpression.Identifier,\n) : TypeException(\"Unknown Identifier Method: ${value.stringify()}\")\n\n@JsExport\ndata class UnknownIdentifierMethodParamsException(\n    val value: ValueExpression.CallExpression,\n) : TypeException(\"Unknown Identifier Method Params: ${value.stringify()}\")\n\n@JsExport\ndata class UnknownMemberMethodException(\n    val value: ValueExpression.MemberExpression,\n) : TypeException(\"Unknown Member Method: ${value.stringify()}\")\n\n@JsExport\ndata class UnknownMemberMethodParamsException(\n    val value: ValueExpression.CallExpression,\n) : TypeException(\"Unknown Member Method Params: ${value.stringify()}\")\n\n@JsExport\ndata class MismatchParamTypeException(\n    val call: ValueExpression.CallExpression,\n    val argument: ValueExpression,\n    val type: PrimitiveType\n) : TypeException(\"Mismatch Param Type: ${argument.stringify()} should be ${type.key}\")\n\n@JsExport\ndata class MismatchExpressionTypeException(\n    val exception: BinaryExpression,\n    val leftType: PrimitiveType,\n    val rightType: PrimitiveType,\n) : TypeException(\"Mismatch Expression Type: ${exception.stringify()}\")\n\n@JsExport\ndata class MismatchOperatorTypeException(\n    val exception: BinaryExpression,\n) : TypeException(\"Mismatch Operator Type: ${exception.stringify()}\")\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/FastQuery.kt",
    "content": "package li.songe.selector\n\nimport li.songe.selector.property.CompareOperator\nimport kotlin.js.JsExport\n\n@JsExport\nsealed class FastQuery(open val value: String) : Stringify {\n    override fun stringify() = value\n    open fun acceptText(text: String): Boolean = text == value\n\n    data class Id(override val value: String) : FastQuery(value)\n    data class Vid(override val value: String) : FastQuery(value)\n    data class Text(override val value: String, val operator: CompareOperator) : FastQuery(value) {\n        override fun acceptText(text: String): Boolean = when (operator) {\n            CompareOperator.Equal -> text == value\n            CompareOperator.Start -> text.startsWith(value)\n            CompareOperator.Include -> text.contains(value)\n            CompareOperator.End -> text.endsWith(value)\n            else -> error(\"Invalid operator: $operator\")\n        }\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/MatchOption.kt",
    "content": "package li.songe.selector\n\nimport kotlin.js.JsExport\n\n@JsExport\ndata class MatchOption(\n    val fastQuery: Boolean = false,\n) {\n    companion object {\n        val default = MatchOption()\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/QueryContext.kt",
    "content": "package li.songe.selector\n\nimport kotlin.js.JsExport\n\n@JsExport\ndata class QueryContext<T>(\n    val current: T,\n    val prev: QueryContext<T>? = null,\n    val matched: Boolean = true,\n) {\n    @Suppress(\"unused\")\n    val originalContext: QueryContext<T>\n        get() {\n            var context = this\n            while (context.prev != null) {\n                context = context.prev\n            }\n            return context\n        }\n\n    fun getPrev(index: Int): QueryContext<T>? {\n        if (index < 0) return null\n        var context = prev ?: return null\n        repeat(index) {\n            context = context.prev ?: return null\n        }\n        return context\n    }\n\n    fun get(index: Int): QueryContext<T> {\n        if (index == 0) return this\n        return getPrev(index - 1) ?: throw IndexOutOfBoundsException()\n    }\n\n    fun toList(): List<T> {\n        val list = mutableListOf(this.current)\n        var context = prev\n        while (context != null) {\n            list.add(context.current)\n            context = context.prev\n        }\n        return list\n    }\n\n    fun toContextList(): List<QueryContext<T>> {\n        val list = mutableListOf(this)\n        var context = prev\n        while (context != null) {\n            list.add(context)\n            context = context.prev\n        }\n        return list\n    }\n\n    @Suppress(\"UNCHECKED_CAST\", \"unused\")\n    fun toArray(): Array<T> {\n        return (toList() as List<Any>).toTypedArray() as Array<T>\n    }\n\n    fun next(value: T): QueryContext<T> {\n        return QueryContext(value, this)\n    }\n\n    fun mismatch(): QueryContext<T> {\n        return copy(matched = false)\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/QueryPath.kt",
    "content": "package li.songe.selector\n\nimport li.songe.selector.connect.ConnectWrapper\nimport li.songe.selector.property.PropertyWrapper\nimport kotlin.js.JsExport\n\n@Suppress(\"unused\")\n@JsExport\ndata class QueryPath<T>(\n    val propertyWrapper: PropertyWrapper,\n    val connectWrapper: ConnectWrapper,\n    val offset: Int,\n    val source: T,\n    val target: T,\n) {\n    val formatConnectOffset: String\n        get() = connectWrapper.segment.operator.formatOffset(offset)\n}"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/QueryResult.kt",
    "content": "package li.songe.selector\n\nimport li.songe.selector.connect.PolynomialExpression\nimport li.songe.selector.unit.LogicalSelectorExpression\nimport li.songe.selector.unit.NotSelectorExpression\nimport li.songe.selector.unit.SelectorExpression\nimport li.songe.selector.unit.UnitSelectorExpression\nimport kotlin.js.JsExport\n\n@Suppress(\"unused\")\n@JsExport\nsealed class QueryResult<T> {\n    abstract val context: QueryContext<T>\n    abstract val expression: SelectorExpression\n    abstract val targetIndex: Int\n    abstract val matched: Boolean\n\n    abstract val unitResults: List<UnitResult<T>>\n\n    val target: T\n        get() = context.get(targetIndex).current\n\n    data class UnitResult<T>(\n        override val context: QueryContext<T>,\n        override val expression: UnitSelectorExpression,\n        override val targetIndex: Int,\n    ) : QueryResult<T>() {\n        override val matched: Boolean\n            get() = context.matched\n        override val unitResults: List<UnitResult<T>>\n            get() = if (matched) listOf(this) else emptyList()\n\n        fun getNodeConnectPath(transform: Transform<T>): List<QueryPath<T>> {\n            val contextList = context.toContextList().reversed()\n            if (contextList.size <= 1) {\n                return emptyList()\n            }\n            val connectSize = expression.connectWrappers.count()\n            if (connectSize != contextList.size - 1) {\n                error(\"Connect size not match\")\n            }\n            val list = mutableListOf<QueryPath<T>>()\n            var current = expression.propertyWrapper\n            var i = 0\n            val polynomialExpression = PolynomialExpression(a = 1, b = 0)\n            while (true) {\n                current.to ?: break\n                val offset = current.to.segment.run {\n                    operator\n                        .traversal(contextList[i], transform, polynomialExpression)\n                        .indexOf(contextList[i + 1].current)\n                }\n                require(offset >= 0)\n                list.add(\n                    QueryPath(\n                        propertyWrapper = current,\n                        connectWrapper = current.to,\n                        offset = offset,\n                        source = contextList[i].current,\n                        target = contextList[i + 1].current,\n                    )\n                )\n                current = current.to.to\n                i++\n            }\n            return list\n        }\n    }\n\n    data class AndResult<T>(\n        override val expression: LogicalSelectorExpression,\n        val left: QueryResult<T>,\n        val right: QueryResult<T>? = null,\n    ) : QueryResult<T>() {\n        override val matched: Boolean\n            get() = left.matched && right?.matched == true\n        override val context: QueryContext<T>\n            get() = right?.context ?: error(\"No matched result\")\n        override val targetIndex: Int\n            get() = right?.targetIndex ?: error(\"No matched result\")\n        override val unitResults: List<UnitResult<T>>\n            get() = if (right != null) {\n                left.unitResults + right.unitResults\n            } else {\n                left.unitResults\n            }\n    }\n\n    data class OrResult<T>(\n        override val expression: LogicalSelectorExpression,\n        val left: QueryResult<T>,\n        val right: QueryResult<T>? = null,\n    ) : QueryResult<T>() {\n        override val matched: Boolean\n            get() = left.matched || right?.matched == true\n        override val context: QueryContext<T>\n            get() = (if (left.matched) left else right)?.context ?: error(\"No matched result\")\n        override val targetIndex: Int\n            get() = (if (left.matched) left else right)?.targetIndex ?: error(\"No matched result\")\n        override val unitResults: List<UnitResult<T>>\n            get() = if (right != null) {\n                left.unitResults + right.unitResults\n            } else {\n                left.unitResults\n            }\n    }\n\n    data class NotResult<T>(\n        override val expression: NotSelectorExpression,\n        val originalContext: QueryContext<T>,\n        val result: QueryResult<T>,\n    ) : QueryResult<T>() {\n        override val matched: Boolean\n            get() = !result.matched\n        override val context: QueryContext<T>\n            get() = originalContext\n        override val targetIndex: Int\n            get() = 0\n        override val unitResults: List<UnitResult<T>>\n            get() = result.unitResults\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/Selector.kt",
    "content": "package li.songe.selector\n\nimport li.songe.selector.parser.AstParser\nimport li.songe.selector.parser.SelectorParser\nimport li.songe.selector.unit.SelectorExpression\nimport kotlin.js.JsExport\n\n@JsExport\nclass Selector(\n    val expression: SelectorExpression,\n) {\n    override fun toString(): String {\n        return expression.stringify()\n    }\n\n    fun <T> matchContext(\n        node: T,\n        transform: Transform<T>,\n        option: MatchOption,\n    ): QueryResult<T> {\n        return expression.matchContext(QueryContext(node), transform, option)\n    }\n\n    fun <T> match(\n        node: T,\n        transform: Transform<T>,\n        option: MatchOption,\n    ): T? {\n        return expression.match(QueryContext(node), transform, option)\n    }\n\n    val fastQueryList = expression.fastQueryList.distinct()\n    val isMatchRoot = expression.isMatchRoot\n\n    fun isSlow(matchOption: MatchOption) = expression.isSlow(matchOption)\n    fun checkType(typeInfo: TypeInfo) = expression.checkType(typeInfo)\n\n    companion object {\n        fun parse(source: String) = SelectorParser(source).readSelector()\n\n        fun parseOrNull(source: String) = try {\n            SelectorParser(source).readSelector()\n        } catch (_: Exception) {\n            null\n        }\n\n        fun parseAst(source: String) = AstParser(source).readAst()\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/Stringify.kt",
    "content": "package li.songe.selector\n\n\ninternal interface Stringify {\n    fun stringify(): String\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/Transform.kt",
    "content": "package li.songe.selector\n\nimport li.songe.selector.connect.ConnectExpression\nimport kotlin.js.JsExport\nimport kotlin.sequences.forEach\n\n@Suppress(\"unused\")\n@JsExport\nclass Transform<T> @JsExport.Ignore constructor(\n    val getAttr: (Any, String) -> Any?,\n    val getInvoke: (Any, String, List<Any>) -> Any? = { _, _, _ -> null },\n    val getName: (T) -> CharSequence?,\n    val getChildren: (T) -> Sequence<T>,\n    val getParent: (T) -> T?,\n\n    val getRoot: (T) -> T? = { node ->\n        var parentVar: T? = getParent(node)\n        while (parentVar != null) {\n            parentVar = getParent(parentVar)\n        }\n        parentVar\n    },\n    val getDescendants: (T) -> Sequence<T> = { node ->\n        sequence { //            深度优先 先序遍历\n            //            https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector\n            val stack = getChildren(node).toMutableList()\n            if (stack.isEmpty()) return@sequence\n            stack.reverse()\n            val tempNodes = mutableListOf<T>()\n            do {\n                val top = stack.removeAt(stack.lastIndex)\n                yield(top)\n                for (childNode in getChildren(top)) {\n                    // 可针对 sequence 优化\n                    tempNodes.add(childNode)\n                }\n                if (tempNodes.isNotEmpty()) {\n                    for (i in tempNodes.size - 1 downTo 0) {\n                        stack.add(tempNodes[i])\n                    }\n                    tempNodes.clear()\n                }\n            } while (stack.isNotEmpty())\n        }\n    },\n\n    val traverseChildren: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->\n        getChildren(node).let {\n            if (connectExpression.maxOffset != null) {\n                it.take(connectExpression.maxOffset!! + 1)\n            } else {\n                it\n            }\n        }.filterIndexed { i, _ ->\n            connectExpression.checkOffset(\n                i\n            )\n        }\n    },\n    val traverseAncestors: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->\n        sequence {\n            var parentVar: T? = getParent(node) ?: return@sequence\n            var offset = 0\n            while (parentVar != null) {\n                parentVar.let {\n                    if (connectExpression.checkOffset(offset)) {\n                        yield(it)\n                    }\n                    offset++\n                    connectExpression.maxOffset?.let { maxOffset ->\n                        if (offset > maxOffset) {\n                            return@sequence\n                        }\n                    }\n                    parentVar = getParent(it)\n                }\n            }\n        }\n    },\n    val traverseBeforeBrothers: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->\n        val parentVal = getParent(node)\n        if (parentVal != null) {\n            val list = getChildren(parentVal).takeWhile { it != node }.toMutableList()\n            list.reverse()\n            list.asSequence().filterIndexed { i, _ ->\n                connectExpression.checkOffset(\n                    i\n                )\n            }\n        } else {\n            emptySequence()\n        }\n    },\n    val traverseAfterBrothers: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->\n        val parentVal = getParent(node)\n        if (parentVal != null) {\n            getChildren(parentVal).dropWhile { it != node }.drop(1).let {\n                if (connectExpression.maxOffset != null) {\n                    it.take(connectExpression.maxOffset!! + 1)\n                } else {\n                    it\n                }\n            }.filterIndexed { i, _ ->\n                connectExpression.checkOffset(\n                    i\n                )\n            }\n        } else {\n            emptySequence()\n        }\n    },\n    val traverseDescendants: (T, ConnectExpression) -> Sequence<T> = { node, connectExpression ->\n        sequence {\n            val stack = getChildren(node).toMutableList()\n            if (stack.isEmpty()) return@sequence\n            stack.reverse()\n            val tempNodes = mutableListOf<T>()\n            var offset = 0\n            do {\n                val top = stack.removeAt(stack.lastIndex)\n                if (connectExpression.checkOffset(offset)) {\n                    yield(top)\n                }\n                offset++\n                connectExpression.maxOffset?.let { maxOffset ->\n                    if (offset > maxOffset) {\n                        return@sequence\n                    }\n                }\n                for (childNode in getChildren(top)) {\n                    tempNodes.add(childNode)\n                }\n                if (tempNodes.isNotEmpty()) {\n                    for (i in tempNodes.size - 1 downTo 0) {\n                        stack.add(tempNodes[i])\n                    }\n                    tempNodes.clear()\n                }\n            } while (stack.isNotEmpty())\n        }\n    },\n\n    val traverseFastQueryDescendants: (T, List<FastQuery>) -> Sequence<T> = { _, _ -> emptySequence() }\n) {\n\n    @JsExport.Ignore\n    fun querySelectorAll(\n        node: T,\n        selector: Selector,\n        option: MatchOption = MatchOption.default,\n    ): Sequence<T> = sequence {\n        (if (option.fastQuery && selector.fastQueryList.isNotEmpty()) {\n            traverseFastQueryDescendants(node, selector.fastQueryList)\n        } else {\n            getDescendants(node)\n        }).forEach { childNode ->\n            selector.match(childNode, this@Transform, option)?.let { yield(it) }\n        }\n    }\n\n    fun querySelector(\n        node: T,\n        selector: Selector,\n        option: MatchOption = MatchOption.default,\n    ): T? {\n        return querySelectorAll(node, selector, option).firstOrNull()\n    }\n\n    @JsExport.Ignore\n    fun querySelectorAllContext(\n        node: T,\n        selector: Selector,\n        option: MatchOption = MatchOption.default,\n    ): Sequence<QueryResult<T>> {\n        return sequence {\n            getDescendants(node).forEach { childNode ->\n                selector.matchContext(childNode, this@Transform, option).let {\n                    if (it.matched) {\n                        yield(it)\n                    }\n                }\n            }\n        }\n    }\n\n    fun querySelectorContext(\n        node: T,\n        selector: Selector,\n        option: MatchOption = MatchOption.default,\n    ): QueryResult<T>? {\n        return querySelectorAllContext(node, selector, option).firstOrNull()\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun querySelectorAllArray(\n        node: T,\n        selector: Selector,\n        option: MatchOption = MatchOption.default,\n    ): Array<T> {\n        val result = querySelectorAll(node, selector, option).toList()\n        return (result as List<Any>).toTypedArray() as Array<T>\n    }\n\n    fun querySelectorAllContextArray(\n        node: T,\n        selector: Selector,\n        option: MatchOption = MatchOption.default,\n    ): Array<QueryResult<T>> {\n        return querySelectorAllContext(node, selector, option).toList().toTypedArray()\n    }\n\n    companion object {\n        fun <T> multiplatformBuild(\n            getAttr: (Any, String) -> Any?,\n            getInvoke: (Any, String, List<Any>) -> Any?,\n            getName: (T) -> String?,\n            getChildren: (T) -> Array<T>,\n            getParent: (T) -> T?,\n        ): Transform<T> {\n            return Transform(\n                getAttr = getAttr,\n                getInvoke = { target, name, args -> getInvoke(target, name, args) },\n                getName = getName,\n                getChildren = { node -> getChildren(node).asSequence() },\n                getParent = getParent,\n            )\n        }\n    }\n}"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/TypeInfo.kt",
    "content": "package li.songe.selector\n\nimport kotlin.js.JsExport\n\n@JsExport\nsealed class PrimitiveType(val key: String) {\n    data object BooleanType : PrimitiveType(\"boolean\")\n    data object IntType : PrimitiveType(\"int\")\n    data object StringType : PrimitiveType(\"string\")\n    data class ObjectType(val name: String) : PrimitiveType(\"object\")\n}\n\n@JsExport\ndata class MethodInfo(\n    val name: String,\n    val returnType: TypeInfo,\n    val params: List<TypeInfo> = emptyList(),\n) : Stringify {\n    override fun stringify(): String {\n        return \"$name(${params.joinToString(\", \") { it.stringify() }}): ${returnType.stringify()}\"\n    }\n}\n\n@JsExport\ndata class PropInfo(\n    val name: String,\n    val type: TypeInfo,\n)\n\n@JsExport\ndata class TypeInfo(\n    val type: PrimitiveType,\n    var props: List<PropInfo> = emptyList(),\n    var methods: List<MethodInfo> = emptyList(),\n) : Stringify {\n    override fun stringify(): String {\n        return if (type is PrimitiveType.ObjectType) {\n            type.name\n        } else {\n            type.key\n        }\n    }\n}"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/connect/ConnectExpression.kt",
    "content": "package li.songe.selector.connect\n\nimport li.songe.selector.Stringify\nimport kotlin.js.JsExport\n\n@JsExport\nsealed class ConnectExpression : Stringify {\n    abstract val minOffset: Int\n    abstract val maxOffset: Int?\n    abstract fun checkOffset(offset: Int): Boolean\n    abstract fun getOffset(i: Int): Int\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/connect/ConnectOperator.kt",
    "content": "package li.songe.selector.connect\n\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport kotlin.js.JsExport\n\n@JsExport\nsealed class ConnectOperator(val key: String) : Stringify {\n    override fun stringify() = key\n    fun formatOffset(offset: Int): String {\n        val n = offset + 1\n        return if (n == 1) {\n            key\n        } else {\n            key + n\n        }\n    }\n\n    internal abstract fun <T> traversal(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        connectExpression: ConnectExpression\n    ): Sequence<T>\n\n    companion object {\n        // https://stackoverflow.com/questions/47648689\n        val allSubClasses by lazy {\n            listOf(\n                BeforeBrother, AfterBrother, Ancestor, Child, Descendant, Previous\n            ).sortedBy { -it.key.length }\n        }\n    }\n\n    /**\n     * A + B, 1,2,3,A,B,7,8\n     */\n    data object BeforeBrother : ConnectOperator(\"+\") {\n        override fun <T> traversal(\n            context: QueryContext<T>, transform: Transform<T>, connectExpression: ConnectExpression\n        ) = transform.traverseBeforeBrothers(context.current, connectExpression)\n\n    }\n\n    /**\n     * A - B, 1,2,3,B,A,7,8\n     */\n    data object AfterBrother : ConnectOperator(\"-\") {\n        override fun <T> traversal(\n            context: QueryContext<T>, transform: Transform<T>, connectExpression: ConnectExpression\n        ) = transform.traverseAfterBrothers(context.current, connectExpression)\n    }\n\n    /**\n     * A > B, A is the ancestor of B\n     */\n    data object Ancestor : ConnectOperator(\">\") {\n        override fun <T> traversal(\n            context: QueryContext<T>, transform: Transform<T>, connectExpression: ConnectExpression\n        ) = transform.traverseAncestors(context.current, connectExpression)\n\n    }\n\n    /**\n     * A < B, A is the child of B\n     */\n    data object Child : ConnectOperator(\"<\") {\n        override fun <T> traversal(\n            context: QueryContext<T>, transform: Transform<T>, connectExpression: ConnectExpression\n        ) = transform.traverseChildren(context.current, connectExpression)\n    }\n\n    /**\n     * A << B, A is the descendant of B\n     */\n    data object Descendant : ConnectOperator(\"<<\") {\n        override fun <T> traversal(\n            context: QueryContext<T>, transform: Transform<T>, connectExpression: ConnectExpression\n        ) = transform.traverseDescendants(context.current, connectExpression)\n    }\n\n    /**\n     * A -> B + C, A is the context previous node of B, A==C\n     * A ->2 B + C + D, A==D\n     */\n    data object Previous : ConnectOperator(\"->\") {\n        override fun <T> traversal(\n            context: QueryContext<T>, transform: Transform<T>, connectExpression: ConnectExpression\n        ) = sequence {\n            var prev = context.getPrev(connectExpression.minOffset)\n            var offset = connectExpression.minOffset\n            while (prev != null) {\n                if (connectExpression.checkOffset(offset)) {\n                    yield(prev.current)\n                }\n                prev = prev.prev\n                offset++\n                if (connectExpression.maxOffset?.let { offset > it } == true) {\n                    break\n                }\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/connect/ConnectSegment.kt",
    "content": "package li.songe.selector.connect\n\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport kotlin.js.JsExport\n\n@JsExport\ndata class ConnectSegment(\n    val operator: ConnectOperator = ConnectOperator.Ancestor,\n    val connectExpression: ConnectExpression = PolynomialExpression(),\n) : Stringify {\n    override fun stringify(): String {\n        if (isMatchAnyAncestor) {\n            return \"\"\n        }\n        return operator.stringify() + connectExpression.stringify()\n    }\n\n    internal fun <T> traversal(context: QueryContext<T>, transform: Transform<T>): Sequence<T?> {\n        return operator.traversal(context, transform, connectExpression)\n    }\n\n    val isMatchAnyAncestor = operator == ConnectOperator.Ancestor\n            && connectExpression is PolynomialExpression\n            && connectExpression.a == 1 && connectExpression.b == 0\n\n    val isMatchAnyDescendant = operator == ConnectOperator.Descendant\n            && connectExpression is PolynomialExpression\n            && connectExpression.a == 1 && connectExpression.b == 0\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/connect/ConnectWrapper.kt",
    "content": "package li.songe.selector.connect\n\nimport li.songe.selector.MatchOption\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport li.songe.selector.property.PropertyWrapper\nimport kotlin.js.JsExport\n\n@JsExport\ndata class ConnectWrapper(\n    val segment: ConnectSegment,\n    val to: PropertyWrapper,\n) : Stringify {\n    override fun stringify(): String {\n        return (to.stringify() + \"\\u0020\" + segment.stringify()).trim()\n    }\n\n    fun <T> matchContext(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption\n    ): QueryContext<T> {\n        if (isMatchRoot) {\n            // C <<n [parent=null] >n A\n            val root = transform.getRoot(context.current) ?: return context.mismatch()\n            return to.matchContext(context.next(root), transform, option)\n        }\n        if (canFq && option.fastQuery) {\n            // C[name='a'||vid='b'] <<n A\n            transform.traverseFastQueryDescendants(context.current, to.fastQueryList).forEach {\n                val r = to.matchContext(context.next(it), transform, option)\n                if (r.matched) return r\n            }\n        } else {\n            segment.traversal(context, transform).forEach {\n                if (it == null) return@forEach\n                val r = to.matchContext(context.next(it), transform, option)\n                if (r.matched) return r\n            }\n        }\n        return context.mismatch()\n    }\n\n    private val isMatchRoot = segment.isMatchAnyAncestor && to.isMatchRoot\n    val canFq = segment.isMatchAnyDescendant && to.fastQueryList.isNotEmpty()\n}"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/connect/PolynomialExpression.kt",
    "content": "package li.songe.selector.connect\n\nimport kotlin.js.JsExport\n\n/**\n * an+b\n */\n@JsExport\ndata class PolynomialExpression(val a: Int = 0, val b: Int = 1) : ConnectExpression() {\n\n    override fun stringify(): String {\n        if (a > 0 && b > 0) {\n            if (a == 1) {\n                return \"(n+$b)\"\n            }\n            return \"(${a}n+$b)\"\n        }\n        if (a < 0 && b > 0) {\n            if (a == -1) {\n                return \"($b-n)\"\n            }\n            return \"($b${a}n)\"\n        }\n        if (b == 0) {\n            if (a == 1) return \"n\"\n            return if (a > 0) {\n                \"${a}n\"\n            } else {\n                \"(${a}n)\"\n            }\n        }\n        if (a == 0) {\n            if (b == 1) return \"\"\n            return if (b > 0) {\n                b.toString()\n            } else {\n                \"(${b})\"\n            }\n        }\n        val bOp = if (b >= 0) \"+\" else \"\"\n        return \"(${a}n${bOp}${b})\"\n    }\n\n    private fun invalidValue(): Nothing {\n        error(\"invalid Polynomial: a=$a, b=$b\")\n    }\n\n    override val minOffset = (if (a > 0) {\n        if (b > 0) {\n            a + b\n        } else if (b == 0) {\n            a\n        } else {\n            // 2n-10 -> n>=6\n            // 3n-10 -> n>=4\n            // 3n-3 -> n>=2\n            // 3n-1 -> n>=1\n            // an+b>0 -> n>-b/a\n            val minN = -b / a + 1\n            a * minN + b\n        }\n    } else if (a == 0) {\n        if (b > 0) {\n            b\n        } else {\n            invalidValue()\n        }\n    } else {\n        if (b > 0) {\n            if (b <= -a) {\n                invalidValue()\n            } else {\n                // -2n+9 -> (1_7,2_5,3_3,4_1) -> (1,3,5,7) -> 1\n                // -3n+9 -> (1_6,2_3) -> (3,6)\n                // -5n+7 -> (1_2) -> (2)\n                val maxN = -b / a - if (b % a == 0) 1 else 0\n                a * maxN + b\n            }\n        } else {\n            invalidValue()\n        }\n    }) - 1\n\n    override val maxOffset = (if (a > 0) {\n        null\n    } else if (a == 0) {\n        if (b > 0) {\n            b\n        } else {\n            invalidValue()\n        }\n    } else {\n        if (b > 0) {\n            if (b <= -a) {\n                invalidValue()\n            } else {\n                a + b\n            }\n        } else {\n            invalidValue()\n        }\n    })?.let { it - 1 }\n\n    private val isConstant = minOffset == maxOffset\n\n    // (2n-1) -> (1,3,5) -> [0,2,4]\n    override fun checkOffset(offset: Int): Boolean {\n        if (isConstant) {\n            return offset == minOffset\n        }\n        val y = (offset + 1) - b\n        return y % a == 0 && y / a >= 1\n    }\n\n    private val innerGetOffset: (Int) -> Int = if (a > 0) {\n        if (b > 0) {\n            { i -> a * i + b }\n        } else if (b == 0) {\n            { i -> a * i + b }\n        } else {\n            val minN = -b / a + 1\n            { i -> a * (minN + i) + b }\n        }\n    } else if (a == 0) {\n        if (b > 0) {\n            { i ->\n                if (i != 0) {\n                    invalidValue()\n                }\n                b\n            }\n        } else {\n            invalidValue()\n        }\n    } else {\n        if (b > 0) {\n            if (b <= -a) {\n                invalidValue()\n            } else {\n                // -2n+9 -> (1_7,2_5,3_3,4_1) -> (1,3,5,7) -> 1\n                // -3n+9 -> (1_6,2_3) -> (3,6)\n                // -5n+7 -> (1_2) -> (2)\n                val maxN = -b / a - if (b % a == 0) 1 else 0\n                { i -> a * (maxN - i) + b }\n            }\n        } else {\n            invalidValue()\n        }\n    }\n\n    override fun getOffset(i: Int) = innerGetOffset(i) - 1\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/connect/TupleExpression.kt",
    "content": "package li.songe.selector.connect\n\nimport kotlin.js.JsExport\n\n@JsExport\ndata class TupleExpression(\n    val numbers: List<Int>,\n) : ConnectExpression() {\n    override val minOffset = (numbers.firstOrNull() ?: 1) - 1\n    override val maxOffset = numbers.lastOrNull()\n\n    private val indexes = numbers.map { x -> x - 1 }\n    override fun checkOffset(offset: Int): Boolean {\n        return indexes.binarySearch(offset) >= 0\n    }\n\n    override fun getOffset(i: Int): Int {\n        return numbers[i]\n    }\n\n    override fun stringify(): String {\n        if (numbers.size == 1) {\n            return if (numbers.first() == 1) {\n                \"\"\n            } else {\n                numbers.first().toString()\n            }\n        }\n        return \"(${numbers.joinToString(\",\")})\"\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/parser/AstNode.kt",
    "content": "package li.songe.selector.parser\n\nimport li.songe.selector.Stringify\nimport kotlin.js.JsExport\n\n@JsExport\ndata class AstNode<T : Any>(\n    val start: Int,\n    val end: Int,\n    val value: T,\n    val children: List<AstNode<Any>>,\n) : Stringify {\n\n    @Suppress(\"unused\")\n    val outChildren by lazy { children.toTypedArray() }\n\n    val name: String\n        get() = value::class.simpleName.toString()\n\n    override fun stringify(): String {\n        if (children.isEmpty()) {\n            return \"{name:\\\"$name\\\", range:[$start, $end]}\"\n        }\n        return \"{name:\\\"$name\\\", range:[$start, $end], children:${children.map { it.stringify() }}}\"\n    }\n\n    fun sameRange(other: AstNode<Any>): Boolean {\n        return start == other.start && end == other.end\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/parser/AstParser.kt",
    "content": "package li.songe.selector.parser\n\nimport li.songe.selector.Selector\nimport li.songe.selector.property.LogicalExpression\nimport li.songe.selector.property.ValueExpression\nimport li.songe.selector.unit.LogicalSelectorExpression\n\nprivate data class AstContext(\n    val parent: AstContext? = null,\n    val children: MutableList<AstNode<Any>> = mutableListOf()\n)\n\ninternal class AstParser(override val source: String) : SelectorParser(source) {\n    private var tempAstContext = AstContext()\n\n    private fun <T : Any> createAstNode(block: () -> T): T {\n        tempAstContext = AstContext(tempAstContext)\n        val start = i\n        return block().apply {\n            val end = i\n            val value = this\n            tempAstContext.parent?.let {\n                it.children.add(AstNode(start, end, value, tempAstContext.children))\n                tempAstContext = it\n            }\n        }\n    }\n\n    private fun traverse(block: (node: AstNode<*>) -> Unit) {\n        val stack = mutableListOf(tempAstContext.children.single())\n        do {\n            val top = stack.run { removeAt(lastIndex) }\n            block(top)\n            stack.addAll(top.children.asReversed())\n        } while (stack.isNotEmpty())\n    }\n\n    private fun prueAst(): Boolean {\n        var changed = false\n        traverse { node ->\n            val children = node.children as MutableList\n            if (children.size == 1) {\n                val child = children.first()\n                if (child.children.isEmpty() && child.start == node.start && child.end == node.end) {\n                    if (child.value is String || child.value is Boolean || child.value is Int) {\n                        children.clear()\n                        changed = true\n                    }\n                }\n            }\n        }\n        traverse { node ->\n            val children = node.children as MutableList\n            children.forEachIndexed { i, child ->\n                if (child.children.size == 1) {\n                    val deepChild = child.children.first()\n                    val isSameType =\n                        child.value === deepChild.value || child.value::class == deepChild.value::class\n                    if (isSameType && child.sameRange(deepChild)) {\n                        children[i] = deepChild\n                        changed = true\n                    }\n                }\n            }\n        }\n        traverse { node ->\n            val children = node.children as MutableList\n            children.forEachIndexed { i, child ->\n                if (child.children.size == 1) {\n                    val deepChild = child.children.first()\n                    val isSameType =\n                        child.value === deepChild.value || child.value::class == deepChild.value::class\n                    if (isSameType && source[child.start] == '(' && source[child.end - 1] == ')') {\n                        children[i] = AstNode(\n                            start = child.start,\n                            end = child.end,\n                            value = child.value,\n                            children = deepChild.children\n                        )\n                        changed = true\n                    }\n                }\n            }\n        }\n        traverse { node ->\n            val children = node.children as MutableList\n            children.forEachIndexed { i, child ->\n                if (child.children.isNotEmpty() && source[child.start] == '(' && source[child.end - 1] == ')' && child.children.first().start > child.start && child.children.last().end < child.end) {\n                    children[i] = AstNode(\n                        start = child.children.first().start,\n                        end = child.children.last().end,\n                        value = child.value,\n                        children = child.children\n                    )\n                    changed = true\n                }\n            }\n        }\n        return changed\n    }\n\n    fun readAst(): AstNode<Selector> {\n        readSelector()\n        while (true) {\n            if (!prueAst()) {\n                break\n            }\n        }\n        @Suppress(\"UNCHECKED_CAST\")\n        return tempAstContext.children.single() as AstNode<Selector>\n    }\n\n    override fun readLiteral(v: String) = super.readLiteral(v)\n    override fun readSelectorExpression() = super.readSelectorExpression()\n\n    fun mergeCommonLogicalExpression(expression: Any, leftIndex: Int) {\n        val children = tempAstContext.children.subList(leftIndex, leftIndex + 3).toMutableList()\n        tempAstContext.children[leftIndex] = AstNode(\n            start = children.first().start,\n            end = children.last().end,\n            value = expression,\n            children = children\n        )\n        repeat(children.size - 1) {\n            tempAstContext.children.removeAt(leftIndex + 1)\n        }\n    }\n\n    override fun mergeLogicalExpression(expression: LogicalExpression) {\n        val leftIndex = tempAstContext.children.indexOfFirst { it.value === expression.left }\n        mergeCommonLogicalExpression(expression, leftIndex)\n    }\n\n    override fun mergeLogicalSelectorExpression(expression: LogicalSelectorExpression) {\n        val leftIndex = tempAstContext.children.indexOfFirst { it.value === expression.left }\n        mergeCommonLogicalExpression(expression, leftIndex)\n    }\n\n    override fun mergeIdentifier(expression: ValueExpression.Identifier) {\n        val lastNode = tempAstContext.children.last()\n        if (lastNode.value is String) {\n            tempAstContext.children[tempAstContext.children.size - 1] = AstNode(\n                start = lastNode.start,\n                end = lastNode.end,\n                children = lastNode.children,\n                value = expression,\n            )\n        }\n    }\n\n    override fun mergeMemberExpression(expression: ValueExpression.MemberExpression) {\n        val children = tempAstContext.children.subList(\n            tempAstContext.children.size - 2,\n            tempAstContext.children.size\n        ).toMutableList()\n        repeat(children.size) {\n            tempAstContext.children.run { removeAt(lastIndex) }\n        }\n        tempAstContext.children.add(\n            AstNode(\n                start = children.first().start,\n                end = children.last().end,\n                children = children,\n                value = expression,\n            )\n        )\n    }\n\n    override fun mergeCallExpression(expression: ValueExpression.CallExpression) {\n        // [lastToken, (, ...arguments, ',', )]\n        val children = tempAstContext.children.subList(\n            tempAstContext.children.indexOfFirst { it.value === expression.callee },\n            tempAstContext.children.size\n        ).toMutableList()\n        repeat(children.size) {\n            tempAstContext.children.run { removeAt(lastIndex) }\n        }\n        tempAstContext.children.add(\n            AstNode(\n                start = children.first().start,\n                end = i,\n                children = children,\n                value = expression,\n            )\n        )\n    }\n\n    override fun <T : Any> readBracketExpression(block: () -> T) = createAstNode {\n        super.readBracketExpression(block)\n    }\n\n    override fun readVariableName() = createAstNode { super.readVariableName() }\n\n    override fun readBinaryExpression() = createAstNode { super.readBinaryExpression() }\n\n    override fun readSelector() = createAstNode { super.readSelector() }\n\n    override fun readExpression() = createAstNode { super.readExpression() }\n\n    override fun readCompareOperator() = createAstNode { super.readCompareOperator() }\n\n    override fun readInt() = createAstNode { super.readInt() }\n\n    override fun readUInt() = createAstNode { super.readUInt() }\n\n    override fun readLogicalOperator() = createAstNode { super.readLogicalOperator() }\n\n    override fun readNotExpression() = createAstNode { super.readNotExpression() }\n\n    override fun readNotSelectorExpression() = createAstNode { super.readNotSelectorExpression() }\n\n    override fun readPropertyName() = createAstNode { super.readPropertyName() }\n\n    override fun readPropertySegment() = createAstNode { super.readPropertySegment() }\n\n    override fun readPropertyUnit() = createAstNode { super.readPropertyUnit() }\n\n    override fun readSelectorLogicalOperator() = createAstNode {\n        super.readSelectorLogicalOperator()\n    }\n\n    override fun readString() = createAstNode { super.readString() }\n\n    override fun readUnitSelectorExpression() = createAstNode { super.readUnitSelectorExpression() }\n\n    override fun readValueExpression() = createAstNode { super.readValueExpression() }\n\n    override fun readConnectExpression() = createAstNode { super.readConnectExpression() }\n\n    override fun readConnectOperator() = createAstNode { super.readConnectOperator() }\n\n    override fun readConnectSegment() = createAstNode { super.readConnectSegment() }\n\n    override fun readMonomial() = createAstNode { super.readMonomial() }\n\n    override fun readPolynomialExpression() = createAstNode { super.readPolynomialExpression() }\n\n    override fun readTupleExpression() = createAstNode { super.readTupleExpression() }\n\n    override fun readPlainChar(v: Char) {\n        createAstNode {\n            super.readPlainChar(v)\n            v.toString()\n        }\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/parser/BaseParser.kt",
    "content": "package li.songe.selector.parser\n\nimport li.songe.selector.SyntaxException\n\ninternal const val WHITESPACE_CHAR = \"\\u0020\\t\\r\\n\"\ninternal const val POSITIVE_DIGIT_CHAR = \"123456789\"\ninternal const val DIGIT_CHAR = \"0$POSITIVE_DIGIT_CHAR\"\ninternal const val HEX_DIGIT_CHAR = \"abcdefABCDEF$DIGIT_CHAR\"\ninternal const val INT_START_CHAR = \"-$DIGIT_CHAR\"\ninternal const val STRING_START_CHAR = \"`'\\\"\"\ninternal const val VAR_START_CHAR = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_\"\ninternal const val VAR_CONTINUE_CHAR = VAR_START_CHAR + DIGIT_CHAR\ninternal const val VALUE_START_CHAR = \"($INT_START_CHAR$STRING_START_CHAR$VAR_START_CHAR\"\ninternal const val EXP_START_CHAR = \"!($VALUE_START_CHAR\"\ninternal const val CONNECT_EXP_START_CHAR = \"(n$DIGIT_CHAR\"\ninternal const val MONOMIAL_START_CHAR = \"+-n$DIGIT_CHAR\"\ninternal const val CONNECT_START_CHAR = \"+-<>\"\ninternal const val PROPERTY_START_CHAR = \"@[*$VAR_START_CHAR\"\n\ninternal data class Monomial(\n    val coefficient: Int,\n    val power: Int,\n)\n\nprivate fun Char?.escapeString(): String {\n    this ?: return \"EOF\"\n    when (this) {\n        '\\n' -> \"\\\\n\"\n        '\\r' -> \"\\\\r\"\n        '\\t' -> \"\\\\t\"\n        '\\b' -> \"\\\\b\"\n        else -> null\n    }?.let { return it }\n    if (code in 0x0000..0x001F || isWhitespace()) {\n        return \"\\\\u\" + code.toString(16).padStart(4, '0')\n    }\n    return toString()\n}\n\ninternal fun Char?.inStr(v: String): Boolean {\n    return this != null && v.contains(this)\n}\n\ninternal sealed interface BaseParser {\n    val source: CharSequence\n    var i: Int\n    val char: Char?\n        get() = source.getOrNull(i)\n\n    fun readWhiteSpace() {\n        while (char.inStr(WHITESPACE_CHAR)) {\n            i++\n        }\n    }\n\n    fun rollbackWhiteSpace() {\n        while (source[i - 1] in WHITESPACE_CHAR) {\n            i--\n        }\n    }\n\n    fun readUInt(): Int {\n        val start = i\n        expectOneOfChar(DIGIT_CHAR, \"digit\")\n        if (char == '0') {\n            i++\n            if (char?.isDigit() == true) {\n                i = start\n                errorExpect(\"no zero start int\")\n            }\n            return 0\n        } else {\n            i++\n        }\n        while (char?.isDigit() == true) {\n            i++\n        }\n        val value = source.substring(start, i).toIntOrNull()\n        if (value == null) {\n            i = start\n            errorExpect(\"legal range int\")\n        }\n        return value\n    }\n\n    fun readInt(): Int {\n        val start = i\n        if (char == '-') {\n            i++\n        }\n        expectOneOfChar(DIGIT_CHAR, \"digit\")\n        if (char == '0') {\n            i++\n            if (char?.isDigit() == true) {\n                i = start\n                errorExpect(\"no zero start int\")\n            }\n            return 0\n        } else {\n            i++\n        }\n        while (char?.isDigit() == true) {\n            i++\n        }\n        val value = source.substring(start, i).toIntOrNull()\n        if (value == null) {\n            i = start\n            errorExpect(\"legal range int\")\n        }\n        return value\n    }\n\n    fun readString(): String {\n        val quote = expectOneOfChar(STRING_START_CHAR)\n        i++\n        val sb = StringBuilder()\n        while (true) {\n            val c = char ?: errorExpect(quote.toString())\n            // https://www.rfc-editor.org/rfc/inline-errata/rfc7159.html\n            if (c.code in 0x0000..0x001F) {\n                errorExpect(\"no control character\")\n            } else if (c == quote) {\n                i++\n                break\n            } else if (c == '\\\\') {\n                i++\n                val realChar = when (char) {\n                    '\\\\' -> '\\\\'\n                    '\\'' -> '\\''\n                    '\"' -> '\"'\n                    '`' -> '`'\n                    'n' -> '\\n'\n                    'r' -> '\\r'\n                    't' -> '\\t'\n                    'b' -> '\\b'\n                    'x' -> {\n                        repeat(2) {\n                            i++\n                            expectOneOfChar(HEX_DIGIT_CHAR, \"hex digit\")\n                        }\n                        source.substring(i + 1 - 2, i + 1).toInt(16).toChar()\n                    }\n\n                    'u' -> {\n                        repeat(4) {\n                            i++\n                            expectOneOfChar(HEX_DIGIT_CHAR, \"hex digit\")\n                        }\n                        source.substring(i + 1 - 4, i + 1).toInt(16).toChar()\n                    }\n\n                    else -> {\n                        errorExpect(\"escape char\")\n                    }\n                }\n                sb.append(realChar)\n                i++\n            } else {\n                sb.append(c)\n                i++\n            }\n        }\n        return sb.toString()\n    }\n\n    fun readLiteral(v: String): Boolean {\n        if (source.startsWith(v, i) && !source.getOrNull(i + v.length).inStr(VAR_CONTINUE_CHAR)) {\n            i += v.length\n            return true\n        }\n        return false\n    }\n\n    fun readPlainChar(v: Char) {\n        expectChar(v)\n        i++\n    }\n\n    fun <T : Any> readBracketExpression(block: () -> T): T {\n        readPlainChar('(')\n        readWhiteSpace()\n        return block().apply {\n            readWhiteSpace()\n            readPlainChar(')')\n        }\n    }\n}\n\ninternal fun BaseParser.errorExpect(name: String): Nothing {\n    throw SyntaxException(\"Expect $name, got ${char.escapeString()} at index $i\", name, i)\n}\n\ninternal fun BaseParser.expectOneOfChar(v: String, name: String? = null): Char {\n    val c = char\n    if (c == null || !v.contains(c)) {\n        if (name != null) {\n            errorExpect(name)\n        } else {\n            errorExpect(\"one of string $v\")\n        }\n    }\n    return c\n}\n\ninternal fun BaseParser.expectChar(v: Char): Char {\n    if (v != char) {\n        errorExpect(\"char $v\")\n    }\n    return v\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/parser/ConnectParser.kt",
    "content": "package li.songe.selector.parser\n\nimport li.songe.selector.connect.ConnectExpression\nimport li.songe.selector.connect.ConnectOperator\nimport li.songe.selector.connect.ConnectSegment\nimport li.songe.selector.connect.PolynomialExpression\nimport li.songe.selector.connect.TupleExpression\n\ninternal sealed interface ConnectParser : BaseParser {\n\n    private fun isTupleExpression(): Boolean {\n        // ^\\(\\s*\\d+\\s*,\n        val start = i\n        try {\n            if (char == '(') {\n                i++\n                while (char.inStr(WHITESPACE_CHAR)) {\n                    i++\n                }\n                if (char.inStr(DIGIT_CHAR)) {\n                    i++\n                    while (char.inStr(DIGIT_CHAR)) {\n                        i++\n                    }\n                    while (char.inStr(WHITESPACE_CHAR)) {\n                        i++\n                    }\n                    if (char == ',') {\n                        return true\n                    }\n                }\n            }\n            return false\n        } finally {\n            i = start\n        }\n    }\n\n    fun readConnectOperator(): ConnectOperator {\n        val operator = ConnectOperator.allSubClasses.find { v ->\n            source.startsWith(v.key, i)\n        }\n        if (operator == null) {\n            errorExpect(\"connect operator\")\n        }\n        i += operator.key.length\n        return operator\n    }\n\n    // [+-][a][n]\n    fun readMonomial(): Monomial {\n        expectOneOfChar(MONOMIAL_START_CHAR, \"MONOMIAL_START_CHAR\")\n        val signal = when (char) {\n            '+' -> {\n                i++\n                1\n            }\n\n            '-' -> {\n                i++\n                -1\n            }\n\n            else -> 1\n        }\n        readWhiteSpace()\n        expectOneOfChar(\"1234567890n\", \"Monomial\")\n        val coefficient = signal * if (char.inStr(DIGIT_CHAR)) {\n            readUInt()\n        } else {\n            1\n        }\n        val power = if (char == 'n') {\n            i++\n            1\n        } else {\n            0\n        }\n        return Monomial(coefficient = coefficient, power = power)\n    }\n\n    // (1,2,3)\n    fun readTupleExpression(): TupleExpression {\n        readPlainChar('(')\n        readWhiteSpace()\n        val numbers = mutableListOf<Int>()\n        expectOneOfChar(POSITIVE_DIGIT_CHAR, \"POSITIVE_DIGIT_CHAR\")\n        while (char.inStr(POSITIVE_DIGIT_CHAR)) {\n            val t = i\n            val v = readUInt()\n            if (numbers.isNotEmpty()) {\n                if (numbers.last() >= v) {\n                    i = t\n                    errorExpect(\"increasing int\")\n                }\n            }\n            numbers.add(v)\n            readWhiteSpace()\n            if (char == ',') {\n                readPlainChar(',')\n                readWhiteSpace()\n            }\n        }\n        readPlainChar(')')\n        return TupleExpression(numbers)\n    }\n\n    // (+-an+-b)\n    fun readPolynomialExpression(): PolynomialExpression {\n        expectOneOfChar(CONNECT_EXP_START_CHAR, \"CONNECT_EXP_START_CHAR\")\n        val start = i\n        val monomials = mutableListOf<Monomial>()\n        if (char == '(') {\n            readPlainChar('(')\n            readWhiteSpace()\n            while (true) {\n                if (monomials.isNotEmpty()) {\n                    expectOneOfChar(\"+-\", \"+-\")\n                }\n                if (monomials.size >= 2) {\n                    errorExpect(\"only support tow monomial\")\n                }\n                val t = i\n                val v = readMonomial()\n                if (monomials.any { it.power == v.power }) {\n                    i = t\n                    errorExpect(\"duplicated monomial power\")\n                }\n                monomials.add(v)\n                readWhiteSpace()\n                if (!char.inStr(\"+-\")) {\n                    break\n                }\n            }\n            readPlainChar(')')\n        } else {\n            monomials.add(readMonomial())\n        }\n        // an+b\n        try {\n            return PolynomialExpression(\n                a = monomials.find { it.power == 1 }?.coefficient ?: 0,\n                b = monomials.find { it.power == 0 }?.coefficient ?: 0\n            )\n        } catch (_: Throwable) {\n            i = start\n            errorExpect(\"valid an+b polynomial\")\n        }\n    }\n\n    fun readConnectExpression(): ConnectExpression {\n        return if (isTupleExpression()) {\n            readTupleExpression()\n        } else {\n            readPolynomialExpression()\n        }\n    }\n\n    fun readConnectSegment(): ConnectSegment {\n        val operator = readConnectOperator()\n        val connectExpression = if (char.inStr(CONNECT_EXP_START_CHAR)) {\n            readConnectExpression()\n        } else {\n            PolynomialExpression()\n        }\n        return ConnectSegment(\n            operator,\n            connectExpression\n        )\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/parser/PropertyParser.kt",
    "content": "package li.songe.selector.parser\n\nimport li.songe.selector.property.BinaryExpression\nimport li.songe.selector.property.CompareOperator\nimport li.songe.selector.property.Expression\nimport li.songe.selector.property.ExpressionToken\nimport li.songe.selector.property.LogicalExpression\nimport li.songe.selector.property.LogicalOperator\nimport li.songe.selector.property.NotExpression\nimport li.songe.selector.property.PropertySegment\nimport li.songe.selector.property.PropertyUnit\nimport li.songe.selector.property.ValueExpression\nimport li.songe.selector.toMatches\n\ninternal sealed interface PropertyParser : BaseParser {\n\n    fun readLogicalOperator(): LogicalOperator {\n        val operator = LogicalOperator.allSubClasses.find { v ->\n            source.startsWith(v.key, i)\n        }\n        if (operator == null) {\n            errorExpect(\"logical operator\")\n        }\n        i += operator.key.length\n        return operator\n    }\n\n    fun readCompareOperator(): CompareOperator {\n        val operator = CompareOperator.allSubClasses.find { v ->\n            source.startsWith(v.key, i)\n        }\n        if (operator == null) {\n            errorExpect(\"compare operator\")\n        }\n        i += operator.key.length\n        return operator\n    }\n\n    fun readVariableName(): String {\n        val start = i\n        expectOneOfChar(VAR_START_CHAR, \"VAR_START_CHAR\")\n        i++\n        while (char.inStr(VAR_CONTINUE_CHAR)) {\n            i++\n        }\n        val v = source.substring(start, i)\n        if (v == \"null\" || v == \"false\" || v == \"true\") {\n            i = start\n            errorExpect(\"no keyword variable\")\n        }\n        return v\n    }\n\n    fun mergeIdentifier(expression: ValueExpression.Identifier) {}\n\n    fun mergeMemberExpression(expression: ValueExpression.MemberExpression) {}\n\n    fun mergeCallExpression(expression: ValueExpression.CallExpression) {}\n\n    fun readValueExpression(): ValueExpression {\n        expectOneOfChar(VALUE_START_CHAR, \"VAL_START_CHAR\")\n        if (readLiteral(\"null\")) {\n            return ValueExpression.NullLiteral\n        }\n        if (readLiteral(\"false\")) {\n            return ValueExpression.BooleanLiteral(false)\n        }\n        if (readLiteral(\"true\")) {\n            return ValueExpression.BooleanLiteral(true)\n        }\n        if (char.inStr(INT_START_CHAR)) {\n            return ValueExpression.IntLiteral(readInt())\n        }\n        if (char.inStr(STRING_START_CHAR)) {\n            return ValueExpression.StringLiteral(readString())\n        }\n        var lastToken: ValueExpression.Variable? = null\n        while (true) {\n            readWhiteSpace()\n            val c = char ?: break\n            if (c.inStr(VAR_START_CHAR)) {\n                if (lastToken != null) {\n                    errorExpect(\"Variable End\")\n                }\n                lastToken =\n                    ValueExpression.Identifier(readVariableName()).apply { mergeIdentifier(this) }\n            } else if (c == '.') {\n                if (lastToken !is ValueExpression.Variable) {\n                    errorExpect(\"Variable End\")\n                }\n                readPlainChar('.')\n                readWhiteSpace()\n                lastToken = ValueExpression.MemberExpression(\n                    lastToken,\n                    readVariableName()\n                ).apply { mergeMemberExpression(this) }\n            } else if (c == '(') {\n                readPlainChar('(')\n                readWhiteSpace()\n                if (lastToken != null) {\n                    // 暂不支持 object()()\n                    if (lastToken is ValueExpression.CallExpression) {\n                        errorExpect(\"Variable\")\n                    }\n                    val arguments = mutableListOf<ValueExpression>()\n                    while (char.inStr(VALUE_START_CHAR)) {\n                        arguments.add(readValueExpression())\n                        if (char == ',') {\n                            readPlainChar(',')\n                            readWhiteSpace()\n                        }\n                    }\n                    readWhiteSpace()\n                    readPlainChar(')')\n                    lastToken = ValueExpression.CallExpression(\n                        lastToken,\n                        arguments\n                    ).apply { mergeCallExpression(this) }\n                } else {\n                    return readValueExpression().apply {\n                        readWhiteSpace()\n                        readPlainChar(')')\n                    }\n                }\n            } else {\n                break\n            }\n        }\n        if (lastToken == null) {\n            errorExpect(\"Variable\")\n        }\n        rollbackWhiteSpace()\n        return lastToken\n    }\n\n    fun readBinaryExpression(): BinaryExpression {\n        val leftValue = readValueExpression()\n        readWhiteSpace()\n        val operator = readCompareOperator()\n        readWhiteSpace()\n        val regexIndex = i\n        val rightValue = readValueExpression().let {\n            if (it is ValueExpression.StringLiteral && (operator == CompareOperator.Matches || operator == CompareOperator.NotMatches)) {\n                val matches = try {\n                    it.value.toMatches()\n                } catch (_: Throwable) {// support kotlin js\n                    i = regexIndex + 1\n                    errorExpect(\"valid regex string\")\n                }\n                it.copy(matches = matches)\n            } else {\n                it\n            }\n        }\n        return BinaryExpression(\n            leftValue,\n            operator,\n            rightValue\n        )\n    }\n\n    fun readNotExpression(): NotExpression {\n        readPlainChar('!')\n        return NotExpression(readBracketExpression {\n            readExpression()\n        })\n    }\n\n    fun mergeLogicalExpression(expression: LogicalExpression) {}\n\n    // a>1 && a>1 || a>1\n    // (a>1 || a>1) && a>1\n    fun readExpression(): Expression {\n        // LogicalOperator/Expression\n        expectOneOfChar(EXP_START_CHAR, \"EXP_START_CHAR\")\n        val tokens = mutableListOf<ExpressionToken>()\n        while (true) {\n            if (tokens.lastOrNull() is LogicalOperator) {\n                expectOneOfChar(EXP_START_CHAR, \"EXP_START_CHAR\")\n            }\n            val c = char\n            if (c == null) {\n                if (tokens.isEmpty()) {\n                    errorExpect(\"EXP_START_CHAR\")\n                } else {\n                    break\n                }\n            }\n            val token: ExpressionToken = if (tokens.lastOrNull() is Expression) {\n                if (c in \"&|\") {\n                    readLogicalOperator()\n                } else {\n                    break\n                }\n            } else if (c == '(') {\n                readBracketExpression {\n                    readExpression()\n                }\n            } else if (c == '!') {\n                readNotExpression()\n            } else {\n                readBinaryExpression()\n            }\n            tokens.add(token)\n            readWhiteSpace()\n        }\n        rollbackWhiteSpace()\n        if (tokens.isEmpty()) {\n            errorExpect(\"EXP_START_CHAR\")\n        }\n        if (tokens.size == 1) {\n            return tokens.first() as Expression\n        }\n        var index = 0\n        while (index < tokens.size) {\n            val token = tokens[index]\n            if (token === LogicalOperator.AndOperator) {\n                // && > ||\n                // a && b || c -> (a && b) || c\n                // 0 1  2 3  4 -> 0  1  2\n                val left = tokens[index - 1] as Expression\n                val right = tokens[index + 1] as Expression\n                tokens[index] = LogicalExpression(\n                    left = left,\n                    operator = token,\n                    right = right\n                ).apply { mergeLogicalExpression(this) }\n                tokens.removeAt(index - 1)\n                tokens.removeAt(index + 1 - 1)\n            } else {\n                index++\n            }\n        }\n        while (tokens.size > 1) {\n            // a || b || c -> (a || b) || c -> (ab || c)\n            val left = tokens[0] as Expression\n            val operator = tokens[1] as LogicalOperator\n            val right = tokens[2] as Expression\n            tokens[1] = LogicalExpression(\n                left = left,\n                operator = operator,\n                right = right\n            ).apply { mergeLogicalExpression(this) }\n            tokens.removeAt(0)\n            tokens.removeAt(2 - 1)\n        }\n        return tokens.first() as Expression\n    }\n\n    fun readPropertyName(): String {\n        val start = i\n        if (char == '*') {\n            i++\n            return \"*\"\n        }\n        expectOneOfChar(VAR_START_CHAR, \"VAR_START_CHAR\")\n        i++\n        while (true) {\n            val c = char ?: break\n            when (c) {\n                '.' -> {\n                    i++\n                    expectOneOfChar(VAR_START_CHAR, \"VAR_START_CHAR\")\n                    i++\n                }\n\n                in VAR_CONTINUE_CHAR -> {\n                    i++\n                }\n\n                else -> break\n            }\n        }\n        return source.substring(start, i)\n\n    }\n\n    // [a=b||c=d]\n    fun readPropertyUnit(): PropertyUnit {\n        readPlainChar('[')\n        readWhiteSpace()\n        return PropertyUnit(readExpression()).apply {\n            readWhiteSpace()\n            readPlainChar(']')\n        }\n    }\n\n    // @a[a=b||c=d]\n    fun readPropertySegment(): PropertySegment {\n        val at = char == '@'\n        if (at) {\n            readPlainChar('@')\n        }\n        val name = if (char == '[') {\n            \"\"\n        } else {\n            readPropertyName()\n        }\n        if (name.isEmpty()) {\n            expectChar('[')\n        }\n        val expressions = mutableListOf<PropertyUnit>()\n        while (char == '[') {\n            expressions.add(readPropertyUnit())\n        }\n        return PropertySegment(\n            at,\n            name,\n            expressions\n        )\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/parser/SelectorParser.kt",
    "content": "package li.songe.selector.parser\n\nimport li.songe.selector.Selector\nimport li.songe.selector.connect.ConnectSegment\nimport li.songe.selector.connect.ConnectWrapper\nimport li.songe.selector.connect.PolynomialExpression\nimport li.songe.selector.property.PropertySegment\nimport li.songe.selector.property.PropertyWrapper\nimport li.songe.selector.unit.LogicalSelectorExpression\nimport li.songe.selector.unit.NotSelectorExpression\nimport li.songe.selector.unit.SelectorExpression\nimport li.songe.selector.unit.SelectorExpressionToken\nimport li.songe.selector.unit.SelectorLogicalOperator\nimport li.songe.selector.unit.UnitSelectorExpression\n\ninternal open class SelectorParser(\n    override val source: CharSequence,\n) : PropertyParser, ConnectParser {\n    override var i = 0\n\n    open fun readUnitSelectorExpression(): UnitSelectorExpression {\n        val top = readPropertySegment()\n        val pairs = mutableListOf<Pair<ConnectSegment, PropertySegment>>()\n        while (char.inStr(WHITESPACE_CHAR)) {\n            readWhiteSpace()\n            if (char.inStr(CONNECT_START_CHAR)) {\n                // A > B\n                val connectSegment = readConnectSegment()\n                expectOneOfChar(WHITESPACE_CHAR, \"WHITESPACE\")\n                readWhiteSpace()\n                val propertySegment = readPropertySegment()\n                pairs.add(connectSegment to propertySegment)\n            } else if (char.inStr(PROPERTY_START_CHAR)) {\n                // A B\n                val connectSegment = ConnectSegment(connectExpression = PolynomialExpression(1, 0))\n                val propertySegment = readPropertySegment()\n                pairs.add(connectSegment to propertySegment)\n            } else {\n                break\n            }\n        }\n        var topWrapper = PropertyWrapper(top)\n        pairs.forEach { (connectSegment, propertySegment) ->\n            topWrapper = PropertyWrapper(\n                propertySegment,\n                ConnectWrapper(connectSegment, topWrapper)\n            )\n        }\n        return UnitSelectorExpression(topWrapper)\n    }\n\n    // !(A > B)\n    open fun readNotSelectorExpression(): NotSelectorExpression {\n        readPlainChar('!')\n        return NotSelectorExpression(readBracketExpression {\n            readSelectorExpression()\n        })\n    }\n\n    // (A + B) || (A - B)\n    // (A + B) && (A - B)\n    open fun readSelectorLogicalOperator(): SelectorLogicalOperator {\n        val operator = SelectorLogicalOperator.allSubClasses.find { v ->\n            source.startsWith(v.key, i)\n        }\n        if (operator == null) {\n            errorExpect(\"selector logical operator\")\n        }\n        i += operator.key.length\n        return operator\n    }\n\n    open fun mergeLogicalSelectorExpression(expression: LogicalSelectorExpression) {}\n\n    // A + B\n    open fun readSelectorExpression(): SelectorExpression {\n        val tokens = mutableListOf<SelectorExpressionToken>()\n        while (true) {\n            if (tokens.lastOrNull() is SelectorLogicalOperator) {\n                expectOneOfChar(\"!(\", \"selector\")\n            }\n            val c = char\n            if (c == null) {\n                if (tokens.isEmpty()) {\n                    errorExpect(\"selector\")\n                } else {\n                    break\n                }\n            }\n            val token: SelectorExpressionToken = if (tokens.lastOrNull() is SelectorExpression) {\n                if (c in \"&|\") {\n                    readSelectorLogicalOperator()\n                } else {\n                    break\n                }\n            } else if (c == '(') {\n                readBracketExpression {\n                    readSelectorExpression()\n                }\n            } else if (c == '!') {\n                readNotSelectorExpression()\n            } else if (c.inStr(PROPERTY_START_CHAR)) {\n                readUnitSelectorExpression()\n            } else {\n                break\n            }\n            tokens.add(token)\n            readWhiteSpace()\n        }\n        rollbackWhiteSpace()\n        if (tokens.isEmpty()) {\n            errorExpect(\"selector\")\n        }\n        if (tokens.size == 1) {\n            return tokens.first() as SelectorExpression\n        }\n        var index = 0\n        while (index < tokens.size) {\n            val token = tokens[index]\n            if (token === SelectorLogicalOperator.AndOperator) {\n                val left = tokens[index - 1] as SelectorExpression\n                val right = tokens[index + 1] as SelectorExpression\n                tokens[index] = LogicalSelectorExpression(\n                    left = left,\n                    operator = token,\n                    right = right\n                ).apply { mergeLogicalSelectorExpression(this) }\n                tokens.removeAt(index - 1)\n                tokens.removeAt(index + 1 - 1)\n            } else {\n                index++\n            }\n        }\n        while (tokens.size > 1) {\n            val left = tokens[0] as SelectorExpression\n            val operator = tokens[1] as SelectorLogicalOperator\n            val right = tokens[2] as SelectorExpression\n            tokens[1] = LogicalSelectorExpression(\n                left = left,\n                operator = operator,\n                right = right\n            ).apply { mergeLogicalSelectorExpression(this) }\n            tokens.removeAt(0)\n            tokens.removeAt(2 - 1)\n        }\n        return tokens.first() as SelectorExpression\n    }\n\n    open fun readSelector(): Selector {\n        readWhiteSpace()\n        return Selector(\n            expression = readSelectorExpression()\n        ).apply {\n            readWhiteSpace()\n            if (char != null) {\n                errorExpect(\"EOF\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/property/BinaryExpression.kt",
    "content": "package li.songe.selector.property\n\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Transform\nimport kotlin.js.JsExport\n\n@JsExport\ndata class BinaryExpression(\n    val left: ValueExpression,\n    val operator: CompareOperator,\n    val right: ValueExpression,\n) : Expression() {\n    override fun <T> match(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n    ): Boolean {\n        return operator.compare(context, transform, left, right)\n    }\n\n    override fun getBinaryExpressionList() = arrayOf(this)\n\n    override fun stringify() = \"${left.stringify()}${operator.stringify()}${right.stringify()}\"\n\n    val properties: Array<String>\n        get() = arrayOf(*left.properties, *right.properties)\n\n    val methods: Array<String>\n        get() = arrayOf(*left.methods, *right.methods)\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/property/CompareOperator.kt",
    "content": "package li.songe.selector.property\n\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport li.songe.selector.comparePrimitiveValue\nimport kotlin.js.JsExport\n\n\n@JsExport\nsealed class CompareOperator(val key: String) : Stringify {\n    override fun stringify() = key\n\n    abstract fun <T> compare(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        leftExp: ValueExpression,\n        rightExp: ValueExpression,\n    ): Boolean\n\n    internal abstract fun allowType(left: ValueExpression, right: ValueExpression): Boolean\n\n    companion object {\n        // https://stackoverflow.com/questions/47648689\n        val allSubClasses by lazy {\n            listOf(\n                Equal,\n                NotEqual,\n                Start,\n                NotStart,\n                Include,\n                NotInclude,\n                End,\n                NotEnd,\n                Less,\n                LessEqual,\n                More,\n                MoreEqual,\n                Matches,\n                NotMatches\n            ).sortedBy { -it.key.length }.toTypedArray()\n        }\n\n    }\n\n    data object Equal : CompareOperator(\"=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n        ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return comparePrimitiveValue(left, right)\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression) = true\n\n    }\n\n    data object NotEqual : CompareOperator(\"!=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n        ): Boolean {\n            return !Equal.compare(context, transform, leftExp, rightExp)\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression) = true\n    }\n\n    data object Start : CompareOperator(\"^=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return if (left is CharSequence && right is CharSequence) {\n                left.startsWith(right)\n            } else {\n                false\n            }\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {\n            return (left is ValueExpression.StringLiteral || left is ValueExpression.Variable) && (right is ValueExpression.StringLiteral || right is ValueExpression.Variable)\n        }\n    }\n\n    data object NotStart : CompareOperator(\"!^=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return if (left is CharSequence && right is CharSequence) {\n                !left.startsWith(right)\n            } else {\n                false\n            }\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression) =\n            Start.allowType(left, right)\n    }\n\n    data object Include : CompareOperator(\"*=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return if (left is CharSequence && right is CharSequence) {\n                left.contains(right)\n            } else {\n                false\n            }\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression) =\n            Start.allowType(left, right)\n    }\n\n    data object NotInclude : CompareOperator(\"!*=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return if (left is CharSequence && right is CharSequence) {\n                !left.contains(right)\n            } else {\n                false\n            }\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression) =\n            Start.allowType(left, right)\n    }\n\n    data object End : CompareOperator(\"$=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return if (left is CharSequence && right is CharSequence) {\n                left.endsWith(right)\n            } else {\n                false\n            }\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression) =\n            Start.allowType(left, right)\n    }\n\n    data object NotEnd : CompareOperator(\"!$=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return if (left is CharSequence && right is CharSequence) {\n                !left.endsWith(\n                    right\n                )\n            } else {\n                false\n            }\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression) =\n            Start.allowType(left, right)\n    }\n\n    data object Less : CompareOperator(\"<\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return if (left is Int && right is Int) left < right else false\n        }\n\n\n        override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {\n            return (left is ValueExpression.Variable || left is ValueExpression.IntLiteral) && (right is ValueExpression.IntLiteral || right is ValueExpression.Variable)\n        }\n    }\n\n    data object LessEqual : CompareOperator(\"<=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return if (left is Int && right is Int) left <= right else false\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression) =\n            Less.allowType(left, right)\n    }\n\n    data object More : CompareOperator(\">\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return if (left is Int && right is Int) left > right else false\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression) =\n            Less.allowType(left, right)\n    }\n\n    data object MoreEqual : CompareOperator(\">=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            val right = rightExp.getAttr(context, transform)\n            return if (left is Int && right is Int) left >= right else false\n        }\n\n\n        override fun allowType(left: ValueExpression, right: ValueExpression) =\n            Less.allowType(left, right)\n    }\n\n    data object Matches : CompareOperator(\"~=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            return if (left is CharSequence && rightExp is ValueExpression.StringLiteral) {\n                rightExp.outMatches(left)\n            } else {\n                false\n            }\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {\n            return (left is ValueExpression.Variable) && (right is ValueExpression.StringLiteral && right.matches != null)\n        }\n    }\n\n    data object NotMatches : CompareOperator(\"!~=\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            leftExp: ValueExpression,\n            rightExp: ValueExpression,\n\n            ): Boolean {\n            val left = leftExp.getAttr(context, transform)\n            return if (left is CharSequence && rightExp is ValueExpression.StringLiteral) {\n                !rightExp.outMatches(left)\n            } else {\n                false\n            }\n        }\n\n        override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {\n            return Matches.allowType(left, right)\n        }\n    }\n\n}"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/property/Expression.kt",
    "content": "package li.songe.selector.property\n\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport kotlin.js.JsExport\n\n// for parser string token merge\ninternal sealed interface ExpressionToken\n\n@JsExport\nsealed class Expression : Stringify, ExpressionToken {\n    abstract fun <T> match(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n    ): Boolean\n\n    abstract fun getBinaryExpressionList(): Array<BinaryExpression>\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/property/LogicalExpression.kt",
    "content": "package li.songe.selector.property\n\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Transform\nimport kotlin.js.JsExport\n\n@JsExport\ndata class LogicalExpression(\n    val left: Expression,\n    val operator: LogicalOperator,\n    val right: Expression,\n) : Expression() {\n    override fun <T> match(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n    ): Boolean {\n        return operator.compare(context, transform, left, right)\n    }\n\n    override fun getBinaryExpressionList() =\n        left.getBinaryExpressionList() + right.getBinaryExpressionList()\n\n    override fun stringify(): String {\n        val leftStr = if (left is LogicalExpression && left.operator != operator) {\n            \"(${left.stringify()})\"\n        } else {\n            left.stringify()\n        }\n        val rightStr = if (right is LogicalExpression && right.operator != operator) {\n            \"(${right.stringify()})\"\n        } else {\n            right.stringify()\n        }\n        return \"$leftStr\\u0020${operator.stringify()}\\u0020$rightStr\"\n    }\n\n    fun getSameExpressionArray(): Array<BinaryExpression>? {\n        if (left is LogicalExpression && left.operator != operator) {\n            return null\n        }\n        if (right is LogicalExpression && right.operator != operator) {\n            return null\n        }\n        return when (left) {\n            is BinaryExpression -> when (right) {\n                is BinaryExpression -> arrayOf(left, right)\n                is LogicalExpression -> {\n                    arrayOf(left) + (right.getSameExpressionArray() ?: return null)\n                }\n\n                is NotExpression -> null\n            }\n\n            is LogicalExpression -> {\n                val leftArray = left.getSameExpressionArray() ?: return null\n                when (right) {\n                    is BinaryExpression -> leftArray + right\n                    is LogicalExpression -> {\n                        return leftArray + (right.getSameExpressionArray() ?: return null)\n                    }\n\n                    is NotExpression -> null\n                }\n            }\n\n            is NotExpression -> null\n        }\n    }\n}"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/property/LogicalOperator.kt",
    "content": "package li.songe.selector.property\n\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport kotlin.js.JsExport\n\n@JsExport\nsealed class LogicalOperator(val key: String) : Stringify, ExpressionToken{\n    override fun stringify() = key\n\n    companion object {\n        // https://stackoverflow.com/questions/47648689\n        val allSubClasses by lazy {\n            listOf(\n                AndOperator, OrOperator\n            ).sortedBy { -it.key.length }\n        }\n    }\n\n    abstract fun <T> compare(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        left: Expression,\n        right: Expression,\n    ): Boolean\n\n    data object AndOperator : LogicalOperator(\"&&\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            left: Expression,\n            right: Expression,\n        ): Boolean {\n            return left.match(context, transform) && right.match(\n                context,\n                transform\n            )\n        }\n    }\n\n    data object OrOperator : LogicalOperator(\"||\") {\n        override fun <T> compare(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            left: Expression,\n            right: Expression,\n        ): Boolean {\n            return left.match(context, transform) || right.match(\n                context,\n                transform\n            )\n        }\n    }\n}\n\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/property/NotExpression.kt",
    "content": "package li.songe.selector.property\n\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Transform\nimport kotlin.js.JsExport\n\n@JsExport\ndata class NotExpression(\n    val expression: Expression\n) : Expression() {\n\n    override fun <T> match(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n    ): Boolean {\n        return !expression.match(context, transform)\n    }\n\n    override fun getBinaryExpressionList() = expression.getBinaryExpressionList()\n\n    override fun stringify(): String {\n        return \"!(${expression.stringify()})\"\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/property/PropertySegment.kt",
    "content": "package li.songe.selector.property\n\nimport li.songe.selector.FastQuery\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport kotlin.js.JsExport\n\n@JsExport\ndata class PropertySegment(\n    val at: Boolean,\n    val name: String,\n    val units: List<PropertyUnit>,\n) : Stringify {\n    private val matchAnyName = name.isEmpty() || name == \"*\"\n\n    fun getBinaryExpressionList() = units.flatMap {\n        it.expression.getBinaryExpressionList().toList()\n    }.toTypedArray()\n\n    override fun stringify(): String {\n        val matchTag = if (at) \"@\" else \"\"\n        return matchTag + name + units.joinToString(\"\") { it.stringify() }\n    }\n\n    private fun <T> matchName(node: T, transform: Transform<T>): Boolean {\n        if (matchAnyName) return true\n        val str = transform.getName(node) ?: return false\n        if (str.length == name.length) {\n            return str.contentEquals(name)\n        } else if (str.length > name.length) {\n            return str[str.length - name.length - 1] == '.' && str.endsWith(name)\n        }\n        return false\n    }\n\n    fun <T> match(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n    ): Boolean {\n        return matchName(context.current, transform) && units.all { ex ->\n            ex.expression.match(\n                context,\n                transform\n            )\n        }\n    }\n\n    val fastQueryList: List<FastQuery>?\n        get() {\n            val exp = units.firstOrNull()?.expression ?: return null\n            if (exp is LogicalExpression) {\n                if (exp.operator != LogicalOperator.OrOperator) return null\n                val expArray = exp.getSameExpressionArray() ?: return null\n                val list = mutableListOf<FastQuery>()\n                expArray.forEach { e ->\n                    val fq = expToFastQuery(e) ?: return null\n                    list.add(fq)\n                }\n                return list\n            }\n            if (exp is BinaryExpression) {\n                val fq = expToFastQuery(exp) ?: return null\n                return List(1) { fq }\n            }\n            return null\n        }\n\n}\n\nprivate fun expToFastQuery(e: BinaryExpression): FastQuery? {\n    if (e.left !is ValueExpression.Identifier) return null\n    if (e.right !is ValueExpression.StringLiteral) return null\n    if (e.right.value.isEmpty()) return null\n    if (e.left.value == \"id\" && e.operator == CompareOperator.Equal) {\n        return FastQuery.Id(e.right.value)\n    } else if (e.left.value == \"vid\" && e.operator == CompareOperator.Equal) {\n        return FastQuery.Vid(e.right.value)\n    } else if (e.left.value == \"text\" && (e.operator == CompareOperator.Equal || e.operator == CompareOperator.Start || e.operator == CompareOperator.Include || e.operator == CompareOperator.End)) {\n        return FastQuery.Text(e.right.value, e.operator)\n    }\n    return null\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/property/PropertyUnit.kt",
    "content": "package li.songe.selector.property\n\nimport li.songe.selector.Stringify\nimport kotlin.js.JsExport\n\n@JsExport\ndata class PropertyUnit(\n    val expression: Expression\n) : Stringify {\n    override fun stringify(): String {\n        return \"[${expression.stringify()}]\"\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/property/PropertyWrapper.kt",
    "content": "package li.songe.selector.property\n\nimport li.songe.selector.MatchOption\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport li.songe.selector.connect.ConnectWrapper\nimport kotlin.js.JsExport\n\n@JsExport\ndata class PropertyWrapper(\n    val segment: PropertySegment,\n    val to: ConnectWrapper? = null,\n) : Stringify {\n    override fun stringify(): String {\n        return (if (to != null) {\n            to.stringify() + \"\\u0020\"\n        } else {\n            \"\"\n        }) + segment.stringify()\n    }\n\n    fun <T> matchContext(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption,\n    ): QueryContext<T> {\n        if (!segment.match(context, transform)) {\n            return context.mismatch()\n        }\n        if (to == null) {\n            return context\n        }\n        return to.matchContext(context, transform, option)\n    }\n\n    val isMatchRoot = segment.units.any {\n        val e = it.expression\n        e is BinaryExpression && e.operator == CompareOperator.Equal && when {\n            // null == Identifier(name=\"parent\")\n            e.right.value == null && e.left.value == \"parent\" -> true\n            e.left.value == null && e.right.value == \"parent\" -> true\n            else -> false\n        }\n    }\n\n    val fastQueryList by lazy { segment.fastQueryList ?: emptyList() }\n\n    val length: Int\n        get() = if (to == null) 1 else to.to.length + 1\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/property/ValueExpression.kt",
    "content": "package li.songe.selector.property\n\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport li.songe.selector.comparePrimitiveValue\nimport li.songe.selector.escapeString\nimport li.songe.selector.optimizeMatchString\nimport li.songe.selector.whenNull\nimport kotlin.js.JsExport\n\n@JsExport\nsealed class ValueExpression(open val value: Any?, open val type: String) : Stringify {\n    override fun stringify() = value.toString()\n    internal abstract fun <T> getAttr(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n    ): Any?\n\n    abstract val properties: Array<String>\n    abstract val methods: Array<String>\n\n    sealed class Variable(\n        override val value: String,\n    ) : ValueExpression(value, \"var\")\n\n    data class Identifier(\n        val name: String,\n    ) : Variable(name) {\n        override fun <T> getAttr(context: QueryContext<T>, transform: Transform<T>): Any? {\n            return transform.getAttr(context, value)\n        }\n\n        override val properties: Array<String>\n            get() = arrayOf(value)\n        override val methods: Array<String>\n            get() = emptyArray()\n\n        val isEqual = name == \"equal\"\n        val isNotEqual = name == \"notEqual\"\n    }\n\n    data class MemberExpression(\n        val object0: Variable,\n        val property: String,\n    ) : Variable(value = \"${object0.stringify()}.$property\") {\n        override fun <T> getAttr(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n        ): Any? {\n            return transform.getAttr(\n                object0.getAttr(context, transform).whenNull { return null },\n                property\n            )\n        }\n\n        override val properties: Array<String>\n            get() = arrayOf(*object0.properties, property)\n        override val methods: Array<String>\n            get() = object0.methods\n\n        val isPropertyOr = property == \"or\"\n        val isPropertyAnd = property == \"and\"\n        val isPropertyIfElse = property == \"ifElse\"\n    }\n\n    data class CallExpression(\n        val callee: Variable,\n        val arguments: List<ValueExpression>,\n    ) : Variable(\n        value = \"${callee.stringify()}(${arguments.joinToString(\",\") { it.stringify() }})\",\n    ) {\n\n        override fun <T> getAttr(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n        ): Any? {\n            return when (callee) {\n                is CallExpression -> {\n                    // not support\n                    null\n                }\n\n                is Identifier -> {\n                    when {\n                        callee.isEqual -> {\n                            comparePrimitiveValue(\n                                arguments[0].getAttr(context, transform),\n                                arguments[1].getAttr(context, transform)\n                            )\n                        }\n\n                        callee.isNotEqual -> {\n                            !comparePrimitiveValue(\n                                arguments[0].getAttr(context, transform),\n                                arguments[1].getAttr(context, transform)\n                            )\n                        }\n\n                        else -> {\n                            transform.getInvoke(\n                                context,\n                                callee.name,\n                                arguments.map {\n                                    it.getAttr(context, transform).whenNull { return null }\n                                }\n                            )\n                        }\n                    }\n                }\n\n                is MemberExpression -> {\n                    val objectValue =\n                        callee.object0.getAttr(context, transform).whenNull { return null }\n                    when {\n                        callee.isPropertyOr -> {\n                            (objectValue as Boolean) ||\n                                    (arguments[0].getAttr(context, transform)\n                                        .whenNull { return null } as Boolean)\n                        }\n\n                        callee.isPropertyAnd -> {\n                            (objectValue as Boolean) &&\n                                    (arguments[0].getAttr(context, transform)\n                                        .whenNull { return null } as Boolean)\n                        }\n\n                        callee.isPropertyIfElse -> {\n                            if (objectValue as Boolean) {\n                                arguments[0].getAttr(context, transform)\n                            } else {\n                                arguments[1].getAttr(context, transform)\n                            }\n                        }\n\n                        else -> transform.getInvoke(\n                            objectValue,\n                            callee.property,\n                            arguments.map {\n                                it.getAttr(context, transform).whenNull { return null }\n                            }\n                        )\n                    }\n\n                }\n            }\n        }\n\n        override val properties: Array<String>\n            get() = callee.properties.toMutableList()\n                .plus(arguments.flatMap { it.properties.toList() })\n                .toTypedArray()\n        override val methods: Array<String>\n            get() = when (callee) {\n                is CallExpression -> callee.methods\n                is Identifier -> arrayOf(callee.name)\n                is MemberExpression -> arrayOf(*callee.object0.methods, callee.property)\n            }.toMutableList().plus(arguments.flatMap { it.methods.toList() })\n                .toTypedArray()\n    }\n\n    sealed class LiteralExpression(\n        override val value: Any?,\n        override val type: String,\n    ) : ValueExpression(value, type) {\n        override fun <T> getAttr(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n        ) = value\n\n        override val properties: Array<String>\n            get() = emptyArray()\n        override val methods: Array<String>\n            get() = emptyArray()\n    }\n\n    data object NullLiteral : LiteralExpression(null, \"null\")\n\n    data class BooleanLiteral(override val value: Boolean) : LiteralExpression(value, \"boolean\")\n\n    data class IntLiteral(override val value: Int) : LiteralExpression(value, \"int\")\n\n    @ConsistentCopyVisibility\n    data class StringLiteral internal constructor(\n        override val value: String,\n        internal val matches: ((CharSequence) -> Boolean)? = null\n    ) : LiteralExpression(value, \"string\") {\n\n        override fun stringify() = escapeString(value)\n\n        internal val outMatches = matches?.let { optimizeMatchString(value) ?: it } ?: { false }\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/toMatches.kt",
    "content": "package li.songe.selector\n\nimport kotlin.js.JsExport\n\n\nexpect fun String.toMatches(): (input: CharSequence) -> Boolean\n\nexpect fun setWasmToMatches(wasmToMatches: (String) -> (String) -> Boolean)\n\n@JsExport\nfun updateWasmToMatches(toMatches: (String) -> (String) -> Boolean) {\n    setWasmToMatches(toMatches)\n}"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/unit/LogicalSelectorExpression.kt",
    "content": "package li.songe.selector.unit\n\nimport li.songe.selector.FastQuery\nimport li.songe.selector.MatchOption\nimport li.songe.selector.QueryContext\nimport li.songe.selector.Transform\nimport li.songe.selector.TypeInfo\nimport li.songe.selector.connect.ConnectSegment\nimport li.songe.selector.property.BinaryExpression\nimport kotlin.js.JsExport\n\n@JsExport\ndata class LogicalSelectorExpression(\n    val left: SelectorExpression,\n    val operator: SelectorLogicalOperator,\n    val right: SelectorExpression,\n) : SelectorExpression() {\n    override fun stringify(): String {\n        val leftStr = if (\n            left is UnitSelectorExpression || (left is LogicalSelectorExpression && left.operator != operator)\n        ) {\n            \"(${left.stringify()})\"\n        } else {\n            left.stringify()\n        }\n        val rightStr = if (\n            right is UnitSelectorExpression || (right is LogicalSelectorExpression && right.operator != operator)\n        ) {\n            \"(${right.stringify()})\"\n        } else {\n            right.stringify()\n        }\n        return \"$leftStr ${operator.stringify()} $rightStr\"\n    }\n\n    override fun <T> match(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption\n    ): T? {\n        return operator.match(context, transform, option, this)\n    }\n\n    override fun <T> matchContext(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption\n    ) = operator.matchContext(context, transform, option, this)\n\n    override fun isSlow(\n        matchOption: MatchOption\n    ) = left.isSlow(matchOption) || right.isSlow(matchOption)\n\n    override fun checkType(typeInfo: TypeInfo) {\n        left.checkType(typeInfo)\n        right.checkType(typeInfo)\n    }\n\n    override val isMatchRoot: Boolean\n        get() = when (operator) {\n            SelectorLogicalOperator.AndOperator -> left.isMatchRoot || right.isMatchRoot\n            SelectorLogicalOperator.OrOperator -> left.isMatchRoot && right.isMatchRoot\n        }\n\n    override val fastQueryList: List<FastQuery>\n        get() = left.fastQueryList + right.fastQueryList\n\n    override val binaryExpressionList: List<BinaryExpression>\n        get() = left.binaryExpressionList + right.binaryExpressionList\n\n    override val connectSegmentList: List<ConnectSegment>\n        get() = left.connectSegmentList + right.connectSegmentList\n}"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/unit/NotSelectorExpression.kt",
    "content": "package li.songe.selector.unit\n\nimport li.songe.selector.FastQuery\nimport li.songe.selector.MatchOption\nimport li.songe.selector.QueryContext\nimport li.songe.selector.QueryResult\nimport li.songe.selector.Transform\nimport li.songe.selector.TypeInfo\nimport li.songe.selector.connect.ConnectSegment\nimport li.songe.selector.property.BinaryExpression\nimport kotlin.js.JsExport\n\n@JsExport\ndata class NotSelectorExpression(\n    val expression: SelectorExpression,\n) : SelectorExpression() {\n    override fun stringify(): String {\n        return \"!(${expression.stringify()})\"\n    }\n\n    override fun <T> match(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption\n    ): T? {\n        val r = expression.match(context, transform, option)\n        if (r != null) {\n            return null\n        }\n        return context.current\n    }\n\n    override fun <T> matchContext(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption\n    ): QueryResult<T> {\n        return QueryResult.NotResult(\n            this,\n            context,\n            expression.matchContext(context, transform, option)\n        )\n    }\n\n    override fun isSlow(\n        matchOption: MatchOption\n    ) = expression.isSlow(matchOption)\n\n    override fun checkType(typeInfo: TypeInfo) = expression.checkType(typeInfo)\n\n    override val isMatchRoot: Boolean\n        get() = false\n\n    override val fastQueryList: List<FastQuery>\n        get() = emptyList()\n\n    override val binaryExpressionList: List<BinaryExpression>\n        get() = expression.binaryExpressionList\n\n    override val connectSegmentList: List<ConnectSegment>\n        get() = expression.connectSegmentList\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/unit/SelectorExpression.kt",
    "content": "package li.songe.selector.unit\n\nimport li.songe.selector.FastQuery\nimport li.songe.selector.MatchOption\nimport li.songe.selector.QueryContext\nimport li.songe.selector.QueryResult\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport li.songe.selector.TypeInfo\nimport li.songe.selector.connect.ConnectSegment\nimport li.songe.selector.property.BinaryExpression\nimport kotlin.js.JsExport\n\ninternal sealed interface SelectorExpressionToken\n\n@JsExport\nsealed class SelectorExpression : Stringify, SelectorExpressionToken {\n\n    abstract fun <T> match(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption\n    ): T?\n\n    abstract fun <T> matchContext(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption,\n    ): QueryResult<T>\n\n    abstract fun isSlow(matchOption: MatchOption): Boolean\n    abstract fun checkType(typeInfo: TypeInfo)\n    abstract val isMatchRoot: Boolean\n    abstract val fastQueryList: List<FastQuery>\n    abstract val binaryExpressionList: List<BinaryExpression>\n    abstract val connectSegmentList: List<ConnectSegment>\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/unit/SelectorLogicalOperator.kt",
    "content": "package li.songe.selector.unit\n\nimport li.songe.selector.MatchOption\nimport li.songe.selector.QueryContext\nimport li.songe.selector.QueryResult\nimport li.songe.selector.Stringify\nimport li.songe.selector.Transform\nimport kotlin.js.JsExport\n\n@JsExport\nsealed class SelectorLogicalOperator(val key: String) : Stringify, SelectorExpressionToken {\n    override fun stringify() = key\n\n    companion object {\n        val allSubClasses by lazy {\n            listOf(\n                AndOperator, OrOperator\n            ).sortedBy { -it.key.length }\n        }\n    }\n\n    abstract fun <T> match(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption,\n        expression: LogicalSelectorExpression,\n    ): T?\n\n    abstract fun <T> matchContext(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption,\n        expression: LogicalSelectorExpression,\n    ): QueryResult<T>\n\n    data object AndOperator : SelectorLogicalOperator(\"&&\") {\n        override fun <T> match(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            option: MatchOption,\n            expression: LogicalSelectorExpression,\n        ): T? {\n            val leftValue = expression.left.match(context, transform, option)\n            if (leftValue == null) {\n                return null\n            }\n            return expression.right.match(\n                context,\n                transform,\n                option\n            )\n        }\n\n        override fun <T> matchContext(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            option: MatchOption,\n            expression: LogicalSelectorExpression,\n        ): QueryResult<T> {\n            val leftValue = expression.left.matchContext(context, transform, option)\n            if (!leftValue.matched) {\n                return QueryResult.AndResult(expression, leftValue)\n            }\n            return QueryResult.AndResult(\n                expression,\n                leftValue,\n                expression.right.matchContext(\n                    context,\n                    transform,\n                    option\n                )\n            )\n        }\n    }\n\n    data object OrOperator : SelectorLogicalOperator(\"||\") {\n        override fun <T> match(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            option: MatchOption,\n            expression: LogicalSelectorExpression,\n        ): T? {\n            val leftValue = expression.left.match(context, transform, option)\n            if (leftValue != null) {\n                return leftValue\n            }\n            return expression.right.match(\n                context,\n                transform,\n                option\n            )\n        }\n\n        override fun <T> matchContext(\n            context: QueryContext<T>,\n            transform: Transform<T>,\n            option: MatchOption,\n            expression: LogicalSelectorExpression,\n        ): QueryResult<T> {\n            val leftValue = expression.left.matchContext(context, transform, option)\n            if (leftValue.matched) {\n                return QueryResult.OrResult(expression, leftValue)\n            }\n            return QueryResult.OrResult(\n                expression,\n                leftValue,\n                expression.right.matchContext(\n                    context,\n                    transform,\n                    option\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/unit/UnitSelectorExpression.kt",
    "content": "package li.songe.selector.unit\n\nimport li.songe.selector.MatchOption\nimport li.songe.selector.MethodInfo\nimport li.songe.selector.MismatchExpressionTypeException\nimport li.songe.selector.MismatchOperatorTypeException\nimport li.songe.selector.MismatchParamTypeException\nimport li.songe.selector.PrimitiveType\nimport li.songe.selector.QueryContext\nimport li.songe.selector.QueryResult\nimport li.songe.selector.Transform\nimport li.songe.selector.TypeException\nimport li.songe.selector.TypeInfo\nimport li.songe.selector.UnknownIdentifierException\nimport li.songe.selector.UnknownIdentifierMethodException\nimport li.songe.selector.UnknownIdentifierMethodParamsException\nimport li.songe.selector.UnknownMemberException\nimport li.songe.selector.UnknownMemberMethodException\nimport li.songe.selector.UnknownMemberMethodParamsException\nimport li.songe.selector.connect.ConnectOperator\nimport li.songe.selector.connect.ConnectSegment\nimport li.songe.selector.connect.ConnectWrapper\nimport li.songe.selector.property.BinaryExpression\nimport li.songe.selector.property.PropertyWrapper\nimport li.songe.selector.property.ValueExpression\nimport kotlin.collections.addAll\nimport kotlin.js.JsExport\n\n@JsExport\ndata class UnitSelectorExpression(\n    val propertyWrapper: PropertyWrapper,\n) : SelectorExpression() {\n    override fun stringify(): String {\n        return propertyWrapper.stringify()\n    }\n\n    override fun <T> match(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption\n    ): T? {\n        propertyWrapper.matchContext(context, transform, option).apply {\n            if (matched) {\n                return get(targetIndex).current\n            }\n        }\n        return null\n    }\n\n    override fun <T> matchContext(\n        context: QueryContext<T>,\n        transform: Transform<T>,\n        option: MatchOption,\n    ): QueryResult<T> {\n        return QueryResult.UnitResult(\n            propertyWrapper.matchContext(context, transform, option),\n            this,\n            targetIndex\n        )\n    }\n\n    val targetIndex = run {\n        val length = propertyWrapper.length\n        var index = 0\n        var c: PropertyWrapper? = propertyWrapper\n        while (c != null) {\n            if (c.segment.at) {\n                return@run length - 1 - index\n            }\n            c = c.to?.to\n            index++\n        }\n        length - 1\n    }\n\n    override val fastQueryList = propertyWrapper.fastQueryList\n    override val isMatchRoot = propertyWrapper.isMatchRoot\n\n    internal val connectWrappers: Sequence<ConnectWrapper>\n        get() = sequence {\n            var c = propertyWrapper.to\n            while (c != null) {\n                yield(c)\n                c = c.to.to\n            }\n        }\n\n    override val binaryExpressionList: List<BinaryExpression>\n        get() {\n            var p: PropertyWrapper? = propertyWrapper\n            val expressions = mutableListOf<BinaryExpression>()\n            while (p != null) {\n                val s = p.segment\n                expressions.addAll(s.getBinaryExpressionList())\n                p = p.to?.to\n            }\n            return expressions\n        }\n    override val connectSegmentList: List<ConnectSegment>\n        get() = connectWrappers.map { it.segment }.toList()\n\n\n    override fun isSlow(matchOption: MatchOption): Boolean {\n        if ((!matchOption.fastQuery || propertyWrapper.fastQueryList.isEmpty()) && !isMatchRoot) {\n            return true\n        }\n        if (connectWrappers.any { c -> c.segment.operator == ConnectOperator.Descendant && !(c.canFq && matchOption.fastQuery) }) {\n            return true\n        }\n        return false\n    }\n\n    @Throws(TypeException::class)\n    override fun checkType(typeInfo: TypeInfo) {\n        binaryExpressionList.forEach { exp ->\n            if (!exp.operator.allowType(exp.left, exp.right)) {\n                throw MismatchOperatorTypeException(exp)\n            }\n            val leftType = getExpType(exp.left, typeInfo)\n            val rightType = getExpType(exp.right, typeInfo)\n            if (leftType != null && rightType != null && leftType != rightType) {\n                throw MismatchExpressionTypeException(exp, leftType, rightType)\n            }\n        }\n    }\n\n}\n\nprivate fun getExpType(\n    exp: ValueExpression,\n    typeInfo: TypeInfo\n): PrimitiveType? {\n    return when (exp) {\n        is ValueExpression.NullLiteral -> null\n        is ValueExpression.BooleanLiteral -> PrimitiveType.BooleanType\n        is ValueExpression.IntLiteral -> PrimitiveType.IntType\n        is ValueExpression.StringLiteral -> PrimitiveType.StringType\n        is ValueExpression.Variable -> checkVariable(exp, typeInfo, typeInfo).type\n    }\n}\n\nprivate fun checkMethod(\n    method: MethodInfo,\n    value: ValueExpression.CallExpression,\n    globalTypeInfo: TypeInfo\n): TypeInfo {\n    method.params.forEachIndexed { index, argTypeInfo ->\n        when (val argExp = value.arguments[index]) {\n            is ValueExpression.NullLiteral -> {}\n            is ValueExpression.BooleanLiteral -> {\n                if (argTypeInfo.type != PrimitiveType.BooleanType) {\n                    throw MismatchParamTypeException(\n                        value,\n                        argExp,\n                        PrimitiveType.BooleanType\n                    )\n                }\n            }\n\n            is ValueExpression.IntLiteral -> {\n                if (argTypeInfo.type != PrimitiveType.IntType) {\n                    throw MismatchParamTypeException(value, argExp, PrimitiveType.IntType)\n                }\n            }\n\n            is ValueExpression.StringLiteral -> {\n                if (argTypeInfo.type != PrimitiveType.StringType) {\n                    throw MismatchParamTypeException(\n                        value,\n                        argExp,\n                        PrimitiveType.StringType\n                    )\n                }\n            }\n\n            is ValueExpression.Variable -> {\n                val type = checkVariable(argExp, argTypeInfo, globalTypeInfo)\n                if (type.type != argTypeInfo.type) {\n                    throw MismatchParamTypeException(\n                        value,\n                        argExp,\n                        type.type\n                    )\n                }\n            }\n        }\n    }\n    return method.returnType\n}\n\nprivate fun checkVariable(\n    value: ValueExpression.Variable,\n    currentTypeInfo: TypeInfo,\n    globalTypeInfo: TypeInfo,\n): TypeInfo {\n    return when (value) {\n        is ValueExpression.CallExpression -> {\n            val methods = when (value.callee) {\n                is ValueExpression.CallExpression -> {\n                    throw IllegalArgumentException(\"Unsupported nested call\")\n                }\n\n                is ValueExpression.Identifier -> {\n                    // getChild(0)\n                    globalTypeInfo.methods\n                        .filter { it.name == value.callee.value }\n                        .apply {\n                            if (isEmpty()) {\n                                throw UnknownIdentifierMethodException(value.callee)\n                            }\n                        }\n                        .filter { it.params.size == value.arguments.size }\n                        .apply {\n                            if (isEmpty()) {\n                                throw UnknownIdentifierMethodParamsException(value)\n                            }\n                        }\n                }\n\n                is ValueExpression.MemberExpression -> {\n                    // parent.getChild(0)\n                    checkVariable(\n                        value.callee.object0,\n                        currentTypeInfo,\n                        globalTypeInfo\n                    ).methods\n                        .filter { it.name == value.callee.property }\n                        .apply {\n                            if (isEmpty()) {\n                                throw UnknownMemberMethodException(value.callee)\n                            }\n                        }.filter { it.params.size == value.arguments.size }.apply {\n                            if (isEmpty()) {\n                                throw UnknownMemberMethodParamsException(value)\n                            }\n                        }\n                }\n            }\n            if (methods.size == 1) {\n                checkMethod(methods[0], value, globalTypeInfo)\n                return methods[0].returnType\n            }\n            methods.forEachIndexed { i, method ->\n                try {\n                    checkMethod(method, value, globalTypeInfo)\n                    return method.returnType\n                } catch (e: TypeException) {\n                    if (i == methods.size - 1) {\n                        throw e\n                    }\n                    // ignore\n                }\n            }\n            if (value.callee is ValueExpression.Identifier) {\n                throw UnknownIdentifierMethodException(value.callee)\n            } else if (value.callee is ValueExpression.MemberExpression) {\n                throw UnknownMemberMethodException(value.callee)\n            }\n            throw IllegalArgumentException(\"Unsupported nested call\")\n        }\n\n        is ValueExpression.Identifier -> {\n            globalTypeInfo.props.find { it.name == value.value }?.type\n                ?: throw UnknownIdentifierException(value)\n        }\n\n        is ValueExpression.MemberExpression -> {\n            checkVariable(\n                value.object0,\n                currentTypeInfo,\n                globalTypeInfo\n            ).props.find { it.name == value.property }?.type\n                ?: throw UnknownMemberException(value)\n        }\n    }\n}\n"
  },
  {
    "path": "selector/src/commonMain/kotlin/li/songe/selector/util.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage li.songe.selector\n\nimport kotlin.js.JsExport\n\ninternal fun escapeString(value: String, wrapChar: Char = '\"'): String {\n    val sb = StringBuilder(value.length + 2)\n    sb.append(wrapChar)\n    value.forEach { c ->\n        val escapeChar = when (c) {\n            wrapChar -> wrapChar\n            '\\n' -> 'n'\n            '\\r' -> 'r'\n            '\\t' -> 't'\n            '\\b' -> 'b'\n            '\\\\' -> '\\\\'\n            else -> null\n        }\n        if (escapeChar != null) {\n            sb.append(\"\\\\\" + escapeChar)\n        } else {\n            when (c.code) {\n                in 0..0xf -> {\n                    sb.append(\"\\\\x0\" + c.code.toString(16))\n                }\n\n                in 0x10..0x1f -> {\n                    sb.append(\"\\\\x\" + c.code.toString(16))\n                }\n\n                else -> {\n                    sb.append(c)\n                }\n            }\n        }\n    }\n    sb.append(wrapChar)\n    return sb.toString()\n}\n\nprivate const val REG_SPECIAL_STRING = \"\\\\^$.?*|+()[]{}\"\nprivate fun getMatchValue(value: String, prefix: String, suffix: String): String? {\n    if (value.startsWith(prefix) && value.endsWith(suffix) && value.length >= (prefix.length + suffix.length)) {\n        for (i in prefix.length until value.length - suffix.length) {\n            if (value[i] in REG_SPECIAL_STRING) {\n                return null\n            }\n        }\n        return value.subSequence(prefix.length, value.length - suffix.length).toString()\n    }\n    return null\n}\n\ninternal fun optimizeMatchString(value: String): ((CharSequence) -> Boolean)? {\n    getMatchValue(value, \"(?is)\", \".*\")?.let { startsWithValue ->\n        return { value -> value.startsWith(startsWithValue, ignoreCase = true) }\n    }\n    getMatchValue(value, \"(?is).*\", \".*\")?.let { containsValue ->\n        return { value -> value.contains(containsValue, ignoreCase = true) }\n    }\n    getMatchValue(value, \"(?is).*\", \"\")?.let { endsWithValue ->\n        return { value -> value.endsWith(endsWithValue, ignoreCase = true) }\n    }\n    return null\n}\n\ninternal inline fun <T> T?.whenNull(block: () -> Nothing): T {\n    if (this == null) {\n        block()\n    }\n    return this\n}\n\n@JsExport\nclass DefaultTypeInfo(\n    val booleanType: TypeInfo,\n    val intType: TypeInfo,\n    val stringType: TypeInfo,\n    val contextType: TypeInfo,\n    val nodeType: TypeInfo,\n    val globalType: TypeInfo\n)\n\n@JsExport\nfun initDefaultTypeInfo(webField: Boolean = false): DefaultTypeInfo {\n    val booleanType = TypeInfo(PrimitiveType.BooleanType)\n    val intType = TypeInfo(PrimitiveType.IntType)\n    val stringType = TypeInfo(PrimitiveType.StringType)\n    val nodeType = TypeInfo(PrimitiveType.ObjectType(\"node\"))\n    val contextType = TypeInfo(PrimitiveType.ObjectType(\"context\"))\n    val globalType = TypeInfo(PrimitiveType.ObjectType(\"global\"))\n\n    fun buildMethods(name: String, returnType: TypeInfo, paramsSize: Int): List<MethodInfo> {\n        return listOf(\n            MethodInfo(name, returnType, List(paramsSize) { booleanType }),\n            MethodInfo(name, returnType, List(paramsSize) { intType }),\n            MethodInfo(name, returnType, List(paramsSize) { stringType }),\n            MethodInfo(name, returnType, List(paramsSize) { nodeType }),\n            MethodInfo(name, returnType, List(paramsSize) { contextType }),\n        )\n    }\n\n    booleanType.methods = listOf(\n        MethodInfo(\"toInt\", intType),\n        MethodInfo(\"or\", booleanType, listOf(booleanType)),\n        MethodInfo(\"and\", booleanType, listOf(booleanType)),\n        MethodInfo(\"not\", booleanType),\n    ) + buildMethods(\"ifElse\", booleanType, 2)\n\n    intType.methods = listOf(\n        MethodInfo(\"toString\", stringType),\n        MethodInfo(\"toString\", stringType, listOf(intType)),\n        MethodInfo(\"plus\", intType, listOf(intType)),\n        MethodInfo(\"minus\", intType, listOf(intType)),\n        MethodInfo(\"times\", intType, listOf(intType)),\n        MethodInfo(\"div\", intType, listOf(intType)),\n        MethodInfo(\"rem\", intType, listOf(intType)),\n        MethodInfo(\"more\", booleanType, listOf(intType)),\n        MethodInfo(\"moreEqual\", booleanType, listOf(intType)),\n        MethodInfo(\"less\", booleanType, listOf(intType)),\n        MethodInfo(\"lessEqual\", booleanType, listOf(intType)),\n    )\n    stringType.props = listOf(\n        PropInfo(\"length\", intType),\n    )\n    stringType.methods = listOf(\n        MethodInfo(\"get\", stringType, listOf(intType)),\n        MethodInfo(\"at\", stringType, listOf(intType)),\n        MethodInfo(\"substring\", stringType, listOf(intType)),\n        MethodInfo(\"substring\", stringType, listOf(intType, intType)),\n        MethodInfo(\"toInt\", intType),\n        MethodInfo(\"toInt\", intType, listOf(intType)),\n        MethodInfo(\"indexOf\", intType, listOf(stringType)),\n        MethodInfo(\"indexOf\", intType, listOf(stringType, intType)),\n    )\n    nodeType.props = listOf(\n        * (if (webField) {\n            arrayOf(\n                PropInfo(\"_id\", intType),\n                PropInfo(\"_pid\", intType),\n            )\n        } else {\n            emptyArray()\n        }),\n\n        PropInfo(\"id\", stringType),\n        PropInfo(\"vid\", stringType),\n        PropInfo(\"name\", stringType),\n        PropInfo(\"text\", stringType),\n        PropInfo(\"desc\", stringType),\n\n        PropInfo(\"clickable\", booleanType),\n        PropInfo(\"focusable\", booleanType),\n        PropInfo(\"checkable\", booleanType),\n        PropInfo(\"checked\", booleanType),\n        PropInfo(\"editable\", booleanType),\n        PropInfo(\"longClickable\", booleanType),\n        PropInfo(\"visibleToUser\", booleanType),\n\n        PropInfo(\"left\", intType),\n        PropInfo(\"top\", intType),\n        PropInfo(\"right\", intType),\n        PropInfo(\"bottom\", intType),\n        PropInfo(\"width\", intType),\n        PropInfo(\"height\", intType),\n\n        PropInfo(\"childCount\", intType),\n        PropInfo(\"index\", intType),\n        PropInfo(\"depth\", intType),\n\n        PropInfo(\"parent\", nodeType),\n    )\n    nodeType.methods = listOf(\n        MethodInfo(\"getChild\", nodeType, listOf(intType)),\n    )\n    contextType.methods = nodeType.methods + listOf(\n        MethodInfo(\"getPrev\", contextType, listOf(intType))\n    )\n    contextType.props = nodeType.props + listOf(\n        PropInfo(\"prev\", contextType),\n        PropInfo(\"current\", nodeType),\n    )\n    globalType.methods = contextType.methods +\n            buildMethods(\"equal\", booleanType, 2) +\n            buildMethods(\"notEqual\", booleanType, 2)\n\n    globalType.props = contextType.props.toList()\n    return DefaultTypeInfo(\n        booleanType = booleanType,\n        intType = intType,\n        stringType = stringType,\n        contextType = contextType,\n        nodeType = nodeType,\n        globalType = globalType\n    )\n}\n\n@JsExport\nfun getIntInvoke(target: Int, name: String, args: List<Any>): Any? {\n    return when (name) {\n        \"plus\" -> {\n            target + args.getInt()\n        }\n\n        \"minus\" -> {\n            target - args.getInt()\n        }\n\n        \"times\" -> {\n            target * args.getInt()\n        }\n\n        \"div\" -> {\n            target / args.getInt().also { if (it == 0) return null }\n        }\n\n        \"rem\" -> {\n            target % args.getInt().also { if (it == 0) return null }\n        }\n\n        \"more\" -> {\n            target > args.getInt()\n        }\n\n        \"moreEqual\" -> {\n            target >= args.getInt()\n        }\n\n        \"less\" -> {\n            target < args.getInt()\n        }\n\n        \"lessEqual\" -> {\n            target <= args.getInt()\n        }\n\n        else -> null\n    }\n}\n\n\ninternal fun List<Any>.getInt(i: Int = 0) = get(i) as Int\n\n@JsExport\nfun getStringInvoke(target: String, name: String, args: List<Any>): Any? {\n    return getCharSequenceInvoke(target, name, args)\n}\n\n@JsExport\nfun getBooleanInvoke(target: Boolean, name: String, args: List<Any>): Any? {\n    return when (name) {\n        \"toInt\" -> if (target) 1 else 0\n        \"not\" -> !target\n        else -> null\n    }\n}\n\nfun getCharSequenceInvoke(target: CharSequence, name: String, args: List<Any>): Any? {\n    return when (name) {\n        \"get\" -> {\n            target.getOrNull(args.getInt()).toString()\n        }\n\n        \"at\" -> {\n            val i = args.getInt()\n            if (i < 0) {\n                target.getOrNull(target.length + i).toString()\n            } else {\n                target.getOrNull(i).toString()\n            }\n        }\n\n        \"substring\" -> {\n            when (args.size) {\n                1 -> {\n                    val start = args.getInt()\n                    if (start < 0) return null\n                    if (start >= target.length) return \"\"\n                    target.substring(\n                        start,\n                        target.length\n                    )\n                }\n\n                2 -> {\n                    val start = args.getInt()\n                    if (start < 0) return null\n                    if (start >= target.length) return \"\"\n                    val end = args.getInt(1)\n                    if (end < start) return null\n                    target.substring(\n                        start,\n                        end.coerceAtMost(target.length)\n                    )\n                }\n\n                else -> {\n                    null\n                }\n            }\n        }\n\n        \"toInt\" -> when (args.size) {\n            0 -> target.toString().toIntOrNull()\n            1 -> {\n                val radix = args.getInt()\n                if (radix !in 2..36) {\n                    return null\n                }\n                target.toString().toIntOrNull(radix)\n            }\n\n            else -> null\n        }\n\n        \"indexOf\" -> {\n            when (args.size) {\n                1 -> {\n                    val str = args[0] as? CharSequence ?: return null\n                    target.indexOf(str.toString())\n                }\n\n                2 -> {\n                    val str = args[0] as? CharSequence ?: return null\n                    val startIndex = args.getInt(1)\n                    target.indexOf(str.toString(), startIndex)\n                }\n\n                else -> null\n            }\n        }\n\n        else -> null\n    }\n}\n\n@JsExport\nfun getStringAttr(target: String, name: String): Any? {\n    return getCharSequenceAttr(target, name)\n}\n\nfun getCharSequenceAttr(target: CharSequence, name: String): Any? {\n    return when (name) {\n        \"length\" -> target.length\n        else -> null\n    }\n}\n\n// example\n// id=\"com.lptiyu.tanke:id/ab1\"\n// id=\"com.lptiyu.tanke:id/ab2\"\ninternal fun CharSequence.contentReversedEquals(other: CharSequence): Boolean {\n    if (this === other) return true\n    if (this.length != other.length) return false\n    for (i in this.length - 1 downTo 0) {\n        if (this[i] != other[i]) return false\n    }\n    return true\n}\n\ninternal fun comparePrimitiveValue(left: Any?, right: Any?): Boolean {\n    return if (left is CharSequence && right is CharSequence) {\n        left.contentReversedEquals(right)\n    } else {\n        left == right\n    }\n}"
  },
  {
    "path": "selector/src/jsMain/kotlin/li/songe/selector/toMatches.js.kt",
    "content": "package li.songe.selector\n\nimport kotlin.js.RegExp\n\nactual fun String.toMatches(): (input: CharSequence) -> Boolean {\n    if (wasmMatchesTemp !== null) {\n        val matches = wasmMatchesTemp!!(this)\n        return { input -> matches(input.toString()) }\n    }\n    if (length >= 4 && startsWith(\"(?\")) {\n        for (i in 2 until length) {\n            when (get(i)) {\n                in 'a'..'z' -> {}\n                in 'A'..'Z' -> {}\n                ')' -> {\n                    val flags = subSequence(2, i).toMutableList()\n                        .apply { add('g'); add('u') }\n                        .joinToString(\"\")\n                    val nativePattern = RegExp(substring(i + 1), flags)\n                    return { input ->\n                        // // https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/js/src/kotlin/text/regex.kt\n                        nativePattern.reset()\n                        val match = nativePattern.exec(input.toString())\n                        match != null && match.index == 0 && nativePattern.lastIndex == input.length\n                    }\n                }\n\n                else -> break\n            }\n        }\n    }\n    val regex = Regex(this)\n    return { input -> regex.matches(input) }\n}\n\nprivate var wasmMatchesTemp: ((String) -> (String) -> Boolean)? = null\nactual fun setWasmToMatches(wasmToMatches: (String) -> (String) -> Boolean) {\n    wasmMatchesTemp = wasmToMatches\n}"
  },
  {
    "path": "selector/src/jvmMain/kotlin/li/songe/selector/toMatches.jvm.kt",
    "content": "package li.songe.selector\n\nactual fun String.toMatches(): (input: CharSequence) -> Boolean {\n    val regex = Regex(this)\n    return { input -> regex.matches(input) }\n}\n\n@Suppress(\"unused\")\nactual fun setWasmToMatches(wasmToMatches: (String) -> (String) -> Boolean) {\n}"
  },
  {
    "path": "selector/src/jvmTest/kotlin/li/songe/selector/ParserUnitTest.kt",
    "content": "package li.songe.selector\n\nimport junit.framework.TestCase.assertTrue\nimport li.songe.selector.connect.ConnectOperator\nimport li.songe.selector.connect.ConnectSegment\nimport li.songe.selector.connect.PolynomialExpression\nimport li.songe.selector.connect.TupleExpression\nimport li.songe.selector.parser.SelectorParser\nimport li.songe.selector.property.CompareOperator\nimport kotlin.test.Test\n\nclass ParserUnitTest {\n\n    @Test\n    fun value() {\n        assert(SelectorParser(\"null\").readValueExpression().value == null)\n        assert(SelectorParser(\"true\").readValueExpression().value == true)\n        assert(SelectorParser(\"false\").readValueExpression().value == false)\n        assert(SelectorParser(\"123\").readValueExpression().value == 123)\n        assert(SelectorParser(\"-123\").readValueExpression().value == -123)\n        assert(SelectorParser(\"``\").readValueExpression().value == \"\")\n        assert(SelectorParser(\"'abc\\\\\\\"'\").readValueExpression().value == \"abc\\\"\")\n        assert(SelectorParser(\"abc.xyz\").readValueExpression().value == \"abc.xyz\")\n        SelectorParser(\"abc.xyz .m(null, false,a,\\n\\t\\r-123,``)\").readValueExpression().let {\n            println(it.value)\n            assert(it.value == \"abc.xyz.m(null,false,a,-123,\\\"\\\")\")\n        }\n    }\n\n    @Test\n    fun regexp() {\n        SelectorParser(\"[x~=`.*`]\")\n            .readUnitSelectorExpression()\n            .stringify().let {\n                println(it)\n                assert(it == \"[x~=\\\".*\\\"]\")\n            }\n        SelectorParser(\"[x!~=`\\\\\\\\d+`]\")\n            .readUnitSelectorExpression()\n            .stringify().let {\n                println(it)\n                assert(it == \"[x!~=\\\"\\\\\\\\d+\\\"]\")\n            }\n    }\n\n    @Test\n    fun expression() {\n        SelectorParser(\"((((a>null&&b>true&&c>1||d>1 || abc.xyz .m(null, false,a,\\n\\t\\n-123,``)>``))))\")\n            .readExpression()\n            .stringify().let {\n                println(it)\n                assert(it == \"(a>null && b>true && c>1) || d>1 || abc.xyz.m(null,false,a,-123,\\\"\\\")>\\\"\\\"\")\n            }\n        SelectorParser(\"View[a>1&&b>1&&c>1||d>1&&x^=1]\")\n            .readUnitSelectorExpression()\n            .stringify().let {\n                println(it)\n                assert(it == \"View[(a>1 && b>1 && c>1) || (d>1 && x^=1)]\")\n            }\n    }\n\n    @Test\n    fun propertySegment() {\n        SelectorParser(\"*\").readPropertySegment()\n        SelectorParser(\"View[a>1&&b>1&&c>1||d>1&&x^=1]\").readPropertySegment()\n        SelectorParser(\"@View[a>1&&b>1&&c>1||d>1&&x^=1][a!=b&&c>d.z11]\").readPropertySegment()\n        println(\n            SelectorParser(\"[a='\\\\\\\"'][a=\\\"'\\\"][a=`\\\\x20\\\\n\\\\uD83D\\\\uDE04`][a=`\\\\x20`][a=\\\"`\\u0020\\\"][a=`\\\\t\\\\n\\\\r\\\\b\\\\x00\\\\x09\\\\x1d`]\").readPropertySegment()\n                .stringify()\n        )\n    }\n\n    @Test\n    fun connectSegment() {\n        assert(SelectorParser(\"+\").readConnectSegment() == ConnectSegment(ConnectOperator.BeforeBrother))\n        assert(SelectorParser(\"->\").readConnectSegment() == ConnectSegment(ConnectOperator.Previous))\n        assert(\n            SelectorParser(\">(1,2,3)\").readConnectSegment() == ConnectSegment(\n                ConnectOperator.Ancestor,\n                TupleExpression(listOf(1, 2, 3))\n            )\n        )\n        assert(\n            SelectorParser(\"<<10n\").readConnectSegment() == ConnectSegment(\n                ConnectOperator.Descendant,\n                PolynomialExpression(a = 10, b = 0)\n            )\n        )\n        assert(\n            SelectorParser(\"-(9n+1)\").readConnectSegment() == ConnectSegment(\n                ConnectOperator.AfterBrother,\n                PolynomialExpression(a = 9, b = 1)\n            )\n        )\n        assert(\n            SelectorParser(\"-(-2n+9)\").readConnectSegment() == ConnectSegment(\n                ConnectOperator.AfterBrother,\n                PolynomialExpression(a = -2, b = 9)\n            )\n        )\n    }\n\n    @Test\n    fun selector() {\n        SelectorParser(\"[_id=15] >(1,2,9) X + Z >(7+9n) *\").readUnitSelectorExpression()\n        SelectorParser(\"A > @B[a>''&&b>1&&c>1||d>1&&x^=1][a!=b&&c>d.z11][a>b.plus(null)] + C[a>b]\").readUnitSelectorExpression()\n        SelectorParser(\"A\").readSelector()\n        assert(\n            SelectorParser(\"([text=`a`||id=`b`||vid=`c`])||([text=`d`||id=`e`||vid=`f`])\")\n                .readSelector()\n                .fastQueryList == listOf(\n                FastQuery.Text(\"a\", CompareOperator.Equal),\n                FastQuery.Id(\"b\"),\n                FastQuery.Vid(\"c\"),\n                FastQuery.Text(\"d\", CompareOperator.Equal),\n                FastQuery.Id(\"e\"),\n                FastQuery.Vid(\"f\"),\n            )\n        )\n        println(SelectorParser(\"(A + @B[a>1]) || (@C + D[f=``])\").readSelector().toString())\n    }\n\n\n    @Test\n    fun query_selector() {\n        val text =\n            \"@[vid=\\\"rv_home_tab\\\"] <<(99-n) [vid=\\\"header_container\\\"] -(-2n+9) [vid=\\\"layout_refresh\\\"] +2 [vid=\\\"home_v10_frag_content\\\"]\"\n        val selector = Selector.parse(text)\n        println(\"selector: $selector\")\n        val node = getSnapshotNode(\"https://i.gkd.li/i/14325747\")\n        val targets = transform.querySelectorAll(node, selector).toList()\n        println(\"target_size: \" + targets.size)\n        println(\"target_id: \" + targets.map { t -> t.id })\n        assertTrue(targets.size == 1)\n        println(\"id: \" + targets.first().id)\n\n        val trackTargets = transform.querySelectorAllContext(node, selector).toList()\n        println(\"trackTargets_size: \" + trackTargets.size)\n        assertTrue(trackTargets.size == 1)\n        println(trackTargets.first())\n    }\n\n    @Test\n    fun check_tuple() {\n        // 1->3, 3->21\n        // 1,3->24\n        val snapshotNode = getSnapshotNode(\"https://i.gkd.li/i/13247733\")\n        val (x1, x2) = (1..6).toList().shuffled().subList(0, 2).sorted()\n        val x1N =\n            transform.querySelectorAll(snapshotNode, Selector.parse(\"[_id=15] >$x1 *\")).count()\n        val x2N =\n            transform.querySelectorAll(snapshotNode, Selector.parse(\"[_id=15] >$x2 *\")).count()\n        val x12N = transform.querySelectorAll(snapshotNode, Selector.parse(\"[_id=15] >($x1,$x2) *\"))\n            .count()\n\n        println(\"$x1->$x1N, $x2->$x2N, ($x1,$x2)->$x12N\")\n    }\n\n    @Test\n    fun ast() {\n        val ast =\n            Selector.parseAst(\"(A + B) || (A +(3n+1) @B[a.b.c(a,b)>1 || b>null && (b~=`233` ) && a=1 ][b=true][(a>b || (c> 0  ))][(a>1 || a>1) && a>1])\")\n        println(\"selector: ${ast.value}\")\n        println(\"ast: ${ast.stringify()}\")\n    }\n\n    @Test\n    fun root() {\n        assert(Selector.parse(\"[null=parent]\").isMatchRoot)\n        assert(Selector.parse(\"[parent=null]\").isMatchRoot)\n    }\n}\n"
  },
  {
    "path": "selector/src/jvmTest/kotlin/li/songe/selector/QueryUnitTest.kt",
    "content": "package li.songe.selector\n\nimport kotlin.test.Test\n\nclass QueryUnitTest {\n\n    val node1 by lazy { getSnapshotNode(\"https://i.gkd.li/i/13247733\") }\n    val node2 by lazy { getSnapshotNode(\"https://i.gkd.li/i/14384152\") }\n    val node3 by lazy { getSnapshotNode(\"https://i.gkd.li/i/13247610\") }\n    val node4 by lazy { getSnapshotNode(\"https://i.gkd.li/i/16076188\") }\n    val node5 by lazy { getSnapshotNode(\"https://i.gkd.li/i/25547842\") }\n\n    @Test\n    fun regexp() {\n        val selector = Selector.parse(\"[text~=`.*\\\\\\\\d+`]\")\n        println(selector)\n        val nodes = transform.querySelectorAll(node1, selector).toList()\n        assert(nodes.size == 27)\n        assert(nodes.subList(0, 5).map { it.id } == listOf(126, 124, 105, 106, 102))\n    }\n\n    @Test\n    fun example1() {\n        val selector = Selector.parse(\n            \"@TextView[getPrev(0).text=`签到提醒`] - [text=`签到提醒`] <<n [vid=`webViewContainer`]\"\n        )\n        println(selector)\n        val nodes = transform.querySelectorAll(node2, selector).toList()\n        assert(nodes.single().id == 32)\n    }\n\n    @Test\n    fun example2() {\n        val selector = Selector.parse(\n            \"@[id=`com.byted.pangle.m:id/tt_splash_skip_btn`] <<n [id=`com.coolapk.market:id/ad_container`]\"\n        )\n        println(selector)\n        val nodes = transform.querySelectorAll(node3, selector).toList()\n        assert(nodes.single().id == 17)\n    }\n\n    @Test\n    fun example3() {\n        val selector = Selector.parse(\n            \"[text=`搜索历史`] + [_id=161] -> [text=`清除`] <2 [id=`com.coolapk.market:id/close_view`]\"\n        )\n        println(selector)\n        val nodes = transform.querySelectorAll(node1, selector).toList()\n        assert(nodes.single().id == 161)\n        val nodes2 = transform.querySelectorAllContext(node1, selector).toList()\n        val pathList = nodes2.single().unitResults.single().getNodeConnectPath(transform)\n        val pathText = pathList.map {\n            \"${it.target.id} ${it.formatConnectOffset} ${it.source.id}\"\n        }.toString()\n        println(pathText)\n        assert(pathText == \"[163 < 161, 161 -> 163, 160 + 161]\")\n    }\n\n    @Test\n    fun example4() {\n        val selector = Selector.parse(\n            \"([_id=62] <<n [_id=28]) && (ImageView < FrameLayout <n ViewGroup[desc^=\\\"直播\\\"] - ViewGroup >4 FrameLayout[index=0] +2 FrameLayout > @TextView[index=parent.childCount.minus(1)] <<n FrameLayout[vid=\\\"content\\\"])\"\n        )\n        println(selector)\n        val nodes = transform.querySelectorAllContext(node4, selector).toList()\n        val pathList = nodes.single().unitResults\n        println(pathList.size)\n        assert(pathList.size == 2)\n        println(pathList.map { p ->\n            p.unitResults.single().getNodeConnectPath(transform).map {\n                \"${it.target.id} ${it.formatConnectOffset} ${it.source.id}\"\n            }\n        })\n    }\n\n    @Test\n    fun example5() {\n        val selector =\n            Selector.parse(\"ImageView[width>540] + View -> ImageView[width<71] - View[text=null][clickable=true]\")\n        println(selector)\n        val nodes = transform.querySelectorAllContext(node5, selector).toList()\n        val pathList = nodes.single().unitResults\n        println(pathList.map { p ->\n            p.unitResults.single().getNodeConnectPath(transform).map {\n                \"${it.target.id} ${it.formatConnectOffset} ${it.source.id}\"\n            }\n        })\n    }\n\n    @Test\n    fun example6() {\n        val selector = Selector.parse(\"[parent=null]\")\n        println(selector)\n        val targetNode = selector.match(node4, transform, MatchOption.default)\n        assert(node4 === targetNode)\n        val nodes = transform.querySelectorAllContext(node4, selector).toList()\n        assert(nodes.isEmpty())\n    }\n}"
  },
  {
    "path": "selector/src/jvmTest/kotlin/li/songe/selector/Snapshot.kt",
    "content": "package li.songe.selector\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonNull\nimport kotlinx.serialization.json.JsonPrimitive\nimport kotlinx.serialization.json.booleanOrNull\nimport kotlinx.serialization.json.intOrNull\nimport java.io.BufferedOutputStream\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.net.URI\nimport java.util.zip.ZipInputStream\n\n@Serializable\ndata class Node(\n    val id: Int,\n    val pid: Int,\n    val attr: Map<String, JsonPrimitive>,\n) {\n    @Transient\n    var parent: Node? = null\n\n    @Transient\n    var children: MutableList<Node> = mutableListOf()\n\n    override fun toString(): String {\n        return id.toString()\n    }\n}\n\n@Serializable\ndata class Snapshot(\n    val nodes: List<Node>\n)\n\nprivate val assetsDir by lazy {\n    File(\"../_assets\").apply {\n        if (!exists()) {\n            mkdir()\n        }\n    }\n}\n\nprivate val json by lazy {\n    Json {\n        ignoreUnknownKeys = true\n    }\n}\n\nprivate fun getNodeAttr(node: Node, name: String): Any? {\n    if (name == \"_id\") return node.id\n    if (name == \"_pid\") return node.pid\n    if (name == \"parent\") return node.parent\n    val value = node.attr[name] ?: return null\n    if (value is JsonNull) return null\n    if (value.isString) {\n        return value.content\n    }\n    return value.intOrNull ?: value.booleanOrNull ?: value.content\n}\n\nprivate fun getNodeInvoke(target: Node, name: String, args: List<Any>): Any? {\n    when (name) {\n        \"getChild\" -> {\n            val arg = args.getInt()\n            return target.children.getOrNull(arg)\n        }\n    }\n    return null\n}\n\nval transform by lazy {\n    Transform<Node>(\n        getAttr = { target, name ->\n            when (target) {\n                is QueryContext<*> -> when (name) {\n                    \"prev\" -> target.prev\n                    \"current\" -> target.current\n                    else -> getNodeAttr(target.current as Node, name)\n                }\n\n                is Node -> getNodeAttr(target, name)\n                is String -> getCharSequenceAttr(target, name)\n\n                else -> null\n            }\n        },\n        getInvoke = { target, name, args ->\n            when (target) {\n                is Boolean -> getBooleanInvoke(target, name, args)\n                is Int -> getIntInvoke(target, name, args)\n                is CharSequence -> getCharSequenceInvoke(target, name, args)\n                is Node -> getNodeInvoke(target, name, args)\n                is QueryContext<*> -> when (name) {\n                    \"getPrev\" -> {\n                        args.getInt().let { target.getPrev(it) }\n                    }\n\n                    else -> getNodeInvoke(target.current as Node, name, args)\n                }\n\n                else -> null\n            }\n        },\n        getName = { node -> node.attr[\"name\"]?.content },\n        getChildren = { node -> node.children.asSequence() },\n        getParent = { node -> node.parent }\n    )\n}\n\nprivate val idToSnapshot by lazy {\n    HashMap<String, Node>()\n}\n\n//val typeInfo by lazy { initDefaultTypeInfo(webField = true).globalType }\n\nfun getSnapshotNode(url: String): Node {\n    val githubAssetId = url.split('/').last()\n    idToSnapshot[githubAssetId]?.let { return it }\n    val file = assetsDir.resolve(\"$githubAssetId.json\")\n    if (!file.exists()) {\n        val remoteUrl = URI.create(\"https://f.gkd.li/${githubAssetId}\").toURL()\n        remoteUrl.openStream().use { inputStream ->\n            val zipInputStream = ZipInputStream(inputStream)\n            var entry = zipInputStream.nextEntry\n            while (entry != null) {\n                if (entry.name.endsWith(\".json\")) {\n                    val outputStream = BufferedOutputStream(FileOutputStream(file))\n                    val buffer = ByteArray(1024)\n                    var bytesRead: Int\n                    while (zipInputStream.read(buffer).also { bytesRead = it } != -1) {\n                        outputStream.write(buffer, 0, bytesRead)\n                    }\n                    outputStream.close()\n                    break\n                }\n                entry = zipInputStream.nextEntry\n            }\n            zipInputStream.closeEntry()\n            zipInputStream.close()\n        }\n    }\n    val nodes = json.decodeFromString<Snapshot>(file.readText()).nodes\n    nodes.forEach { node ->\n        node.parent = nodes.getOrNull(node.pid)\n        node.parent?.apply {\n            children.add(node)\n        }\n    }\n    return nodes.first().apply {\n        idToSnapshot[githubAssetId] = this\n    }\n}"
  },
  {
    "path": "selector/src/jvmTest/kotlin/li/songe/selector/TypeUnitTest.kt",
    "content": "package li.songe.selector\n\nimport kotlin.test.Test\n\nclass TypeUnitTest {\n\n    @Test\n    fun test() {\n    }\n}"
  },
  {
    "path": "settings.gradle.kts",
    "content": "rootProject.name = \"gkd\"\ninclude(\n    \":app\",\n    \":hidden_api\",\n    \":selector\",\n)\n\npluginManagement {\n    repositories {\n        mavenCentral()\n        google {\n            content {\n                includeGroupByRegex(\"com\\\\.android.*\")\n                includeGroupByRegex(\"com\\\\.google.*\")\n                includeGroupByRegex(\"androidx.*\")\n            }\n        }\n        maven(\"https://jitpack.io\")\n        gradlePluginPortal()\n    }\n}\n\ndependencyResolutionManagement {\n    repositories {\n        mavenCentral()\n        google {\n            content {\n                includeGroupByRegex(\"com\\\\.android.*\")\n                includeGroupByRegex(\"com\\\\.google.*\")\n                includeGroupByRegex(\"androidx.*\")\n            }\n        }\n        maven(\"https://jitpack.io\")\n    }\n}\n"
  },
  {
    "path": "stability_config.conf",
    "content": "li.songe.gkd.*\nkotlin.collections.*\n"
  }
]