├── .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 | [](https://pub.dev/packages/walle_kit)
4 | [](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 | 
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 |
--------------------------------------------------------------------------------