├── .gitattributes ├── .github └── workflows │ ├── build.yml │ ├── publish.yml │ └── publish_manually.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── consumer-vendor-rules.pro ├── settings.gradle ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── io │ │ └── github │ │ └── v7lin │ │ └── walle_kit │ │ └── WalleKitPlugin.java ├── walle_kit.gradle ├── walle_kit_v2.gradle └── walle_kit_v3.gradle ├── example ├── .gitignore ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ ├── channel │ │ ├── channel.json │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ │ └── io │ │ │ │ │ └── github │ │ │ │ │ └── v7lin │ │ │ │ │ └── walle_kit_example │ │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── lib │ └── main.dart ├── pubspec.lock ├── pubspec.yaml └── test │ └── widget_test.dart ├── lib ├── src │ ├── model │ │ ├── channel_info.dart │ │ └── channel_info.g.dart │ ├── walle.dart │ ├── walle_kit_method_channel.dart │ └── walle_kit_platform_interface.dart ├── walle_kit.dart └── walle_kit_platform_interface.dart ├── pubspec.yaml └── test ├── walle_kit_method_channel_test.dart └── walle_kit_test.dart /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-language=Dart -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build_android_debug: 7 | name: Build Android Debug on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: [windows-latest, ubuntu-latest, macos-latest] 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-java@v2 16 | with: 17 | distribution: 'zulu' 18 | java-version: '11' 19 | - uses: subosito/flutter-action@v2 20 | with: 21 | channel: 'stable' 22 | - run: flutter --version 23 | - run: flutter pub get 24 | - run: dart format --set-exit-if-changed . 25 | - run: flutter pub publish --dry-run 26 | - run: flutter analyze lib example/lib 27 | - run: cd example; flutter build apk --debug 28 | 29 | build_android_release: 30 | name: Build Android Release on ${{ matrix.os }} 31 | runs-on: ${{ matrix.os }} 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | os: [windows-latest, ubuntu-latest, macos-latest] 36 | steps: 37 | - uses: actions/checkout@v3 38 | - uses: actions/setup-java@v2 39 | with: 40 | distribution: 'zulu' 41 | java-version: '11' 42 | - uses: subosito/flutter-action@v2 43 | with: 44 | channel: 'stable' 45 | - run: flutter --version 46 | - run: flutter pub get 47 | - run: dart format --set-exit-if-changed . 48 | - run: flutter pub publish --dry-run 49 | - run: flutter analyze lib example/lib 50 | - run: cd example; flutter build apk 51 | - run: ls example/build/app/outputs/apk/walle/channels/ 52 | - run: ls example/build/app/outputs/apk/walle/channels/market/ 53 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | publish: 10 | name: Publish 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: subosito/flutter-action@v2 15 | with: 16 | channel: 'stable' 17 | - name: Run pub.dev/inject-credentials@shell 18 | env: 19 | CREDENTIALS: ${{ secrets.CREDENTIALS_JSON }} 20 | run: | 21 | if [ -z $PUB_CACHE ];then 22 | PUB_CACHE=~/.pub-cache 23 | fi 24 | mkdir -p $PUB_CACHE 25 | echo $CREDENTIALS > $PUB_CACHE/credentials.json 26 | - run: flutter --version 27 | - run: flutter pub get 28 | - run: dart format --set-exit-if-changed . 29 | - run: echo "y" | flutter pub publish 30 | -------------------------------------------------------------------------------- /.github/workflows/publish_manually.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 8 | channel: stable 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 17 | base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 18 | - platform: android 19 | create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 20 | base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.4 2 | 3 | * gradle 插件 v2 4 | 5 | ## 3.0.3 6 | 7 | * 360让白嫖党使用CLI 8 | 9 | ## 3.0.2 10 | 11 | * 支持 extraInfo 12 | 13 | ## 3.0.1 14 | 15 | * fix window copy file 16 | 17 | ## 3.0.0 18 | 19 | * 升级Flutter 3.0 20 | 21 | ## 2.0.4 22 | 23 | * bugfix 24 | 25 | ## 2.0.3 26 | 27 | * 兼容 Windows 28 | 29 | ## 2.0.2 30 | 31 | * 优化打包 32 | 33 | ## 2.0.1 34 | 35 | * channel json 36 | 37 | ## 2.0.0 38 | 39 | * nullsafety 40 | * 不再兼容v1插件 41 | * 升级walle 1.1.7 42 | 43 | ## 1.1.1 44 | 45 | * channel json 46 | * 升级walle 1.1.7 47 | 48 | ## 1.1.0 49 | 50 | * 优化 51 | 52 | ## 1.0.1 53 | 54 | * 优化 55 | 56 | ## 1.0.0 57 | 58 | * 优化 59 | 60 | ## 0.1.0 61 | 62 | * 添加文档 63 | 64 | ## 0.0.1 65 | 66 | * walle 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # walle_kit 2 | 3 | [![Pub Package](https://img.shields.io/pub/v/walle_kit.svg)](https://pub.dev/packages/walle_kit) 4 | [![License](https://img.shields.io/github/license/RxReader/walle_kit)](https://github.com/RxReader/walle_kit/blob/master/LICENSE) 5 | 6 | flutter版walle多渠道打包工具 7 | 8 | ## 相关工具 9 | 10 | * [Flutter版微信SDK](https://github.com/RxReader/wechat_kit) 11 | * [Flutter版腾讯(QQ)SDK](https://github.com/RxReader/tencent_kit) 12 | * [Flutter版新浪微博SDK](https://github.com/RxReader/weibo_kit) 13 | * [Flutter版支付宝SDK](https://github.com/RxReader/alipay_kit) 14 | * [Flutter版深度链接](https://github.com/RxReader/link_kit) 15 | * [Flutter版walle渠道打包工具](https://github.com/RxReader/walle_kit) 16 | 17 | ## dart/flutter 私服 18 | 19 | * [simple_pub_server](https://github.com/rxreader/simple_pub_server) 20 | 21 | ## docs 22 | 23 | * [Meituan-Dianping/walle](https://github.com/Meituan-Dianping/walle) 24 | * [rxreader/qihoo360-jiagu-docker](https://github.com/rxreader/qihoo360-jiagu-docker) 25 | * [rxreader/tencentcloud-legu](https://github.com/rxreader/tencentcloud-legu) 26 | * [移动安全-应用加固命令行工具jar包使用说明](https://cloud.tencent.com/developer/article/1193406) 27 | * [腾讯云·访问管理](https://console.cloud.tencent.com/cam/capi) 28 | * [腾讯云·移动应用安全](https://console.cloud.tencent.com/ms/reinforce/list) 29 | 30 | ## Android 31 | 32 | > ⚠️ 360不让白嫖党使用CLI 33 | 34 | > ⚠️⚠️⚠️ 辣鸡加固服务,全特么翻车 ... 大家伙洗洗睡吧,360加固/腾讯乐固已死 ... 35 | ```shell 36 | Failure [-124: Failed parse during installPackageLI: Targeting R+ (version 30 and above) requires the resources.arsc of installed APKs to be stored uncompressed and aligned on a 4-byte boundary] 37 | ``` 38 | ```shell 39 | # 解决方案 - 对 360 加固有效,对腾讯乐固无效(会报错) 40 | zipalign -p -f -v 4 input.apk output.apk 41 | ``` 42 | 43 | * apply 44 | 45 | ```groovy 46 | // android/app/build.gradle 47 | apply from: "${project(":walle_kit").projectDir}/walle_kit_v3.gradle" // 推荐(非cli方式,不支持360加固、不支持腾讯乐固) 48 | // 或 49 | apply from: "${project(":walle_kit").projectDir}/walle_kit_v2.gradle" // 推荐(非cli方式,不支持360加固、支持腾讯乐固) 50 | // 或 51 | apply from: "${project(":walle_kit").projectDir}/walle_kit.gradle" // 不推荐(cli方式,支持360加固、支持腾讯乐固) 52 | ``` 53 | 54 | * fileNameFormat 55 | 56 | ```groovy 57 | // appName: 58 | // projectName: 59 | // buildType: 60 | // versionName: 61 | // versionCode: 62 | // packageName: 63 | // flavorName: 64 | // channelId: 65 | ``` 66 | 67 | * channelFile 68 | * [配置文件示例 - channel](example/android/app/channel) 69 | * [配置文件示例 - channel.json](example/android/app/channel.json) 70 | 71 | ### walle_kit_v3.gradle 72 | 73 | > 应用宝上架,已不再强制要求加固 74 | > 用法同 walle_kit_v2.gradle 一致,不同的是已不再支持腾讯乐固功能 75 | 76 | ### walle_kit_v2.gradle 77 | 78 | * without flavors 79 | 80 | ```groovy 81 | // android/app/build.gradle 82 | walle { 83 | enabled = true 84 | 85 | // [访问管理](https://console.cloud.tencent.com/cam/capi) 86 | // [移动应用安全](https://console.cloud.tencent.com/ms/reinforce/list) 87 | tencent { 88 | secretId = 'xxx' 89 | secretKey = 'xxx' 90 | // region = 'ap-guangzhou' // 可选:'ap-guangzhou'、'ap-shanghai',默认:'ap-guangzhou' 91 | channels = ['tencent', 'tencent-alias'] 92 | } 93 | 94 | outputDir = file("${project.buildDir}/outputs/apk/walle") // 默认:file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") 95 | fileNameFormat = '${appName}-${buildType}-${channelId}.apk' // 默认:'${appName}-${buildType}-${channelId}.apk' 96 | channelFile = file('channel') 97 | } 98 | ``` 99 | 100 | ```groovy 101 | // android/app/build.gradle 102 | android { 103 | walleConfigs { 104 | release { 105 | enabled = true 106 | 107 | // [访问管理](https://console.cloud.tencent.com/cam/capi) 108 | // [移动应用安全](https://console.cloud.tencent.com/ms/reinforce/list) 109 | tencent { 110 | secretId = 'xxx' 111 | secretKey = 'xxx' 112 | // region = 'ap-guangzhou' // 可选:'ap-guangzhou'、'ap-shanghai',默认:'ap-guangzhou' 113 | channels = ['tencent', 'tencent-alias'] 114 | } 115 | 116 | outputDir = file("${project.buildDir}/outputs/apk/walle") // 默认:file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") 117 | fileNameFormat = '${appName}-${buildType}-${channelId}.apk' // 默认:'${appName}-${buildType}-${channelId}.apk' 118 | channelFile = file('channel') 119 | } 120 | } 121 | } 122 | 123 | walle { 124 | enabled = false 125 | } 126 | ``` 127 | 128 | * flavors 129 | 130 | ```groovy 131 | // android/app/build.gradle 132 | android { 133 | productFlavors { 134 | prod { 135 | } 136 | } 137 | 138 | walleConfigs { 139 | prod { 140 | enabled = true 141 | 142 | // [访问管理](https://console.cloud.tencent.com/cam/capi) 143 | // [移动应用安全](https://console.cloud.tencent.com/ms/reinforce/list) 144 | tencent { 145 | secretId = 'xxx' 146 | secretKey = 'xxx' 147 | // region = 'ap-guangzhou' // 可选:'ap-guangzhou'、'ap-shanghai',默认:'ap-guangzhou' 148 | channels = ['tencent', 'tencent-alias'] 149 | } 150 | 151 | outputDir = file("${project.buildDir}/outputs/apk/walle") // 默认:file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") 152 | fileNameFormat = '${appName}-${buildType}-${channelId}.apk' // 默认:'${appName}-${buildType}-${channelId}.apk' 153 | channelFile = file('channel') 154 | } 155 | } 156 | } 157 | 158 | walle { 159 | enabled = false 160 | } 161 | ``` 162 | 163 | ### walle_kit.gradle 164 | 165 | * without flavors 166 | 167 | ```groovy 168 | // android/app/build.gradle 169 | walle { 170 | enabled = true 171 | 172 | // // https://github.com/rxreader/walle-docker 173 | // jarFile = file('script/walle-cli-all.jar') // 默认:file('script/walle-cli-all.jar') 174 | 175 | qihoo360 { 176 | // // https://github.com/rxreader/qihoo360-jiagu-docker 177 | // jiaguJarFile = file('script/jiagu/jiagu.jar') // 默认:file('script/jiagu/jiagu.jar') 178 | 179 | account = 'xxx' 180 | password = 'xxx' 181 | channels = ['qihu360', 'qihu360-alias'] 182 | } 183 | 184 | // [访问管理](https://console.cloud.tencent.com/cam/capi) 185 | // [移动应用安全](https://console.cloud.tencent.com/ms/reinforce/list) 186 | tencent { 187 | // // https://github.com/rxreader/tencentcloud-legu 188 | // leguJarFile = file('script/legu-all.jar') // 默认:file('script/legu-all.jar') 189 | 190 | secretId = 'xxx' 191 | secretKey = 'xxx' 192 | // region = 'ap-guangzhou' // 可选:'ap-guangzhou'、'ap-shanghai',默认:'ap-guangzhou' 193 | channels = ['tencent', 'tencent-alias'] 194 | } 195 | 196 | outputDir = file("${project.buildDir}/outputs/apk/walle") // 默认:file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") 197 | fileNameFormat = '${appName}-${buildType}-${channelId}.apk' // 默认:'${appName}-${buildType}-${channelId}.apk' 198 | channelFile = file('channel') 199 | } 200 | ``` 201 | 202 | ```groovy 203 | // android/app/build.gradle 204 | android { 205 | walleConfigs { 206 | release { 207 | enabled = true 208 | 209 | // // https://github.com/rxreader/walle-docker 210 | // jarFile = file('script/walle-cli-all.jar') // 默认:file('script/walle-cli-all.jar') 211 | 212 | qihoo360 { 213 | // // https://github.com/rxreader/qihoo360-jiagu-docker 214 | // jiaguJarFile = file('script/jiagu/jiagu.jar') // 默认:file('script/jiagu/jiagu.jar') 215 | 216 | account = 'xxx' 217 | password = 'xxx' 218 | channels = ['qihu360', 'qihu360-alias'] 219 | } 220 | 221 | // [访问管理](https://console.cloud.tencent.com/cam/capi) 222 | // [移动应用安全](https://console.cloud.tencent.com/ms/reinforce/list) 223 | tencent { 224 | // // https://github.com/rxreader/tencentcloud-legu 225 | // leguJarFile = file('script/legu-all.jar') // 默认:file('script/legu-all.jar') 226 | 227 | secretId = 'xxx' 228 | secretKey = 'xxx' 229 | // region = 'ap-guangzhou' // 可选:'ap-guangzhou'、'ap-shanghai',默认:'ap-guangzhou' 230 | channels = ['tencent', 'tencent-alias'] 231 | } 232 | 233 | outputDir = file("${project.buildDir}/outputs/apk/walle") // 默认:file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") 234 | fileNameFormat = '${appName}-${buildType}-${channelId}.apk' // 默认:'${appName}-${buildType}-${channelId}.apk' 235 | channelFile = file('channel') 236 | } 237 | } 238 | } 239 | 240 | walle { 241 | enabled = false 242 | } 243 | ``` 244 | 245 | * flavors 246 | 247 | ```groovy 248 | // android/app/build.gradle 249 | android { 250 | productFlavors { 251 | prod { 252 | } 253 | } 254 | 255 | walleConfigs { 256 | prod { 257 | enabled = true 258 | 259 | // // https://github.com/rxreader/walle-docker 260 | // jarFile = file('script/walle-cli-all.jar') // 默认:file('script/walle-cli-all.jar') 261 | 262 | qihoo360 { 263 | // // https://github.com/rxreader/qihoo360-jiagu-docker 264 | // jiaguJarFile = file('script/jiagu/jiagu.jar') // 默认:file('script/jiagu/jiagu.jar') 265 | 266 | account = 'xxx' 267 | password = 'xxx' 268 | channels = ['qihu360', 'qihu360-alias'] 269 | } 270 | 271 | // [访问管理](https://console.cloud.tencent.com/cam/capi) 272 | // [移动应用安全](https://console.cloud.tencent.com/ms/reinforce/list) 273 | tencent { 274 | // // https://github.com/rxreader/tencentcloud-legu 275 | // leguJarFile = file('script/legu-all.jar') // 默认:file('script/legu-all.jar') 276 | 277 | secretId = 'xxx' 278 | secretKey = 'xxx' 279 | // region = 'ap-guangzhou' // 可选:'ap-guangzhou'、'ap-shanghai',默认:'ap-guangzhou' 280 | channels = ['tencent', 'tencent-alias'] 281 | } 282 | 283 | outputDir = file("${project.buildDir}/outputs/apk/walle") // 默认:file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") 284 | fileNameFormat = '${appName}-${buildType}-${channelId}.apk' // 默认:'${appName}-${buildType}-${channelId}.apk' 285 | channelFile = file('channel') 286 | } 287 | } 288 | } 289 | 290 | walle { 291 | enabled = false 292 | } 293 | ``` 294 | 295 | ## Flutter 296 | 297 | * snapshot 298 | 299 | ```yaml 300 | dependencies: 301 | walle_kit: 302 | git: 303 | url: https://github.com/rxreader/walle_kit.git 304 | ``` 305 | 306 | * release 307 | 308 | ```yaml 309 | dependencies: 310 | walle_kit: ^${latestTag} 311 | ``` 312 | 313 | ## Star History 314 | 315 | ![stars](https://starchart.cc/rxreader/walle_kit.svg) 316 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Specify analysis options. 2 | # 3 | # Until there are meta linter rules, each desired lint must be explicitly enabled. 4 | # See: https://github.com/dart-lang/linter/issues/288 5 | # 6 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 7 | # See the configuration guide for more 8 | # https://github.com/dart-lang/sdk/tree/main/pkg/analyzer#configuring-the-analyzer 9 | # 10 | # There are other similar analysis options files in the flutter repos, 11 | # which should be kept in sync with this file: 12 | # 13 | # - analysis_options.yaml (this file) 14 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml 15 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml 16 | # - https://github.com/flutter/packages/blob/master/analysis_options.yaml 17 | # 18 | # This file contains the analysis options used by Flutter tools, such as IntelliJ, 19 | # Android Studio, and the `flutter analyze` command. 20 | 21 | analyzer: 22 | language: 23 | strict-raw-types: true 24 | errors: 25 | # treat missing required parameters as a warning (not a hint) 26 | missing_required_param: warning 27 | # treat missing returns as a warning (not a hint) 28 | missing_return: warning 29 | # allow having TODO comments in the code 30 | todo: ignore 31 | # allow self-reference to deprecated members (we do this because otherwise we have 32 | # to annotate every member in every test, assert, etc, when we deprecate something) 33 | deprecated_member_use_from_same_package: ignore 34 | # TODO(ianh): https://github.com/flutter/flutter/issues/74381 35 | # Clean up existing unnecessary imports, and remove line to ignore. 36 | unnecessary_import: ignore 37 | # Turned off until null-safe rollout is complete. 38 | unnecessary_null_comparison: ignore 39 | exclude: 40 | - "bin/cache/**" 41 | # Ignore protoc generated files 42 | - "dev/conductor/lib/proto/*" 43 | # 44 | - "lib/*.g.dart" 45 | - "lib/**/*.g.dart" 46 | 47 | linter: 48 | rules: 49 | # these rules are documented on and in the same order as 50 | # the Dart Lint rules page to make maintenance easier 51 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 52 | - always_declare_return_types 53 | - always_put_control_body_on_new_line 54 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 55 | - always_require_non_null_named_parameters 56 | - always_specify_types 57 | - always_use_package_imports # we do this commonly 58 | - annotate_overrides 59 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 60 | - avoid_bool_literals_in_conditional_expressions 61 | # - avoid_catches_without_on_clauses # blocked on https://github.com/dart-lang/linter/issues/3023 62 | # - avoid_catching_errors # blocked on https://github.com/dart-lang/linter/issues/3023 63 | - avoid_classes_with_only_static_members 64 | - avoid_double_and_int_checks 65 | - avoid_dynamic_calls 66 | - avoid_empty_else 67 | - avoid_equals_and_hash_code_on_mutable_classes 68 | - avoid_escaping_inner_quotes 69 | - avoid_field_initializers_in_const_classes 70 | # - avoid_final_parameters # incompatible with prefer_final_parameters 71 | - avoid_function_literals_in_foreach_calls 72 | - avoid_implementing_value_types 73 | - avoid_init_to_null 74 | - avoid_js_rounded_ints 75 | # - avoid_multiple_declarations_per_line # seems to be a stylistic choice we don't subscribe to 76 | - avoid_null_checks_in_equality_operators 77 | # - avoid_positional_boolean_parameters # would have been nice to enable this but by now there's too many places that break it 78 | - avoid_print 79 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 80 | # - avoid_redundant_argument_values 81 | - avoid_relative_lib_imports 82 | - avoid_renaming_method_parameters 83 | - avoid_return_types_on_setters 84 | # - avoid_returning_null # still violated by some pre-nnbd code that we haven't yet migrated 85 | - avoid_returning_null_for_future 86 | - avoid_returning_null_for_void 87 | # - avoid_returning_this # there are enough valid reasons to return `this` that this lint ends up with too many false positives 88 | - avoid_setters_without_getters 89 | - avoid_shadowing_type_parameters 90 | - avoid_single_cascade_in_expression_statements 91 | - avoid_slow_async_io 92 | - avoid_type_to_string 93 | - avoid_types_as_parameter_names 94 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 95 | - avoid_unnecessary_containers 96 | - avoid_unused_constructor_parameters 97 | - avoid_void_async 98 | # - avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere 99 | - await_only_futures 100 | - camel_case_extensions 101 | - camel_case_types 102 | - cancel_subscriptions 103 | # - cascade_invocations # doesn't match the typical style of this repo 104 | - cast_nullable_to_non_nullable 105 | # - close_sinks # not reliable enough 106 | # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 107 | # - conditional_uri_does_not_exist # not yet tested 108 | # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 109 | - control_flow_in_finally 110 | # - curly_braces_in_flow_control_structures # not required by flutter style 111 | - depend_on_referenced_packages 112 | - deprecated_consistency 113 | # - diagnostic_describe_all_properties # enabled only at the framework level (packages/flutter/lib) 114 | - directives_ordering 115 | # - do_not_use_environment # there are appropriate times to use the environment, especially in our tests and build logic 116 | - empty_catches 117 | - empty_constructor_bodies 118 | - empty_statements 119 | - eol_at_end_of_file 120 | - exhaustive_cases 121 | - file_names 122 | - flutter_style_todos 123 | - hash_and_equals 124 | - implementation_imports 125 | # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 126 | - iterable_contains_unrelated_type 127 | # - join_return_with_assignment # not required by flutter style 128 | - leading_newlines_in_multiline_strings 129 | - library_names 130 | - library_prefixes 131 | - library_private_types_in_public_api 132 | # - lines_longer_than_80_chars # not required by flutter style 133 | - list_remove_unrelated_type 134 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/linter/issues/453 135 | - missing_whitespace_between_adjacent_strings 136 | - no_adjacent_strings_in_list 137 | - no_default_cases 138 | - no_duplicate_case_values 139 | - no_leading_underscores_for_library_prefixes 140 | - no_leading_underscores_for_local_identifiers 141 | - no_logic_in_create_state 142 | # - no_runtimeType_toString # ok in tests; we enable this only in packages/ 143 | - non_constant_identifier_names 144 | - noop_primitive_operations 145 | - null_check_on_nullable_type_parameter 146 | - null_closures 147 | # - omit_local_variable_types # opposite of always_specify_types 148 | # - one_member_abstracts # too many false positives 149 | - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al 150 | - overridden_fields 151 | - package_api_docs 152 | - package_names 153 | - package_prefixed_library_names 154 | # - parameter_assignments # we do this commonly 155 | - prefer_adjacent_string_concatenation 156 | - prefer_asserts_in_initializer_lists 157 | # - prefer_asserts_with_message # not required by flutter style 158 | - prefer_collection_literals 159 | - prefer_conditional_assignment 160 | # - prefer_const_constructors 161 | - prefer_const_constructors_in_immutables 162 | - prefer_const_declarations 163 | # - prefer_const_literals_to_create_immutables 164 | # - prefer_constructors_over_static_methods # far too many false positives 165 | - prefer_contains 166 | # - prefer_double_quotes # opposite of prefer_single_quotes 167 | - prefer_equal_for_default_values 168 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 169 | - prefer_final_fields 170 | - prefer_final_in_for_each 171 | - prefer_final_locals 172 | # - prefer_final_parameters # we should enable this one day when it can be auto-fixed (https://github.com/dart-lang/linter/issues/3104), see also parameter_assignments 173 | - prefer_for_elements_to_map_fromIterable 174 | - prefer_foreach 175 | - prefer_function_declarations_over_variables 176 | - prefer_generic_function_type_aliases 177 | - prefer_if_elements_to_conditional_expressions 178 | - prefer_if_null_operators 179 | - prefer_initializing_formals 180 | - prefer_inlined_adds 181 | # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants 182 | - prefer_interpolation_to_compose_strings 183 | - prefer_is_empty 184 | - prefer_is_not_empty 185 | - prefer_is_not_operator 186 | - prefer_iterable_whereType 187 | # - prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018 188 | # - prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere 189 | - prefer_null_aware_operators 190 | # - prefer_relative_imports 191 | - prefer_single_quotes 192 | - prefer_spread_collections 193 | - prefer_typing_uninitialized_variables 194 | - prefer_void_to_null 195 | - provide_deprecation_message 196 | # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml 197 | - recursive_getters 198 | # - require_trailing_commas # blocked on https://github.com/dart-lang/sdk/issues/47441 199 | - secure_pubspec_urls 200 | - sized_box_for_whitespace 201 | # - sized_box_shrink_expand # not yet tested 202 | - slash_for_doc_comments 203 | - sort_child_properties_last 204 | - sort_constructors_first 205 | # - sort_pub_dependencies # prevents separating pinned transitive dependencies 206 | - sort_unnamed_constructors_first 207 | - test_types_in_equals 208 | - throw_in_finally 209 | - tighten_type_of_initializing_formals 210 | # - type_annotate_public_apis # subset of always_specify_types 211 | - type_init_formals 212 | # - unawaited_futures # too many false positives, especially with the way AnimationController works 213 | - unnecessary_await_in_return 214 | - unnecessary_brace_in_string_interps 215 | - unnecessary_const 216 | - unnecessary_constructor_name 217 | # - unnecessary_final # conflicts with prefer_final_locals 218 | - unnecessary_getters_setters 219 | # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 220 | - unnecessary_late 221 | - unnecessary_new 222 | - unnecessary_null_aware_assignments 223 | - unnecessary_null_checks 224 | - unnecessary_null_in_if_null_operators 225 | - unnecessary_nullable_for_final_variable_declarations 226 | - unnecessary_overrides 227 | - unnecessary_parenthesis 228 | # - unnecessary_raw_strings # what's "necessary" is a matter of opinion; consistency across strings can help readability more than this lint 229 | - unnecessary_statements 230 | - unnecessary_string_escapes 231 | - unnecessary_string_interpolations 232 | - unnecessary_this 233 | - unrelated_type_equality_checks 234 | - unsafe_html 235 | - use_build_context_synchronously 236 | # - use_decorated_box # not yet tested 237 | - use_full_hex_values_for_flutter_colors 238 | - use_function_type_syntax_for_parameters 239 | - use_if_null_to_convert_nulls_to_bools 240 | - use_is_even_rather_than_modulo 241 | - use_key_in_widget_constructors 242 | - use_late_for_private_fields_and_variables 243 | - use_named_constants 244 | - use_raw_strings 245 | - use_rethrow_when_possible 246 | - use_setters_to_change_properties 247 | # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 248 | - use_test_throws_matchers 249 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 250 | - valid_regexps 251 | - void_checks 252 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .cxx 10 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | File pubspec = new File(project.projectDir.parentFile, 'pubspec.yaml') 2 | String yaml = pubspec.text 3 | // Using \s*['|"]?([^\n|'|"]*)['|"]? to extract version number. 4 | java.util.regex.Matcher versionMatcher = java.util.regex.Pattern.compile("^version:\\s*['|\"]?([^\\n|'|\"]*)['|\"]?\$", java.util.regex.Pattern.MULTILINE).matcher(yaml) 5 | versionMatcher.find() 6 | String library_version = versionMatcher.group(1).replaceAll("\\+", "-") 7 | 8 | group 'io.github.v7lin.walle_kit' 9 | version library_version 10 | 11 | buildscript { 12 | repositories { 13 | google() 14 | mavenCentral() 15 | } 16 | 17 | dependencies { 18 | classpath 'com.android.tools.build:gradle:7.1.2' 19 | } 20 | } 21 | 22 | rootProject.allprojects { 23 | repositories { 24 | google() 25 | mavenCentral() 26 | 27 | // 阿里云jcenter镜像 28 | maven { url 'https://maven.aliyun.com/repository/jcenter' } 29 | } 30 | } 31 | 32 | apply plugin: 'com.android.library' 33 | 34 | android { 35 | compileSdkVersion 31 36 | 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | 42 | defaultConfig { 43 | minSdkVersion 16 44 | 45 | // library 混淆 -> 随 library 引用,自动添加到 apk 打包混淆 46 | consumerProguardFiles 'consumer-rules.pro' 47 | } 48 | 49 | flavorDimensions 'vendor' 50 | 51 | productFlavors { 52 | vendor { 53 | dimension 'vendor' 54 | 55 | // library 混淆 -> 随 library 引用,自动添加到 apk 打包混淆 56 | consumerProguardFiles 'consumer-vendor-rules.pro' 57 | } 58 | } 59 | } 60 | 61 | dependencies { 62 | vendorImplementation 'com.meituan.android.walle:library:1.1.7' 63 | } 64 | -------------------------------------------------------------------------------- /android/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxReader/walle_kit/5b1ee6e96598b047890d3bdaea84f2950ac33c43/android/consumer-rules.pro -------------------------------------------------------------------------------- /android/consumer-vendor-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxReader/walle_kit/5b1ee6e96598b047890d3bdaea84f2950ac33c43/android/consumer-vendor-rules.pro -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'walle_kit' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/java/io/github/v7lin/walle_kit/WalleKitPlugin.java: -------------------------------------------------------------------------------- 1 | package io.github.v7lin.walle_kit; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import com.meituan.android.walle.ChannelInfo; 8 | import com.meituan.android.walle.WalleChannelReader; 9 | 10 | import java.util.HashMap; 11 | 12 | import io.flutter.embedding.engine.plugins.FlutterPlugin; 13 | import io.flutter.plugin.common.MethodCall; 14 | import io.flutter.plugin.common.MethodChannel; 15 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 16 | import io.flutter.plugin.common.MethodChannel.Result; 17 | 18 | /** 19 | * WalleKitPlugin 20 | */ 21 | public class WalleKitPlugin implements FlutterPlugin, MethodCallHandler { 22 | /// The MethodChannel that will the communication between Flutter and native Android 23 | /// 24 | /// This local reference serves to register the plugin with the Flutter Engine and unregister it 25 | /// when the Flutter Engine is detached from the Activity 26 | private MethodChannel channel; 27 | private Context applicationContext; 28 | 29 | // --- FlutterPlugin 30 | 31 | @Override 32 | public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { 33 | channel = new MethodChannel(binding.getBinaryMessenger(), "v7lin.github.io/walle_kit"); 34 | channel.setMethodCallHandler(this); 35 | applicationContext = binding.getApplicationContext(); 36 | } 37 | 38 | @Override 39 | public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { 40 | channel.setMethodCallHandler(null); 41 | channel = null; 42 | applicationContext = null; 43 | } 44 | 45 | // --- MethodCallHandler 46 | 47 | @Override 48 | public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { 49 | if ("getChannelId".equals(call.method)) { 50 | result.success(WalleChannelReader.getChannel(applicationContext)); 51 | } else if ("getChannelInfo".equals(call.method)) { 52 | final ChannelInfo info = WalleChannelReader.getChannelInfo(applicationContext); 53 | result.success(info != null ? new HashMap() { 54 | { 55 | put("channel", info.getChannel()); 56 | put("extra_info", info.getExtraInfo()); 57 | } 58 | } : null); 59 | } else { 60 | result.notImplemented(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /android/walle_kit.gradle: -------------------------------------------------------------------------------- 1 | // 2 | //使用方法 3 | // 4 | //apply from: 'walle.gradle' 5 | // 6 | //android { 7 | // productFlavors { 8 | // prod {...} 9 | // } 10 | // 11 | // walleConfigs { 12 | // prod { 13 | // enabled = true 14 | // 15 | //// // https://github.com/rxreader/walle-docker 16 | //// jarFile = file('script/walle-cli-all-1.1.7.jar') // 默认:file('script/walle-cli-all-1.1.7.jar') 17 | // 18 | // qihoo360 { 19 | //// // https://github.com/rxreader/qihoo360-jiagu-docker 20 | //// jiaguJarFile = file('script/jiagu/jiagu.jar') // 默认:file('script/jiagu/jiagu.jar') 21 | // 22 | // account = 'xxx' 23 | // password = 'xxx' 24 | // channels = ['qihu360', 'qihu360-alias'] 25 | // } 26 | // 27 | // // [访问管理](https://console.cloud.tencent.com/cam/capi) 28 | // // [移动应用安全](https://console.cloud.tencent.com/ms/reinforce/list) 29 | // tencent { 30 | //// // https://github.com/rxreader/tencentcloud-legu 31 | //// leguJarFile = file('script/legu-all.jar') // 默认:file('script/legu-all.jar') 32 | // 33 | // secretId = 'xxx' 34 | // secretKey = 'xxx' 35 | //// region = 'ap-guangzhou' // 可选:'ap-guangzhou'、'ap-shanghai',默认:'ap-guangzhou' 36 | // channels = ['tencent', 'tencent-alias'] 37 | // } 38 | // 39 | // outputDir = file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") // 默认:file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") 40 | // fileNameFormat = '${appName}-${buildType}-${channelId}.apk' // 默认:'${appName}-${buildType}-${channelId}.apk' 41 | //// channelType = 0 // 0:默认;1:json 42 | // channelFile = file('channel') 43 | // } 44 | // } 45 | //} 46 | // 47 | //walle { 48 | // enabled = false 49 | //} 50 | // 51 | 52 | buildscript { 53 | repositories { 54 | google() 55 | mavenCentral() 56 | } 57 | dependencies { 58 | classpath 'com.android.tools.build:gradle:7.1.2' 59 | } 60 | } 61 | 62 | android { 63 | compileOptions { 64 | sourceCompatibility JavaVersion.VERSION_1_8 65 | targetCompatibility JavaVersion.VERSION_1_8 66 | } 67 | } 68 | 69 | class Qihoo360 { 70 | File jiaguJarFile 71 | String account 72 | String password 73 | List channels 74 | 75 | void validate(String variant) { 76 | if (account == null || account.empty) { 77 | throw new RuntimeException("Qihoo360 account is empty for variant '$variant'") 78 | } 79 | if (password == null || password.empty) { 80 | throw new RuntimeException("Qihoo360 password is empty for variant '$variant'") 81 | } 82 | if (channels == null || channels.empty) { 83 | throw new RuntimeException("Qihoo360 channels is empty for variant '$variant'") 84 | } 85 | } 86 | } 87 | 88 | class Tencent { 89 | File leguJarFile 90 | String secretId 91 | String secretKey 92 | String region 93 | List channels 94 | 95 | void validate(String variant) { 96 | if (secretId == null || secretId.empty) { 97 | throw new RuntimeException("Tencent secretId is empty for variant '$variant'") 98 | } 99 | if (secretKey == null || secretKey.empty) { 100 | throw new RuntimeException("Tencent secretKey is empty for variant '$variant'") 101 | } 102 | if (channels == null || channels.empty) { 103 | throw new RuntimeException("Tencent channels is empty for variant '$variant'") 104 | } 105 | } 106 | } 107 | 108 | class Walle { 109 | final String name 110 | Boolean enabled 111 | File jarFile 112 | Qihoo360 qihoo360 113 | Tencent tencent 114 | File outputDir 115 | String fileNameFormat 116 | Integer channelType 117 | File channelFile 118 | 119 | Walle(String name = 'default') { 120 | this.name = name 121 | } 122 | 123 | void qihoo360(Closure closure){ 124 | qihoo360 = new Qihoo360() 125 | closure.delegate = qihoo360 126 | closure() 127 | } 128 | 129 | void tencent(Closure closure){ 130 | tencent = new Tencent() 131 | closure.delegate = tencent 132 | closure() 133 | } 134 | 135 | // --- 136 | 137 | void validate() { 138 | if (enabled == null || !enabled.booleanValue()) { 139 | return 140 | } 141 | qihoo360?.validate(name) 142 | tencent?.validate(name) 143 | if (channelType != null && channelType != 0 && channelType != 1) { 144 | throw new RuntimeException("walle channel type is unsupported for variant '$name'") 145 | } 146 | if (channelFile == null) { 147 | throw new RuntimeException("walle channel file is null for variant '$name'") 148 | } 149 | } 150 | 151 | Walle mergeWith(Walle other) { 152 | if (other == null) { 153 | return this 154 | } 155 | Walle mergeWalle = new Walle(name == 'default' ? other.name : (other.name == 'default' ? name : "$name${other.name.capitalize()}")) 156 | mergeWalle.enabled = other.enabled != null ? other.enabled : enabled 157 | mergeWalle.jarFile = other.jarFile ?: jarFile 158 | mergeWalle.qihoo360 = other.qihoo360 ?: qihoo360 159 | mergeWalle.tencent = other.tencent ?: tencent 160 | mergeWalle.outputDir = other.outputDir ?: outputDir 161 | mergeWalle.fileNameFormat = other.fileNameFormat ?: fileNameFormat 162 | mergeWalle.channelType = other.channelType ?: channelType 163 | mergeWalle.channelFile = other.channelFile ?: channelFile 164 | return mergeWalle 165 | } 166 | } 167 | 168 | apply plugin: WallePlugin 169 | 170 | class WallePlugin implements Plugin { 171 | 172 | @Override 173 | void apply(Project target) { 174 | target.extensions.create('walle', Walle.class) 175 | target.plugins.withId('com.android.application') { 176 | Walle baseWalle = target.walle 177 | def walleConfigs = target.container(Walle.class) 178 | target.android.extensions.walleConfigs = walleConfigs 179 | target.android.applicationVariants.whenObjectAdded { variant -> 180 | Walle mergeWalle = null 181 | List flavorWalles = variant.productFlavors?.stream()?.map{flavor -> walleConfigs.findByName(flavor.name)}?.collect()?.toList() ?: Collections.emptyList() 182 | Walle buildTypeWalle = walleConfigs.findByName(variant.buildType.name) 183 | if (buildTypeWalle == null && (variant.buildType.name == 'debug' || variant.buildType.name == 'profile')) { 184 | buildTypeWalle = new Walle(variant.buildType.name) 185 | buildTypeWalle.enabled = false 186 | } 187 | // buildType > flavor > base 188 | List walles = [] 189 | walles.add(baseWalle) 190 | walles.addAll(flavorWalles) 191 | walles.add(buildTypeWalle) 192 | for (Walle walle in walles) { 193 | if (mergeWalle == null) { 194 | mergeWalle = walle 195 | } else { 196 | mergeWalle = mergeWalle.mergeWith(walle) 197 | } 198 | } 199 | 200 | mergeWalle?.validate() 201 | 202 | variant.assemble.doLast { 203 | if (mergeWalle == null || mergeWalle.enabled == null || !mergeWalle.enabled.booleanValue()) { 204 | target.logger.info("Gradle Walle is disabled for variant '${variant.name}'.") 205 | return 206 | } 207 | 208 | if (!variant.signingReady && !variant.outputsAreSigned) { 209 | target.logger.error("Signing not ready for Gradle Walle. Be sure to specify a signingConfig for variant '${variant.name}'.") 210 | return 211 | } 212 | 213 | if (!org.gradle.internal.os.OperatingSystem.current().isMacOsX() && !org.gradle.internal.os.OperatingSystem.current().isLinux() && !org.gradle.internal.os.OperatingSystem.current().isWindows()) { 214 | target.logger.info("Gradle Walle 仅能运行于 MacOS/Linux/Windows,不能运行于 ${org.gradle.internal.os.OperatingSystem.current().osName}。") 215 | return 216 | } 217 | 218 | println '--- walle ---' 219 | 220 | File apkFile = variant.outputs.first().outputFile as File 221 | println "apk file: ${apkFile.path}" 222 | 223 | boolean v2SigningEnabled = v2SigningEnabled(variant) 224 | println "v2SigningEnabled: ${v2SigningEnabled}" 225 | if (!v2SigningEnabled) { 226 | throw new RuntimeException("${apkFile.path} has no v2 signature in Apk Signing Block!") 227 | } 228 | 229 | // 预备输出目录 230 | File outputDir = mergeWalle.outputDir 231 | if (outputDir == null) { 232 | outputDir = new File(apkFile.parentFile, 'walle') 233 | } 234 | File channelsDir = new File(outputDir, 'channels') 235 | if (!channelsDir.exists()) { 236 | channelsDir.mkdirs() 237 | } 238 | 239 | def nameVariantMap = [ 240 | 'appName' : target.name, 241 | 'projectName': target.rootProject.name, 242 | 'buildType' : variant.buildType.name, 243 | 'versionName': variant.versionName, 244 | 'versionCode': variant.versionCode, 245 | 'packageName': variant.applicationId, 246 | 'flavorName' : variant.flavorName, 247 | 'channelId': 'channel' 248 | ] 249 | 250 | String fileNameFormat = mergeWalle.fileNameFormat ?: '${appName}-${buildType}-${channelId}.apk' 251 | File targetApkFile = new File(outputDir, new groovy.text.SimpleTemplateEngine().createTemplate(fileNameFormat).make(nameVariantMap).toString())// new File(outputDir, apkFile.name) 252 | 253 | // 复制 254 | java.nio.file.Files.copy(apkFile.toPath(), targetApkFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) 255 | 256 | println "target apk file: ${targetApkFile.path}" 257 | 258 | // 读取渠道信息 259 | def channels = [] 260 | if (mergeWalle.channelType == 1) { 261 | def slurper = new groovy.json.JsonSlurper() 262 | channels = slurper.parse(mergeWalle.channelFile) 263 | } else { 264 | mergeWalle.channelFile.eachLine { line -> 265 | String lineTrim = line.trim() 266 | if (lineTrim.length() != 0 && !lineTrim.startsWith("#")) { 267 | def channelId = line.split("#").first().trim() 268 | if (channelId.length() != 0) { 269 | channels.add(['alias': channelId, 'channelId': channelId]) 270 | } 271 | } 272 | } 273 | } 274 | 275 | channels.each { channel -> 276 | nameVariantMap['channelId'] = channel.channelId 277 | File storeDir = channelsDir 278 | if (channel.storeDir != null) { 279 | storeDir = new File(storeDir, channel.storeDir) 280 | if (!storeDir.exists()) { 281 | storeDir.mkdirs() 282 | } 283 | } 284 | File channelApkFile = new File(storeDir, new groovy.text.SimpleTemplateEngine().createTemplate(fileNameFormat).make(nameVariantMap).toString()) 285 | 286 | File originalApkFile 287 | if (mergeWalle.qihoo360?.channels?.contains(channel.channelId) ?: false) { 288 | originalApkFile = qihoo360JiaguApk(target, variant, mergeWalle, targetApkFile, outputDir) 289 | } else if (mergeWalle.tencent?.channels?.contains(channel.channelId) ?: false) { 290 | originalApkFile = tencentLeguApk(target, variant, mergeWalle, targetApkFile, outputDir) 291 | } else { 292 | originalApkFile = targetApkFile 293 | } 294 | walleApk(target, mergeWalle, channel, originalApkFile, channelApkFile) 295 | } 296 | 297 | println '--- walle ---' 298 | } 299 | } 300 | } 301 | target.afterEvaluate { 302 | if (!target.plugins.hasPlugin('com.android.application')) { 303 | target.logger.warn("The Android Gradle Plugin was not applied. Gradle Walle will not be configured.") 304 | } 305 | } 306 | } 307 | 308 | boolean v2SigningEnabled(variant) { 309 | def signingConfig = variant.signingConfig 310 | return signingConfig.v2SigningEnabled 311 | } 312 | 313 | File qihoo360JiaguApk(Project target, def variant, Walle walle, File apkFile, File outputDir) { 314 | File jarFile = walle.qihoo360.jiaguJarFile 315 | if (jarFile == null) { 316 | jarFile = target.file('script/jiagu/jiagu.jar') 317 | } 318 | if (!jarFile.exists()) { 319 | jarFile.parentFile.parentFile.mkdirs() 320 | File jiaguZipFile = null 321 | String downloadUrl = null 322 | if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) { 323 | jiaguZipFile = new File(jarFile.parentFile.parentFile, 'qihoo360-jiagu-mac.zip') 324 | downloadUrl = 'https://down.360safe.com/360Jiagu/360jiagubao_mac.zip' 325 | } else if (org.gradle.internal.os.OperatingSystem.current().isLinux()) { 326 | jiaguZipFile = new File(jarFile.parentFile.parentFile, 'qihoo360-jiagu-linux-64.zip') 327 | downloadUrl = 'https://down.360safe.com/360Jiagu/360jiagubao_linux_64.zip' 328 | } else if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { 329 | jiaguZipFile = new File(jarFile.parentFile.parentFile, 'qihoo360-jiagu-windows-32.zip') 330 | downloadUrl = 'https://down.360safe.com/360Jiagu/360jiagubao_windows_32.zip' 331 | } 332 | download(downloadUrl, jiaguZipFile) 333 | unzip(jiaguZipFile, jiaguZipFile.parentFile) 334 | } 335 | if (!jarFile.exists()) { 336 | throw new RuntimeException("下载 ${jarFile.parentFile.parentFile.toPath().relativize(jarFile.toPath()).toString().replace("\\", "/")} 失败") 337 | } 338 | File jiaguDir = new File(outputDir, 'jiagu') 339 | jiaguDir.mkdirs() 340 | File jiaguSignedApk = new File(jiaguDir.path, apkFile.name.replace('.apk', '_jiagu_signed.apk')) 341 | if (jiaguSignedApk.exists()) { 342 | return jiaguSignedApk 343 | } 344 | if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) { 345 | target.exec { 346 | commandLine 'bash', '-lc', "java -jar ${jarFile.path} -login ${walle.qihoo360.account} ${walle.qihoo360.password}" 347 | } 348 | target.exec { 349 | commandLine 'bash', '-lc', "java -jar ${jarFile.path} -jiagu ${apkFile.path} ${jiaguDir.path}" 350 | } 351 | } else if (org.gradle.internal.os.OperatingSystem.current().isLinux()) { 352 | target.exec { 353 | commandLine 'bash', '-lc', "${jarFile.parentFile.path}/java/bin/java -jar ${jarFile.path} -login ${walle.qihoo360.account} ${walle.qihoo360.password}" 354 | } 355 | target.exec { 356 | commandLine 'bash', '-lc', "${jarFile.parentFile.path}/java/bin/java -jar ${jarFile.path} -jiagu ${apkFile.path} ${jiaguDir.path}" 357 | } 358 | } else if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { 359 | target.exec { 360 | commandLine 'cmd', '/c', "${jarFile.parentFile.path}\\java\\bin\\java -jar ${jarFile.path} -login ${walle.qihoo360.account} ${walle.qihoo360.password}" 361 | } 362 | target.exec { 363 | commandLine 'cmd', '/c', "${jarFile.parentFile.path}\\java\\bin\\java -jar ${jarFile.path} -jiagu ${apkFile.path} ${jiaguDir.path}" 364 | } 365 | } 366 | File jiaguApk = jiaguDir.listFiles(new FilenameFilter() { 367 | @Override 368 | boolean accept(File dir, String name) { 369 | return name.toLowerCase().endsWith('.apk') 370 | } 371 | }).first() 372 | signApk(target, variant, jiaguApk, jiaguSignedApk) 373 | return jiaguSignedApk 374 | } 375 | 376 | File tencentLeguApk(Project target, def variant, Walle walle, File apkFile, File outputDir) { 377 | File jarFile = walle.tencent.leguJarFile 378 | if (jarFile == null) { 379 | jarFile = target.file('script/legu-all.jar') 380 | } 381 | if (!jarFile.exists()) { 382 | jarFile.parentFile.mkdirs() 383 | def downloadUrl = 'https://github.com/rxreader/tencentcloud-legu/releases/download/3.0.60/legu-all.jar' 384 | download(downloadUrl, jarFile) 385 | } 386 | if (!jarFile.exists()) { 387 | throw new RuntimeException("下载 ${jarFile.name} 失败") 388 | } 389 | File leguDir = new File(outputDir, 'legu') 390 | leguDir.mkdirs() 391 | File leguSignedApk = new File(leguDir.path, apkFile.name.replace('.apk', '_legu_signed.apk')) 392 | if (leguSignedApk.exists()) { 393 | return leguSignedApk 394 | } 395 | if (org.gradle.internal.os.OperatingSystem.current().isMacOsX() || org.gradle.internal.os.OperatingSystem.current().isLinux()) { 396 | target.exec { 397 | commandLine 'bash', '-lc', "java -jar ${jarFile.path} configure " + 398 | "-secretId ${walle.tencent.secretId} " + 399 | "-secretKey ${walle.tencent.secretKey} " + 400 | "-region ${walle.tencent.region ?: 'ap-guangzhou'}" 401 | } 402 | target.exec { 403 | commandLine 'bash', '-lc', "java -jar ${jarFile.path} legu " + 404 | "-in ${apkFile.path} " + 405 | "-out ${leguDir.path}" 406 | } 407 | } else if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { 408 | target.exec { 409 | commandLine 'cmd', '/c', "java -jar ${jarFile.path} configure " + 410 | "-secretId ${walle.tencent.secretId} " + 411 | "-secretKey ${walle.tencent.secretKey} " + 412 | "-region ${walle.tencent.region ?: 'ap-guangzhou'}" 413 | } 414 | target.exec { 415 | commandLine 'cmd', '/c', "java -jar ${jarFile.path} legu " + 416 | "-in ${apkFile.path} " + 417 | "-out ${leguDir.path}" 418 | } 419 | } 420 | File leguApk = leguDir.listFiles(new FilenameFilter() { 421 | @Override 422 | boolean accept(File dir, String name) { 423 | return name.toLowerCase().endsWith('.apk') 424 | } 425 | }).first() 426 | signApk(target, variant, leguApk, leguSignedApk) 427 | return leguSignedApk 428 | } 429 | 430 | void signApk(Project target, def variant, File apkFile, File signedApkFile) { 431 | if (org.gradle.internal.os.OperatingSystem.current().isMacOsX() || org.gradle.internal.os.OperatingSystem.current().isLinux()) { 432 | target.exec { 433 | commandLine 'bash', '-lc', "${target.android.sdkDirectory.path}/build-tools/${target.android.buildToolsVersion}/apksigner sign " + 434 | "-ks ${variant.signingConfig.storeFile.path} " + 435 | "-ks-pass pass:${variant.signingConfig.storePassword} " + 436 | "-ks-key-alias ${variant.signingConfig.keyAlias} " + 437 | "--key-pass pass:${variant.signingConfig.keyPassword} " + 438 | "--out ${signedApkFile.path} ${apkFile.path}" 439 | } 440 | } else if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { 441 | exec { 442 | commandLine 'cmd', '/c', "${target.android.sdkDirectory.path}\\build-tools\\${target.android.buildToolsVersion}\\apksigner sign " + 443 | "-ks ${variant.signingConfig.storeFile.path} " + 444 | "-ks-pass pass:${variant.signingConfig.storePassword} " + 445 | "-ks-key-alias ${variant.signingConfig.keyAlias} " + 446 | "--key-pass pass:${variant.signingConfig.keyPassword} " + 447 | "--out ${signedApkFile.path} ${apkFile.path}" 448 | } 449 | } 450 | } 451 | 452 | void walleApk(Project target, Walle walle, def channel, File apkFile, File channelApkFile) { 453 | File jarFile = walle.jarFile 454 | if (jarFile == null) { 455 | jarFile = target.file('script/walle-cli-all-1.1.7.jar') 456 | } 457 | if (!jarFile.exists()) { 458 | jarFile.parentFile.mkdirs() 459 | String downloadUrl = 'https://github.com/rxreader/walle/releases/download/v1.1.7/walle-cli-all-1.1.7.jar' 460 | download(downloadUrl, jarFile) 461 | } 462 | if (!jarFile.exists()) { 463 | throw new RuntimeException("下载 ${jarFile.name} 失败") 464 | } 465 | def extraInfo = channel.extraInfo?.entrySet()?.stream()?.map{ "${it.key}=${it.value}" }?.collect()?.toList()?.join(",") 466 | if (org.gradle.internal.os.OperatingSystem.current().isMacOsX() || org.gradle.internal.os.OperatingSystem.current().isLinux()) { 467 | target.exec { 468 | commandLine 'bash', '-lc', "java -jar ${jarFile.path} put -c ${channel.channelId} ${extraInfo != null ? "-e $extraInfo" : ""} ${apkFile.path} ${channelApkFile.path}" 469 | } 470 | } else if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { 471 | target.exec { 472 | commandLine 'cmd', '/c', "java -jar ${jarFile.path} put -c ${channel.channelId} ${extraInfo != null ? "-e $extraInfo" : ""} ${apkFile.path} ${channelApkFile.path}" 473 | } 474 | } 475 | } 476 | 477 | // --- 478 | void download(String url, File file) { 479 | file.withOutputStream { it << new URL(url).newInputStream() } 480 | } 481 | 482 | void unzip(File zipFile, File unzipDir) { 483 | def zip = new java.util.zip.ZipFile(zipFile) 484 | zip.entries().each{ 485 | if (!it.isDirectory() && it.size > 0){ 486 | def child = new File(unzipDir, it.name) 487 | child.parentFile.mkdirs() 488 | def zis = zip.getInputStream(it) 489 | def fos = new FileOutputStream(child) 490 | def buf = new byte[4 * 1024] 491 | def len 492 | while ((len = zis.read(buf)) != -1) { 493 | fos.write(buf, 0, len) 494 | } 495 | fos.close() 496 | } 497 | } 498 | zip.close() 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /android/walle_kit_v2.gradle: -------------------------------------------------------------------------------- 1 | // 2 | //使用方法 3 | // 4 | //apply from: 'walle.gradle' 5 | // 6 | //android { 7 | // productFlavors { 8 | // prod {...} 9 | // } 10 | // 11 | // walleConfigs { 12 | // prod { 13 | // enabled = true 14 | // 15 | // // [访问管理](https://console.cloud.tencent.com/cam/capi) 16 | // // [移动应用安全](https://console.cloud.tencent.com/ms/reinforce/list) 17 | // tencent { 18 | // secretId = 'xxx' 19 | // secretKey = 'xxx' 20 | //// region = 'ap-guangzhou' // 可选:'ap-guangzhou'、'ap-shanghai',默认:'ap-guangzhou' 21 | // channels = ['tencent', 'qihu360'] 22 | // } 23 | // 24 | // outputDir = file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") // 默认:file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") 25 | // fileNameFormat = '${appName}-${buildType}-${channelId}.apk' // 默认:'${appName}-${buildType}-${channelId}.apk' 26 | //// channelType = 0 // 0:默认;1:json 27 | // channelFile = file('channel') 28 | // } 29 | // } 30 | //} 31 | // 32 | //walle { 33 | // enabled = false 34 | //} 35 | // 36 | 37 | buildscript { 38 | repositories { 39 | google() 40 | mavenCentral() 41 | 42 | // 阿里云jcenter镜像 43 | maven { url 'https://maven.aliyun.com/repository/jcenter' } 44 | } 45 | dependencies { 46 | classpath 'com.android.tools.build:gradle:7.1.2' 47 | classpath 'com.meituan.android.walle:payload_writer:1.1.7' 48 | classpath 'com.tencentcloudapi:tencentcloud-sdk-java:3.0.60' 49 | classpath 'com.qcloud:cos_api:5.5.1' 50 | } 51 | } 52 | 53 | android { 54 | compileOptions { 55 | sourceCompatibility JavaVersion.VERSION_1_8 56 | targetCompatibility JavaVersion.VERSION_1_8 57 | } 58 | } 59 | 60 | class Tencent { 61 | String secretId 62 | String secretKey 63 | String region 64 | List channels 65 | 66 | void validate(String variant) { 67 | if (secretId == null || secretId.empty) { 68 | throw new RuntimeException("Tencent secretId is empty for variant '$variant'") 69 | } 70 | if (secretKey == null || secretKey.empty) { 71 | throw new RuntimeException("Tencent secretKey is empty for variant '$variant'") 72 | } 73 | if (channels == null || channels.empty) { 74 | throw new RuntimeException("Tencent channels is empty for variant '$variant'") 75 | } 76 | } 77 | } 78 | 79 | class Walle { 80 | final String name 81 | Boolean enabled 82 | Tencent tencent 83 | File outputDir 84 | String fileNameFormat 85 | Integer channelType 86 | File channelFile 87 | 88 | Walle(String name = 'default') { 89 | this.name = name 90 | } 91 | 92 | void tencent(Closure closure){ 93 | tencent = new Tencent() 94 | closure.delegate = tencent 95 | closure() 96 | } 97 | 98 | // --- 99 | 100 | void validate() { 101 | if (enabled == null || !enabled.booleanValue()) { 102 | return 103 | } 104 | tencent?.validate(name) 105 | if (channelType != null && channelType != 0 && channelType != 1) { 106 | throw new RuntimeException("walle channel type is unsupported for variant '$name'") 107 | } 108 | if (channelFile == null) { 109 | throw new RuntimeException("walle channel file is null for variant '$name'") 110 | } 111 | } 112 | 113 | Walle mergeWith(Walle other) { 114 | if (other == null) { 115 | return this 116 | } 117 | Walle mergeWalle = new Walle(name == 'default' ? other.name : (other.name == 'default' ? name : "$name${other.name.capitalize()}")) 118 | mergeWalle.enabled = other.enabled != null ? other.enabled : enabled 119 | mergeWalle.tencent = other.tencent ?: tencent 120 | mergeWalle.outputDir = other.outputDir ?: outputDir 121 | mergeWalle.fileNameFormat = other.fileNameFormat ?: fileNameFormat 122 | mergeWalle.channelType = other.channelType ?: channelType 123 | mergeWalle.channelFile = other.channelFile ?: channelFile 124 | return mergeWalle 125 | } 126 | } 127 | 128 | apply plugin: WallePlugin 129 | 130 | class WallePlugin implements Plugin { 131 | 132 | @Override 133 | void apply(Project target) { 134 | target.extensions.create('walle', Walle.class) 135 | target.plugins.withId('com.android.application') { 136 | Walle baseWalle = target.walle 137 | def walleConfigs = target.container(Walle.class) 138 | target.android.extensions.walleConfigs = walleConfigs 139 | target.android.applicationVariants.whenObjectAdded { variant -> 140 | Walle mergeWalle = null 141 | List flavorWalles = variant.productFlavors?.stream()?.map{flavor -> walleConfigs.findByName(flavor.name)}?.collect()?.toList() ?: Collections.emptyList() 142 | Walle buildTypeWalle = walleConfigs.findByName(variant.buildType.name) 143 | if (buildTypeWalle == null && (variant.buildType.name == 'debug' || variant.buildType.name == 'profile')) { 144 | buildTypeWalle = new Walle(variant.buildType.name) 145 | buildTypeWalle.enabled = false 146 | } 147 | // buildType > flavor > base 148 | List walles = [] 149 | walles.add(baseWalle) 150 | walles.addAll(flavorWalles) 151 | walles.add(buildTypeWalle) 152 | for (Walle walle in walles) { 153 | if (mergeWalle == null) { 154 | mergeWalle = walle 155 | } else { 156 | mergeWalle = mergeWalle.mergeWith(walle) 157 | } 158 | } 159 | 160 | mergeWalle?.validate() 161 | 162 | variant.assemble.doLast { 163 | walleWork(target, variant, mergeWalle) 164 | } 165 | } 166 | } 167 | target.afterEvaluate { 168 | if (!target.plugins.hasPlugin('com.android.application')) { 169 | target.logger.warn("The Android Gradle Plugin was not applied. Gradle Walle will not be configured.") 170 | } 171 | } 172 | } 173 | 174 | void walleWork(Project target, def variant, Walle walle) { 175 | if (walle == null || walle.enabled == null || !walle.enabled.booleanValue()) { 176 | target.logger.info("Gradle Walle is disabled for variant '${variant.name}'.") 177 | return 178 | } 179 | 180 | if (!variant.signingReady && !variant.outputsAreSigned) { 181 | target.logger.error("Signing not ready for Gradle Walle. Be sure to specify a signingConfig for variant '${variant.name}'.") 182 | return 183 | } 184 | 185 | println '--- walle ---' 186 | 187 | File apkFile = variant.outputs.first().outputFile as File 188 | println "apk file: ${apkFile.path}" 189 | 190 | boolean v2SigningEnabled = v2SigningEnabled(variant) 191 | println "v2SigningEnabled: ${v2SigningEnabled}" 192 | if (!v2SigningEnabled) { 193 | throw new RuntimeException("${apkFile.path} has no v2 signature in Apk Signing Block!") 194 | } 195 | 196 | // 预备输出目录 197 | File outputDir = walle.outputDir 198 | if (outputDir == null) { 199 | outputDir = new File(apkFile.parentFile, 'walle') 200 | } 201 | File channelsDir = new File(outputDir, 'channels') 202 | if (!channelsDir.exists()) { 203 | channelsDir.mkdirs() 204 | } 205 | 206 | def nameVariantMap = [ 207 | 'appName' : target.name, 208 | 'projectName': target.rootProject.name, 209 | 'buildType' : variant.buildType.name, 210 | 'versionName': variant.versionName, 211 | 'versionCode': variant.versionCode, 212 | 'packageName': variant.applicationId, 213 | 'flavorName' : variant.flavorName, 214 | 'channelId': 'channel' 215 | ] 216 | 217 | String fileNameFormat = walle.fileNameFormat ?: '${appName}-${buildType}-${channelId}.apk' 218 | File targetApkFile = new File(outputDir, new groovy.text.SimpleTemplateEngine().createTemplate(fileNameFormat).make(nameVariantMap).toString())// new File(outputDir, apkFile.name) 219 | 220 | // 复制 221 | java.nio.file.Files.copy(apkFile.toPath(), targetApkFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) 222 | 223 | println "target apk file: ${targetApkFile.path}" 224 | 225 | // 读取渠道信息 226 | def channels = [] 227 | if (walle.channelType == 1) { 228 | def slurper = new groovy.json.JsonSlurper() 229 | channels = slurper.parse(walle.channelFile) 230 | } else { 231 | walle.channelFile.eachLine { line -> 232 | String lineTrim = line.trim() 233 | if (lineTrim.length() != 0 && !lineTrim.startsWith("#")) { 234 | def channelId = line.split("#").first().trim() 235 | if (channelId.length() != 0) { 236 | channels.add(['alias': channelId, 'channelId': channelId]) 237 | } 238 | } 239 | } 240 | } 241 | 242 | channels.each { channel -> 243 | nameVariantMap['channelId'] = channel.channelId 244 | File storeDir = channelsDir 245 | if (channel.storeDir != null) { 246 | storeDir = new File(storeDir, channel.storeDir) 247 | if (!storeDir.exists()) { 248 | storeDir.mkdirs() 249 | } 250 | } 251 | File channelApkFile = new File(storeDir, new groovy.text.SimpleTemplateEngine().createTemplate(fileNameFormat).make(nameVariantMap).toString()) 252 | 253 | File originalApkFile 254 | if (walle.tencent?.channels?.contains(channel.channelId) ?: false) { 255 | originalApkFile = tencentLeguApk(target, variant, walle.tencent, targetApkFile, outputDir) 256 | } else { 257 | originalApkFile = targetApkFile 258 | } 259 | writePayload(target, channel, originalApkFile, channelApkFile) 260 | } 261 | 262 | 263 | println '--- walle ---' 264 | } 265 | 266 | boolean v2SigningEnabled(variant) { 267 | def signingConfig = variant.signingConfig 268 | return signingConfig.v2SigningEnabled 269 | } 270 | 271 | File tencentLeguApk(Project target, def variant, Tencent tencent, File apkFile, File outputDir) { 272 | File shieldDir = new File(outputDir, 'legu') 273 | shieldDir.mkdirs() 274 | File shieldApkFile = new File(shieldDir, apkFile.name.replace('.apk', '_legu.apk')) 275 | File shieldSignedApkFile = new File(shieldDir, apkFile.name.replace('.apk', '_legu_signed.apk')) 276 | if (shieldSignedApkFile.exists()) { 277 | return shieldSignedApkFile 278 | } 279 | def msClient = new com.tencentcloudapi.ms.v20180408.MsClient(new com.tencentcloudapi.common.Credential(tencent.secretId, tencent.secretKey), tencent.region ?: 'ap-guangzhou') 280 | // 上传文件到COS文件存储 281 | println '上传APK到COS文件存储...' 282 | def cosSecKeyResp = msClient.CreateCosSecKeyInstance(new com.tencentcloudapi.ms.v20180408.models.CreateCosSecKeyInstanceRequest()) 283 | def cosClient = new com.qcloud.cos.COSClient(new com.qcloud.cos.auth.BasicSessionCredentials(cosSecKeyResp.getCosId(), cosSecKeyResp.getCosKey(), cosSecKeyResp.getCosTocken()), new com.qcloud.cos.ClientConfig(new com.qcloud.cos.region.Region(cosSecKeyResp.getCosRegion()))); 284 | def bucket = "${cosSecKeyResp.getCosBucket()}-${cosSecKeyResp.getCosAppid()}" 285 | def fileKeyPrefix = cosSecKeyResp.getCosPrefix() 286 | if (!fileKeyPrefix.isEmpty() && !fileKeyPrefix.endsWith("/")) { 287 | fileKeyPrefix = fileKeyPrefix + "/" 288 | } 289 | fileKeyPrefix = fileKeyPrefix + variant.applicationId + "/" + variant.versionName 290 | def fileMd5 = com.qcloud.cos.utils.Md5Utils.computeMD5Hash(apkFile) 291 | def fileKey = fileKeyPrefix + "/" + com.qcloud.cos.utils.BinaryUtils.toHex(fileMd5) + "_" + apkFile.getName() 292 | def fileMetadata 293 | try { 294 | fileMetadata = cosClient.getObjectMetadata(bucket, fileKey) 295 | } catch(com.qcloud.cos.exception.CosServiceException e) { 296 | if (e.getStatusCode() == java.net.HttpURLConnection.HTTP_NOT_FOUND) { 297 | fileMetadata = null 298 | } else { 299 | throw e 300 | } 301 | } 302 | if (fileMetadata == null) { 303 | def fileReq = new com.qcloud.cos.model.PutObjectRequest(bucket, fileKey, apkFile) 304 | fileMetadata = new com.qcloud.cos.model.ObjectMetadata() 305 | fileMetadata.setContentMD5(com.qcloud.cos.utils.BinaryUtils.toBase64(fileMd5)) 306 | fileReq.setMetadata(fileMetadata) 307 | cosClient.putObject(fileReq) 308 | } 309 | def urlReq = new com.qcloud.cos.model.GeneratePresignedUrlRequest(bucket, fileKey, com.qcloud.cos.http.HttpMethodName.GET) 310 | urlReq.setExpiration(new Date(System.currentTimeMillis() + 1800000L)) 311 | urlReq.addRequestParameter("x-cos-security-token", cosSecKeyResp.getCosTocken()) 312 | def apkUrl = cosClient.generatePresignedUrl(urlReq) 313 | // 加固 314 | println '开始加固...' 315 | def appInfo = new com.tencentcloudapi.ms.v20180408.models.AppInfo() 316 | appInfo.setAppUrl(apkUrl.toString()) 317 | appInfo.setAppMd5(com.qcloud.cos.utils.BinaryUtils.toHex(fileMd5)) 318 | appInfo.setAppPkgName(variant.applicationId) 319 | def serviceInfo = new com.tencentcloudapi.ms.v20180408.models.ServiceInfo() 320 | serviceInfo.setServiceEdition("basic") 321 | serviceInfo.setSubmitSource("legu-cli") 322 | serviceInfo.setCallbackUrl("") 323 | def shieldReq = new com.tencentcloudapi.ms.v20180408.models.CreateShieldInstanceRequest() 324 | shieldReq.setAppInfo(appInfo) 325 | shieldReq.setServiceInfo(serviceInfo) 326 | def shieldResp = msClient.CreateShieldInstance(shieldReq) 327 | def shieldApkUrl 328 | if (shieldResp.getItemId() != null && !shieldResp.getItemId().isEmpty()) { 329 | if (shieldResp.getProgress() == 2) { 330 | // 处理中 331 | println '加固中...' 332 | Thread.sleep(5000L) 333 | } 334 | def resultReq = new com.tencentcloudapi.ms.v20180408.models.DescribeShieldResultRequest() 335 | resultReq.setItemId(shieldResp.getItemId()) 336 | while(true) { 337 | def resultResp = msClient.DescribeShieldResult(resultReq) 338 | def status = resultResp.getTaskStatus() 339 | if (status == 1) { 340 | println '加固完成...' 341 | def shieldInfo = resultResp.getShieldInfo() 342 | shieldApkUrl = shieldInfo.getAppUrl() 343 | break 344 | } else if (status == 2) { 345 | println '加固中...' 346 | Thread.sleep(20000L) 347 | } else if (status == 3) { 348 | throw new Exception(String.format("DescribeShieldResult[%s] %s, ShieldCode=%d\n 错误指引: %s", resultResp.getRequestId(), resultResp.getStatusDesc(), resultResp.getShieldInfo().getShieldCode(), resultResp.getStatusRef())) 349 | } else { 350 | throw new Exception(String.format("DescribeShieldResult[%s] %s, taskStatus=%d\n 错误指引: %s", resultResp.getRequestId(), resultResp.getStatusDesc(), resultResp.getTaskStatus(), resultResp.getStatusRef())) 351 | } 352 | } 353 | } else { 354 | throw new Exception(String.format("CreateShieldInstance[%s] failed, item id is empty", shieldResp.getRequestId())) 355 | } 356 | println '下载加固包...' 357 | shieldApkFile.withOutputStream { it << new URL(shieldApkUrl).newInputStream() } 358 | println '加固包重签...' 359 | signApk(target, variant, shieldApkFile, shieldSignedApkFile) 360 | return shieldSignedApkFile 361 | } 362 | 363 | void signApk(Project target, def variant, File apkFile, File signedApkFile) { 364 | String storeType = variant.signingConfig.storeType 365 | File storeFile = variant.signingConfig.storeFile 366 | String storePassword = variant.signingConfig.storePassword 367 | String keyAlias = variant.signingConfig.keyAlias 368 | String keyPassword = variant.signingConfig.keyPassword 369 | 370 | java.security.KeyStore store = java.security.KeyStore.getInstance(storeType != null ? storeType : java.security.KeyStore.getDefaultType()) 371 | store.load(new FileInputStream(storeFile), storePassword.toCharArray()) 372 | 373 | def signerConfig = new com.android.apksig.ApkSigner.SignerConfig.Builder(keyAlias, store.getKey(keyAlias, keyPassword.toCharArray()), Arrays.asList(store.getCertificateChain(keyAlias))) 374 | .build() 375 | def apkSigner = new com.android.apksig.ApkSigner.Builder(Arrays.asList(signerConfig)) 376 | .setInputApk(apkFile) 377 | .setOutputApk(signedApkFile) 378 | .build() 379 | apkSigner.sign() 380 | } 381 | 382 | void writePayload(Project target, def channel, File apkFile, File channelApkFile) { 383 | java.nio.file.Files.copy(apkFile.toPath(), channelApkFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) 384 | com.meituan.android.walle.ChannelWriter.put(channelApkFile, channel.channelId, channel.extraInfo) 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /android/walle_kit_v3.gradle: -------------------------------------------------------------------------------- 1 | // 2 | //使用方法 3 | // 4 | //apply from: 'walle.gradle' 5 | // 6 | //android { 7 | // productFlavors { 8 | // prod {...} 9 | // } 10 | // 11 | // walleConfigs { 12 | // prod { 13 | // enabled = true 14 | // 15 | // outputDir = file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") // 默认:file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") 16 | // fileNameFormat = '${appName}-${buildType}-${channelId}.apk' // 默认:'${appName}-${buildType}-${channelId}.apk' 17 | //// channelType = 0 // 0:默认;1:json 18 | // channelFile = file('channel') 19 | // } 20 | // } 21 | //} 22 | // 23 | //walle { 24 | // enabled = false 25 | //} 26 | // 27 | 28 | buildscript { 29 | repositories { 30 | google() 31 | mavenCentral() 32 | 33 | // 阿里云jcenter镜像 34 | maven { url 'https://maven.aliyun.com/repository/jcenter' } 35 | } 36 | dependencies { 37 | classpath 'com.android.tools.build:gradle:7.1.2' 38 | classpath 'com.meituan.android.walle:payload_writer:1.1.7' 39 | } 40 | } 41 | 42 | android { 43 | compileOptions { 44 | sourceCompatibility JavaVersion.VERSION_1_8 45 | targetCompatibility JavaVersion.VERSION_1_8 46 | } 47 | } 48 | 49 | class Walle { 50 | final String name 51 | Boolean enabled 52 | File outputDir 53 | String fileNameFormat 54 | Integer channelType 55 | File channelFile 56 | 57 | Walle(String name = 'default') { 58 | this.name = name 59 | } 60 | 61 | // --- 62 | 63 | void validate() { 64 | if (enabled == null || !enabled.booleanValue()) { 65 | return 66 | } 67 | if (channelType != null && channelType != 0 && channelType != 1) { 68 | throw new RuntimeException("walle channel type is unsupported for variant '$name'") 69 | } 70 | if (channelFile == null) { 71 | throw new RuntimeException("walle channel file is null for variant '$name'") 72 | } 73 | } 74 | 75 | Walle mergeWith(Walle other) { 76 | if (other == null) { 77 | return this 78 | } 79 | def mergeWalle = new Walle(name == 'default' ? other.name : (other.name == 'default' ? name : "$name${other.name.capitalize()}")) 80 | mergeWalle.enabled = other.enabled != null ? other.enabled : enabled 81 | mergeWalle.outputDir = other.outputDir ?: outputDir 82 | mergeWalle.fileNameFormat = other.fileNameFormat ?: fileNameFormat 83 | mergeWalle.channelType = other.channelType ?: channelType 84 | mergeWalle.channelFile = other.channelFile ?: channelFile 85 | return mergeWalle 86 | } 87 | } 88 | 89 | apply plugin: WallePlugin 90 | 91 | class WallePlugin implements Plugin { 92 | 93 | @Override 94 | void apply(Project target) { 95 | target.extensions.create('walle', Walle.class) 96 | target.plugins.withId('com.android.application') { 97 | def baseWalle = target.walle 98 | def walleConfigs = target.container(Walle.class) 99 | target.android.extensions.walleConfigs = walleConfigs 100 | target.android.applicationVariants.whenObjectAdded { variant -> 101 | def mergeWalle = null 102 | List flavorWalles = variant.productFlavors?.stream()?.map{flavor -> walleConfigs.findByName(flavor.name)}?.collect()?.toList() ?: Collections.emptyList() 103 | def buildTypeWalle = walleConfigs.findByName(variant.buildType.name) 104 | if (buildTypeWalle == null && (variant.buildType.name == 'debug' || variant.buildType.name == 'profile')) { 105 | buildTypeWalle = new Walle(variant.buildType.name) 106 | buildTypeWalle.enabled = false 107 | } 108 | // buildType > flavor > base 109 | List walles = [] 110 | walles.add(baseWalle) 111 | walles.addAll(flavorWalles) 112 | walles.add(buildTypeWalle) 113 | for (def walle in walles) { 114 | if (mergeWalle == null) { 115 | mergeWalle = walle 116 | } else { 117 | mergeWalle = mergeWalle.mergeWith(walle) 118 | } 119 | } 120 | 121 | mergeWalle?.validate() 122 | 123 | variant.assemble.doLast { 124 | walleWork(target, variant, mergeWalle) 125 | } 126 | } 127 | } 128 | target.afterEvaluate { 129 | if (!target.plugins.hasPlugin('com.android.application')) { 130 | target.logger.warn("The Android Gradle Plugin was not applied. Gradle Walle will not be configured.") 131 | } 132 | } 133 | } 134 | 135 | void walleWork(Project target, def variant, Walle walle) { 136 | if (walle == null || walle.enabled == null || !walle.enabled.booleanValue()) { 137 | target.logger.info("Gradle Walle is disabled for variant '${variant.name}'.") 138 | return 139 | } 140 | 141 | if (!variant.signingReady && !variant.outputsAreSigned) { 142 | target.logger.error("Signing not ready for Gradle Walle. Be sure to specify a signingConfig for variant '${variant.name}'.") 143 | return 144 | } 145 | 146 | println '--- walle ---' 147 | 148 | def apkFile = variant.outputs.first().outputFile as File 149 | println "apk file: ${apkFile.path}" 150 | 151 | def v2SigningEnabled = v2SigningEnabled(variant) 152 | println "v2SigningEnabled: ${v2SigningEnabled}" 153 | if (!v2SigningEnabled) { 154 | throw new RuntimeException("${apkFile.path} has no v2 signature in Apk Signing Block!") 155 | } 156 | 157 | // 预备输出目录 158 | def outputDir = walle.outputDir 159 | if (outputDir == null) { 160 | outputDir = new File(apkFile.parentFile, 'walle') 161 | } 162 | def channelsDir = new File(outputDir, 'channels') 163 | if (!channelsDir.exists()) { 164 | channelsDir.mkdirs() 165 | } 166 | 167 | def nameVariantMap = [ 168 | 'appName' : target.name, 169 | 'projectName': target.rootProject.name, 170 | 'buildType' : variant.buildType.name, 171 | 'versionName': variant.versionName, 172 | 'versionCode': variant.versionCode, 173 | 'packageName': variant.applicationId, 174 | 'flavorName' : variant.flavorName, 175 | 'channelId': 'channel' 176 | ] 177 | 178 | def fileNameFormat = walle.fileNameFormat ?: '${appName}-${buildType}-${channelId}.apk' 179 | def targetApkFile = new File(outputDir, new groovy.text.SimpleTemplateEngine().createTemplate(fileNameFormat).make(nameVariantMap).toString())// new File(outputDir, apkFile.name) 180 | 181 | // 复制 182 | java.nio.file.Files.copy(apkFile.toPath(), targetApkFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) 183 | 184 | println "target apk file: ${targetApkFile.path}" 185 | 186 | // 读取渠道信息 187 | def channels = [] 188 | if (walle.channelType == 1) { 189 | def slurper = new groovy.json.JsonSlurper() 190 | channels = slurper.parse(walle.channelFile) 191 | } else { 192 | walle.channelFile.eachLine { line -> 193 | def lineTrim = line.trim() 194 | if (lineTrim.length() != 0 && !lineTrim.startsWith("#")) { 195 | def channelId = line.split("#").first().trim() 196 | if (channelId.length() != 0) { 197 | channels.add(['alias': channelId, 'channelId': channelId]) 198 | } 199 | } 200 | } 201 | } 202 | 203 | channels.each { channel -> 204 | nameVariantMap['channelId'] = channel.channelId 205 | def storeDir = channelsDir 206 | if (channel.storeDir != null) { 207 | storeDir = new File(storeDir, channel.storeDir) 208 | if (!storeDir.exists()) { 209 | storeDir.mkdirs() 210 | } 211 | } 212 | def channelApkFile = new File(storeDir, new groovy.text.SimpleTemplateEngine().createTemplate(fileNameFormat).make(nameVariantMap).toString()) 213 | 214 | def originalApkFile = targetApkFile 215 | 216 | if (originalApkFile != targetApkFile) { 217 | println "${channel.alias ?: channel.channelId} original apk file: ${originalApkFile.path}" 218 | } 219 | println "${channel.alias ?: channel.channelId} channel apk file: ${channelApkFile.path}" 220 | 221 | writePayload(target, channel, originalApkFile, channelApkFile) 222 | } 223 | 224 | 225 | println '--- walle ---' 226 | } 227 | 228 | boolean v2SigningEnabled(variant) { 229 | def signingConfig = variant.signingConfig 230 | return signingConfig.v2SigningEnabled 231 | } 232 | 233 | void writePayload(Project target, def channel, File apkFile, File channelApkFile) { 234 | java.nio.file.Files.copy(apkFile.toPath(), channelApkFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) 235 | com.meituan.android.walle.ChannelWriter.put(channelApkFile, channel.channelId, channel.extraInfo) 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # walle_kit_example 2 | 3 | Demonstrates how to use the walle_kit plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Take our settings from the repo's main analysis_options.yaml file, but include 2 | # an additional rule to validate that public members are documented. 3 | 4 | include: ../analysis_options.yaml 5 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | 15 | # 16 | app/script/ 17 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | // walle 28 | apply from: "${project(":walle_kit").projectDir}/walle_kit_v3.gradle" 29 | 30 | android { 31 | compileSdkVersion flutter.compileSdkVersion 32 | ndkVersion flutter.ndkVersion 33 | 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "io.github.v7lin.walle_kit_example" 42 | // You can update the following values to match your application needs. 43 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 44 | minSdkVersion flutter.minSdkVersion 45 | targetSdkVersion flutter.targetSdkVersion 46 | versionCode flutterVersionCode.toInteger() 47 | versionName flutterVersionName 48 | } 49 | 50 | buildTypes { 51 | release { 52 | // TODO: Add your own signing config for the release build. 53 | // Signing with the debug keys for now, so `flutter run --release` works. 54 | signingConfig signingConfigs.debug 55 | } 56 | } 57 | 58 | walleConfigs { 59 | release { 60 | enabled = true 61 | 62 | outputDir = file("${project.buildDir}/outputs/apk/walle") // 默认:file("${project.buildDir}/outputs/apk/${flavorName}/${buildType}/walle") 63 | fileNameFormat = '${appName}-${buildType}-${channelId}.apk' // 默认:'${appName}-${buildType}-${channelId}.apk' 64 | // channelType = 0 // 0:默认;1:json 65 | // channelFile = file('channel') 66 | channelType = 1 67 | channelFile = file('channel.json') 68 | } 69 | } 70 | } 71 | 72 | walle { 73 | enabled = false 74 | } 75 | 76 | flutter { 77 | source '../..' 78 | } 79 | -------------------------------------------------------------------------------- /example/android/app/channel: -------------------------------------------------------------------------------- 1 | # 渠道配置列表 2 | # 默认 3 | official 4 | # 奇虎360 5 | qihu360 6 | # 百度 7 | baidu 8 | # 小米 9 | xiaomi 10 | # 腾讯 11 | tencent 12 | # 阿里云 13 | aliyun 14 | # 华为 15 | huawei 16 | # 荣耀 17 | honor 18 | # Oppo 19 | oppo 20 | # Vivo 21 | vivo 22 | # 抖音 23 | douyin 24 | -------------------------------------------------------------------------------- /example/android/app/channel.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "alias": "默认", 4 | "channelId": "official" 5 | }, 6 | { 7 | "alias": "奇虎360", 8 | "channelId": "qihu360", 9 | "storeDir": "market" 10 | }, 11 | { 12 | "alias": "百度", 13 | "channelId": "baidu", 14 | "storeDir": "market" 15 | }, 16 | { 17 | "alias": "小米", 18 | "channelId": "xiaomi", 19 | "extraInfo": { 20 | "secretKey": "12345678" 21 | }, 22 | "storeDir": "market" 23 | }, 24 | { 25 | "alias": "腾讯", 26 | "channelId": "tencent", 27 | "storeDir": "market" 28 | }, 29 | { 30 | "alias": "阿里云", 31 | "channelId": "aliyun", 32 | "storeDir": "market" 33 | }, 34 | { 35 | "alias": "华为", 36 | "channelId": "huawei", 37 | "storeDir": "market" 38 | }, 39 | { 40 | "alias": "荣耀", 41 | "channelId": "honor", 42 | "storeDir": "market" 43 | }, 44 | { 45 | "alias": "Oppo", 46 | "channelId": "oppo", 47 | "storeDir": "market" 48 | }, 49 | { 50 | "alias": "Vivo", 51 | "channelId": "vivo", 52 | "storeDir": "market" 53 | }, 54 | { 55 | "alias": "抖音", 56 | "channelId": "douyin", 57 | "storeDir": "market" 58 | } 59 | ] -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/io/github/v7lin/walle_kit_example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.v7lin.walle_kit_example; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxReader/walle_kit/5b1ee6e96598b047890d3bdaea84f2950ac33c43/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxReader/walle_kit/5b1ee6e96598b047890d3bdaea84f2950ac33c43/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxReader/walle_kit/5b1ee6e96598b047890d3bdaea84f2950ac33c43/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxReader/walle_kit/5b1ee6e96598b047890d3bdaea84f2950ac33c43/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RxReader/walle_kit/5b1ee6e96598b047890d3bdaea84f2950ac33c43/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:walle_kit/walle_kit.dart'; 5 | 6 | void main() => runApp(MyApp()); 7 | 8 | class MyApp extends StatefulWidget { 9 | const MyApp({ 10 | super.key, 11 | }); 12 | 13 | @override 14 | State createState() => _MyAppState(); 15 | } 16 | 17 | class _MyAppState extends State { 18 | String _channelId = ''; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | _initChannelId(); 24 | } 25 | 26 | Future _initChannelId() async { 27 | final String? channelId = await Walle.instance.getChannelId(); 28 | 29 | if (!mounted) { 30 | return; 31 | } 32 | 33 | setState(() { 34 | _channelId = channelId ?? 'unknown'; 35 | }); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return MaterialApp( 41 | home: Scaffold( 42 | appBar: AppBar( 43 | title: const Text('walle_kit'), 44 | ), 45 | body: Center( 46 | child: Text('channelId: $_channelId\n'), 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.9.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "1.2.1" 25 | clock: 26 | dependency: transitive 27 | description: 28 | name: clock 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.1.1" 32 | collection: 33 | dependency: transitive 34 | description: 35 | name: collection 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.16.0" 39 | cupertino_icons: 40 | dependency: "direct main" 41 | description: 42 | name: cupertino_icons 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.0.4" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "1.3.1" 53 | flutter: 54 | dependency: "direct main" 55 | description: flutter 56 | source: sdk 57 | version: "0.0.0" 58 | flutter_lints: 59 | dependency: "direct dev" 60 | description: 61 | name: flutter_lints 62 | url: "https://pub.flutter-io.cn" 63 | source: hosted 64 | version: "2.0.1" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | json_annotation: 71 | dependency: transitive 72 | description: 73 | name: json_annotation 74 | url: "https://pub.flutter-io.cn" 75 | source: hosted 76 | version: "4.5.0" 77 | lints: 78 | dependency: transitive 79 | description: 80 | name: lints 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "2.0.0" 84 | matcher: 85 | dependency: transitive 86 | description: 87 | name: matcher 88 | url: "https://pub.flutter-io.cn" 89 | source: hosted 90 | version: "0.12.12" 91 | material_color_utilities: 92 | dependency: transitive 93 | description: 94 | name: material_color_utilities 95 | url: "https://pub.flutter-io.cn" 96 | source: hosted 97 | version: "0.1.5" 98 | meta: 99 | dependency: transitive 100 | description: 101 | name: meta 102 | url: "https://pub.flutter-io.cn" 103 | source: hosted 104 | version: "1.8.0" 105 | path: 106 | dependency: transitive 107 | description: 108 | name: path 109 | url: "https://pub.flutter-io.cn" 110 | source: hosted 111 | version: "1.8.2" 112 | plugin_platform_interface: 113 | dependency: transitive 114 | description: 115 | name: plugin_platform_interface 116 | url: "https://pub.flutter-io.cn" 117 | source: hosted 118 | version: "2.1.2" 119 | sky_engine: 120 | dependency: transitive 121 | description: flutter 122 | source: sdk 123 | version: "0.0.99" 124 | source_span: 125 | dependency: transitive 126 | description: 127 | name: source_span 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "1.9.0" 131 | stack_trace: 132 | dependency: transitive 133 | description: 134 | name: stack_trace 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "1.10.0" 138 | stream_channel: 139 | dependency: transitive 140 | description: 141 | name: stream_channel 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "2.1.0" 145 | string_scanner: 146 | dependency: transitive 147 | description: 148 | name: string_scanner 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "1.1.1" 152 | term_glyph: 153 | dependency: transitive 154 | description: 155 | name: term_glyph 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "1.2.1" 159 | test_api: 160 | dependency: transitive 161 | description: 162 | name: test_api 163 | url: "https://pub.flutter-io.cn" 164 | source: hosted 165 | version: "0.4.12" 166 | vector_math: 167 | dependency: transitive 168 | description: 169 | name: vector_math 170 | url: "https://pub.flutter-io.cn" 171 | source: hosted 172 | version: "2.1.2" 173 | walle_kit: 174 | dependency: "direct main" 175 | description: 176 | path: ".." 177 | relative: true 178 | source: path 179 | version: "3.0.4" 180 | sdks: 181 | dart: ">=2.17.1 <3.0.0" 182 | flutter: ">=2.5.0" 183 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: walle_kit_example 2 | description: Demonstrates how to use the walle_kit plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.17.1 <3.0.0" 10 | 11 | # Dependencies specify other packages that your package needs in order to work. 12 | # To automatically upgrade your package dependencies to the latest versions 13 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 14 | # dependencies can be manually updated by changing the version numbers below to 15 | # the latest version available on pub.dev. To see which dependencies have newer 16 | # versions available, run `flutter pub outdated`. 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | 21 | walle_kit: 22 | # When depending on this package from a real application you should use: 23 | # walle_kit: ^x.y.z 24 | # See https://dart.dev/tools/pub/dependencies#version-constraints 25 | # The example app is bundled with the plugin so we use a path dependency on 26 | # the parent directory to use the current plugin's version. 27 | path: ../ 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^1.0.2 32 | 33 | dev_dependencies: 34 | flutter_test: 35 | sdk: flutter 36 | 37 | # The "flutter_lints" package below contains a set of recommended lints to 38 | # encourage good coding practices. The lint set provided by the package is 39 | # activated in the `analysis_options.yaml` file located at the root of your 40 | # package. See that file for information about deactivating specific lint 41 | # rules and activating additional ones. 42 | flutter_lints: ^2.0.0 43 | 44 | # For information on the generic Dart part of this file, see the 45 | # following page: https://dart.dev/tools/pub/pubspec 46 | 47 | # The following section is specific to Flutter packages. 48 | flutter: 49 | 50 | # The following line ensures that the Material Icons font is 51 | # included with your application, so that you can use the icons in 52 | # the material Icons class. 53 | uses-material-design: true 54 | 55 | # To add assets to your application, add an assets section, like this: 56 | # assets: 57 | # - images/a_dot_burr.jpeg 58 | # - images/a_dot_ham.jpeg 59 | 60 | # An image asset can refer to one or more resolution-specific "variants", see 61 | # https://flutter.dev/assets-and-images/#resolution-aware 62 | 63 | # For details regarding adding assets from package dependencies, see 64 | # https://flutter.dev/assets-and-images/#from-packages 65 | 66 | # To add custom fonts to your application, add a fonts section here, 67 | # in this "flutter" section. Each entry in this list should have a 68 | # "family" key with the font family name, and a "fonts" key with a 69 | # list giving the asset and other descriptors for the font. For 70 | # example: 71 | # fonts: 72 | # - family: Schyler 73 | # fonts: 74 | # - asset: fonts/Schyler-Regular.ttf 75 | # - asset: fonts/Schyler-Italic.ttf 76 | # style: italic 77 | # - family: Trajan Pro 78 | # fonts: 79 | # - asset: fonts/TrajanPro.ttf 80 | # - asset: fonts/TrajanPro_Bold.ttf 81 | # weight: 700 82 | # 83 | # For details regarding fonts from package dependencies, 84 | # see https://flutter.dev/custom-fonts/#from-packages 85 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:walle_kit_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('smoke test', (WidgetTester tester) async {}); 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/model/channel_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'channel_info.g.dart'; 4 | 5 | @JsonSerializable( 6 | explicitToJson: true, 7 | fieldRename: FieldRename.snake, 8 | ) 9 | class ChannelInfo { 10 | const ChannelInfo({ 11 | required this.channel, 12 | this.extraInfo, 13 | }); 14 | 15 | factory ChannelInfo.fromJson(Map json) => 16 | _$ChannelInfoFromJson(json); 17 | 18 | final String channel; 19 | final Map? extraInfo; 20 | 21 | Map toJson() => _$ChannelInfoToJson(this); 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/model/channel_info.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'channel_info.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | ChannelInfo _$ChannelInfoFromJson(Map json) => ChannelInfo( 10 | channel: json['channel'] as String, 11 | extraInfo: (json['extra_info'] as Map?)?.map( 12 | (k, e) => MapEntry(k, e as String), 13 | ), 14 | ); 15 | 16 | Map _$ChannelInfoToJson(ChannelInfo instance) => 17 | { 18 | 'channel': instance.channel, 19 | 'extra_info': instance.extraInfo, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/src/walle.dart: -------------------------------------------------------------------------------- 1 | import 'package:walle_kit/src/walle_kit_platform_interface.dart'; 2 | 3 | class Walle { 4 | const Walle._(); 5 | 6 | static WalleKitPlatform get instance => WalleKitPlatform.instance; 7 | } 8 | -------------------------------------------------------------------------------- /lib/src/walle_kit_method_channel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:walle_kit/src/model/channel_info.dart'; 6 | import 'package:walle_kit/src/walle_kit_platform_interface.dart'; 7 | 8 | /// An implementation of [WalleKitPlatform] that uses method channels. 9 | class MethodChannelWalleKit extends WalleKitPlatform { 10 | /// The method channel used to interact with the native platform. 11 | @visibleForTesting 12 | final MethodChannel methodChannel = 13 | const MethodChannel('v7lin.github.io/walle_kit'); 14 | 15 | @override 16 | Future getChannelId() { 17 | assert( 18 | Platform.isAndroid || Platform.environment['FLUTTER_TEST'] == 'true'); 19 | return methodChannel.invokeMethod('getChannelId'); 20 | } 21 | 22 | @override 23 | Future getChannelInfo() async { 24 | assert( 25 | Platform.isAndroid || Platform.environment['FLUTTER_TEST'] == 'true'); 26 | final Map? json = 27 | await methodChannel.invokeMapMethod('getChannelInfo'); 28 | return json != null ? ChannelInfo.fromJson(json) : null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/walle_kit_platform_interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 2 | import 'package:walle_kit/src/model/channel_info.dart'; 3 | import 'package:walle_kit/src/walle_kit_method_channel.dart'; 4 | 5 | abstract class WalleKitPlatform extends PlatformInterface { 6 | /// Constructs a WalleKitPlatform. 7 | WalleKitPlatform() : super(token: _token); 8 | 9 | static final Object _token = Object(); 10 | 11 | static WalleKitPlatform _instance = MethodChannelWalleKit(); 12 | 13 | /// The default instance of [WalleKitPlatform] to use. 14 | /// 15 | /// Defaults to [MethodChannelWalleKit]. 16 | static WalleKitPlatform get instance => _instance; 17 | 18 | /// Platform-specific implementations should set this with their own 19 | /// platform-specific class that extends [WalleKitPlatform] when 20 | /// they register themselves. 21 | static set instance(WalleKitPlatform instance) { 22 | PlatformInterface.verifyToken(instance, _token); 23 | _instance = instance; 24 | } 25 | 26 | Future getChannelId() { 27 | throw UnimplementedError('getChannelId() has not been implemented.'); 28 | } 29 | 30 | Future getChannelInfo() { 31 | throw UnimplementedError('getChannelInfo() has not been implemented.'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/walle_kit.dart: -------------------------------------------------------------------------------- 1 | library walle_kit; 2 | 3 | export 'src/model/channel_info.dart'; 4 | export 'src/walle.dart'; 5 | -------------------------------------------------------------------------------- /lib/walle_kit_platform_interface.dart: -------------------------------------------------------------------------------- 1 | library walle_kit_platform_interface; 2 | 3 | export 'src/model/channel_info.dart'; 4 | export 'src/walle_kit_method_channel.dart'; 5 | export 'src/walle_kit_platform_interface.dart'; 6 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: walle_kit 2 | description: A powerful Flutter plugin allowing developers to read/write channelId to apk with Walle Tools/SDKs. 3 | version: 3.0.4 4 | # author: v7lin 5 | homepage: https://github.com/RxReader/walle_kit 6 | 7 | environment: 8 | sdk: ">=2.17.1 <3.0.0" 9 | flutter: ">=2.5.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | plugin_platform_interface: ^2.0.2 15 | 16 | json_annotation: ^4.5.0 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | flutter_lints: ^2.0.0 22 | 23 | build_runner: 24 | json_serializable: 25 | 26 | # For information on the generic Dart part of this file, see the 27 | # following page: https://dart.dev/tools/pub/pubspec 28 | 29 | # The following section is specific to Flutter packages. 30 | flutter: 31 | # This section identifies this Flutter project as a plugin project. 32 | # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) 33 | # which should be registered in the plugin registry. This is required for 34 | # using method channels. 35 | # The Android 'package' specifies package in which the registered class is. 36 | # This is required for using method channels on Android. 37 | # The 'ffiPlugin' specifies that native code should be built and bundled. 38 | # This is required for using `dart:ffi`. 39 | # All these are used by the tooling to maintain consistency when 40 | # adding or updating assets for this project. 41 | plugin: 42 | platforms: 43 | android: 44 | package: io.github.v7lin.walle_kit 45 | pluginClass: WalleKitPlugin 46 | 47 | # To add assets to your plugin package, add an assets section, like this: 48 | # assets: 49 | # - images/a_dot_burr.jpeg 50 | # - images/a_dot_ham.jpeg 51 | # 52 | # For details regarding assets in packages, see 53 | # https://flutter.dev/assets-and-images/#from-packages 54 | # 55 | # An image asset can refer to one or more resolution-specific "variants", see 56 | # https://flutter.dev/assets-and-images/#resolution-aware 57 | 58 | # To add custom fonts to your plugin package, add a fonts section here, 59 | # in this "flutter" section. Each entry in this list should have a 60 | # "family" key with the font family name, and a "fonts" key with a 61 | # list giving the asset and other descriptors for the font. For 62 | # example: 63 | # fonts: 64 | # - family: Schyler 65 | # fonts: 66 | # - asset: fonts/Schyler-Regular.ttf 67 | # - asset: fonts/Schyler-Italic.ttf 68 | # style: italic 69 | # - family: Trajan Pro 70 | # fonts: 71 | # - asset: fonts/TrajanPro.ttf 72 | # - asset: fonts/TrajanPro_Bold.ttf 73 | # weight: 700 74 | # 75 | # For details regarding fonts in packages, see 76 | # https://flutter.dev/custom-fonts/#from-packages 77 | -------------------------------------------------------------------------------- /test/walle_kit_method_channel_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:walle_kit/src/model/channel_info.dart'; 4 | import 'package:walle_kit/src/walle_kit_method_channel.dart'; 5 | 6 | void main() { 7 | final MethodChannelWalleKit platform = MethodChannelWalleKit(); 8 | const MethodChannel channel = MethodChannel('v7lin.github.io/walle_kit'); 9 | 10 | TestWidgetsFlutterBinding.ensureInitialized(); 11 | 12 | setUp(() { 13 | channel.setMockMethodCallHandler((MethodCall methodCall) async { 14 | switch (methodCall.method) { 15 | case 'getChannelId': 16 | return 'official'; 17 | case 'getChannelInfo': 18 | return { 19 | 'channel': 'official', 20 | }; 21 | } 22 | }); 23 | }); 24 | 25 | tearDown(() { 26 | channel.setMockMethodCallHandler(null); 27 | }); 28 | 29 | test('getChannelId', () async { 30 | expect(await platform.getChannelId(), 'official'); 31 | }); 32 | 33 | test('getChannelInfo', () async { 34 | final ChannelInfo? info = await platform.getChannelInfo(); 35 | expect(info?.channel, 'official'); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/walle_kit_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 5 | import 'package:walle_kit/src/model/channel_info.dart'; 6 | import 'package:walle_kit/src/walle.dart'; 7 | import 'package:walle_kit/src/walle_kit_method_channel.dart'; 8 | import 'package:walle_kit/src/walle_kit_platform_interface.dart'; 9 | 10 | class MockWalleKitPlatform 11 | with MockPlatformInterfaceMixin 12 | implements WalleKitPlatform { 13 | @override 14 | Future getChannelId() { 15 | return Future.value('official'); 16 | } 17 | 18 | @override 19 | Future getChannelInfo() { 20 | return Future.value(ChannelInfo(channel: 'official')); 21 | } 22 | } 23 | 24 | void main() { 25 | final WalleKitPlatform initialPlatform = WalleKitPlatform.instance; 26 | 27 | test('$MethodChannelWalleKit is the default instance', () { 28 | expect(initialPlatform, isInstanceOf()); 29 | }); 30 | 31 | test('getChannelId', () async { 32 | final MockWalleKitPlatform fakePlatform = MockWalleKitPlatform(); 33 | WalleKitPlatform.instance = fakePlatform; 34 | 35 | expect(await Walle.instance.getChannelId(), 'official'); 36 | }); 37 | 38 | test('getChannelInfo', () async { 39 | final MockWalleKitPlatform fakePlatform = MockWalleKitPlatform(); 40 | WalleKitPlatform.instance = fakePlatform; 41 | 42 | final ChannelInfo? info = await Walle.instance.getChannelInfo(); 43 | expect(info?.channel, 'official'); 44 | }); 45 | } 46 | --------------------------------------------------------------------------------