├── .github
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── LICENSE
├── Polyfill.png
├── README.md
├── README_zh.md
├── android-arsc-parser
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── main
│ └── kotlin
│ │ ├── android
│ │ └── util
│ │ │ └── TypedValue.java
│ │ └── me
│ │ └── xx2bab
│ │ └── polyfill
│ │ └── arsc
│ │ ├── base
│ │ ├── CommonConstants.kt
│ │ ├── Header.kt
│ │ ├── IParsable.kt
│ │ └── ResTable.kt
│ │ ├── export
│ │ ├── IResArscTweaker.kt
│ │ ├── SimpleResource.kt
│ │ ├── SupportedResConfig.kt
│ │ └── SupportedResType.kt
│ │ ├── io
│ │ ├── ByteBufferExtension.kt
│ │ ├── LittleEndianInputStream.kt
│ │ └── LittleEndianOutputStream.kt
│ │ ├── pack
│ │ ├── AbsResType.kt
│ │ ├── ResPackage.kt
│ │ ├── TypeSpec.kt
│ │ ├── TypeType.kt
│ │ └── type
│ │ │ ├── ResConfig.kt
│ │ │ ├── ResEntry.kt
│ │ │ ├── ResMapValue.kt
│ │ │ └── ResValue.kt
│ │ └── stringpool
│ │ ├── StringPool.kt
│ │ └── UtfUtil.kt
│ └── test
│ ├── kotlin
│ └── me
│ │ └── xx2bab
│ │ └── polyfill
│ │ └── arsc
│ │ └── ResourceTableIntegrationTest.kt
│ └── resources
│ └── resources.arsc
├── android-manifest-parser
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── main
│ └── kotlin
│ │ └── me
│ │ └── xx2bab
│ │ └── polyfill
│ │ └── manifest
│ │ └── bytes
│ │ ├── ManifestInBytesProvider.kt
│ │ └── parser
│ │ ├── Header.kt
│ │ ├── IManifestBytesTweaker.kt
│ │ ├── ManifestBlock.kt
│ │ ├── ManifestBytesTweaker.kt
│ │ ├── ResourceIdBlock.kt
│ │ ├── StringPoolBlock.kt
│ │ └── body
│ │ ├── Attribute.kt
│ │ ├── EndNamespaceXmlBody.kt
│ │ ├── EndTagXmlBody.kt
│ │ ├── StartNamespaceXmlBody.kt
│ │ ├── StartTagXmlBody.kt
│ │ ├── TextXmlBody.kt
│ │ ├── XMLBody.kt
│ │ └── XMLBodyType.kt
│ └── test
│ ├── kotlin
│ └── me
│ │ └── xx2bab
│ │ └── polyfill
│ │ └── manifest
│ │ └── ManifestInBytesTweakerIntegrationTest.kt
│ └── resources
│ └── AndroidManifest.xml
├── build.gradle.kts
├── buildSrc
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── me
│ └── xx2bab
│ └── polyfill
│ └── buildscript
│ ├── BuildConfig.kt
│ ├── functional-test-setup.gradle.kts
│ ├── github-release.gradle.kts
│ └── maven-central-publish.gradle.kts
├── deps.versions.toml
├── functional-test
├── build.gradle.kts
└── src
│ └── functionalTest
│ └── kotlin
│ └── me
│ └── xx2bab
│ └── koncat
│ ├── CaseInsensitiveSubstringMatcher.java
│ └── SampleProjectTest.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── polyfill-backport
├── build.gradle.kts
├── gradle.properties
└── src
│ └── main
│ └── kotlin
│ └── me
│ └── xx2bab
│ └── polyfill
│ ├── BackportPatch.kt
│ └── tools
│ ├── ReflectionKit.kt
│ └── SemanticVersionLite.kt
├── polyfill-test-plugin
├── .gitignore
├── build.gradle.kts
├── gradle.properties
└── src
│ └── main
│ └── kotlin
│ └── me
│ └── xx2bab
│ └── polyfill
│ └── test
│ └── TestPlugin.kt
├── polyfill
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── main
│ └── kotlin
│ │ └── me
│ │ └── xx2bab
│ │ └── polyfill
│ │ ├── ArtifactExtension.kt
│ │ ├── ArtifactsRepository.kt
│ │ ├── PolyfillAction.kt
│ │ ├── PolyfillExtension.kt
│ │ ├── PolyfillPlugin.kt
│ │ ├── PolyfilledArtifacts.kt
│ │ ├── TaskExtendConfiguration.kt
│ │ ├── VariantExtension.kt
│ │ ├── artifact
│ │ ├── ArtifactContainer.kt
│ │ └── DefaultArtifactsRepository.kt
│ │ ├── jar
│ │ ├── JavaResourceMergeOfExtProjectsPreHookConfiguration.kt
│ │ ├── JavaResourceMergeOfSubProjectsPreHookConfiguration.kt
│ │ └── JavaResourceMergePreHookConfiguration.kt
│ │ ├── manifest
│ │ └── ManifestMergePreHookConfiguration.kt
│ │ ├── res
│ │ ├── ResourceMergePostHookConfiguration.kt
│ │ └── ResourceMergePreHookConfiguration.kt
│ │ └── tools
│ │ └── CommandLineKit.kt
│ └── test
│ └── kotlin
│ └── me
│ └── xx2bab
│ └── polyfill
│ └── PolyfillTest.kt
├── publish.sh
├── publish_to_local.sh
├── scripts
├── all-test.sh
├── function-test.sh
└── unit-and-integration-test.sh
├── settings.gradle.kts
└── test-app
├── .gitignore
├── android-lib
├── .gitignore
├── build.gradle.kts
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── me
│ │ └── xx2bab
│ │ └── polyfill
│ │ └── sample
│ │ └── android
│ │ └── ExportedAndroidLibraryRunnable.kt
│ └── resources
│ └── android-lib-java-res.txt
├── app
├── build.gradle.kts
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── kotlin
│ └── me
│ │ └── xx2bab
│ │ └── polyfill
│ │ └── sample
│ │ └── MainActivity.kt
│ └── res
│ ├── layout
│ └── activity_main.xml
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ └── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | - dev
8 | paths-ignore:
9 | - '*.md'
10 | push:
11 | branches:
12 | - master
13 | paths-ignore:
14 | - '*.md'
15 |
16 | env:
17 | CI: true
18 | GRADLE_OPTS: -Dorg.gradle.daemon=false -Dkotlin.incremental=false
19 | TERM: dumb
20 |
21 | jobs:
22 | assemble:
23 | name: Assemble
24 | runs-on: ubuntu-latest
25 | env:
26 | JAVA_TOOL_OPTIONS: -Xmx4g
27 |
28 | steps:
29 | - uses: actions/checkout@v2
30 | - uses: gradle/wrapper-validation-action@v1
31 | - uses: actions/setup-java@v2
32 | with:
33 | distribution: 'zulu'
34 | java-version: '17'
35 | - uses: actions/cache@v2
36 | with:
37 | path: |
38 | ~/.gradle/caches
39 | ~/.gradle/wrapper
40 | key: ${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
41 | restore-keys: |
42 | ${{ runner.os }}-${{ github.job }}-
43 | - run: |
44 | ./gradlew assemble
45 | checks:
46 | name: Checks (unit tests and static analysis, TODO:add detekt check after set it up)
47 | runs-on: ubuntu-latest
48 | env:
49 | JAVA_TOOL_OPTIONS: -Xmx4g
50 |
51 | steps:
52 | - uses: actions/checkout@v2
53 | - uses: gradle/wrapper-validation-action@v1
54 | - uses: actions/setup-java@v2
55 | with:
56 | distribution: 'zulu'
57 | java-version: '17'
58 | - uses: actions/cache@v2
59 | with:
60 | path: |
61 | ~/.gradle/caches
62 | ~/.gradle/wrapper
63 | key: ${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
64 | restore-keys: |
65 | ${{ runner.os }}-${{ github.job }}-
66 | - run: |
67 | ./gradlew test
68 | functional-tests:
69 | name: Functional Tests
70 | runs-on: ubuntu-latest
71 | env:
72 | JAVA_TOOL_OPTIONS: -Xmx4g
73 |
74 | steps:
75 | - uses: actions/checkout@v2
76 | - uses: gradle/wrapper-validation-action@v1
77 | - uses: actions/setup-java@v2
78 | with:
79 | distribution: 'zulu'
80 | java-version: '17'
81 | - uses: actions/cache@v2
82 | with:
83 | path: |
84 | ~/.gradle/caches
85 | ~/.gradle/wrapper
86 | key: ${{ runner.os }}-${{ github.job }}-${{ matrix.agp-version }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}
87 | restore-keys: |
88 | ${{ runner.os }}-${{ github.job }}-${{ matrix.agp-version }}-
89 |
90 | - name: Prepare environment
91 | env:
92 | SIGNING_SECRET_KEY_CONTENT: ${{ secrets.SIGNING_SECRET_KEY_CONTENT }}
93 | SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
94 | run: |
95 | git fetch --unshallow
96 | sudo bash -c "echo '$SIGNING_SECRET_KEY_CONTENT' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'"
97 |
98 | - name: Build & Release all Polyfill libraries to MavenLocal
99 | run: chmod +x ./publish_to_local.sh | ./publish_to_local.sh
100 | env:
101 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
102 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
103 | SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
104 | SIGNING_SECRET_KEY_CONTENT: ${{ secrets.SIGNING_SECRET_KEY_CONTENT }}
105 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
106 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
107 | GH_DEV_TOKEN: ${{ secrets.GH_DEV_TOKEN }}
108 |
109 | - run: |
110 | ./gradlew functionalTest
111 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | polyfill-release:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Check out
13 | uses: actions/checkout@v2
14 |
15 | - name: Set up JDK 17
16 | uses: actions/setup-java@v2
17 | with:
18 | distribution: 'zulu'
19 | java-version: '17'
20 |
21 | - name: Prepare environment
22 | env:
23 | SIGNING_SECRET_KEY_CONTENT: ${{ secrets.SIGNING_SECRET_KEY_CONTENT }}
24 | SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
25 | run: |
26 | git fetch --unshallow
27 | sudo bash -c "echo '$SIGNING_SECRET_KEY_CONTENT' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'"
28 |
29 | - name: Build & Release all Polyfill libraries to MavenCentral
30 | run: chmod +x ./publish.sh | ./publish.sh
31 | env:
32 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
33 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
34 | SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}
35 | SIGNING_SECRET_KEY_CONTENT: ${{ secrets.SIGNING_SECRET_KEY_CONTENT }}
36 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
37 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
38 | GH_DEV_TOKEN: ${{ secrets.GH_DEV_TOKEN }}
39 |
40 | - name: Upload Github Artifacts
41 | uses: actions/upload-artifact@v2
42 | with:
43 | name: artifacts
44 | path: build/libs/
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea/
3 | .gradle/
4 | build
5 | *.iml
6 | local.properties
7 | local/
--------------------------------------------------------------------------------
/Polyfill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2BAB/Polyfill/86b198afbd5616c80eb54063cc11034a3577865e/Polyfill.png
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://search.maven.org/artifact/me.2bab/polyfill)
4 | [](https://github.com/2bab/Polyfill/actions)
5 | [](https://www.apache.org/licenses/LICENSE-2.0)
6 |
7 | [[English]](./README.md) [中文]
8 |
9 | 🚧 **孵化中...**
10 |
11 | Polyfill 是一个第三方的**工件仓库**,服务于编写 Android 构建环境下的 Gradle 插件,提供了与 Android Gradle Plugin(AGP) 的 Artifacts API 风格类似的接口给第三方插件开发者。
12 |
13 | 如果你不熟悉 AGP 的新 Artifact/Variant,请查看这份 @AndroidDevelopers 的官方指南 [Gradle and AGP build APIs - MAD Skills](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc8fyNmwKXYvA2CqxMhXqKXX)。更多信息请参考下方“为什么需要 Polyfill”小节。
14 |
15 |
16 | ## 快速上手
17 |
18 | 1. 添加 Polyfill 至你的 Gradle 插件工程(独立的插件工程或者 `buildSrc`):
19 |
20 | ``` kotlin
21 | dependencies {
22 | compileOnly("com.android.tools.build:gradle:8.1.2")
23 | implementation("me.2bab:polyfill:0.9.1")
24 | }
25 | ```
26 |
27 | 2. 应用 Polyfill 插件至你的插件 `apply(...)` 方法(最好在一切开始之前):
28 |
29 |
30 | ``` Kotlin
31 | import org.gradle.kotlin.dsl.apply
32 |
33 | class TestPlugin : Plugin {
34 | override fun apply(project: Project) {
35 | project.apply(plugin = "me.2bab.polyfill") <--
36 | ...
37 | }
38 | }
39 | ```
40 |
41 | 3. 借助 Polyfill 的 `variant.artifactsPolyfill.*` 相关 API 配置你的 `TaskProvider`(仅获取 Artifact 时) 或 `PolyfillAction`(需要修改 Artifact 时),其风格与 AGP 的 `variant.artifacts` 相近:
42 |
43 | ``` kotlin
44 | val androidExtension = project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
45 | androidExtension.onVariants { variant ->
46 |
47 | // get()/getAll()
48 | val printManifestTask = project.tasks.register(
49 | "getAllInputManifestsFor${variant.name.capitalize()}"
50 | ) {
51 | beforeMergeInputs.set(
52 | variant.artifactsPolyfill.getAll(PolyfilledMultipleArtifact.ALL_MANIFESTS) <--
53 | )
54 | }
55 | ...
56 |
57 | // use()
58 | val preHookManifestTaskAction1 = PreUpdateManifestsTaskAction(buildDir, id = "preHookManifestTaskAction1")
59 | variant.artifactsPolyfill.use(
60 | action = preHookManifestTaskAction1,
61 | toInPlaceUpdate = PolyfilledMultipleArtifact.ALL_MANIFESTS
62 | )
63 | }
64 |
65 | ...
66 | class PreUpdateManifestsTaskAction(
67 | buildDir: File,
68 | id: String
69 | ) : PolyfillAction> {
70 |
71 | override fun onTaskConfigure(task: Task) {}
72 |
73 | override fun onExecute(artifact: Provider>) {
74 | artifact.get().let { files ->
75 | files.forEach {
76 | val manifestFile = it.asFile
77 | // Check per manifest input and filter whatever you want, remove broken pieces, etc.
78 | // val updatedContent = manifestFile.readText().replace("abc", "def")
79 | // manifestFile.writeText(updatedContent)
80 | }
81 | }
82 | }
83 |
84 | }
85 | ```
86 |
87 | 所有 Polyfill 支持的工件已在下方列出:
88 |
89 |
90 | |PolyfilledSingleArtifact|Data Type|Description|
91 | |:---:|:---:|:---:|
92 | |MERGED_RESOURCES|`Provider`|To retrieve merged `/res` directory.|
93 |
94 |
95 | |PolyfilledMultipleArtifact|Data Type|Description|
96 | |:---:|:---:|:---:|
97 | | ALL_MANIFESTS |`ListProvider`| To retrieve all `AndroidManifest.xml` regular files that will paticipate merge process. |
98 | | ALL_RESOURCES |`ListProvider`| To retrieve all `/res` directories that will paticipate merge process. |
99 | | ALL_JAVA_RES |`ListProvider`| To retrieve all Java Resources that will paticipate merge process. |
100 |
101 | 另外 `Artifact.Single`、`Artifact.Multiple` 和它们的实现类例如 `InternalArtifactType` 均被 `get(...)/getAll(...)` 支持,通过它们你可以获取更多 AGP 内部的 Artifacts.
102 |
103 | 4. 如果上述 API 集无法满足你的需求,Polyfill 提供了其底层的数据管道机制以及获取数据的便捷工具,方便注册自定义的工件(同样欢迎直接提交 PR)。
104 |
105 | ``` Kotlin
106 | project.extensions.getByType()
107 | .registerTaskExtensionConfig(DUMMY_SINGLE_ARTIFACT, DummySingleArtifactImpl::class)
108 | ```
109 |
110 | 更多信息请查看 `./polyfill-test-plugin` 和`./polyfill/src/functionalTest`.
111 |
112 |
113 | ## 为什么需要 Polyfill?
114 |
115 | 顾名思义(Polyfill 直译为垫片),该框架是一个建立在 **AGP** (Android Gradle Plugin) 基础之上的,介于 **AGP** 和**第三方 Gradle Plugin** 之间的一个中间件。以 [ScratchPaper](https://github.com/2BAB/ScratchPaper) 项目为例,它是一个 Gradle 插件,基于 AGP 用于在 App 的启动图标上添加一层半透明信息,它需要这些输入:
116 |
117 | 1. SDK Locations / BuildToolInfo instance(用以运行 aapt2 命令)
118 | 2. 所有输入的 `res` 文件夹(用以查找启动图标文件来源)
119 | 3. 合并后的 AndroidManifest.xml 文件(用以获取解析后的图标名字)
120 |
121 | 在我刚创建 ScratchPaper 项目时,AGP 还未提供任何与上述三份数据有关的公开 API,我们只能使用一些骇客式的 Hooks 来解决。2018 年时,我开始思考是否可以为第三方 Android Gradle 插件开发者做一个 Polyfill 层(中间层),并且最终在 2020 年我发布了第一个版本,也即您在这所看到的。Polyfill 这个名字来自于前端技术栈,一个使 JS code 可以和一些老的/罕见的浏览器 API 兼容的库。
122 |
123 | 而从 AGP 7.0.0 开始,AGP 开发团队正式提供了一个新的公开 API 集,**"Artifacts"**。你可以从这里查看到最新公开的 Artifacts:[SingleArtifact](https://developer.android.com/reference/tools/gradle-api/current/com/android/build/api/artifact/SingleArtifact)
124 | , [MultipleArtifact](https://developer.android.com/reference/tools/gradle-api/current/com/android/build/api/artifact/MultipleArtifact)
125 | ("Known Direct Subclasses" 的部分)。新的 `Variant/Artifact` 还处在较为早期的阶段,只提供了不到 10 个的 Artifacts API
126 | 给开发者们去使用。**由于 AGP 每年只发布 2-3 个小版本,开发者们需要紧跟更新,以期待获得自己的需求得到满足。** 回到上述案例,目前仅第三项数据是被 Artifacts API 所支持,剩余的两项则需要开发者自行处理。为了满足这些不被公开数据集支持的开发需求,我们能做的是:
127 |
128 | 1. 在 [AGP](https://issuetracker.google.com/issues?q=componentid:192709) 的 issues tracker 板块提出我们的需求。
129 | 2. 同时,构建一个非官方的数据管道用于承载我们的 hooks(借鉴 `artifacts.use()` 的机制),既作为临时的解决方案也方便未来过渡到官方的 Artifacts API。
130 |
131 | 这就是我为什么依然坚持去创造一个 Polyfill 库,并且希望有一天我们可以做到 100% 的迁移到 Artifacts API。你可从下方的链接获取更多的 Artifaces API 资讯:
132 |
133 | - [gradle-recipes](https://github.com/android/gradle-recipes):官方 Artifacts API 的展示案例。
134 | - [New APIs in the Android Gradle Plugin](https://medium.com/androiddevelopers/new-apis-in-the-android-gradle-plugin-f5325742e614) :一个简要的新 Artifacts API 介绍。
135 | - [Extend the Android Gradle plugin](https://developer.android.com/studio/build/extend-agp):Android 官方 2021 年 10 月放出的 Variant/Artifact API 官方文档。
136 |
137 |
138 | ## 兼容说明
139 |
140 | Polyfill 只支持并在最新的两个 Android Gradle Plugin (minor) 版本进行测试。
141 |
142 | | AGP Version | Latest Support Version |
143 | |:-------------:|:--------------------------------:|
144 | | 8.1.x / 8.0.x | 0.9.1 |
145 | | 7.2.x / 7.1.x | 0.8.1 |
146 | | 7.2.x / 7.1.x | 0.7.0 |
147 | | 7.1.x | 0.6.2 |
148 | | 7.0.x | 0.4.1 |
149 | | 4.2.0 | 0.3.1 (Migrated to MavenCentral) |
150 |
151 |
152 | ## Git Commit Check
153 |
154 | 关于 Git Commit
155 | 的规则,请阅读这个 [link](https://medium.com/walmartlabs/check-out-these-5-git-tips-before-your-next-commit-c1c7a5ae34d1),以确保自己写的是有意义的提交信息。
156 |
157 | 目前为止我还没有添加任何 git hook 工具,但是写 git commit message 时请遵守以下正则表达式:
158 |
159 | ```
160 | (chore|feat|docs|fix|refactor|style|test|hack|release)(:)( )(.{0,80})
161 | ```
162 |
163 | ## License
164 |
165 | >
166 | > Copyright Since 2018 2BAB
167 | >
168 | >Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
169 | >
170 | > http://www.apache.org/licenses/LICENSE-2.0
171 | >
172 | > Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
173 |
--------------------------------------------------------------------------------
/android-arsc-parser/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import me.xx2bab.polyfill.buildscript.BuildConfig.Versions
2 |
3 | plugins {
4 | kotlin("jvm")
5 | id("me.xx2bab.polyfill.buildscript.maven-central-publish")
6 | }
7 |
8 | dependencies {
9 | implementation(fileTree(mapOf("dir" to "libs", "include" to arrayOf("*.jar"))))
10 |
11 | implementation(gradleApi())
12 | implementation(deps.kotlin.std)
13 |
14 | compileOnly(deps.android.gradle.plugin)
15 | api(deps.guava)
16 |
17 | testImplementation(deps.junit)
18 | testImplementation(deps.mockito)
19 | testImplementation(deps.mockitoInline)
20 | }
21 |
22 | java {
23 | withSourcesJar()
24 | sourceCompatibility = Versions.polyfillSourceCompatibilityVersion
25 | targetCompatibility = Versions.polyfillTargetCompatibilityVersion
26 | }
--------------------------------------------------------------------------------
/android-arsc-parser/gradle.properties:
--------------------------------------------------------------------------------
1 | me.2bab.maven.publish.type=jar
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/base/CommonConstants.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.base
2 |
3 | const val INVALID_VALUE_BYTE: Byte = (Byte.MIN_VALUE + 1).toByte()
4 | const val INVALID_VALUE_SHORT: Short = (Short.MIN_VALUE + 1).toShort()
5 | const val INVALID_VALUE_INT: Int = Int.MIN_VALUE + 1
6 |
7 | const val UTF8_FLAG = 1 shl 8
8 |
9 | const val RES_TABLE_TYPE_SPEC_TYPE: Short = 0x0202
10 | const val RES_TABLE_TYPE_TYPE: Short = 0x0201
11 |
12 | const val NO_ENTRY_INDEX = 0xFFFFFFFF
13 |
14 | const val RES_TABLE_ENTRY_FLAG_COMPLEX: Short = 0x0001
15 | const val RES_TABLE_ENTRY_FLAG_PUBLIC: Short = 0x0002
16 |
17 | const val SIZE_INT = 4
18 | const val SIZE_SHORT = 2
19 | const val SIZE_BYTE = 1
20 | const val SIZE_CHAR = 2
21 | const val SIZE_LONG = 8
22 | const val SIZE_FLOAT = 4
23 | const val SIZE_DOUBLE = 8
24 |
25 | fun sizeOf(data: Any?): Int {
26 | if (data == null) throw NullPointerException()
27 | val dataType = data.javaClass
28 | return when (data) {
29 | is Int -> SIZE_INT
30 | is Short -> SIZE_SHORT
31 | is Byte -> SIZE_BYTE
32 | is Char -> SIZE_CHAR
33 | is Long -> SIZE_LONG
34 | is Float -> SIZE_FLOAT
35 | is Double -> SIZE_DOUBLE
36 | else -> 0
37 | }
38 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/base/Header.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.base
2 |
3 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
4 | import java.io.IOException
5 |
6 | /**
7 | * The common header for per chunk.
8 | */
9 | class Header: IParsable {
10 |
11 | var start: Long = 0
12 | var type: Short = INVALID_VALUE_SHORT
13 | var headSize: Short = 0
14 | var chunkSize: Int = 0
15 |
16 | @Throws(IOException::class)
17 | override fun parse(input: LittleEndianInputStream, start: Long) {
18 | input.seek(start)
19 | this.start = start
20 | type = input.readShort()
21 | headSize = input.readShort()
22 | chunkSize = input.readInt()
23 | }
24 |
25 | override fun toString(): String {
26 | return "Header(start=$start, type=$type, headSize=$headSize, chunkSize=$chunkSize)"
27 | }
28 |
29 | /**
30 | * To generate a header by ByteArray, you should create it manually by
31 | *
32 | * - Passing current start index;
33 | * - Passing the type from the original Header;
34 | * - Passing the headSize from the original Header (So far the size is fixed per chunk);
35 | * - Passing the chunkSize calculating from chunk's #toByteArray().
36 | *
37 | * DO NOT CALL THIS METHOD IN ANY CASES.
38 | */
39 | override fun toByteArray(): ByteArray {
40 | return ByteArray(0)
41 | }
42 |
43 | fun size(): Int {
44 | return sizeOf(type) + sizeOf(headSize) + sizeOf(chunkSize)
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/base/IParsable.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.base
2 |
3 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
4 | import java.io.IOException
5 |
6 | /**
7 | * To denote a class support parsing its properties from byte streams, and vice versa.
8 | */
9 | interface IParsable {
10 |
11 | /**
12 | * To parse properties from byte stream for it self.
13 | * Do not call this function in a constructor since that is a bad practice to throw exception in constructors.
14 | *
15 | * Reference: https://stackoverflow.com/questions/6086334/is-it-good-practice-to-make-the-constructor-throw-an-exception/6086399
16 | */
17 | @Throws(IOException::class)
18 | fun parse(input: LittleEndianInputStream, start: Long)
19 |
20 | /**
21 | * To generate ByteArray of its exportable properties.
22 | * It's the reverse operation of parse(...) .
23 | */
24 | fun toByteArray(): ByteArray
25 |
26 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/base/ResTable.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.base
2 |
3 | import android.util.TypedValue.*
4 | import me.xx2bab.polyfill.arsc.export.IResArscTweaker
5 | import me.xx2bab.polyfill.arsc.export.SimpleResource
6 | import me.xx2bab.polyfill.arsc.export.SupportedResConfig
7 | import me.xx2bab.polyfill.arsc.export.SupportedResType
8 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
9 | import me.xx2bab.polyfill.arsc.io.LittleEndianOutputStream
10 | import me.xx2bab.polyfill.arsc.io.flipToArray
11 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
12 | import me.xx2bab.polyfill.arsc.pack.ResPackage
13 | import me.xx2bab.polyfill.arsc.pack.TypeType
14 | import me.xx2bab.polyfill.arsc.pack.type.ResEntry
15 | import me.xx2bab.polyfill.arsc.stringpool.StringPool
16 | import java.io.File
17 | import java.io.IOException
18 | import java.nio.ByteBuffer
19 | import java.util.*
20 |
21 | /**
22 | * The parser of resource.arsc binary artifact.
23 | */
24 | class ResTable : IParsable, IResArscTweaker {
25 |
26 | lateinit var header: Header
27 | var packageCount = 0
28 | lateinit var stringPool: StringPool
29 | val packages = mutableListOf()
30 |
31 | @Throws(IOException::class)
32 | override fun parse(input: LittleEndianInputStream, start: Long) {
33 | // 1. Header
34 | header = Header()
35 | header.parse(input, start)
36 |
37 | // 2. Package Count
38 | packageCount = input.readInt()
39 |
40 | // 3. Global StringPool
41 | stringPool = StringPool()
42 | stringPool.parse(input, input.filePointer)
43 |
44 | // 4. Package
45 | for (i in 0 until packageCount) {
46 | val resPackage = ResPackage()
47 | resPackage.parse(input, input.filePointer)
48 | packages.add(resPackage)
49 | }
50 |
51 | // println("Done")
52 | }
53 |
54 | override fun toByteArray(): ByteArray {
55 | val headerSize = header.size() + sizeOf(packageCount)
56 |
57 | val stringPoolByteArray = stringPool.toByteArray()
58 | val stringPoolSize = stringPoolByteArray.size
59 | val packageByteArrays = packages.map { it.toByteArray() }
60 | val packageSize = packageByteArrays.sumBy { it.size }
61 |
62 | val newChunkSize = headerSize + stringPoolSize + packageSize
63 | val bf = ByteBuffer.allocate(newChunkSize)
64 | bf.takeLittleEndianOrder()
65 | bf.putShort(header.type)
66 | bf.putShort(headerSize.toShort())
67 | bf.putInt(newChunkSize)
68 | bf.putInt(packages.size)
69 | bf.put(stringPoolByteArray)
70 | packageByteArrays.forEach { bf.put(it) }
71 |
72 | return bf.flipToArray()
73 | }
74 |
75 |
76 | override fun read(source: File) {
77 | if (source.exists() && source.isFile && source.extension == "arsc") {
78 | val inputStream = LittleEndianInputStream(source)
79 | parse(inputStream, 0)
80 | return
81 | }
82 | throw IllegalArgumentException("The arsc file is illegal.")
83 | }
84 |
85 | override fun write(dest: File) {
86 | if (dest.exists()) {
87 | dest.delete()
88 | }
89 | dest.parentFile.mkdirs()
90 | dest.createNewFile()
91 | val outputStream = LittleEndianOutputStream(dest)
92 | outputStream.writeByte(toByteArray())
93 | outputStream.close()
94 | }
95 |
96 | override fun getResourceTypes(): Map {
97 | return Collections.emptyMap()
98 | }
99 |
100 | override fun findResourceById(id: Int): List {
101 | val filteredPackages = packages.filter { it.packageId == getPackageId(id) }
102 | return findResourceEntriesById(id, SupportedResConfig()).map {
103 | val type = parseSupportType(it.resValue.dataType.toInt())
104 | val name = parseName(filteredPackages[0], it.stringPoolIndex)
105 | val value = parseValue(type, it.resValue.data)
106 | SimpleResource(id, type, name, value)
107 | }
108 | }
109 |
110 | override fun removeResourceById(id: Int): Boolean {
111 | return false
112 | }
113 |
114 | override fun updateResourceById(resource: SimpleResource,
115 | config: SupportedResConfig): Boolean {
116 | val entries = findResourceEntriesById(resource.id, config)
117 | if (entries.isEmpty()) {
118 | return false
119 | }
120 | when (resource.type) {
121 | SupportedResType.COLOR -> {
122 | entries.forEach {
123 | it.resValue.data = parseColor(resource.value!!)
124 | }
125 | }
126 |
127 | SupportedResType.STRING -> {
128 | entries.forEach {
129 | stringPool.strings[it.resValue.data] = resource.value
130 | }
131 | }
132 |
133 | SupportedResType.UNSUPPORTED -> return false
134 | }
135 | return true
136 | }
137 |
138 | private fun findResourceEntriesById(id: Int, config: SupportedResConfig): List {
139 | val packageId = getPackageId(id)
140 | val typeId = getResourceTypeId(id)
141 | val entryId = getResourceEntryId(id)
142 | val filteredPackages = packages.filter { it.packageId == packageId }
143 | if (filteredPackages.isNullOrEmpty()) {
144 | return Collections.emptyList()
145 | }
146 | val filteredTypes = filteredPackages[0].resTypes.filter { it.typeId.toInt() == typeId }
147 | if (filteredTypes.isNullOrEmpty()) {
148 | return Collections.emptyList()
149 | }
150 | return filteredTypes.filter {
151 | it is TypeType && it.entries.size > entryId // List
152 | }.filter {
153 | val tt = it as TypeType
154 | val result1 = if (config.minOsVersion != INVALID_VALUE_INT) {
155 | val v = tt.config.sdkVersion.toInt()
156 | if (v == 0) {
157 | false
158 | } else {
159 | config.minOsVersion >= v
160 | }
161 | } else {
162 | true
163 | }
164 | val result2 = if (config.language.isNotBlank()) {
165 | val lang = tt.config.unpackLanguage(tt.config.language)
166 | if (lang == "") {
167 | false
168 | } else {
169 | config.language.equals(lang, true)
170 | }
171 | } else {
172 | true
173 | }
174 | result1 && result2
175 | }.mapNotNull {
176 | (it as TypeType).entries[entryId] // List
177 | }.filter {
178 | it.pairCount == 0 // List, here only support simple ResValue
179 | }.filter {
180 | val type = it.resValue.dataType.toInt()
181 | parseSupportType(type) != SupportedResType.UNSUPPORTED
182 | }
183 | }
184 |
185 | private fun parseSupportType(type: Int): SupportedResType {
186 | return when (type) {
187 | in TYPE_FIRST_COLOR_INT..TYPE_LAST_COLOR_INT -> {
188 | SupportedResType.COLOR
189 | }
190 | TYPE_STRING -> {
191 | SupportedResType.STRING
192 | }
193 | else -> {
194 | SupportedResType.UNSUPPORTED
195 | }
196 | }
197 | }
198 |
199 | private fun parseName(resPackage: ResPackage, nameIndex: Int): String? {
200 | return resPackage.resKeywordStringPool.strings[nameIndex]
201 | }
202 |
203 | private fun parseValue(type: SupportedResType, value: Int): String? {
204 | if (type == SupportedResType.COLOR) { // Color
205 | return "#${Integer.toHexString(value)}"
206 | } else if (type == SupportedResType.STRING) {
207 | return stringPool.strings[value]
208 | }
209 | return ""
210 | }
211 |
212 |
213 | private fun getPackageId(resourceId: Int): Int {
214 | return resourceId and 0xFF000000.toInt() shr 24
215 | }
216 |
217 | private fun getResourceTypeId(resourceId: Int): Int {
218 | return resourceId and 0x00FF0000 shr 16
219 | }
220 |
221 | private fun getResourceEntryId(resourceId: Int): Int {
222 | return resourceId and 0x0000FFFF
223 | }
224 |
225 | private fun generateResourceId(packageId: Int, typeId: Int, entryId: Int): Int {
226 | return (packageId shl 24) + (typeId shl 16) + (entryId)
227 | }
228 |
229 | /**
230 | * Converted from Android Source Code [android.graphic.Color#parseColor(color: String)]
231 | * [Source Link](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/graphics/java/android/graphics/Color.java?q=android.graphics.Color)
232 | */
233 | private fun parseColor(colorString: String): Int {
234 | if (colorString[0] == '#') {
235 | // Use a long to avoid rollovers on #ffXXXXXX
236 | var color = colorString.substring(1).toLong(16)
237 | if (colorString.length == 7) {
238 | // Set the alpha value
239 | color = color or -0x1000000
240 | } else require(colorString.length == 9) { "Unknown color" }
241 | return color.toInt()
242 | }
243 | throw java.lang.IllegalArgumentException("Unknown color")
244 | }
245 |
246 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/export/IResArscTweaker.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.export
2 |
3 | import java.io.File
4 | import java.io.IOException
5 |
6 | /**
7 | * The export api for resource.arsc tool.
8 | */
9 | interface IResArscTweaker {
10 |
11 | /**
12 | * To load the arsc file into tweaker.
13 | * @param
14 | */
15 | @Throws(IOException::class)
16 | fun read(source: File)
17 |
18 | /**
19 | * To write a new arsc file to specify file.
20 | * @param
21 | */
22 | @Throws(IOException::class)
23 | fun write(dest: File)
24 |
25 | /**
26 | * @return Return types with
27 | */
28 | fun getResourceTypes(): Map
29 |
30 | /**
31 | * @param id resource ID
32 | * @return SimpleResource instance or null
33 | */
34 | fun findResourceById(id: Int): List
35 |
36 | /**
37 | * @param id resource ID
38 | * @return SimpleResource instance or null
39 | */
40 | fun removeResourceById(id: Int): Boolean
41 |
42 | fun updateResourceById(resource: SimpleResource,
43 | config: SupportedResConfig): Boolean
44 |
45 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/export/SimpleResource.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.export
2 |
3 | data class SimpleResource(
4 | val id: Int,
5 | val type: SupportedResType,
6 | val name: String?,
7 | val value: String?) {
8 |
9 | companion object {
10 |
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/export/SupportedResConfig.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.export
2 |
3 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_INT
4 |
5 | class SupportedResConfig {
6 |
7 | var language = ""
8 | var minOsVersion = INVALID_VALUE_INT
9 |
10 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/export/SupportedResType.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.export
2 |
3 | enum class SupportedResType {
4 |
5 | COLOR,
6 | STRING,
7 | UNSUPPORTED
8 |
9 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/io/ByteBufferExtension.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.io
2 |
3 | import java.nio.ByteBuffer
4 | import java.nio.ByteOrder
5 |
6 | fun ByteBuffer.takeLittleEndianOrder() {
7 | order(ByteOrder.LITTLE_ENDIAN)
8 | clear()
9 | }
10 |
11 | fun ByteBuffer.flipToArray(): ByteArray {
12 | flip()
13 | return array()
14 | }
15 |
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/io/LittleEndianInputStream.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.io
2 |
3 | import java.io.File
4 | import java.io.IOException
5 | import java.io.InputStream
6 | import java.io.RandomAccessFile
7 | import java.nio.ByteBuffer
8 | import java.nio.ByteOrder
9 |
10 | /**
11 | * Convert from Matrix repository.
12 | *
13 | * https://github.com/Tencent/matrix/blob/master/matrix/matrix-android/matrix-arscutil/src/main/java/com/tencent/mm/arscutil/io/LittleEndianInputStream.java
14 | *
15 | * Created by jinqiuchen on 18/7/29.
16 | */
17 | class LittleEndianInputStream(private val original: RandomAccessFile) : InputStream() {
18 |
19 | constructor(file: File) : this(RandomAccessFile(file, "r")) {}
20 |
21 | constructor(file: String) : this(RandomAccessFile(file, "r")) {}
22 |
23 | @Throws(IOException::class)
24 | override fun read(): Int {
25 | // TODO Auto-generated method stub
26 | return original.read()
27 | }
28 |
29 | @Throws(IOException::class)
30 | fun readShort(): Short {
31 | val byteBuffer = ByteBuffer.allocate(2)
32 | byteBuffer.clear()
33 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN)
34 | byteBuffer.put(original.readByte())
35 | byteBuffer.put(original.readByte())
36 | byteBuffer.flip()
37 | return byteBuffer.short
38 | }
39 |
40 | @Throws(IOException::class)
41 | fun readInt(): Int {
42 | val byteBuffer = ByteBuffer.allocate(4)
43 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN)
44 | byteBuffer.clear()
45 | for (i in 1..4) {
46 | byteBuffer.put(original.readByte())
47 | }
48 | byteBuffer.flip()
49 | return byteBuffer.int
50 | }
51 |
52 | @Throws(IOException::class)
53 | fun readByte(): Byte {
54 | return original.readByte()
55 | }
56 |
57 | @JvmOverloads
58 | @Throws(IOException::class)
59 | fun readByte(buffer: ByteArray, offset: Int = 0, length: Int = buffer.size) {
60 | val byteBuffer = ByteBuffer.allocate(length)
61 | byteBuffer.clear()
62 | for (i in 1..length) {
63 | byteBuffer.put(original.readByte())
64 | }
65 | byteBuffer.flip()
66 | byteBuffer[buffer, offset, length]
67 | }
68 |
69 | @Throws(IOException::class)
70 | fun seek(pos: Long) {
71 | original.seek(pos)
72 | }
73 |
74 | @get:Throws(IOException::class)
75 | val filePointer: Long
76 | get() = original.filePointer
77 |
78 | @get:Throws(IOException::class)
79 | val fileLength: Long
80 | get() = original.length()
81 |
82 | @Throws(IOException::class)
83 | override fun close() {
84 | // TODO Auto-generated method stub
85 | super.close()
86 | original.close()
87 | }
88 |
89 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/io/LittleEndianOutputStream.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.io
2 |
3 | import java.io.File
4 | import java.io.IOException
5 | import java.io.OutputStream
6 | import java.io.RandomAccessFile
7 | import java.nio.ByteBuffer
8 | import java.nio.ByteOrder
9 |
10 | /**
11 | * Convert from Matrix repository.
12 | *
13 | * https://github.com/Tencent/matrix/blob/master/matrix/matrix-android/matrix-arscutil/src/main/java/com/tencent/mm/arscutil/io/LittleEndianOutputStream.java
14 | *
15 | * Created by jinqiuchen on 18/7/29.
16 | */
17 | class LittleEndianOutputStream(private val original: RandomAccessFile) : OutputStream() {
18 |
19 | constructor(file: File?) : this(RandomAccessFile(file, "rw")) {}
20 |
21 | constructor(file: String?) : this(RandomAccessFile(file, "rw")) {}
22 |
23 | @Throws(IOException::class)
24 | override fun write(b: Int) {
25 | original.write(b)
26 | }
27 |
28 | @Throws(IOException::class)
29 | fun writeShort(data: Short) {
30 | val byteBuffer = ByteBuffer.allocate(2)
31 | byteBuffer.clear()
32 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN)
33 | byteBuffer.putShort(data)
34 | byteBuffer.flip()
35 | original.write(byteBuffer.array())
36 | }
37 |
38 | @Throws(IOException::class)
39 | fun writeInt(data: Int) {
40 | val byteBuffer = ByteBuffer.allocate(4)
41 | byteBuffer.clear()
42 | byteBuffer.order(ByteOrder.LITTLE_ENDIAN)
43 | byteBuffer.putInt(data)
44 | byteBuffer.flip()
45 | original.write(byteBuffer.array())
46 | }
47 |
48 | @Throws(IOException::class)
49 | fun writeByte(data: Byte) {
50 | original.write(data.toInt())
51 | }
52 |
53 | @Throws(IOException::class)
54 | fun writeByte(buffer: ByteArray?) {
55 | original.write(buffer)
56 | }
57 |
58 | @Throws(IOException::class)
59 | fun writeByte(buffer: ByteArray?, offset: Int, length: Int) {
60 | original.write(buffer, offset, length)
61 | }
62 |
63 | @Throws(IOException::class)
64 | fun seek(pos: Long) {
65 | original.seek(pos)
66 | }
67 |
68 | @get:Throws(IOException::class)
69 | val filePointer: Long
70 | get() = original.filePointer
71 |
72 | @get:Throws(IOException::class)
73 | val fileLength: Long
74 | get() = original.length()
75 |
76 | @Throws(IOException::class)
77 | override fun close() {
78 | super.close()
79 | original.close()
80 | }
81 |
82 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/pack/AbsResType.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.pack
2 |
3 | import me.xx2bab.polyfill.arsc.base.Header
4 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_BYTE
5 | import me.xx2bab.polyfill.arsc.base.IParsable
6 | import me.xx2bab.polyfill.arsc.base.sizeOf
7 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
8 |
9 | abstract class AbsResType: IParsable {
10 |
11 | lateinit var header: Header // Pass the header instance from resTable before using it or calling parse(...)
12 | var typeId: Byte = INVALID_VALUE_BYTE
13 | protected var reservedField0: Byte = 0 // So far can ignore it
14 | protected var reservedField1: Short = 0 // So far can ignore it
15 |
16 | override fun parse(input: LittleEndianInputStream, start: Long) {
17 | // The header should passed from outside, the start value is
18 | typeId = input.readByte()
19 | reservedField0 = input.readByte()
20 | reservedField1 = input.readShort()
21 | }
22 |
23 | fun commonHeaderSize(): Int {
24 | return header.size() + sizeOf(typeId) + sizeOf(reservedField0) + sizeOf(reservedField1)
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/pack/ResPackage.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.pack
2 |
3 | import me.xx2bab.polyfill.arsc.base.*
4 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
5 | import me.xx2bab.polyfill.arsc.io.flipToArray
6 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
7 | import me.xx2bab.polyfill.arsc.stringpool.StringPool
8 | import java.io.IOException
9 | import java.nio.ByteBuffer
10 |
11 | class ResPackage : IParsable {
12 |
13 | lateinit var header: Header
14 | var packageId: Int = 0
15 | val packageName: ByteArray = ByteArray(256) // Can convert to a string
16 | var resTypeStringPoolOffset: Int = INVALID_VALUE_INT
17 | var lastPublicType: Int = INVALID_VALUE_INT
18 | var resKeywordStringPoolOffset: Int = INVALID_VALUE_INT
19 | var lastPublicKey: Int = INVALID_VALUE_INT
20 | var typeIdOffset: Int = INVALID_VALUE_INT
21 |
22 | lateinit var resTypeStringPool: StringPool
23 | lateinit var resKeywordStringPool: StringPool
24 | val resTypes = mutableListOf()
25 |
26 | @Throws(IOException::class)
27 | override fun parse(input: LittleEndianInputStream, start: Long) {
28 | input.seek(start)
29 |
30 | // the header size counts:
31 | //
32 | // packageId,
33 | // packageName,
34 | // resTypeStringPoolOffset,
35 | // lastPublicType,
36 | // resKeywordStringPoolOffset,
37 | // lastPublicKey
38 | header = Header()
39 | header.parse(input, start)
40 | packageId = input.readInt()
41 | input.read(packageName)
42 | resTypeStringPoolOffset = input.readInt()
43 | lastPublicType = input.readInt()
44 | resKeywordStringPoolOffset = input.readInt()
45 | lastPublicKey = input.readInt()
46 | typeIdOffset = input.readInt()
47 |
48 | resTypeStringPool = StringPool()
49 | resTypeStringPool.parse(input, header.start + resTypeStringPoolOffset)
50 | resKeywordStringPool = StringPool()
51 | resKeywordStringPool.parse(input, header.start + resKeywordStringPoolOffset)
52 |
53 | while (input.filePointer < header.start + header.chunkSize) {
54 | val typeHeader = Header()
55 | val currStart = input.filePointer
56 | typeHeader.parse(input, currStart)
57 | if (typeHeader.type == RES_TABLE_TYPE_SPEC_TYPE) {
58 | val typeSpec = TypeSpec()
59 | typeSpec.header = typeHeader
60 | typeSpec.parse(input, currStart)
61 | resTypes.add(typeSpec)
62 | } else if (typeHeader.type == RES_TABLE_TYPE_TYPE) {
63 | val typeTypeConfigList = TypeType()
64 | typeTypeConfigList.header = typeHeader
65 | typeTypeConfigList.parse(input, currStart)
66 | resTypes.add(typeTypeConfigList)
67 | }
68 | input.seek(typeHeader.start + typeHeader.chunkSize)
69 | }
70 | }
71 |
72 | override fun toByteArray(): ByteArray {
73 | val headSize = header.size()
74 | val packageIdSize = sizeOf(packageId)
75 | val packageNameSize = packageName.size
76 | val resTypeStringPoolOffsetSize = sizeOf(resKeywordStringPoolOffset)
77 | val lastPublicTypeSize = sizeOf(lastPublicType)
78 | val resKeywordStringPoolOffsetSize = sizeOf(resKeywordStringPoolOffset)
79 | val lastPublicKeySize = sizeOf(lastPublicKey)
80 | val typeIdOffsetSize = sizeOf(typeIdOffset)
81 |
82 | val resTypeStringPoolByteArray = resTypeStringPool.toByteArray()
83 | val resTypeStringPoolByteArraySize = resTypeStringPoolByteArray.size
84 | val resKeywordStringPoolByteArray = resKeywordStringPool.toByteArray()
85 | val resKeywordStringPoolByteArraySize = resKeywordStringPoolByteArray.size
86 |
87 | val resTypesByteArrays = resTypes.map { it.toByteArray() }
88 | val resTypesSize = resTypesByteArrays.sumBy { it.size }
89 |
90 | val newChunkSize = (headSize
91 | + packageIdSize
92 | + packageNameSize
93 | + resTypeStringPoolOffsetSize
94 | + lastPublicTypeSize
95 | + resKeywordStringPoolOffsetSize
96 | + lastPublicKeySize
97 | + typeIdOffsetSize
98 | + resTypeStringPoolByteArraySize
99 | + resKeywordStringPoolByteArraySize
100 | + resTypesSize)
101 | val headerSize = (newChunkSize - resTypeStringPoolByteArraySize
102 | - resKeywordStringPoolByteArraySize - resTypesSize)
103 |
104 | val newResTypeStringPoolOffset = headerSize
105 | val newResKeywordStringPoolOffset = if (resKeywordStringPoolByteArraySize == 0) {
106 | 0
107 | } else {
108 | newResTypeStringPoolOffset + resTypeStringPoolByteArraySize
109 | }
110 |
111 |
112 | val bf = ByteBuffer.allocate(newChunkSize)
113 | bf.takeLittleEndianOrder()
114 | bf.putShort(header.type)
115 | bf.putShort(headerSize.toShort())
116 | bf.putInt(newChunkSize)
117 | bf.putInt(packageId)
118 | bf.put(packageName)
119 | bf.putInt(newResTypeStringPoolOffset)
120 | bf.putInt(lastPublicType)
121 | bf.putInt(newResKeywordStringPoolOffset)
122 | bf.putInt(lastPublicKey)
123 | bf.putInt(typeIdOffset)
124 | bf.put(resTypeStringPoolByteArray)
125 | bf.put(resKeywordStringPoolByteArray)
126 | resTypesByteArrays.forEach { bf.put(it) }
127 |
128 | return bf.flipToArray()
129 | }
130 |
131 |
132 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/pack/TypeSpec.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.pack
2 |
3 | import me.xx2bab.polyfill.arsc.base.SIZE_INT
4 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
5 | import me.xx2bab.polyfill.arsc.io.flipToArray
6 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
7 | import java.nio.ByteBuffer
8 |
9 | class TypeSpec : AbsResType() {
10 |
11 | var specCount: Int = 0
12 | lateinit var specArray: IntArray
13 |
14 | override fun parse(input: LittleEndianInputStream, start: Long) {
15 | super.parse(input, start)
16 | specCount = input.readInt()
17 | specArray = IntArray(specCount) { input.readInt() }
18 | }
19 |
20 | override fun toByteArray(): ByteArray {
21 | val commonHeaderSize = commonHeaderSize()
22 | val specCountSize = SIZE_INT
23 | val specArraySize = SIZE_INT * specArray.size
24 | val newChunkSize = commonHeaderSize + specCountSize + specArraySize
25 |
26 | val bf = ByteBuffer.allocate(newChunkSize)
27 | bf.takeLittleEndianOrder()
28 | bf.putShort(header.type)
29 | bf.putShort((commonHeaderSize + specCountSize).toShort())
30 | bf.putInt(newChunkSize)
31 | bf.put(typeId)
32 | bf.put(reservedField0)
33 | bf.putShort(reservedField1)
34 | bf.putInt(specArray.size)
35 | specArray.forEach { bf.putInt(it) }
36 |
37 | return bf.flipToArray()
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/pack/TypeType.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.pack
2 |
3 | import me.xx2bab.polyfill.arsc.base.NO_ENTRY_INDEX
4 | import me.xx2bab.polyfill.arsc.base.SIZE_INT
5 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
6 | import me.xx2bab.polyfill.arsc.io.flipToArray
7 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
8 | import me.xx2bab.polyfill.arsc.pack.type.ResConfig
9 | import me.xx2bab.polyfill.arsc.pack.type.ResEntry
10 | import java.nio.ByteBuffer
11 |
12 | class TypeType : AbsResType() {
13 |
14 | var entryCount: Int = 0
15 | var entryStart: Int = 0
16 | lateinit var config: ResConfig
17 | lateinit var entryOffsets: IntArray
18 | lateinit var entries: Array
19 |
20 | override fun parse(input: LittleEndianInputStream, start: Long) {
21 | // The header should passed from outside, the start value is
22 | super.parse(input, start)
23 | entryCount = input.readInt()
24 | entryStart = input.readInt()
25 |
26 | config = ResConfig()
27 | config.parse(input, input.filePointer)
28 |
29 | entryOffsets = IntArray(entryCount) { input.readInt() }
30 | input.seek(header.start + entryStart)
31 | entries = Array(entryCount) {
32 | if (entryOffsets[it] != NO_ENTRY_INDEX.toInt()) {
33 | input.seek(header.start + entryStart + entryOffsets[it])
34 | val entry = ResEntry()
35 | entry.parse(input, input.filePointer)
36 | entry
37 | } else {
38 | null
39 | }
40 | }
41 | }
42 |
43 | override fun toByteArray(): ByteArray {
44 | val commonHeaderSize = commonHeaderSize()
45 |
46 | val configByteArray = config.toByteArray()
47 | val configSize = configByteArray.size
48 | val newEntryCount = entries.size
49 | val entryCountSize = SIZE_INT
50 | val newEntryByteArray: List = entries.map { it?.toByteArray() }
51 | val entrySize = newEntryByteArray.sumBy { it?.size ?: 0 }
52 |
53 | val newEntryOffsets = IntArray(entryCount)
54 | var currentPointer = 0
55 | var lastSize = 0
56 | for (i in 0 until newEntryCount) {
57 | val eba = newEntryByteArray[i]
58 | if (i == 0) {
59 | if (eba == null) {
60 | newEntryOffsets[i] = NO_ENTRY_INDEX.toInt()
61 | } else {
62 | newEntryOffsets[i] = 0
63 | lastSize = eba.size
64 | }
65 | } else {
66 | if (eba == null) {
67 | newEntryOffsets[i] = NO_ENTRY_INDEX.toInt()
68 | } else {
69 | currentPointer += lastSize
70 | lastSize = eba.size
71 | newEntryOffsets[i] = currentPointer
72 | }
73 | }
74 | }
75 | val entryOffsetsSize = entries.size * SIZE_INT
76 | val entryStartSize = SIZE_INT
77 |
78 | val newChunkSize = (commonHeaderSize
79 | + entryCountSize
80 | + entryStartSize
81 | + configSize
82 | + entryOffsetsSize
83 | + entrySize)
84 | val newEntryStart = newChunkSize - entrySize
85 |
86 |
87 | val bf = ByteBuffer.allocate(newChunkSize)
88 | bf.takeLittleEndianOrder()
89 |
90 | bf.putShort(header.type)
91 | bf.putShort((newChunkSize - entryOffsetsSize - entrySize).toShort())
92 | bf.putInt(newChunkSize)
93 |
94 | bf.put(typeId)
95 | bf.put(reservedField0)
96 | bf.putShort(reservedField1)
97 |
98 | bf.putInt(newEntryCount)
99 | bf.putInt(newEntryStart)
100 |
101 | bf.put(configByteArray)
102 |
103 | newEntryOffsets.forEach { bf.putInt(it) }
104 |
105 | newEntryByteArray.forEach { it?.let { bf.put(it) } }
106 |
107 | return bf.flipToArray()
108 | }
109 |
110 |
111 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/pack/type/ResEntry.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.pack.type
2 |
3 | import me.xx2bab.polyfill.arsc.base.*
4 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
5 | import me.xx2bab.polyfill.arsc.io.flipToArray
6 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
7 | import java.nio.ByteBuffer
8 |
9 | class ResEntry : IParsable {
10 | var start: Long = 0
11 | var size: Short = 0 // Header size that contains size, flag, stringPoolIndex only
12 | var flag: Short = INVALID_VALUE_SHORT // Either RES_TABLE_ENTRY_FLAG_COMPLEX or RES_TABLE_ENTRY_FLAG_PUBLIC
13 | var stringPoolIndex = INVALID_VALUE_INT // The resource name index of Global String Pool
14 |
15 | // When FLAG_COMPLEX is 0
16 | lateinit var resValue: ResValue
17 |
18 | // When FLAG_COMPLEX is 1
19 | var parent: Int = INVALID_VALUE_INT // The parent ResMapEntry
20 | var pairCount: Int = 0 // The pair amount
21 | val resMapValues = mutableListOf()
22 |
23 | override fun parse(input: LittleEndianInputStream, start: Long) {
24 | this.start = start
25 | size = input.readShort()
26 | flag = input.readShort()
27 | stringPoolIndex = input.readInt()
28 |
29 | if (flag.toInt() == 0) {
30 | resValue = ResValue()
31 | resValue.parse(input, input.filePointer)
32 | } else {
33 | parent = input.readInt()
34 | pairCount = input.readInt()
35 | if (pairCount > 0) {
36 | for (i in 0 until pairCount) {
37 | val mapValue = ResMapValue()
38 | mapValue.parse(input, input.filePointer)
39 | resMapValues.add(mapValue)
40 | }
41 | }
42 | }
43 | }
44 |
45 | override fun toByteArray(): ByteArray {
46 | val sizeSize = SIZE_SHORT
47 | val flagSize = SIZE_SHORT
48 | val stringPoolIndexSize = SIZE_INT
49 | val contentByteArray = if (flag.toInt() == 0) {
50 | resValue.toByteArray()
51 | } else {
52 | val resMapByteArrays = resMapValues.map { it.toByteArray() }
53 | val resMapSize = resMapByteArrays.sumBy { it.size }
54 | val parentSize = SIZE_INT
55 | val pairCount = SIZE_INT
56 | val mapChunkBuffer = ByteBuffer.allocate(parentSize + pairCount + resMapSize)
57 | mapChunkBuffer.takeLittleEndianOrder()
58 | mapChunkBuffer.putInt(parent)
59 | mapChunkBuffer.putInt(resMapByteArrays.size)
60 | resMapByteArrays.forEach { mapChunkBuffer.put(it) }
61 | mapChunkBuffer.flipToArray()
62 | }
63 |
64 | val newChunkSize = (sizeSize
65 | + flagSize
66 | + stringPoolIndexSize
67 | + contentByteArray.size)
68 | val bf = ByteBuffer.allocate(newChunkSize)
69 | bf.takeLittleEndianOrder()
70 | bf.putShort(size)
71 | bf.putShort(flag)
72 | bf.putInt(stringPoolIndex)
73 | bf.put(contentByteArray)
74 | return bf.flipToArray()
75 | }
76 |
77 |
78 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/pack/type/ResMapValue.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.pack.type
2 |
3 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_INT
4 | import me.xx2bab.polyfill.arsc.base.IParsable
5 | import me.xx2bab.polyfill.arsc.base.SIZE_INT
6 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
7 | import me.xx2bab.polyfill.arsc.io.flipToArray
8 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
9 | import java.nio.ByteBuffer
10 |
11 | class ResMapValue: IParsable {
12 |
13 | var name: Int = INVALID_VALUE_INT
14 | lateinit var resValue: ResValue
15 |
16 | override fun parse(input: LittleEndianInputStream, start: Long) {
17 | name = input.readInt()
18 | resValue = ResValue()
19 | resValue.parse(input, start + 4)
20 | }
21 |
22 | override fun toByteArray(): ByteArray {
23 | val nameSize = SIZE_INT
24 | val resValueByteArray = resValue.toByteArray()
25 | val resValueSize = resValueByteArray.size
26 | val bf = ByteBuffer.allocate(nameSize + resValueSize)
27 | bf.takeLittleEndianOrder()
28 | bf.putInt(name)
29 | bf.put(resValueByteArray)
30 | return bf.flipToArray()
31 | }
32 |
33 |
34 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/pack/type/ResValue.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.pack.type
2 |
3 | import me.xx2bab.polyfill.arsc.base.*
4 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
5 | import me.xx2bab.polyfill.arsc.io.flipToArray
6 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
7 | import java.nio.ByteBuffer
8 |
9 | class ResValue: IParsable {
10 |
11 | var size: Short = INVALID_VALUE_SHORT
12 | var res0: Byte = INVALID_VALUE_BYTE
13 | var dataType: Byte = INVALID_VALUE_BYTE
14 | var data: Int = INVALID_VALUE_INT
15 |
16 | override fun parse(input: LittleEndianInputStream, start: Long) {
17 | size = input.readShort()
18 | res0 = input.readByte()
19 | dataType = input.readByte()
20 | data = input.readInt()
21 | }
22 |
23 | override fun toByteArray(): ByteArray {
24 | val sizeSize = SIZE_SHORT
25 | val res0Size = SIZE_BYTE
26 | val dataTypeSize = SIZE_BYTE
27 | val dataSize = SIZE_INT
28 | val bf = ByteBuffer.allocate(sizeSize + res0Size + dataTypeSize + dataSize)
29 | bf.takeLittleEndianOrder()
30 | bf.putShort(size)
31 | bf.put(res0)
32 | bf.put(dataType)
33 | bf.putInt(data)
34 | return bf.flipToArray()
35 | }
36 |
37 |
38 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/stringpool/StringPool.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.stringpool
2 |
3 | import me.xx2bab.polyfill.arsc.base.*
4 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
5 | import me.xx2bab.polyfill.arsc.io.flipToArray
6 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
7 | import java.io.IOException
8 | import java.nio.ByteBuffer
9 |
10 | class StringPool : IParsable {
11 |
12 | // Common header
13 | lateinit var header: Header
14 |
15 | var stringCount: Int = 0
16 | var styleCount: Int = 0
17 | var flag: Int = INVALID_VALUE_INT
18 | var stringStartPosition: Int = INVALID_VALUE_INT
19 | var styleStartPosition: Int = INVALID_VALUE_INT
20 |
21 | // The header has a little bit padding before move to string offset array
22 | // var paddingSize: Int = 0
23 |
24 | lateinit var stringOffsets: IntArray
25 | lateinit var styleOffsets: IntArray
26 | lateinit var stringByteArrays: Array
27 | lateinit var stylesByteArrays: Array
28 | lateinit var strings: Array
29 | lateinit var styles: Array
30 |
31 |
32 | @Throws(IOException::class)
33 | override fun parse(input: LittleEndianInputStream, start: Long) {
34 | input.seek(start)
35 | header = Header()
36 | header.parse(input, start)
37 |
38 | stringCount = input.readInt()
39 | styleCount = input.readInt()
40 | flag = input.readInt()
41 | stringStartPosition = input.readInt()
42 | styleStartPosition = input.readInt()
43 |
44 | // This block is populated at very beginning place, so Int is quite enough to store and long is safe to convert
45 | // paddingSize = header.start.toInt() + header.headSize - input.filePointer.toInt()
46 | input.seek(header.start + header.headSize)
47 |
48 | stringOffsets = if (stringCount > 0) IntArray(stringCount) { input.readInt() } else IntArray(0)
49 | styleOffsets = if (styleCount > 0) IntArray(styleCount) { input.readInt() } else IntArray(0)
50 |
51 | input.seek(header.start + stringStartPosition)
52 |
53 | strings = Array(stringCount) { null }
54 | stringByteArrays = if (stringCount > 0) {
55 | Array(stringCount) { i ->
56 | val array = if (i < stringCount - 1) {
57 | ByteArray(stringOffsets[i + 1] - stringOffsets[i])
58 | } else {
59 | if (styleCount > 0) {
60 | ByteArray(styleStartPosition - stringOffsets[i] - stringStartPosition)
61 | } else {
62 | ByteArray(header.chunkSize - stringStartPosition - stringOffsets[i])
63 | }
64 | }
65 | input.read(array)
66 | strings[i] = if (array.isEmpty()) null else UtfUtil.byteArrayToString(array, flag)
67 | array
68 | }
69 | } else {
70 | emptyArray()
71 | }
72 | styles = Array(styleCount) { null }
73 | stylesByteArrays = if (styleCount > 0) {
74 | Array(styleCount) { i ->
75 | val array = if (i < styleCount - 1) {
76 | ByteArray(styleOffsets[i + 1] - styleOffsets[i])
77 | } else {
78 | ByteArray(header.chunkSize - styleStartPosition - styleOffsets[i])
79 | }
80 | input.read(array)
81 | styles[i] = if (array.isEmpty()) null else UtfUtil.byteArrayToString(array, flag)
82 | array
83 | }
84 | } else {
85 | emptyArray()
86 | }
87 | }
88 |
89 | override fun toByteArray(): ByteArray {
90 | val headerSize = header.size()
91 | val stringCountSize = sizeOf(stringCount)
92 | val styleCountSize = sizeOf(styleCount)
93 | val flagSize = sizeOf(flag)
94 | val stringStartPositionSize = sizeOf(stringStartPosition)
95 | val styleStartPositionSize = sizeOf(styleStartPosition)
96 |
97 | val newStringByteArrays = Array(strings.size) {
98 | val s = strings[it]
99 | if (s == null) ByteArray(0) else UtfUtil.stringToByteArray(s, flag)
100 | }
101 | val stringsSize = newStringByteArrays.sumBy { it.size }
102 | val stringsByteAlignedSupplementCount = 4 - stringsSize % 4
103 | val stringOffsetsSize = newStringByteArrays.size * SIZE_INT
104 |
105 | val newStyleByteArrays = Array(styles.size) {
106 | val s = styles[it]
107 | if (s == null) ByteArray(0) else UtfUtil.stringToByteArray(s, flag)
108 | }
109 | val stylesSize = newStyleByteArrays.sumBy { it.size }
110 | val stylesByteAlignedSupplementCount = 4 - stylesSize % 4
111 | val styleOffsetsSize = newStyleByteArrays.size * SIZE_INT
112 |
113 | val newStringOffsets = calculateOffsets(newStringByteArrays)
114 | val newStyleOffsets = calculateOffsets(newStyleByteArrays)
115 |
116 | val newChunkSize = (headerSize
117 | + stringCountSize
118 | + styleCountSize
119 | + flagSize
120 | + stringStartPositionSize
121 | + styleStartPositionSize
122 | + stringsSize
123 | + stringsByteAlignedSupplementCount % 4
124 | + stringOffsetsSize
125 | + stylesSize
126 | + stylesByteAlignedSupplementCount % 4
127 | + styleOffsetsSize)
128 |
129 | val newStringStartPosition = newChunkSize - stylesSize - stringsSize - stringsByteAlignedSupplementCount % 4
130 | val newStyleStartPosition = if (stylesSize == 0) 0 else newChunkSize - stylesSize - stylesByteAlignedSupplementCount % 4
131 |
132 |
133 | val bf = ByteBuffer.allocate(newChunkSize)
134 | bf.takeLittleEndianOrder()
135 | bf.putShort(header.type)
136 | bf.putShort((headerSize
137 | + stringCountSize
138 | + styleCountSize
139 | + flagSize
140 | + stringStartPositionSize
141 | + styleStartPositionSize).toShort())
142 | bf.putInt(newChunkSize)
143 | bf.putInt(newStringByteArrays.size)
144 | bf.putInt(newStyleByteArrays.size)
145 | bf.putInt(flag)
146 | bf.putInt(newStringStartPosition)
147 | bf.putInt(newStyleStartPosition)
148 | newStringOffsets.forEach { bf.putInt(it) }
149 | newStyleOffsets.forEach { bf.putInt(it) }
150 | newStringByteArrays.forEach { bf.put(it) }
151 | val zeroInByte: Byte = 0
152 | if (stringsByteAlignedSupplementCount != 4) {
153 | for (i in 0 until stringsByteAlignedSupplementCount) {
154 | bf.put(zeroInByte)
155 | }
156 | }
157 | newStyleByteArrays.forEach { bf.put(it) }
158 | if (stylesByteAlignedSupplementCount != 4) {
159 | for (i in 0 until stylesByteAlignedSupplementCount) {
160 | bf.put(zeroInByte)
161 | }
162 | }
163 |
164 | return bf.flipToArray()
165 | }
166 |
167 | private fun calculateOffsets(array: Array): IntArray {
168 | val offsets = IntArray(array.size)
169 | var currentPointer = 0
170 | var lastSize = 0
171 | for (i in array.indices) {
172 | val s = array[i]
173 | if (i == 0) {
174 | offsets[i] = 0
175 | lastSize = s.size
176 | } else {
177 | currentPointer += lastSize
178 | lastSize = s.size
179 | offsets[i] = currentPointer
180 | }
181 | }
182 | return offsets
183 | }
184 |
185 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/main/kotlin/me/xx2bab/polyfill/arsc/stringpool/UtfUtil.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc.stringpool
2 |
3 | import com.google.common.io.ByteStreams
4 | import com.google.common.primitives.UnsignedBytes
5 | import me.xx2bab.polyfill.arsc.base.UTF8_FLAG
6 | import java.io.DataOutput
7 | import java.nio.ByteBuffer
8 | import java.nio.ByteOrder
9 |
10 | /**
11 | * Convert from ArscBlamer repository.
12 | *
13 | * https://github.com/google/android-arscblamer/blob/master/java/com/google/devrel/gmscore/tools/apk/arsc/UtfUtil.java
14 | *
15 | * Created by Google.
16 | */
17 | object UtfUtil {
18 |
19 | fun byteArrayToString(array: ByteArray, flag: Int): String {
20 | val buffer = ByteBuffer.wrap(array)
21 | buffer.order(ByteOrder.LITTLE_ENDIAN)
22 | var offset = 0
23 | val charCount = decodeLength(buffer, offset, flag)
24 | offset += computeLengthOffset(charCount, flag)
25 | return if (flag == UTF8_FLAG) {
26 | val length = decodeLength(buffer, offset, flag)
27 | offset += computeLengthOffset(length, flag)
28 | val originPosition = buffer.position()
29 | buffer.position(offset)
30 | try {
31 | String(decodeUtf8OrModifiedUtf8(buffer, charCount))
32 | } finally {
33 | buffer.position(originPosition)
34 | }
35 | } else {
36 | String(buffer.array(), offset, charCount * 2, Charsets.UTF_16LE)
37 | }
38 | }
39 |
40 | fun stringToByteArray(str: String, flag: Int): ByteArray {
41 | val bytes: ByteArray = str.toByteArray(if (flag == UTF8_FLAG) Charsets.UTF_8 else Charsets.UTF_16LE)
42 | val dataOutput = ByteStreams.newDataOutput(bytes.size + 5);
43 | encodeLength(dataOutput, str.length, flag)
44 | if (flag == UTF8_FLAG) {
45 | encodeLength(dataOutput, bytes.size, flag)
46 | }
47 | dataOutput.write(bytes)
48 | if (flag == UTF8_FLAG) {
49 | dataOutput.write(0)
50 | } else {
51 | dataOutput.writeShort(0)
52 | }
53 | return dataOutput.toByteArray()
54 | }
55 |
56 | private fun encodeLength(output: DataOutput, length: Int, flag: Int) {
57 | if (length < 0) {
58 | output.write(0)
59 | return
60 | }
61 | if (flag == UTF8_FLAG) {
62 | if (length > 0x7F) {
63 | output.write(length and 0x7F00 shr 8 or 0x80)
64 | }
65 | output.write(length and 0xFF)
66 | } else { // UTF-16
67 | // TODO(acornwall): Replace output with a little-endian output.
68 | if (length > 0x7FFF) {
69 | val highBytes = length and 0x7FFF0000 shr 16 or 0x8000
70 | output.write(highBytes and 0xFF)
71 | output.write(highBytes and 0xFF00 shr 8)
72 | }
73 | val lowBytes = length and 0xFFFF
74 | output.write(lowBytes and 0xFF)
75 | output.write(lowBytes and 0xFF00 shr 8)
76 | }
77 | }
78 |
79 | private fun decodeLength(buffer: ByteBuffer, offset: Int, flag: Int): Int {
80 | return if (flag == UTF8_FLAG) {
81 | decodeLengthUTF8(buffer, offset)
82 | } else {
83 | decodeLengthUTF16(buffer, offset)
84 | }
85 | }
86 |
87 | private fun decodeLengthUTF8(buffer: ByteBuffer, offset: Int): Int {
88 | // UTF-8 strings use a clever variant of the 7-bit integer for packing the string length.
89 | // If the first byte is >= 0x80, then a second byte follows. For these values, the length
90 | // is WORD-length in big-endian & 0x7FFF.
91 | var length = UnsignedBytes.toInt(buffer[offset])
92 | if (length and 0x80 != 0) {
93 | length = length and 0x7F shl 8 or UnsignedBytes.toInt(buffer[offset + 1])
94 | }
95 | return length
96 | }
97 |
98 | private fun decodeLengthUTF16(buffer: ByteBuffer, offset: Int): Int {
99 | // UTF-16 strings use a clever variant of the 7-bit integer for packing the string length.
100 | // If the first word is >= 0x8000, then a second word follows. For these values, the length
101 | // is DWORD-length in big-endian & 0x7FFFFFFF.
102 | var length: Int = buffer.getShort(offset).toInt() and 0xFFFF
103 | if (length and 0x8000 != 0) {
104 | length = ((length and 0x7FFF) shl 16) or (buffer.getShort(offset + 2).toInt() and 0xFFFF)
105 | }
106 | return length
107 | }
108 |
109 | private fun computeLengthOffset(length: Int, flag: Int): Int {
110 | return (if (flag == UTF8_FLAG) 1 else 2) * (if (length >= (if (flag == UTF8_FLAG) 0x80 else 0x8000)) 2 else 1)
111 | }
112 |
113 | private fun decodeUtf8OrModifiedUtf8(utf8Buffer: ByteBuffer, characterCount: Int): CharArray {
114 | val charBuffer = CharArray(characterCount)
115 | var offset = 0
116 | while (offset < characterCount) {
117 | offset = decodeUtf8OrModifiedUtf8CodePoint(utf8Buffer, charBuffer, offset)
118 | }
119 | return charBuffer
120 | }
121 |
122 | // This is a Javafied version of the implementation in ART:
123 | // cs/android/art/libdexfile/dex/utf-inl.h?l=32&rcl=4da82e1e9f201cb0e408499ee3b38cbca575698e
124 | private fun decodeUtf8OrModifiedUtf8CodePoint(`in`: ByteBuffer, out: CharArray, offset: Int): Int {
125 | var offset = offset
126 | val one = `in`.get().toInt()
127 | if (one and 0x80 == 0) {
128 | out[offset++] = one.toChar()
129 | return offset
130 | }
131 | val two = `in`.get().toInt()
132 | if (one and 0x20 == 0) {
133 | out[offset++] = (one and 0x1f shl 6 or (two and 0x3f)).toChar()
134 | return offset
135 | }
136 | val three = `in`.get().toInt()
137 | if (one and 0x10 == 0) {
138 | out[offset++] = (one and 0x0f shl 12 or (two and 0x3f shl 6) or (three and 0x3f)).toChar()
139 | return offset
140 | }
141 | val four = `in`.get().toInt()
142 | val codePoint: Int = one and 0x0f shl 18 or (two and 0x3f shl 12) or (three and 0x3f shl 6) or (four.toInt() and 0x3f)
143 |
144 | // Write the code point out as a surrogate pair
145 | out[offset++] = ((codePoint shr 10) + 0xd7c0 and 0xffff).toChar()
146 | out[offset++] = ((codePoint and 0x03ff) + 0xdc00).toChar()
147 | return offset
148 | }
149 |
150 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/test/kotlin/me/xx2bab/polyfill/arsc/ResourceTableIntegrationTest.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.arsc
2 |
3 | import com.google.common.io.Resources.getResource
4 | import me.xx2bab.polyfill.arsc.base.ResTable
5 | import me.xx2bab.polyfill.arsc.export.SimpleResource
6 | import me.xx2bab.polyfill.arsc.export.SupportedResConfig
7 | import me.xx2bab.polyfill.arsc.export.SupportedResType
8 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
9 | import me.xx2bab.polyfill.arsc.pack.TypeType
10 | import me.xx2bab.polyfill.arsc.stringpool.UtfUtil
11 | import org.junit.Assert.*
12 | import org.junit.Test
13 | import java.io.File
14 | import java.nio.file.Files
15 | import java.nio.file.Paths
16 |
17 | class ResourceTableIntegrationTest {
18 |
19 | @Test
20 | fun simpleARSCTest() {
21 | val originArscFile = File(getResource("resources.arsc").toURI())
22 | val input = LittleEndianInputStream(originArscFile) // Used for validation
23 | val resTable = ResTable()
24 | resTable.read(originArscFile)
25 |
26 | validateStrings(resTable)
27 | validateResConfigs(input, resTable)
28 | validateResEntries(input, resTable)
29 | validateTypes(input, resTable)
30 | validateStringPools(input, resTable)
31 | validatePackages(input, resTable)
32 | validateTable(input, resTable)
33 | validateFile(originArscFile, resTable)
34 | }
35 |
36 | @Test
37 | fun findResByIdTest_Regular() {
38 | val originArscFile = File(getResource("resources.arsc").toURI())
39 | val resTable = ResTable()
40 | resTable.read(originArscFile)
41 |
42 | val result = resTable.findResourceById(0x7f040036)
43 | assertEquals(result[0]!!.value!!, "#ff80cbc4")
44 | }
45 |
46 | @Test
47 | fun updateResByIdTest_DefaultConfig() {
48 | // Write
49 | val originArscFile = File(getResource("resources.arsc").toURI())
50 | val resTable = ResTable()
51 | resTable.read(originArscFile)
52 |
53 | val result = resTable.updateResourceById(SimpleResource(0x7f0b0027,
54 | SupportedResType.STRING,
55 | // It doesn't matter if you pass a null or empty string when update,
56 | // since we only do locating by id
57 | "app_name",
58 | // To change the app name to SP2
59 | "SP2"),
60 | // A default config without any customization
61 | SupportedResConfig())
62 | assertTrue(result)
63 | val result2 = resTable.updateResourceById(SimpleResource(0x7f040027,
64 | SupportedResType.COLOR,
65 | // It doesn't matter if you pass a null or empty string when update,
66 | // since we only do locating by id
67 | "colorPrimary",
68 | // To change the colorPrimary to Red
69 | "#ff450d"),
70 | // A default config without any customization
71 | SupportedResConfig())
72 | assertTrue(result2)
73 |
74 | // Read
75 | val generatedArscFile = File(originArscFile.parentFile,
76 | "${originArscFile.nameWithoutExtension}-modified.arsc")
77 | resTable.write(generatedArscFile)
78 | val newResTable = ResTable()
79 | newResTable.read(generatedArscFile)
80 | val appNameChangeResult = newResTable.findResourceById(0x7f0b0027)
81 | assertEquals(appNameChangeResult[0]!!.value, "SP2")
82 | val colorPrimaryChangeResult = newResTable.findResourceById(0x7f040027)
83 | assertEquals(colorPrimaryChangeResult[0]!!.value, "#ffff450d")
84 | generatedArscFile.delete()
85 | }
86 |
87 | private fun validateStrings(resTable: ResTable) {
88 | var byteCount = 0
89 | resTable.stringPool.stringByteArrays.forEachIndexed { index, element ->
90 | byteCount += validateString(element, resTable.stringPool.flag,
91 | index == resTable.stringPool.stringByteArrays.size - 1, byteCount)
92 | }
93 | resTable.packages.forEach { pack ->
94 | byteCount = 0
95 | pack.resTypeStringPool.stringByteArrays.forEachIndexed { index, element ->
96 | byteCount += validateString(element, pack.resTypeStringPool.flag,
97 | index == pack.resTypeStringPool.stringByteArrays.size - 1, byteCount)
98 | }
99 | byteCount = 0
100 | pack.resKeywordStringPool.stringByteArrays.forEachIndexed { index, element ->
101 | byteCount += validateString(element, pack.resKeywordStringPool.flag,
102 | index == pack.resKeywordStringPool.stringByteArrays.size - 1, byteCount)
103 | }
104 | }
105 | }
106 |
107 | private fun validateString(it: ByteArray, flag: Int, isLastItem: Boolean, byteCount: Int): Int {
108 | val string = UtfUtil.byteArrayToString(it, flag)
109 | val byteArray = UtfUtil.stringToByteArray(string, flag)
110 |
111 | if (isLastItem) {
112 | val zeroCount = 4 - (byteCount + byteArray.size) % 4 // 4 bytes aligned
113 | val origin = if (zeroCount != 4) {
114 | it.take(it.size - zeroCount).toByteArray()
115 | } else {
116 | it
117 | }
118 | assertArrayEquals(origin, byteArray)
119 | } else {
120 | assertArrayEquals(it, byteArray)
121 | }
122 |
123 | return byteArray.size
124 | }
125 |
126 | private fun validateResConfigs(input: LittleEndianInputStream, resTable: ResTable) {
127 | resTable.packages.forEach { pack ->
128 | pack.resTypes.forEach {
129 | if (it is TypeType) {
130 | input.seek(it.config.start)
131 | val configByteArrayInput = ByteArray(it.config.configSize)
132 | input.read(configByteArrayInput)
133 | val configByteArrayOutput = it.config.toByteArray()
134 | assertArrayEquals(configByteArrayInput, configByteArrayOutput)
135 | }
136 | }
137 | }
138 | }
139 |
140 | private fun validateResEntries(input: LittleEndianInputStream, resTable: ResTable) {
141 | resTable.packages.forEach { pack ->
142 | pack.resTypes.forEach { type ->
143 | if (type is TypeType) {
144 | type.entries.forEach { entry ->
145 | if (entry != null) {
146 | val entryByteArrayOutput = entry.toByteArray()
147 | val size = entryByteArrayOutput.size
148 | input.seek(entry.start)
149 | val entryByteArrayInput = ByteArray(size)
150 | input.read(entryByteArrayInput)
151 | assertArrayEquals(entryByteArrayInput, entryByteArrayOutput)
152 | }
153 | }
154 | }
155 | }
156 | }
157 | }
158 |
159 | private fun validateTypes(input: LittleEndianInputStream, resTable: ResTable) {
160 | resTable.packages.forEach { pack ->
161 | pack.resTypes.forEach { type ->
162 | // Validate both TypeType and TypeSpec
163 | val typeByteArrayOutput = type.toByteArray()
164 | input.seek(type.header.start)
165 | val typeByteArrayInput = ByteArray(typeByteArrayOutput.size)
166 | input.read(typeByteArrayInput)
167 | assertArrayEquals(typeByteArrayInput, typeByteArrayOutput)
168 | // println(typeByteArrayInput.contentEquals(typeByteArrayOutput))
169 | }
170 | }
171 | }
172 |
173 | private fun validateStringPools(input: LittleEndianInputStream, resTable: ResTable) {
174 | resTable.packages.forEach { pack ->
175 | val resTypeStringPoolOutput = pack.resTypeStringPool.toByteArray()
176 | val resTypeStringPoolInput = ByteArray(resTypeStringPoolOutput.size)
177 | input.seek(pack.header.start + pack.resTypeStringPoolOffset.toLong())
178 | input.read(resTypeStringPoolInput)
179 | assertArrayEquals(resTypeStringPoolInput, resTypeStringPoolOutput)
180 | }
181 | }
182 |
183 |
184 | private fun validatePackages(input: LittleEndianInputStream, resTable: ResTable) {
185 | resTable.packages.forEach { pack ->
186 | val packageOutput = pack.toByteArray()
187 | val packageInput = ByteArray(pack.header.chunkSize)
188 | input.seek(pack.header.start)
189 | input.read(packageInput)
190 | assertArrayEquals(packageInput, packageOutput)
191 | }
192 | }
193 |
194 | private fun validateTable(input: LittleEndianInputStream, resTable: ResTable) {
195 | val tableOutput = resTable.toByteArray()
196 | val tableInput = ByteArray(resTable.header.chunkSize)
197 | input.seek(0)
198 | input.read(tableInput)
199 | assertArrayEquals(tableInput, tableOutput)
200 | }
201 |
202 | private fun validateFile(originArscFile: File, resTable: ResTable) {
203 | val generatedArscFile = File(originArscFile.parentFile,
204 | "${originArscFile.nameWithoutExtension}-modified.arsc")
205 | resTable.write(generatedArscFile)
206 | assertArrayEquals(Files.readAllBytes(Paths.get(originArscFile.absolutePath)),
207 | Files.readAllBytes(Paths.get(generatedArscFile.absolutePath)))
208 | generatedArscFile.delete()
209 | }
210 |
211 |
212 | }
--------------------------------------------------------------------------------
/android-arsc-parser/src/test/resources/resources.arsc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2BAB/Polyfill/86b198afbd5616c80eb54063cc11034a3577865e/android-arsc-parser/src/test/resources/resources.arsc
--------------------------------------------------------------------------------
/android-manifest-parser/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import me.xx2bab.polyfill.buildscript.BuildConfig.Versions
2 |
3 | plugins {
4 | kotlin("jvm")
5 | id("me.xx2bab.polyfill.buildscript.maven-central-publish")
6 | }
7 |
8 | dependencies {
9 | implementation(fileTree(mapOf("dir" to "libs", "include" to arrayOf("*.jar"))))
10 | implementation(projects.androidArscParser)
11 |
12 | implementation(gradleApi())
13 | implementation(deps.kotlin.std)
14 |
15 | compileOnly(deps.android.gradle.plugin)
16 |
17 | testImplementation(deps.junit)
18 | testImplementation(deps.mockito)
19 | testImplementation(deps.mockitoInline)
20 | }
21 |
22 | java {
23 | withSourcesJar()
24 | sourceCompatibility = Versions.polyfillSourceCompatibilityVersion
25 | targetCompatibility = Versions.polyfillTargetCompatibilityVersion
26 | }
27 |
28 |
29 |
--------------------------------------------------------------------------------
/android-manifest-parser/gradle.properties:
--------------------------------------------------------------------------------
1 | me.2bab.maven.publish.type=jar
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/ManifestInBytesProvider.kt:
--------------------------------------------------------------------------------
1 | //package me.xx2bab.polyfill.manifest.bytes
2 | //
3 | //import com.android.build.api.variant.AndroidComponentsExtension
4 | //import com.android.build.api.variant.Variant
5 | //import me.xx2bab.polyfill.matrix.base.ApplicationSelfManageableProvider
6 | //import org.gradle.api.Project
7 | //import org.gradle.api.file.RegularFile
8 | //
9 | //class ManifestInBytesProvider: ApplicationSelfManageableProvider {
10 | //
11 | // override fun initialize(project: Project,
12 | // androidExtension: AndroidComponentsExtension<*, *, *>,
13 | // variant: Variant) {
14 | //
15 | // }
16 | //
17 | //
18 | // override fun obtain(defaultValue: RegularFile?): RegularFile {
19 | // throw UnsupportedOperationException()
20 | // // return null
21 | // }
22 | //
23 | //}
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/Header.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser
2 |
3 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_INT
4 | import me.xx2bab.polyfill.arsc.base.IParsable
5 | import me.xx2bab.polyfill.arsc.base.sizeOf
6 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
7 |
8 | class Header: IParsable {
9 |
10 | var start: Long = 0
11 | var chunkType: Int = INVALID_VALUE_INT
12 | var chunkSize: Int = 0
13 |
14 | override fun parse(input: LittleEndianInputStream, start: Long) {
15 | this.start = start
16 | chunkType = input.readInt()
17 | chunkSize = input.readInt()
18 | }
19 |
20 | override fun toByteArray(): ByteArray {
21 | return ByteArray(0)
22 | }
23 |
24 | fun size(): Int {
25 | return sizeOf(chunkType) + sizeOf(chunkSize)
26 | }
27 |
28 |
29 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/IManifestBytesTweaker.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser
2 |
3 | import java.io.File
4 |
5 | interface IManifestBytesTweaker {
6 |
7 | fun read(source: File)
8 |
9 | fun write(dest: File)
10 |
11 | fun write(dest: File, manifest: ManifestBlock)
12 |
13 | fun updatePackageName(newPackageName: String)
14 |
15 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/ManifestBlock.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser
2 |
3 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_INT
4 | import me.xx2bab.polyfill.arsc.base.IParsable
5 | import me.xx2bab.polyfill.arsc.base.sizeOf
6 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
7 | import me.xx2bab.polyfill.arsc.io.flipToArray
8 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
9 | import me.xx2bab.polyfill.manifest.bytes.parser.body.*
10 | import java.nio.ByteBuffer
11 |
12 | class ManifestBlock : IParsable {
13 |
14 | var magicNumber: Int = INVALID_VALUE_INT // It's a fixed number 0x80003
15 | var fileSize: Int = INVALID_VALUE_INT
16 | lateinit var stringBlock: StringPoolBlock
17 | lateinit var resourceIdBlock: ResourceIdBlock
18 | val bodyList = mutableListOf()
19 |
20 | override fun parse(input: LittleEndianInputStream, start: Long) {
21 | magicNumber = input.readInt()
22 | fileSize = input.readInt()
23 |
24 | stringBlock = StringPoolBlock()
25 | stringBlock.parse(input, input.filePointer)
26 |
27 | resourceIdBlock = ResourceIdBlock()
28 | resourceIdBlock.parse(input, input.filePointer)
29 |
30 | while (input.filePointer < fileSize) {
31 | val bodyHeader = Header()
32 | bodyHeader.parse(input, input.filePointer)
33 | val xmlBody = when (bodyHeader.chunkType) {
34 | XMLBodyType.START_NAMESPACE -> StartNamespaceXmlBody()
35 | XMLBodyType.END_NAMESPACE -> EndNamespaceXmlBody()
36 | XMLBodyType.START_TAG -> StartTagXmlBody()
37 | XMLBodyType.END_TAG -> EndTagXmlBody()
38 | XMLBodyType.TEXT -> TextXmlBody()
39 | else -> throw Exception("Unsupported XMLBodyType: ${bodyHeader.chunkType}")
40 | }
41 | xmlBody.header = bodyHeader
42 | xmlBody.parse(input, input.filePointer)
43 | bodyList.add(xmlBody)
44 |
45 | input.seek(bodyHeader.start)
46 | input.skip(bodyHeader.chunkSize.toLong())
47 | }
48 | }
49 |
50 | override fun toByteArray(): ByteArray {
51 | val stringBlockByteArray = stringBlock.toByteArray()
52 | val resourceIdBlockByteArray = resourceIdBlock.toByteArray()
53 | val bodyListByteArrayList = bodyList.map { it.toByteArray() }
54 | val bodyListByteArrayListSize = bodyListByteArrayList.sumBy { it.size }
55 | val newChunkSize = (sizeOf(magicNumber)
56 | + sizeOf(fileSize)
57 | + stringBlockByteArray.size
58 | + resourceIdBlockByteArray.size
59 | + bodyListByteArrayListSize)
60 | val bf = ByteBuffer.allocate(newChunkSize)
61 | bf.takeLittleEndianOrder()
62 |
63 | bf.putInt(magicNumber)
64 | bf.putInt(newChunkSize)
65 | bf.put(stringBlockByteArray)
66 | bf.put(resourceIdBlockByteArray)
67 | bodyListByteArrayList.forEach { bf.put(it) }
68 |
69 | return bf.flipToArray()
70 | }
71 |
72 |
73 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/ManifestBytesTweaker.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser
2 |
3 | import com.google.common.annotations.VisibleForTesting
4 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
5 | import me.xx2bab.polyfill.arsc.io.LittleEndianOutputStream
6 | import me.xx2bab.polyfill.manifest.bytes.parser.body.Attribute
7 | import me.xx2bab.polyfill.manifest.bytes.parser.body.StartTagXmlBody
8 | import me.xx2bab.polyfill.manifest.bytes.parser.body.XMLBodyType
9 | import java.io.File
10 |
11 | class ManifestBytesTweaker : IManifestBytesTweaker {
12 |
13 | private val manifestBlock = ManifestBlock()
14 |
15 | override fun read(source: File) {
16 | if (source.exists() && source.isFile /*&& source.name == "AndroidManifest.xml"*/) {
17 | val inputStream = LittleEndianInputStream(source)
18 | manifestBlock.parse(inputStream, 0)
19 | return
20 | }
21 | throw IllegalArgumentException("The input file is illegal.")
22 | }
23 |
24 | override fun write(dest: File) {
25 | write(dest, manifestBlock)
26 | }
27 |
28 | override fun write(dest: File, manifest: ManifestBlock) {
29 | if (dest.exists()) {
30 | dest.delete()
31 | }
32 | dest.parentFile.mkdirs()
33 | dest.createNewFile()
34 | val outputStream = LittleEndianOutputStream(dest)
35 | outputStream.writeByte(manifest.toByteArray())
36 | outputStream.close()
37 | }
38 |
39 | @VisibleForTesting
40 | fun getManifestBlock(): ManifestBlock {
41 | return manifestBlock
42 | }
43 |
44 | override fun updatePackageName(newPackageName: String) {
45 | val applicationTag = getSpecifyStartTagBodyByName("manifest")
46 | ?: throw IllegalStateException("Could not found tag")
47 | val ackageName = getAttrFromTagAttrs(applicationTag, "package")
48 | ?: throw IllegalStateException("Could not found package")
49 | manifestBlock.stringBlock.strings[ackageName.valueIndex] = newPackageName
50 | }
51 |
52 | fun getSpecifyStartTagBodyByName(tagName: String): StartTagXmlBody? {
53 | val list = manifestBlock.bodyList
54 | .filter { it.header.chunkType == XMLBodyType.START_TAG }
55 | .filter { manifestBlock.stringBlock.strings[(it as StartTagXmlBody).name] == tagName }
56 | return if (list.isNullOrEmpty()) {
57 | null
58 | } else {
59 | list[0] as StartTagXmlBody
60 | }
61 | }
62 |
63 | fun getAttrFromTagAttrs(tag: StartTagXmlBody, attrName: String): Attribute? { // ignore the uri so far
64 | val res = tag.attrs.filter { manifestBlock.stringBlock.strings[it.nameIndex] == attrName }
65 | return if (res.isNullOrEmpty()) null else res[0]
66 | }
67 |
68 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/ResourceIdBlock.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser
2 |
3 | import me.xx2bab.polyfill.arsc.base.IParsable
4 | import me.xx2bab.polyfill.arsc.base.SIZE_INT
5 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
6 | import me.xx2bab.polyfill.arsc.io.flipToArray
7 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
8 | import java.nio.ByteBuffer
9 |
10 | class ResourceIdBlock: IParsable {
11 |
12 | lateinit var header: Header
13 | lateinit var idArray: IntArray
14 |
15 | override fun parse(input: LittleEndianInputStream, start: Long) {
16 | input.seek(start)
17 |
18 | header = Header()
19 | header.parse(input, start)
20 |
21 | val resourceIdChunkCount = (header.chunkSize - header.size()) / 4
22 | idArray = IntArray(resourceIdChunkCount)
23 | for (i in 0 until resourceIdChunkCount) {
24 | idArray[i] = input.readInt()
25 | }
26 | }
27 |
28 | override fun toByteArray(): ByteArray {
29 | val newChunkSize = header.size() + idArray.size * SIZE_INT
30 | val bf = ByteBuffer.allocate(newChunkSize)
31 | bf.takeLittleEndianOrder()
32 |
33 | bf.putInt(header.chunkType)
34 | bf.putInt(newChunkSize)
35 | idArray.forEach { bf.putInt(it) }
36 |
37 | return bf.flipToArray()
38 | }
39 |
40 |
41 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/StringPoolBlock.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser
2 |
3 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_INT
4 | import me.xx2bab.polyfill.arsc.base.IParsable
5 | import me.xx2bab.polyfill.arsc.base.SIZE_INT
6 | import me.xx2bab.polyfill.arsc.base.sizeOf
7 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
8 | import me.xx2bab.polyfill.arsc.io.flipToArray
9 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
10 | import me.xx2bab.polyfill.arsc.stringpool.UtfUtil
11 | import java.nio.ByteBuffer
12 |
13 | class StringPoolBlock: IParsable {
14 |
15 | lateinit var header: Header
16 | var stringCount: Int = 0
17 | var styleCount: Int = 0
18 | var reservedField0: Int = INVALID_VALUE_INT
19 | var stringStartPosition: Int = INVALID_VALUE_INT
20 | var styleStartPosition: Int = INVALID_VALUE_INT
21 |
22 | lateinit var stringOffsets: IntArray
23 | lateinit var styleOffsets: IntArray
24 | lateinit var stringByteArrays: Array
25 | lateinit var stylesByteArrays: Array
26 | lateinit var strings: Array
27 | lateinit var styles: Array
28 |
29 | override fun parse(input: LittleEndianInputStream, start: Long) {
30 | input.seek(start)
31 |
32 | header = Header()
33 | header.parse(input, start)
34 | stringCount = input.readInt()
35 | styleCount = input.readInt()
36 | reservedField0 = input.readInt()
37 | stringStartPosition = input.readInt()
38 | styleStartPosition = input.readInt()
39 |
40 | stringOffsets = if (stringCount > 0) IntArray(stringCount) { input.readInt() } else IntArray(0)
41 | styleOffsets = if (styleCount > 0) IntArray(styleCount) { input.readInt() } else IntArray(0)
42 |
43 | input.seek(start + stringStartPosition)
44 |
45 | strings = Array(stringCount) { null }
46 | stringByteArrays = if (stringCount > 0) {
47 | Array(stringCount) { i ->
48 | val array = if (i < stringCount - 1) {
49 | ByteArray(stringOffsets[i + 1] - stringOffsets[i])
50 | } else {
51 | if (styleCount > 0) {
52 | ByteArray(styleStartPosition - stringOffsets[i] - stringStartPosition)
53 | } else {
54 | ByteArray(header.chunkSize - stringStartPosition - stringOffsets[i])
55 | }
56 | }
57 | input.read(array)
58 | strings[i] = if (array.isEmpty()) null else UtfUtil.byteArrayToString(array, -1)
59 | array
60 | }
61 | } else {
62 | emptyArray()
63 | }
64 | styles = Array(styleCount) { null }
65 | stylesByteArrays = if (styleCount > 0) {
66 | Array(styleCount) { i ->
67 | val array = if (i < styleCount - 1) {
68 | ByteArray(styleOffsets[i + 1] - styleOffsets[i])
69 | } else {
70 | ByteArray(header.chunkSize - styleStartPosition - styleOffsets[i])
71 | }
72 | input.read(array)
73 | styles[i] = if (array.isEmpty()) null else UtfUtil.byteArrayToString(array, -1)
74 | array
75 | }
76 | } else {
77 | emptyArray()
78 | }
79 | }
80 |
81 | override fun toByteArray(): ByteArray {
82 | val chunkTypeSize = sizeOf(header.chunkType)
83 | val chunkSizeSize = sizeOf(header.chunkSize)
84 | val stringCountSize = sizeOf(stringCount)
85 | val styleCountSize = sizeOf(styleCount)
86 | val reservedFieldSize = sizeOf(reservedField0)
87 | val stringStartPositionSize = sizeOf(stringStartPosition)
88 | val styleStartPositionSize = sizeOf(styleStartPosition)
89 |
90 | val newStringByteArrays = Array(strings.size) {
91 | val s = strings[it]
92 | if (s == null) ByteArray(0) else UtfUtil.stringToByteArray(s, -1)
93 | }
94 | val stringsSize = newStringByteArrays.sumBy { it.size }
95 | val stringsByteAlignedSupplementCount = 4 - stringsSize % 4
96 | val stringOffsetsSize = newStringByteArrays.size * SIZE_INT
97 |
98 | val newStyleByteArrays = Array(styles.size) {
99 | val s = styles[it]
100 | if (s == null) ByteArray(0) else UtfUtil.stringToByteArray(s, -1)
101 | }
102 | val stylesSize = newStyleByteArrays.sumBy { it.size }
103 | val stylesByteAlignedSupplementCount = 4 - stylesSize % 4
104 | val styleOffsetsSize = newStyleByteArrays.size * SIZE_INT
105 |
106 | val newStringOffsets = calculateOffsets(newStringByteArrays)
107 | val newStyleOffsets = calculateOffsets(newStyleByteArrays)
108 |
109 | val newChunkSize = (chunkTypeSize
110 | + chunkSizeSize
111 | + stringCountSize
112 | + styleCountSize
113 | + reservedFieldSize
114 | + stringStartPositionSize
115 | + styleStartPositionSize
116 | + stringsSize
117 | + stringsByteAlignedSupplementCount % 4
118 | + stringOffsetsSize
119 | + stylesSize
120 | + stylesByteAlignedSupplementCount % 4
121 | + styleOffsetsSize)
122 |
123 | val newStringStartPosition = newChunkSize - stylesSize - stringsSize - stringsByteAlignedSupplementCount % 4
124 | val newStyleStartPosition = if (stylesSize == 0) 0 else newChunkSize - stylesSize - stylesByteAlignedSupplementCount % 4
125 |
126 |
127 | val bf = ByteBuffer.allocate(newChunkSize)
128 | bf.takeLittleEndianOrder()
129 |
130 | bf.putInt(header.chunkType)
131 | bf.putInt(newChunkSize)
132 | bf.putInt(newStringByteArrays.size)
133 | bf.putInt(newStyleByteArrays.size)
134 | bf.putInt(reservedField0)
135 | bf.putInt(newStringStartPosition)
136 | bf.putInt(newStyleStartPosition)
137 | newStringOffsets.forEach { bf.putInt(it) }
138 | newStyleOffsets.forEach { bf.putInt(it) }
139 | newStringByteArrays.forEach { bf.put(it) }
140 | val zeroInByte: Byte = 0
141 | if (stringsByteAlignedSupplementCount != 4) {
142 | for (i in 0 until stringsByteAlignedSupplementCount) {
143 | bf.put(zeroInByte)
144 | }
145 | }
146 | newStyleByteArrays.forEach { bf.put(it) }
147 | if (stylesByteAlignedSupplementCount != 4) {
148 | for (i in 0 until stylesByteAlignedSupplementCount) {
149 | bf.put(zeroInByte)
150 | }
151 | }
152 |
153 | return bf.flipToArray()
154 | }
155 |
156 | private fun calculateOffsets(array: Array): IntArray {
157 | val offsets = IntArray(array.size)
158 | var currentPointer = 0
159 | var lastSize = 0
160 | for (i in array.indices) {
161 | val s = array[i]
162 | if (i == 0) {
163 | offsets[i] = 0
164 | lastSize = s.size
165 | } else {
166 | currentPointer += lastSize
167 | lastSize = s.size
168 | offsets[i] = currentPointer
169 | }
170 | }
171 | return offsets
172 | }
173 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/body/Attribute.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser.body
2 |
3 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_INT
4 | import me.xx2bab.polyfill.arsc.base.IParsable
5 | import me.xx2bab.polyfill.arsc.base.sizeOf
6 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
7 | import me.xx2bab.polyfill.arsc.io.flipToArray
8 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
9 | import java.nio.ByteBuffer
10 |
11 | class Attribute: IParsable {
12 |
13 | var namespaceUriAttr = INVALID_VALUE_INT // -1 means null
14 | var nameIndex = INVALID_VALUE_INT // -1 means null
15 | var valueIndex = INVALID_VALUE_INT // -1 means null
16 | var type = INVALID_VALUE_INT // >> 24
17 | var data = INVALID_VALUE_INT
18 |
19 | override fun parse(input: LittleEndianInputStream, start: Long) {
20 | namespaceUriAttr = input.readInt()
21 | nameIndex = input.readInt()
22 | valueIndex = input.readInt()
23 | type = input.readInt()
24 | data = input.readInt()
25 | }
26 |
27 | override fun toByteArray(): ByteArray {
28 | val newChunkSize = (sizeOf(namespaceUriAttr)
29 | + sizeOf(nameIndex)
30 | + sizeOf(valueIndex)
31 | + sizeOf(type)
32 | + sizeOf(data))
33 | val bf = ByteBuffer.allocate(newChunkSize)
34 | bf.takeLittleEndianOrder()
35 |
36 | bf.putInt(namespaceUriAttr)
37 | bf.putInt(nameIndex)
38 | bf.putInt(valueIndex)
39 | bf.putInt(type)
40 | bf.putInt(data)
41 |
42 | return bf.flipToArray()
43 | }
44 |
45 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/body/EndNamespaceXmlBody.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser.body
2 |
3 | class EndNamespaceXmlBody: StartNamespaceXmlBody()
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/body/EndTagXmlBody.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser.body
2 |
3 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_INT
4 | import me.xx2bab.polyfill.arsc.base.sizeOf
5 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
6 | import me.xx2bab.polyfill.arsc.io.flipToArray
7 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
8 | import java.nio.ByteBuffer
9 |
10 | class EndTagXmlBody: XMLBody() {
11 |
12 | var prefix = INVALID_VALUE_INT
13 | var uri = INVALID_VALUE_INT
14 |
15 | override fun parse(input: LittleEndianInputStream, start: Long) {
16 | super.parse(input, start)
17 | prefix = input.readInt()
18 | uri = input.readInt()
19 | }
20 |
21 | override fun toByteArray(): ByteArray {
22 | val newChunkSize = (header.size()
23 | + sizeOf(lineNumber)
24 | + sizeOf(reservedField0)
25 | + sizeOf(prefix)
26 | + sizeOf(uri))
27 | val bf = ByteBuffer.allocate(newChunkSize)
28 | bf.takeLittleEndianOrder()
29 |
30 | bf.putInt(header.chunkType)
31 | bf.putInt(newChunkSize)
32 | bf.putInt(lineNumber)
33 | bf.putInt(reservedField0)
34 | bf.putInt(prefix)
35 | bf.putInt(uri)
36 |
37 | return bf.flipToArray()
38 | }
39 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/body/StartNamespaceXmlBody.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser.body
2 |
3 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_INT
4 | import me.xx2bab.polyfill.arsc.base.sizeOf
5 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
6 | import me.xx2bab.polyfill.arsc.io.flipToArray
7 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
8 | import java.nio.ByteBuffer
9 |
10 | open class StartNamespaceXmlBody: XMLBody() {
11 |
12 | var prefix = INVALID_VALUE_INT
13 | var uri = INVALID_VALUE_INT
14 |
15 | override fun parse(input: LittleEndianInputStream, start: Long) {
16 | super.parse(input, start)
17 | prefix = input.readInt()
18 | uri = input.readInt()
19 | }
20 |
21 | override fun toByteArray(): ByteArray {
22 | val newChunkSize = (header.size()
23 | + sizeOf(lineNumber)
24 | + sizeOf(reservedField0)
25 | + sizeOf(prefix)
26 | + sizeOf(uri))
27 | val bf = ByteBuffer.allocate(newChunkSize)
28 | bf.takeLittleEndianOrder()
29 |
30 | bf.putInt(header.chunkType)
31 | bf.putInt(newChunkSize)
32 | bf.putInt(lineNumber)
33 | bf.putInt(reservedField0)
34 | bf.putInt(prefix)
35 | bf.putInt(uri)
36 |
37 | return bf.flipToArray()
38 | }
39 |
40 |
41 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/body/StartTagXmlBody.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser.body
2 |
3 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_INT
4 | import me.xx2bab.polyfill.arsc.base.sizeOf
5 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
6 | import me.xx2bab.polyfill.arsc.io.flipToArray
7 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
8 | import java.nio.ByteBuffer
9 |
10 | open class StartTagXmlBody: XMLBody() {
11 |
12 | var namespaceUri = INVALID_VALUE_INT
13 | var name = INVALID_VALUE_INT
14 | var reservedField1 = 0x140014
15 | var attributeCount = INVALID_VALUE_INT
16 | var classAttribute = INVALID_VALUE_INT
17 | val attrs = mutableListOf()
18 |
19 | override fun parse(input: LittleEndianInputStream, start: Long) {
20 | super.parse(input, start)
21 |
22 | namespaceUri = input.readInt()
23 | name = input.readInt()
24 | reservedField1 = input.readInt()
25 | attributeCount = input.readInt()
26 | classAttribute = input.readInt()
27 | for (i in 0 until attributeCount) {
28 | val attr = Attribute()
29 | attr.parse(input, input.filePointer)
30 | attrs.add(attr)
31 | }
32 | }
33 |
34 | override fun toByteArray(): ByteArray {
35 | val newAttributeCount = attrs.size
36 | val attrsByteArray = attrs.map { it.toByteArray() }
37 | val attrsLength = attrsByteArray.sumBy { it.size }
38 | val newChunkSize = (header.size()
39 | + sizeOf(lineNumber)
40 | + sizeOf(reservedField0)
41 | + sizeOf(namespaceUri)
42 | + sizeOf(name)
43 | + sizeOf(reservedField1)
44 | + sizeOf(attributeCount)
45 | + sizeOf(classAttribute)
46 | + attrsLength)
47 | val bf = ByteBuffer.allocate(newChunkSize)
48 | bf.takeLittleEndianOrder()
49 |
50 | bf.putInt(header.chunkType)
51 | bf.putInt(newChunkSize)
52 | bf.putInt(lineNumber)
53 | bf.putInt(reservedField0)
54 | bf.putInt(namespaceUri)
55 | bf.putInt(name)
56 | bf.putInt(reservedField1)
57 | bf.putInt(newAttributeCount)
58 | bf.putInt(classAttribute)
59 | attrsByteArray.forEach { bf.put(it) }
60 |
61 | return bf.flipToArray()
62 | }
63 |
64 |
65 |
66 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/body/TextXmlBody.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser.body
2 |
3 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
4 | import me.xx2bab.polyfill.arsc.io.flipToArray
5 | import me.xx2bab.polyfill.arsc.io.takeLittleEndianOrder
6 | import java.nio.ByteBuffer
7 |
8 | /**
9 | * Haven't done the content parsing, will add when some libs require changing it.
10 | */
11 | class TextXmlBody: XMLBody() {
12 |
13 | lateinit var content: ByteArray
14 |
15 | override fun parse(input: LittleEndianInputStream, start: Long) {
16 | content = ByteArray(header.chunkSize - header.size())
17 | input.read(content)
18 | }
19 |
20 | override fun toByteArray(): ByteArray {
21 | val newChunkSize = header.size() + content.size
22 | val bf = ByteBuffer.allocate(newChunkSize)
23 | bf.takeLittleEndianOrder()
24 |
25 | bf.putInt(header.chunkType)
26 | bf.putInt(newChunkSize)
27 | bf.put(content)
28 |
29 | return bf.flipToArray()
30 | }
31 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/body/XMLBody.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser.body
2 |
3 | import me.xx2bab.polyfill.arsc.base.INVALID_VALUE_INT
4 | import me.xx2bab.polyfill.arsc.base.IParsable
5 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
6 | import me.xx2bab.polyfill.manifest.bytes.parser.Header
7 |
8 | abstract class XMLBody: IParsable {
9 |
10 | lateinit var header: Header // Passed from outside
11 | var lineNumber: Int = 0
12 | var reservedField0 = INVALID_VALUE_INT
13 |
14 | override fun parse(input: LittleEndianInputStream, start: Long) {
15 | lineNumber = input.readInt()
16 | reservedField0 = input.readInt()
17 | }
18 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/main/kotlin/me/xx2bab/polyfill/manifest/bytes/parser/body/XMLBodyType.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest.bytes.parser.body
2 |
3 | class XMLBodyType {
4 |
5 | companion object {
6 | const val START_NAMESPACE = 0x00100100
7 | const val END_NAMESPACE = 0x00100101
8 | const val START_TAG = 0x00100102
9 | const val END_TAG = 0x00100103
10 | const val TEXT = 0x00100104
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/test/kotlin/me/xx2bab/polyfill/manifest/ManifestInBytesTweakerIntegrationTest.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest
2 |
3 | import com.google.common.io.Resources.getResource
4 | import me.xx2bab.polyfill.arsc.io.LittleEndianInputStream
5 | import me.xx2bab.polyfill.manifest.bytes.parser.ManifestBlock
6 | import me.xx2bab.polyfill.manifest.bytes.parser.ManifestBytesTweaker
7 | import me.xx2bab.polyfill.manifest.bytes.parser.body.XMLBodyType
8 | import org.junit.Assert.assertArrayEquals
9 | import org.junit.Assert.assertEquals
10 | import org.junit.Before
11 | import org.junit.Test
12 | import java.io.File
13 | import java.nio.file.Files
14 | import java.nio.file.Paths
15 |
16 | /**
17 | * Currently we are doing integration test only for "ScratchPaper" project's Manifest file in bytes.
18 | */
19 | class ManifestInBytesTweakerIntegrationTest {
20 |
21 | @Before
22 | fun setup() {
23 |
24 | }
25 |
26 | @Test
27 | fun fullIntegrationTest() {
28 | val originManifestFile = File(getResource("AndroidManifest.xml").toURI())
29 | val input = LittleEndianInputStream(originManifestFile)
30 | val manifestPostTweaker = ManifestBytesTweaker()
31 | manifestPostTweaker.read(originManifestFile)
32 | val manifest = manifestPostTweaker.getManifestBlock()
33 |
34 | validateStringPoolBlock(input, manifest)
35 | validateResourceIdBlock(input, manifest)
36 | validateNamespaceXmlBody(input, manifest)
37 | validateTagXmlBody(input, manifest)
38 | validateFile(originManifestFile, manifestPostTweaker)
39 | validatePackageNameModification(originManifestFile, manifestPostTweaker)
40 | }
41 |
42 | private fun validateStringPoolBlock(input: LittleEndianInputStream,
43 | manifest: ManifestBlock) {
44 | val originByteArray = ByteArray(manifest.stringBlock.header.chunkSize)
45 | input.seek(manifest.stringBlock.header.start)
46 | input.read(originByteArray)
47 | val outputByteArray = manifest.stringBlock.toByteArray()
48 | assertArrayEquals(originByteArray, outputByteArray)
49 | }
50 |
51 | private fun validateResourceIdBlock(input: LittleEndianInputStream,
52 | manifest: ManifestBlock) {
53 | val originByteArray = ByteArray(manifest.resourceIdBlock.header.chunkSize)
54 | input.seek(manifest.resourceIdBlock.header.start)
55 | input.read(originByteArray)
56 | val outputByteArray = manifest.resourceIdBlock.toByteArray()
57 | assertArrayEquals(originByteArray, outputByteArray)
58 | }
59 |
60 | private fun validateNamespaceXmlBody(input: LittleEndianInputStream,
61 | manifest: ManifestBlock) {
62 | val namespaceList = manifest.bodyList.filter {
63 | it.header.chunkType == XMLBodyType.START_NAMESPACE
64 | || it.header.chunkType == XMLBodyType.END_NAMESPACE
65 | }
66 | for (namespace in namespaceList) {
67 | val originByteArray = ByteArray(namespace.header.chunkSize)
68 | input.seek(namespace.header.start)
69 | input.read(originByteArray)
70 | val outputByteArray = namespace.toByteArray()
71 | assertArrayEquals(originByteArray, outputByteArray)
72 | }
73 | }
74 |
75 | private fun validateTagXmlBody(input: LittleEndianInputStream,
76 | manifest: ManifestBlock) {
77 | val tagList = manifest.bodyList.filter {
78 | it.header.chunkType == XMLBodyType.START_TAG
79 | || it.header.chunkType == XMLBodyType.END_TAG
80 | }
81 | for (tag in tagList) {
82 | val originByteArray = ByteArray(tag.header.chunkSize)
83 | input.seek(tag.header.start)
84 | input.read(originByteArray)
85 | val outputByteArray = tag.toByteArray()
86 | assertArrayEquals(originByteArray, outputByteArray)
87 | }
88 | }
89 |
90 | private fun validateFile(originManifestFile: File,
91 | manifestPostTweaker: ManifestBytesTweaker) {
92 | val generatedManifestFile = File(originManifestFile.parentFile,
93 | "${originManifestFile.nameWithoutExtension}-modified.arsc")
94 | manifestPostTweaker.write(generatedManifestFile)
95 | assertArrayEquals(Files.readAllBytes(Paths.get(originManifestFile.absolutePath)),
96 | Files.readAllBytes(Paths.get(generatedManifestFile.absolutePath)))
97 | generatedManifestFile.delete()
98 | }
99 |
100 | private fun validatePackageNameModification(originManifestFile: File,
101 | manifestPostTweaker: ManifestBytesTweaker) {
102 | val newPackageName = "me.xx2bab.polyfill.manifest.test.packagename"
103 | val generatedManifestFile = File(originManifestFile.parentFile,
104 | "${originManifestFile.nameWithoutExtension}-modified.arsc")
105 | manifestPostTweaker.updatePackageName(newPackageName)
106 | manifestPostTweaker.write(generatedManifestFile)
107 |
108 | val newTweaker = ManifestBytesTweaker()
109 | newTweaker.read(generatedManifestFile)
110 | val valueIndex = newTweaker.getAttrFromTagAttrs(
111 | newTweaker.getSpecifyStartTagBodyByName("manifest")!!, "package")!!
112 | .valueIndex
113 | val value = newTweaker.getManifestBlock().stringBlock.strings[valueIndex]
114 | assertEquals(newPackageName, value)
115 | }
116 |
117 | }
--------------------------------------------------------------------------------
/android-manifest-parser/src/test/resources/AndroidManifest.xml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2BAB/Polyfill/86b198afbd5616c80eb54063cc11034a3577865e/android-manifest-parser/src/test/resources/AndroidManifest.xml
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import me.xx2bab.polyfill.buildscript.BuildConfig.Path
2 | import me.xx2bab.polyfill.buildscript.BuildConfig.Versions
3 |
4 | plugins {
5 | id("me.xx2bab.polyfill.buildscript.github-release")
6 | }
7 |
8 | allprojects {
9 | version = Versions.polyfillDevVersion
10 | group = "me.2bab"
11 | }
12 |
13 | task("clean") {
14 | delete(rootProject.buildDir)
15 | }
16 |
17 | val aggregateJars by tasks.registering {
18 | doLast {
19 | val output = Path.getAggregatedJarDirectory(project)
20 | output.mkdir()
21 | subprojects {
22 | File(buildDir.absolutePath + File.separator + "libs").walk()
23 | .filter { it.name.startsWith(this.name) && it.extension == "jar" }
24 | .forEach { it.copyTo(File(output, it.name)) }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/buildSrc/.gitignore:
--------------------------------------------------------------------------------
1 | /local.properties
2 | .DS_Store
3 |
4 | # files for the dex VM
5 | *.dex
6 |
7 | # Java class files
8 | *.class
9 |
10 | # generated files
11 | bin/
12 | gen/
13 |
14 | # Android Studio
15 | /*.iml
16 | .idea
17 | /build
18 | .gradle
19 | captures/
20 |
21 | # Beta distribution
22 | release_notes.txt
23 | group_aliases.txt
24 |
25 | release-script/
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | google()
7 | mavenCentral()
8 | maven {
9 | setUrl("https://plugins.gradle.org/m2/")
10 | }
11 | }
12 |
13 | dependencies {
14 | implementation(kotlin("stdlib"))
15 |
16 | // Github Release
17 | implementation("com.github.breadmoirai:github-release:2.4.1")
18 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/me/xx2bab/polyfill/buildscript/BuildConfig.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.buildscript
2 |
3 | import org.gradle.api.JavaVersion
4 | import org.gradle.api.Project
5 | import java.io.File
6 |
7 | object BuildConfig {
8 |
9 | object Path {
10 | fun getAggregatedJarDirectory(project: Project) = File(
11 | project.rootProject.buildDir.absolutePath + File.separator + "libs")
12 | }
13 |
14 | object Versions {
15 | const val polyfillDevVersion = "0.9.1"
16 |
17 | val polyfillSourceCompatibilityVersion = JavaVersion.VERSION_11
18 | val polyfillTargetCompatibilityVersion = JavaVersion.VERSION_17
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/me/xx2bab/polyfill/buildscript/functional-test-setup.gradle.kts:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.buildscript
2 |
3 | import org.gradle.api.tasks.testing.logging.TestLogEvent
4 | import org.gradle.kotlin.dsl.*
5 |
6 | plugins {
7 | `java-gradle-plugin`
8 | idea
9 | }
10 | val versionCatalog = extensions.getByType().named("deps")
11 | val defaultAGPVer = versionCatalog.findVersion("agpVer").get().requiredVersion
12 | val defaultAGP = versionCatalog.findLibrary("android-gradle-plugin").get()
13 |
14 | val fixtureClasspath: Configuration by configurations.creating
15 | tasks.pluginUnderTestMetadata {
16 | pluginClasspath.from(fixtureClasspath)
17 | }
18 |
19 | val functionalTestSourceSet: SourceSet = sourceSets.create("functionalTest") {
20 | compileClasspath += sourceSets.main.get().output + configurations.testRuntimeClasspath.get()
21 | runtimeClasspath += output + compileClasspath
22 | }
23 |
24 | val functionalTestImplementation: Configuration by configurations.getting {
25 | extendsFrom(configurations.testImplementation.get())
26 | }
27 |
28 | gradlePlugin.testSourceSets(functionalTestSourceSet)
29 |
30 | idea {
31 | module {
32 | testSourceDirs = testSourceDirs.plus(functionalTestSourceSet.allSource.srcDirs)
33 | testResourceDirs = testResourceDirs.plus(functionalTestSourceSet.resources.srcDirs)
34 |
35 | val plusCollection = scopes["TEST"]?.get("plus")
36 | plusCollection?.addAll(functionalTestImplementation.all.filter {
37 | it.name.contains("functionalTestCompileClasspath")
38 | || it.name.contains("functionalTestRuntimeClasspath")
39 | })
40 | }
41 | }
42 |
43 | val functionalTest by tasks.registering(Test::class) {
44 | failFast = true
45 | description = "Runs functional tests."
46 | group = "verification"
47 | testClassesDirs = functionalTestSourceSet.output.classesDirs
48 | classpath = functionalTestSourceSet.runtimeClasspath
49 | testLogging {
50 | events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
51 | }
52 | }
53 |
54 | val check by tasks.getting(Task::class) {
55 | dependsOn(functionalTest)
56 | }
57 |
58 | val test by tasks.getting(Test::class) {
59 | testLogging {
60 | events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
61 | }
62 | }
63 |
64 | val fixtureAgpVersion: String = providers
65 | .environmentVariable("AGP_VERSION")
66 | .orElse(providers.gradleProperty("agpVersion"))
67 | .getOrElse(defaultAGPVer)
68 |
69 |
70 | dependencies {
71 | compileOnly(defaultAGP) // Let the test resource or user decide
72 |
73 | functionalTestImplementation("com.android.tools.build:gradle:${fixtureAgpVersion}")
74 | fixtureClasspath("com.android.tools.build:gradle:${fixtureAgpVersion}")
75 | }
76 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/me/xx2bab/polyfill/buildscript/github-release.gradle.kts:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.buildscript
2 |
3 | import com.github.breadmoirai.githubreleaseplugin.GithubReleaseTask
4 | import me.xx2bab.polyfill.buildscript.BuildConfig.Path
5 | import me.xx2bab.polyfill.buildscript.BuildConfig.Versions
6 | import java.util.*
7 |
8 | val taskName = "releaseArtifactsToGithub"
9 |
10 | val tokenFromEnv: String? = System.getenv("GH_DEV_TOKEN")
11 | val token: String = if (!tokenFromEnv.isNullOrBlank()) {
12 | tokenFromEnv
13 | } else if (project.rootProject.file("local.properties").exists()){
14 | val properties = Properties()
15 | properties.load(project.rootProject.file("local.properties").inputStream())
16 | properties.getProperty("github.devtoken")
17 | } else {
18 | ""
19 | }
20 |
21 | val repo = "polyfill"
22 | val tagBranch = "master"
23 | val version = Versions.polyfillDevVersion
24 | val releaseNotes = ""
25 | createGithubReleaseTaskInternal(token, repo, tagBranch, version, releaseNotes)
26 |
27 |
28 | fun createGithubReleaseTaskInternal(
29 | token: String,
30 | repo: String,
31 | tagBranch: String,
32 | version: String,
33 | releaseNotes: String
34 | ): TaskProvider {
35 | return project.tasks.register("releaseArtifactsToGithub") {
36 | authorization.set("Token $token")
37 | owner.set("2bab")
38 | this.repo.set(repo)
39 | tagName.set(version)
40 | targetCommitish.set(tagBranch)
41 | releaseName.set("v${version}")
42 | body.set(releaseNotes)
43 | draft.set(false)
44 | prerelease.set(false)
45 | overwrite.set(true)
46 | allowUploadToExisting.set(true)
47 | apiEndpoint.set("https://api.github.com")
48 | dryRun.set(false)
49 | generateReleaseNotes.set(false)
50 | releaseAssets.from(fileTree(Path.getAggregatedJarDirectory(project)))
51 | }
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/me/xx2bab/polyfill/buildscript/maven-central-publish.gradle.kts:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.buildscript
2 |
3 | plugins {
4 | `maven-publish`
5 | signing
6 | }
7 |
8 | val publishType = (project.properties["me.2bab.maven.publish.type"] as String) ?: "jar"
9 |
10 | // Stub secrets to let the project sync and build without the publication values set up
11 | ext["signing.keyId"] = null
12 | ext["signing.password"] = null
13 | ext["signing.secretKeyRingFile"] = null
14 | ext["ossrh.username"] = null
15 | ext["ossrh.password"] = null
16 |
17 | // Grabbing secrets from local.properties file or from environment variables,
18 | // which could be used on CI
19 | val secretPropsFile = project.rootProject.file("local.properties")
20 | if (secretPropsFile.exists()) {
21 | secretPropsFile.reader().use {
22 | java.util.Properties().apply {
23 | load(it)
24 | }
25 | }.onEach { (name, value) ->
26 | ext[name.toString()] = value
27 | }
28 | } else {
29 | ext["signing.keyId"] = System.getenv("SIGNING_KEY_ID")
30 | ext["signing.password"] = System.getenv("SIGNING_PASSWORD")
31 | ext["signing.secretKeyRingFile"] = System.getenv("SIGNING_SECRET_KEY_RING_FILE")
32 | ext["ossrh.username"] = System.getenv("OSSRH_USERNAME")
33 | ext["ossrh.password"] = System.getenv("OSSRH_PASSWORD")
34 | }
35 | val javadocJar by tasks.registering(Jar::class) {
36 | archiveClassifier.set("javadoc")
37 | }
38 | fun getExtraString(name: String) = ext[name]?.toString()
39 |
40 |
41 | val groupName = "me.2bab"
42 | val projectName = "polyfill"
43 | val mavenDesc = "Hook Toolset for Android App Build System."
44 | val baseUrl = "https://github.com/2BAB/Polyfill"
45 | val siteUrl = baseUrl
46 | val gitUrl = "$baseUrl.git"
47 | val issueUrl = "$baseUrl/issues"
48 |
49 | val licenseIds = "Apache-2.0"
50 | val licenseNames = arrayOf("The Apache Software License, Version 2.0")
51 | val licenseUrls = arrayOf("http://www.apache.org/licenses/LICENSE-2.0.txt")
52 | val inception = "2018"
53 |
54 | val username = "2BAB"
55 |
56 | fun MavenPublication.configMetadata(publishType: String) {
57 | artifact(javadocJar)
58 | if (publishType != "plugin") {
59 | from(components["java"])
60 | }
61 | pom {
62 | // Description
63 | name.set(projectName)
64 | description.set(mavenDesc)
65 | url.set(siteUrl)
66 |
67 | // Archive
68 | groupId = groupName
69 | artifactId = project.name
70 | version = BuildConfig.Versions.polyfillDevVersion
71 |
72 | // License
73 | inceptionYear.set(inception)
74 | licenses {
75 | licenseNames.forEachIndexed { ln, li ->
76 | license {
77 | name.set(li)
78 | url.set(licenseUrls[ln])
79 | }
80 | }
81 | }
82 | developers {
83 | developer {
84 | name.set(username)
85 | }
86 | }
87 | scm {
88 | connection.set(gitUrl)
89 | developerConnection.set(gitUrl)
90 | url.set(siteUrl)
91 | }
92 | }
93 | }
94 |
95 |
96 | publishing {
97 | publications {
98 | afterEvaluate {
99 | when (publishType) {
100 | "jar" -> {
101 | create("PolyfillArtifact") {
102 | configMetadata(publishType)
103 | }
104 | }
105 |
106 | "plugin" -> {
107 | named("pluginMaven") {
108 | configMetadata(publishType)
109 | }
110 | }
111 | }
112 | }
113 | }
114 |
115 | // Configure MavenCentral repository
116 | repositories {
117 | maven {
118 | name = "sonatype"
119 | setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
120 | credentials {
121 | username = getExtraString("ossrh.username")
122 | password = getExtraString("ossrh.password")
123 | }
124 | }
125 | }
126 |
127 | // Configure MavenLocal repository
128 | repositories {
129 | maven {
130 | name = "myMavenlocal"
131 | url = uri(System.getProperty("user.home") + "/.m2/repository")
132 | }
133 | }
134 | }
135 |
136 | afterEvaluate {
137 | signing {
138 | sign(publishing.publications)
139 | }
140 | }
--------------------------------------------------------------------------------
/deps.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlinVer = "1.9.22"
3 | buildConfigVer = "3.0.3"
4 |
5 | agpVer = "8.1.2"
6 | agpPatchIgnoredVer = "8.1.0" # To be used by backport version matching
7 | agpBackportVer = "8.0.1"
8 | agpBackportPatchIgnoredVer = "8.0.0" # To be used by backport version matching, e.g. apply backport patches when (7.1.0 <= ver < 7.2.0)
9 | agpNextBetaVer = "8.2.0-beta06"
10 |
11 | # Please refer to https://mvnrepository.com/artifact/com.android.tools/sdk-common?repo=google
12 | # The minor and patch version are synced with agpVer
13 | androidToolVer = "31.1.2"
14 | mockitoVer = "3.9.0"
15 |
16 | [libraries]
17 | android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agpVer" }
18 | android-gradle-backport = { module = "com.android.tools.build:gradle", version.ref = "agpBackportVer" }
19 | android-tools-sdkcommon = { module = "com.android.tools:sdk-common", version.ref = "androidToolVer" }
20 | android-tools-common = { module = "com.android.tools:common", version.ref = "androidToolVer" }
21 | android-tools-sdklib = { module = "com.android.tools:sdklib", version.ref = "androidToolVer" }
22 | kotlin-std = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlinVer" }
23 | kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinVer" }
24 | kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.5.1" }
25 | guava = { module = "com.google.guava:guava", version = "30.1.1-jre" }
26 | fastJson = { module = "com.alibaba:fastjson", version = "1.2.73" }
27 | hamcrest = { module = "org.hamcrest:hamcrest-library", version = "2.2" }
28 | junit = { module = "junit:junit", version = "4.12" }
29 | mockito = { module = "org.mockito:mockito-core", version.ref = "mockitoVer" }
30 | mockitoInline = { module = "org.mockito:mockito-inline", version.ref = "mockitoVer" }
31 |
32 | [bundles]
33 | android-tools = ["android-tools-common", "android-tools-sdklib"]
34 | test-suite = []
35 |
36 | [plugins]
37 | kt = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlinVer" }
--------------------------------------------------------------------------------
/functional-test/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | kotlin("plugin.serialization")
4 | }
5 |
6 | group = "me.2bab"
7 |
8 | java {
9 | withSourcesJar()
10 | sourceCompatibility = JavaVersion.VERSION_11
11 | targetCompatibility = JavaVersion.VERSION_17
12 | }
13 |
14 |
15 | testing {
16 | suites {
17 | val functionalTest by registering(JvmTestSuite::class) {
18 | useJUnitJupiter()
19 | testType.set(TestSuiteType.FUNCTIONAL_TEST)
20 | dependencies {
21 | implementation(deps.hamcrest)
22 | implementation(deps.kotlin.serialization)
23 | implementation(deps.fastJson)
24 | }
25 | }
26 | }
27 | }
28 |
29 | dependencies {
30 | implementation(deps.kotlin.std)
31 | "functionalTestImplementation"(gradleTestKit())
32 | }
33 |
34 | tasks.named("check") {
35 | dependsOn(testing.suites.named("functionalTest"))
36 | }
37 |
38 | tasks.withType {
39 | testLogging {
40 | this.showStandardStreams = true
41 | }
42 | }
--------------------------------------------------------------------------------
/functional-test/src/functionalTest/kotlin/me/xx2bab/koncat/CaseInsensitiveSubstringMatcher.java:
--------------------------------------------------------------------------------
1 | package me.xx2bab.koncat;
2 |
3 | import org.hamcrest.Description;
4 | import org.hamcrest.Matcher;
5 | import org.hamcrest.TypeSafeMatcher;
6 |
7 | public class CaseInsensitiveSubstringMatcher extends TypeSafeMatcher {
8 |
9 | private final String subString;
10 |
11 | private CaseInsensitiveSubstringMatcher(final String subString) {
12 | this.subString = subString;
13 | }
14 |
15 | @Override
16 | protected boolean matchesSafely(final String actualString) {
17 | return actualString.toLowerCase().contains(this.subString.toLowerCase());
18 | }
19 |
20 | @Override
21 | public void describeTo(final Description description) {
22 | description.appendText("containing substring \"" + this.subString + "\"");
23 | }
24 |
25 | public static Matcher containsIgnoringCase(final String subString) {
26 | return new CaseInsensitiveSubstringMatcher(subString);
27 | }
28 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2BAB/Polyfill/86b198afbd5616c80eb54063cc11034a3577865e/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/polyfill-backport/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import me.xx2bab.polyfill.buildscript.BuildConfig.Versions
2 |
3 | plugins {
4 | kotlin("jvm")
5 | id("com.github.gmazzo.buildconfig")
6 | id("me.xx2bab.polyfill.buildscript.maven-central-publish")
7 | }
8 |
9 | dependencies {
10 | implementation(fileTree(mapOf("dir" to "libs", "include" to arrayOf("*.jar"))))
11 |
12 | implementation(gradleApi())
13 | implementation(deps.kotlin.std)
14 | compileOnly(deps.android.gradle.backport)
15 | compileOnly(deps.android.tools.common)
16 | compileOnly(deps.android.tools.sdkcommon)
17 | compileOnly(deps.android.tools.sdklib)
18 | }
19 |
20 | java {
21 | withSourcesJar()
22 | sourceCompatibility = Versions.polyfillSourceCompatibilityVersion
23 | targetCompatibility = Versions.polyfillTargetCompatibilityVersion
24 | }
25 |
26 |
27 | val versionCatalog = extensions.getByType().named("deps")
28 | val agpPatchIgnoredVer = versionCatalog.findVersion("agpPatchIgnoredVer").get().requiredVersion
29 | val agpBackportPatchIgnoredVer = versionCatalog.findVersion("agpBackportPatchIgnoredVer").get().requiredVersion
30 | buildConfig {
31 | buildConfigField("String", "AGP_PATCH_IGNORED_VERSION", "\"$agpPatchIgnoredVer\"")
32 | buildConfigField("String", "AGP_BACKPORT_PATCH_IGNORED_VERSION", "\"$agpBackportPatchIgnoredVer\"")
33 | }
34 |
35 |
36 |
--------------------------------------------------------------------------------
/polyfill-backport/gradle.properties:
--------------------------------------------------------------------------------
1 | me.2bab.maven.publish.type=jar
--------------------------------------------------------------------------------
/polyfill-backport/src/main/kotlin/me/xx2bab/polyfill/BackportPatch.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill
2 |
3 | import com.android.Version
4 | import me._bab.polyfill_backport.BuildConfig.AGP_BACKPORT_PATCH_IGNORED_VERSION
5 | import me._bab.polyfill_backport.BuildConfig.AGP_PATCH_IGNORED_VERSION
6 | import me.xx2bab.polyfill.tools.SemanticVersionLite
7 |
8 | /**
9 | * This is not a reusable design, we create it to solve the AGP compatible issues solely.
10 | * The target is to provide quick and smooth upgrade experience of code base whenever AGP moves on,
11 | * so that Polyfill can match the latest AGP internal changes.
12 | */
13 | abstract class BackportPatch {
14 |
15 | /**
16 | * Depending on the AGP version that current project uses, the function decides to
17 | * - apply the patch for backport AGP (e.g. 7.1).
18 | * - or execute a given default action for latest stable AGP (e.g. 7.2).
19 | */
20 | fun applyOrDefault(action: () -> Result): Result {
21 | val targetVer = SemanticVersionLite(AGP_PATCH_IGNORED_VERSION)
22 | val backportVer = SemanticVersionLite(AGP_BACKPORT_PATCH_IGNORED_VERSION)
23 | val currVer = SemanticVersionLite(Version.ANDROID_GRADLE_PLUGIN_VERSION)
24 | return if (currVer >= backportVer && currVer < targetVer) {
25 | apply()
26 | } else { // backportVer > targetVer
27 | action.invoke()
28 | }
29 | }
30 |
31 | abstract fun apply(): Result
32 |
33 | }
--------------------------------------------------------------------------------
/polyfill-backport/src/main/kotlin/me/xx2bab/polyfill/tools/ReflectionKit.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.tools
2 |
3 | object ReflectionKit {
4 |
5 | fun getField(clazz: Class, instance: T, fieldName: String): Any {
6 | val field = clazz.declaredFields.first { it.name == fieldName }
7 | field.isAccessible = true
8 | return field.get(instance) as Any
9 | }
10 |
11 | }
--------------------------------------------------------------------------------
/polyfill-backport/src/main/kotlin/me/xx2bab/polyfill/tools/SemanticVersionLite.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.tools
2 |
3 |
4 | class SemanticVersionLite(private var version: String) : Comparable {
5 |
6 | init {
7 | // Any alpha/beta/rc version we deem it as the formal one
8 | if (version.contains("-")) {
9 | val indexOfDash = version.indexOf("-")
10 | version = version.substring(0, indexOfDash)
11 | }
12 | // Should only includes number and
13 | if (!version.matches("[0-9]+(\\.[0-9]+)*".toRegex())) {
14 | throw IllegalArgumentException("Invalid version format")
15 | }
16 | }
17 |
18 | override fun compareTo(other: SemanticVersionLite): Int {
19 | val thisParts = this.get().split("\\.".toRegex())
20 | val thatParts = other.get().split("\\.".toRegex())
21 | val length = thisParts.size.coerceAtLeast(thatParts.size)
22 | for (i in 0 until length) {
23 | val thisPart = if (i < thisParts.size)
24 | Integer.parseInt(thisParts[i])
25 | else
26 | 0
27 | val thatPart = if (i < thatParts.size)
28 | Integer.parseInt(thatParts[i])
29 | else
30 | 0
31 | if (thisPart < thatPart) {
32 | return -1
33 | }
34 | if (thisPart > thatPart) {
35 | return 1
36 | }
37 | }
38 | return 0
39 | }
40 |
41 | fun get(): String {
42 | return version
43 | }
44 |
45 |
46 | override fun equals(other: Any?): Boolean {
47 | if (this === other) return true
48 | if (javaClass != other?.javaClass) return false
49 |
50 | other as SemanticVersionLite
51 |
52 | if (this !== other) return false
53 |
54 | return true
55 | }
56 |
57 | override fun hashCode(): Int {
58 | return version.hashCode()
59 | }
60 |
61 | override fun toString(): String {
62 | return version
63 | }
64 | }
--------------------------------------------------------------------------------
/polyfill-test-plugin/.gitignore:
--------------------------------------------------------------------------------
1 | /local.properties
2 | .DS_Store
3 |
4 | # files for the dex VM
5 | *.dex
6 |
7 | # Java class files
8 | *.class
9 |
10 | # generated files
11 | bin/
12 | gen/
13 |
14 | # Android Studio
15 | /*.iml
16 | .idea
17 | /build
18 | ./libs
19 | .gradle
20 | captures/
21 |
22 | # Beta distribution
23 | release_notes.txt
24 | group_aliases.txt
25 |
26 | release-script/
--------------------------------------------------------------------------------
/polyfill-test-plugin/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | `java-gradle-plugin`
4 | id("me.xx2bab.polyfill.buildscript.maven-central-publish")
5 | }
6 |
7 | repositories {
8 | google()
9 | mavenCentral()
10 | maven {
11 | setUrl("https://plugins.gradle.org/m2/")
12 | }
13 | }
14 |
15 | dependencies {
16 | implementation(deps.kotlin.std)
17 | implementation(deps.kotlin.reflect)
18 | implementation(deps.fastJson)
19 |
20 | compileOnly(deps.android.gradle.plugin)
21 | compileOnly(deps.android.tools.sdklib)
22 | implementation(projects.polyfill)
23 | }
24 |
25 | gradlePlugin {
26 | plugins.register("polyfill-test-plugin") {
27 | id = "polyfill-test-plugin"
28 | implementationClass = "me.xx2bab.polyfill.test.TestPlugin"
29 | }
30 | }
--------------------------------------------------------------------------------
/polyfill-test-plugin/gradle.properties:
--------------------------------------------------------------------------------
1 | me.2bab.maven.publish.type=plugin
--------------------------------------------------------------------------------
/polyfill/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import me.xx2bab.polyfill.buildscript.BuildConfig.Versions
2 |
3 | plugins {
4 | `kotlin-dsl`
5 | `java-gradle-plugin`
6 | id("me.xx2bab.polyfill.buildscript.maven-central-publish")
7 | id("me.xx2bab.polyfill.buildscript.functional-test-setup")
8 | }
9 |
10 | dependencies {
11 | implementation(fileTree(mapOf("dir" to "libs", "include" to arrayOf("*.jar"))))
12 | implementation(projects.polyfillBackport)
13 | implementation(projects.androidManifestParser)
14 | implementation(projects.androidArscParser)
15 |
16 | implementation(gradleApi())
17 | implementation(deps.kotlin.std)
18 | implementation(deps.kotlin.reflect)
19 |
20 | // Let the test resource or user decide
21 | compileOnly(deps.android.gradle.plugin)
22 | compileOnly(deps.android.tools.common)
23 | compileOnly(deps.android.tools.sdkcommon)
24 | compileOnly(deps.android.tools.sdklib)
25 | }
26 |
27 | java {
28 | withSourcesJar()
29 | sourceCompatibility = Versions.polyfillSourceCompatibilityVersion
30 | targetCompatibility = Versions.polyfillTargetCompatibilityVersion
31 | }
32 |
33 | gradlePlugin {
34 | plugins.register("me.2bab.polyfill") {
35 | id = "me.2bab.polyfill"
36 | implementationClass = "me.xx2bab.polyfill.PolyfillPlugin"
37 | }
38 | }
39 | tasks.withType {
40 | testLogging {
41 | this.showStandardStreams = true
42 | }
43 | }
--------------------------------------------------------------------------------
/polyfill/gradle.properties:
--------------------------------------------------------------------------------
1 | me.2bab.maven.publish.type=plugin
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/ArtifactExtension.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill
2 |
3 | import com.android.build.api.variant.ApplicationVariant
4 | import com.android.build.api.variant.LibraryVariant
5 | import me.xx2bab.polyfill.artifact.ApplicationArtifactsRepository
6 | import me.xx2bab.polyfill.artifact.LibraryArtifactsRepository
7 |
8 | /**
9 | * Main entry of the Polyfill library, to provide similar function of
10 | * [ApplicationVariant.artifacts].
11 | *
12 | * @return [ApplicationArtifactsRepository]
13 | */
14 | val ApplicationVariant.artifactsPolyfill: ApplicationArtifactsRepository
15 | get() = getExtension(ApplicationArtifactsRepository::class.java)
16 | ?: throw PolyfillUninitializedException()
17 |
18 | /**
19 | * Main entry of the Polyfill library, to provide similar function of
20 | * [LibraryVariant.artifacts].
21 | *
22 | * @return [ApplicationArtifactsRepository]
23 | */
24 | val LibraryVariant.artifactsPolyfill: LibraryArtifactsRepository
25 | get() = getExtension(LibraryArtifactsRepository::class.java)
26 | ?: throw PolyfillUninitializedException()
27 |
28 |
29 | class PolyfillUninitializedException : Exception(
30 | "Polyfill is not yet initialized," +
31 | " please apply Polyfill plugin before calling any APIs following by the instruction."
32 | )
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/ArtifactsRepository.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill
2 |
3 | import com.android.build.api.artifact.Artifact
4 | import com.android.build.api.artifact.Artifacts
5 | import com.android.build.api.artifact.impl.ArtifactsImpl
6 | import org.gradle.api.file.FileSystemLocation
7 | import org.gradle.api.provider.Provider
8 |
9 | /**
10 | * The polyfill version of [Artifacts], to access more intermediate artifacts
11 | * on a Variant object. To know more about Variant&Artifact APIs, please refer to
12 | * [Configure build variants](https://developer.android.com/studio/build/build-variants)
13 | * and [Extend Android Gradle Plugin](https://developer.android.com/studio/build/extend-agp).
14 | */
15 | interface ArtifactsRepository {
16 |
17 | /**
18 | * The polyfill version of [Artifacts.get]. For the usage can refer to [getall] comments.
19 | *
20 | * @param type The target artifact type, must be the internal object of [PolyfilledSingleArtifact].
21 | * @return The artifact wrapper by [Provider] that can be consumed by TaskProvider configuration.
22 | */
23 | fun get(
24 | type: PolyfilledSingleArtifact
25 | ): Provider
26 |
27 | /**
28 | * The delegation of [ArtifactsImpl.get].
29 | */
30 | fun get(
31 | type: Artifact.Single
32 | ): Provider
33 |
34 | /**
35 | * The polyfill version of [Artifacts.getAll].
36 | *
37 | * ``` Kotlin
38 | * val androidExtension = project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
39 | * androidExtension.onVariants { variant ->
40 | * val printManifestTask = project.tasks.register(
41 | * "getAllInputManifestsFor${variant.name.capitalize()}") {
42 | * beforeMergeInputs.set(
43 | * variant.artifactsPolyfill.getAll(PolyfilledMultipleArtifact.ALL_MANIFESTS)
44 | * )
45 | * }
46 | * }
47 | * ```
48 | * @param type The target artifact type, must be the internal object of [PolyfilledMultipleArtifact].
49 | * @return The artifact wrapper by [Provider] that can be consumed by TaskProvider configuration.
50 | */
51 | fun getAll(
52 | type: PolyfilledMultipleArtifact
53 | ): Provider>
54 |
55 | /**
56 | * The delegation of [ArtifactsImpl.getAll].
57 | */
58 | fun getAll(
59 | type: Artifact.Multiple
60 | ): Provider>
61 |
62 | /**
63 | * The polyfill version of [Artifacts.use] that update artifacts within a TaskAction.
64 | * It's not feasible for external plugins to provide `toTransform()` `toCreate()` `toAppend()` tasks
65 | * since 3rd party developers can not modify the internal data flow of AGP tasks. Instead of the
66 | * original pipeline, we could build a simple data flow which modifies files in place by TaskAction to make it
67 | * easier for plugin authors to work on - that is about `toInPlaceUpdate`.
68 | *
69 | * ``` Kotlin
70 | * val androidExtension = project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
71 | * androidExtension.onVariants { variant ->
72 | * val preHookManifestTask = project.tasks.register(
73 | * "preUpdate${variant.name.capitalize()}Manifest")
74 | * variant.artifactsPolyfill.use(
75 | * taskProvider = preHookManifestTask2,
76 | * wiredWith = PreUpdateManifestsTask::beforeMergeInputs,
77 | * toInPlaceUpdate = PolyfilledMultipleArtifact.ALL_MANIFESTS
78 | * )
79 | * }
80 | *
81 | * ...
82 | *
83 | * class PreUpdateManifestsTask(
84 | * private val buildDir: File,
85 | * private val id: String
86 | * ) : PolyfillAction> {
87 | * override fun onTaskConfigure(task: Task) {
88 | * }
89 | *
90 | * override fun onExecute(beforeMergeInputs: Provider>) {
91 | * val manifestPathsOutput = getOutputFile(buildDir, "all-manifests-by-${id}.json")
92 | * manifestPathsOutput.createNewFile()
93 | * beforeMergeInputs.get().let { files ->
94 | * manifestPathsOutput.writeText(JSON.toJSONString(files.map { it.asFile.absolutePath }))
95 | * }
96 | * }
97 | * }
98 | * ```
99 | *
100 | * @param action The Action which will be added to a target task to modify/update target artifact.
101 | * @param toInPlaceUpdate The target artifact type, must be the internal object of [PolyfilledSingleArtifact].
102 | */
103 | fun use(
104 | action: PolyfillAction,
105 | toInPlaceUpdate: PolyfilledSingleArtifact
106 | )
107 |
108 | /**
109 | * The polyfill version of [Artifacts.use], same as [use] above.
110 | *
111 | * @param action The Action which will be added to a target task to modify/update target artifacts.
112 | * @param toInPlaceUpdate The target artifact type, must be the internal object of [PolyfilledMultipleArtifact].
113 | */
114 | fun use(
115 | action: PolyfillAction>,
116 | toInPlaceUpdate: PolyfilledMultipleArtifact
117 | )
118 |
119 | }
120 |
121 |
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/PolyfillAction.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill
2 |
3 | import org.gradle.api.Task
4 | import org.gradle.api.provider.Provider
5 |
6 | interface PolyfillAction {
7 |
8 | fun onTaskConfigure(task: Task)
9 |
10 | fun onExecute(artifact: Provider)
11 |
12 | }
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/PolyfillExtension.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill
2 |
3 | import me.xx2bab.polyfill.jar.JavaResourceMergeOfExtProjectsPreHookConfiguration
4 | import me.xx2bab.polyfill.jar.JavaResourceMergeOfSubProjectsPreHookConfiguration
5 | import me.xx2bab.polyfill.jar.JavaResourceMergePreHookConfiguration
6 | import me.xx2bab.polyfill.manifest.ManifestMergePreHookConfiguration
7 | import me.xx2bab.polyfill.res.ResourceMergePostHookConfiguration
8 | import me.xx2bab.polyfill.res.ResourceMergePreHookConfiguration
9 | import me.xx2bab.polyfill.task.MultipleArtifactTaskExtendConfiguration
10 | import me.xx2bab.polyfill.task.SingleArtifactTaskExtendConfiguration
11 | import me.xx2bab.polyfill.task.TaskExtendConfiguration
12 | import java.util.concurrent.atomic.AtomicBoolean
13 | import kotlin.reflect.KClass
14 |
15 | abstract class PolyfillExtension {
16 |
17 | internal val locked = AtomicBoolean(false)
18 |
19 | internal val singleArtifactMap = mutableMapOf,
20 | KClass>>(
21 | PolyfilledSingleArtifact.MERGED_RESOURCES to ResourceMergePostHookConfiguration::class
22 | )
23 |
24 | internal val multipleArtifactMap = mutableMapOf,
25 | KClass>>(
26 | PolyfilledMultipleArtifact.ALL_MANIFESTS to ManifestMergePreHookConfiguration::class,
27 | PolyfilledMultipleArtifact.ALL_RESOURCES to ResourceMergePreHookConfiguration::class,
28 | PolyfilledMultipleArtifact.ALL_JAVA_RES to JavaResourceMergePreHookConfiguration::class,
29 | PolyfilledMultipleArtifact.ALL_JAVA_RES_OF_SUB_PROJECTS to JavaResourceMergeOfSubProjectsPreHookConfiguration::class,
30 | PolyfilledMultipleArtifact.ALL_JAVA_RES_OF_EXT_PROJECTS to JavaResourceMergeOfExtProjectsPreHookConfiguration::class,
31 | )
32 |
33 | /**
34 | * To register a custom [SingleArtifactTaskExtendConfiguration] for [PolyfilledSingleArtifact].
35 | */
36 | fun registerTaskExtensionConfig(
37 | artifactType: PolyfilledSingleArtifact<*, *>,
38 | kClass: KClass>
39 | ) {
40 | if (locked.get()) {
41 | return
42 | }
43 | singleArtifactMap[artifactType] = kClass
44 | }
45 |
46 | /**
47 | * To register a custom [MultipleArtifactTaskExtendConfiguration] for [PolyfilledMultipleArtifact].
48 | */
49 | fun registerTaskExtensionConfig(
50 | artifactType: PolyfilledMultipleArtifact<*, *>,
51 | kClass: KClass>
52 | ) {
53 | if (locked.get()) {
54 | return
55 | }
56 | multipleArtifactMap[artifactType] = kClass
57 | }
58 |
59 | }
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/PolyfillPlugin.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill
2 |
3 | import com.android.Version
4 | import com.android.build.api.variant.ApplicationAndroidComponentsExtension
5 | import com.android.build.api.variant.DslExtension
6 | import com.android.build.api.variant.LibraryAndroidComponentsExtension
7 | import com.android.build.gradle.AppPlugin
8 | import com.android.build.gradle.LibraryPlugin
9 | import me.xx2bab.polyfill.artifact.ApplicationArtifactsRepository
10 | import me.xx2bab.polyfill.artifact.DefaultArtifactsRepository
11 | import me.xx2bab.polyfill.artifact.LibraryArtifactsRepository
12 | import me.xx2bab.polyfill.tools.SemanticVersionLite
13 | import org.gradle.api.Plugin
14 | import org.gradle.api.Project
15 | import org.gradle.kotlin.dsl.create
16 | import org.gradle.kotlin.dsl.withType
17 |
18 | class PolyfillPlugin : Plugin {
19 |
20 | private val artifactsPolyfills = mutableListOf>()
21 |
22 | override fun apply(project: Project) {
23 | checkSupportedGradleVersion()
24 | val ext = project.extensions.create("artifactsPolyfill")
25 |
26 | project.plugins.withType {
27 | val androidExt = project.extensions.getByType(
28 | ApplicationAndroidComponentsExtension::class.java
29 | )
30 |
31 | val hackyDslExt = DslExtension.Builder(ApplicationArtifactsRepository::class.simpleName!!).build()
32 | androidExt.registerExtension(hackyDslExt) { variantExtConfig ->
33 | val artifactsPolyfill = ApplicationArtifactsRepository(project, variantExtConfig.variant)
34 | artifactsPolyfills.add(artifactsPolyfill)
35 | artifactsPolyfill
36 | }
37 | androidExt.finalizeDsl {
38 | ext.locked.set(true)
39 | }
40 | }
41 |
42 | project.plugins.withType {
43 | val androidExt = project.extensions.getByType(
44 | LibraryAndroidComponentsExtension::class.java
45 | )
46 | val hackyDslExt = DslExtension.Builder(LibraryArtifactsRepository::class.simpleName!!).build()
47 | androidExt.registerExtension(hackyDslExt) { variantExtConfig ->
48 | val artifactsPolyfill = LibraryArtifactsRepository(project, variantExtConfig.variant)
49 | artifactsPolyfills.add(artifactsPolyfill)
50 | artifactsPolyfill
51 | }
52 | androidExt.finalizeDsl {
53 | ext.locked.set(true)
54 | }
55 | }
56 |
57 | }
58 |
59 | private fun checkSupportedGradleVersion() {
60 | val curr = SemanticVersionLite(Version.ANDROID_GRADLE_PLUGIN_VERSION)
61 | val min = SemanticVersionLite("8.0")
62 | if (curr < min) {
63 | throw throw UnsupportedAGPVersionException("Required minimum Android Gradle Plugin version $min, currently it is $curr")
64 | }
65 | }
66 |
67 | class UnsupportedAGPVersionException(msg: String) : Exception(msg)
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/PolyfilledArtifacts.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill
2 |
3 | import com.android.build.api.artifact.Artifact
4 | import com.android.build.api.artifact.ArtifactKind
5 | import com.android.build.api.artifact.MultipleArtifact
6 | import com.android.build.api.artifact.SingleArtifact
7 | import org.gradle.api.file.Directory
8 | import org.gradle.api.file.FileSystemLocation
9 | import org.gradle.api.file.RegularFile
10 |
11 | /**
12 | * To define the plugin type that is associated with supported Artifact types.
13 | */
14 | interface PolyfilledPluginType
15 |
16 | /**
17 | * To indicate an Artifact can be used in Application module only.
18 | */
19 | interface PolyfilledApplicationArtifact : PolyfilledPluginType
20 |
21 | /**
22 | * To indicate an Artifact can be used in Library module only.
23 | */
24 | interface PolyfilledLibraryArtifact : PolyfilledPluginType
25 |
26 |
27 | /**
28 | * The polyfill version of [Artifact].
29 | */
30 | abstract class PolyfilledArtifact(val kind: ArtifactKind)
31 |
32 | /**
33 | * The polyfill version of [SingleArtifact].
34 | */
35 | sealed class PolyfilledSingleArtifact(kind: ArtifactKind) :
37 | PolyfilledArtifact(kind) {
38 |
39 | // For MERGED_MANIFEST you can use
40 | // [com.android.build.api.artifact.SingleArtifact.MERGED_MANIFEST] directly.
41 | // object MERGED_MANIFEST :
42 | // PolyfilledSingleArtifact(ArtifactKind.FILE)
43 |
44 | object MERGED_RESOURCES :
45 | PolyfilledSingleArtifact(ArtifactKind.DIRECTORY)
46 | }
47 |
48 | /**
49 | * The polyfill version of [MultipleArtifact].
50 | */
51 | sealed class PolyfilledMultipleArtifact(kind: ArtifactKind) :
53 | PolyfilledArtifact(kind) {
54 |
55 | object ALL_MANIFESTS :
56 | PolyfilledMultipleArtifact(ArtifactKind.FILE)
57 |
58 | object ALL_RESOURCES :
59 | PolyfilledMultipleArtifact(ArtifactKind.DIRECTORY)
60 |
61 | @Deprecated(
62 | message = "Since AGP 8.1, sub projects and external projects has different " +
63 | "ArtifactKind type, so we will need to separate them.",
64 | replaceWith = ReplaceWith(
65 | "Do find these two separated artifacts.",
66 | "ALL_JAVA_RES_OF_SUB_PROJECTS",
67 | "ALL_JAVA_RES_OF_EXT_PROJECTS"
68 | )
69 | )
70 | object ALL_JAVA_RES :
71 | PolyfilledMultipleArtifact(ArtifactKind.FILE)
72 |
73 | object ALL_JAVA_RES_OF_SUB_PROJECTS :
74 | PolyfilledMultipleArtifact(ArtifactKind.DIRECTORY)
75 |
76 | object ALL_JAVA_RES_OF_EXT_PROJECTS :
77 | PolyfilledMultipleArtifact(ArtifactKind.FILE)
78 | }
79 |
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/TaskExtendConfiguration.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.task
2 |
3 | import com.android.build.api.variant.Variant
4 | import me.xx2bab.polyfill.ArtifactsRepository
5 | import me.xx2bab.polyfill.PolyfillAction
6 | import me.xx2bab.polyfill.PolyfilledMultipleArtifact
7 | import me.xx2bab.polyfill.PolyfilledSingleArtifact
8 | import org.gradle.api.Project
9 | import org.gradle.api.file.FileSystemLocation
10 | import org.gradle.api.provider.Property
11 | import org.gradle.api.provider.Provider
12 | import org.gradle.api.tasks.TaskCollection
13 | import org.gradle.api.tasks.TaskProvider
14 |
15 | /**
16 | * The core configuration action for artifact-consuming tasks to support
17 | * [ArtifactsRepository.get] / [ArtifactsRepository.getAll] / [ArtifactsRepository.use].
18 | *
19 | * To make them work, there are two major materials we have to prepare:
20 | * - Input Data.
21 | * - Running sequence adjustment (Task Dependencies).
22 | *
23 | * The Artifacts API of AGP leverages implicit task dependencies feature of [Property],
24 | * which attaches *Task Dependencies* on *Input Data*, check more from below link.
25 | * [Implicit Task Dependencies](https://docs.gradle.org/current/userguide/lazy_configuration.html#working_with_task_dependencies_in_lazy_properties)
26 | *
27 | * Nevertheless, as an external library, it is not able to modify the AGP and its Artifacts' work flow with
28 | * additional tasks. Ways on how we retrieve data (Providers) are various and hacky, therefore Polyfill finds
29 | * a fine approach to interact with the artifact within Task by adding more TaskActions. To bind them
30 | * to existing AGP tasks, we still have to deal with [data] retrieve and [orchestrate] process respectively.
31 | *
32 | * Get(All) functions here should run after all InPlaceUpdateTaskAction completed, to get final results of *Input Data*.
33 | * From our end we do not care about if they will run independently or are associated with some other AGP tasks,
34 | * above graphs only denote their predecessors and that's about it. (Please do not take them as the
35 | * parallel-execution since the actual sequence is not predicable from current stage.)
36 | *
37 | * [orchestrate] is designed to schedule above two [TaskProvider]s. To be noticed, [orchestrate] is executed
38 | * immediately once the [TaskExtendConfiguration] instance is created, at this moment many other dependencies
39 | * are not ready to interact with, developers who implement this function should put the logic into a post
40 | * Gradle lifecycle callback, such as [Project.afterEvaluate] / [TaskCollection.whenTaskAdded].
41 | *
42 | * @param actionList List of [PolyfillAction] that passed from users to receive artifacts and hence can update it in-place.
43 | */
44 | abstract class TaskExtendConfiguration(
45 | val project: Project,
46 | val variant: Variant,
47 | var actionList: () -> List>
48 | ) {
49 | /**
50 | * To retrieve data from AGP internal components and export it as an Artifact
51 | * to external callers.
52 | * Please make use of [Provider] lazy configuration APIs to avoid eager consumption.
53 | */
54 | abstract val data: Provider
55 |
56 | /**
57 | * To set up task/action dependencies or initialize data lazily.
58 | */
59 | abstract fun orchestrate()
60 |
61 | }
62 |
63 | /**
64 | * A dedicated [TaskExtendConfiguration] for configuring [PolyfilledSingleArtifact].
65 | * It provides data in `Provider<[FileTypeT]>` type.
66 | */
67 | abstract class SingleArtifactTaskExtendConfiguration(
68 | project: Project,
69 | variant: Variant,
70 | actionList: () -> List>
71 | ) : TaskExtendConfiguration(project, variant, actionList)
72 |
73 | /**
74 | * A dedicated [TaskExtendConfiguration] for configuring [PolyfilledMultipleArtifact].
75 | * It provides data in `Provider>` type.
76 | */
77 | abstract class MultipleArtifactTaskExtendConfiguration(
78 | project: Project,
79 | variant: Variant,
80 | actionList: () -> List>>
81 | ) : TaskExtendConfiguration>(project, variant, actionList)
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/VariantExtension.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill
2 |
3 | import com.android.Version
4 | import com.android.build.api.artifact.Artifacts
5 | import com.android.build.api.artifact.impl.ArtifactsImpl
6 | import com.android.build.api.component.analytics.AnalyticsEnabledApplicationVariant
7 | import com.android.build.api.component.analytics.AnalyticsEnabledArtifacts
8 | import com.android.build.api.component.analytics.AnalyticsEnabledLibraryVariant
9 | import com.android.build.api.variant.ApplicationVariant
10 | import com.android.build.api.variant.LibraryVariant
11 | import com.android.build.api.variant.Variant
12 | import com.android.build.api.variant.impl.ApplicationVariantImpl
13 | import com.android.build.api.variant.impl.LibraryVariantImpl
14 | import com.android.build.gradle.internal.dependency.VariantDependencies
15 | import com.android.build.gradle.internal.plugins.BasePlugin
16 | import com.android.build.gradle.internal.scope.MutableTaskContainer
17 | import com.android.build.gradle.internal.services.VersionedSdkLoaderService
18 | import com.android.sdklib.BuildToolInfo
19 | import me.xx2bab.polyfill.tools.ReflectionKit
20 | import me.xx2bab.polyfill.tools.SemanticVersionLite
21 | import org.gradle.api.Project
22 | import org.gradle.api.provider.Provider
23 | import org.gradle.api.tasks.TaskProvider
24 | import org.gradle.configurationcache.extensions.capitalized
25 |
26 |
27 | ////////// Common Variant //////////
28 |
29 | /**
30 | * `kotlin-dsl` has compatible issues with replaceFirstChar(),
31 | * so we use this deprecated method instead as a workaround.
32 | * To capitalized first letter for task name usage.
33 | */
34 | fun Variant.getCapitalizedName() = name.capitalized()
35 |
36 | /**
37 | * To get current Android Gradle Plugin version.
38 | */
39 | fun Variant.getAgpVersion() = SemanticVersionLite(Version.ANDROID_GRADLE_PLUGIN_VERSION)
40 |
41 | /**
42 | * To get BuildToolInfo instance provider, later you can use like below to retrieve some tools' information.
43 | * e.g. `buildToolInfoProvider.get().getPath(BuildToolInfo.PathId.AAPT2)`
44 | *
45 | * @return [BuildToolInfo] wrapped by [Provider].
46 | */
47 | fun Variant.getBuildToolInfo(project: Project): Provider {
48 | val plugin = when (this) {
49 | is ApplicationVariant -> {
50 | project.plugins.getPlugin(com.android.build.gradle.internal.plugins.AppPlugin::class.java)
51 | }
52 |
53 | is LibraryVariant -> {
54 | project.plugins.getPlugin(com.android.build.gradle.internal.plugins.LibraryPlugin::class.java)
55 | }
56 |
57 | else -> {
58 | throw UnsupportedOperationException("Can not find corresponding plugin associated to $this.")
59 | }
60 | }
61 | val sdkLoaderServiceLazy = ReflectionKit.getField(
62 | BasePlugin::class.java,
63 | plugin, "versionedSdkLoaderService\$delegate"
64 | ) as Lazy
65 | return sdkLoaderServiceLazy.value.versionedSdkLoader.get().buildToolInfoProvider
66 | }
67 |
68 |
69 | ////////// ApplicationVariant //////////
70 |
71 | /**
72 | * Casting ApplicationVariant to its actual implementation.
73 | * This is helpful as Variant instance is one of the most important public API
74 | * for us to interact with AGP. It contains a bunch of tools / data providers
75 | * to access more intermediates of the Android build.
76 | *
77 | * @return [ApplicationVariantImpl]
78 | */
79 | fun ApplicationVariant.getApplicationVariantImpl(): ApplicationVariantImpl {
80 | return when (this) {
81 | is ApplicationVariantImpl -> {
82 | this
83 | }
84 |
85 | is AnalyticsEnabledApplicationVariant -> {
86 | this.delegate as ApplicationVariantImpl
87 | }
88 |
89 | else -> {
90 | throw UnsupportedOperationException("Can not convert $this to ApplicationVariantImpl.")
91 | }
92 | }
93 | }
94 |
95 | /**
96 | * The [VariantDependencies] provides `getArtifactCollection(...)` and more APIs to collect
97 | * artifacts from all dependencies.
98 | *
99 | * @return [VariantDependencies]
100 | */
101 | fun ApplicationVariant.getVariantDependenciesImpl() = getApplicationVariantImpl().variantDependencies
102 |
103 | /**
104 | * The [ArtifactsImpl] provides internal Artifacts APIs
105 | * that can be consumed for more intermediate files.
106 | *
107 | * @return [ArtifactsImpl]
108 | */
109 | fun ApplicationVariant.getArtifactsImpl() = getApplicationVariantImpl().artifacts
110 |
111 |
112 | /**
113 | * To access partial common used AGP [TaskProvider]s.
114 | * For example the [MutableTaskContainer.assembAleTask].
115 | *
116 | * @return [MutableTaskContainer]
117 | */
118 | fun ApplicationVariant.getTaskContainer() = getApplicationVariantImpl().taskContainer
119 |
120 |
121 | ////////// LibraryVariant //////////
122 |
123 | /**
124 | * Same as [getApplicationVariantImpl], but is used for LibraryVariant.
125 | *
126 | * @return [LibraryVariantImpl]
127 | */
128 | fun LibraryVariant.getLibraryVariantImpl(): LibraryVariantImpl {
129 | return when (this) {
130 | is LibraryVariantImpl -> {
131 | this
132 | }
133 |
134 | is AnalyticsEnabledLibraryVariant -> {
135 | this.delegate as LibraryVariantImpl
136 | }
137 |
138 | else -> {
139 | throw UnsupportedOperationException("Can not convert $this to LibraryVariantImpl.")
140 | }
141 | }
142 | }
143 |
144 | fun Artifacts.toImplementation(): ArtifactsImpl {
145 | return when (this) {
146 | is ArtifactsImpl -> this
147 | is AnalyticsEnabledArtifacts -> this.delegate as ArtifactsImpl
148 | else -> throw UnsupportedOperationException("Can not convert $this to ArtifactsImpl.")
149 | }
150 | }
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/artifact/ArtifactContainer.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.artifact
2 |
3 | import com.android.build.api.variant.Variant
4 | import me.xx2bab.polyfill.PolyfillAction
5 | import me.xx2bab.polyfill.PolyfillExtension
6 | import me.xx2bab.polyfill.PolyfilledArtifact
7 | import me.xx2bab.polyfill.task.TaskExtendConfiguration
8 | import org.gradle.api.Project
9 | import org.gradle.api.file.FileSystemLocation
10 | import org.gradle.api.provider.Provider
11 | import kotlin.reflect.KClass
12 |
13 | /**
14 | * For per artifact delegation.
15 | */
16 | abstract class ArtifactContainer(
17 | private val artifactType: PolyfilledArtifact<*>,
18 | private val project: Project,
19 | private val variant: Variant,
20 | private val map: Map, KClass>>
21 | ) {
22 |
23 | private val taskExtConfig: TaskExtendConfiguration
24 | private val actionList: MutableList> = mutableListOf()
25 |
26 | init {
27 | val configureAction = map[artifactType]!! as (KClass>)
28 | taskExtConfig = configureAction.constructors.first().call(
29 | project, variant, { actionList }
30 | )
31 | taskExtConfig.orchestrate()
32 | }
33 |
34 | fun get(): Provider {
35 | return taskExtConfig.data
36 | }
37 |
38 | fun inPlaceUpdate(action: PolyfillAction) {
39 | actionList.add(action)
40 | }
41 |
42 | }
43 |
44 | class SingleArtifactContainer(
45 | artifactType: PolyfilledArtifact<*>,
46 | project: Project,
47 | variant: Variant
48 | ) : ArtifactContainer(
49 | artifactType,
50 | project,
51 | variant,
52 | project.extensions.getByType(PolyfillExtension::class.java).singleArtifactMap
53 | )
54 |
55 | class MultipleArtifactContainer(
56 | artifactType: PolyfilledArtifact<*>,
57 | project: Project,
58 | variant: Variant
59 | ) : ArtifactContainer>(
60 | artifactType,
61 | project,
62 | variant,
63 | project.extensions.getByType(PolyfillExtension::class.java).multipleArtifactMap
64 | )
65 |
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/artifact/DefaultArtifactsRepository.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.artifact
2 |
3 | import com.android.build.api.artifact.Artifact
4 | import com.android.build.api.artifact.ArtifactKind
5 | import com.android.build.api.variant.Variant
6 | import com.android.build.api.variant.VariantExtension
7 | import me.xx2bab.polyfill.*
8 | import org.gradle.api.Project
9 | import org.gradle.api.file.Directory
10 | import org.gradle.api.file.FileSystemLocation
11 | import org.gradle.api.file.RegularFile
12 | import org.gradle.api.provider.Provider
13 |
14 | /**
15 | * For all artifacts management.
16 | */
17 | abstract class DefaultArtifactsRepository(
18 | private val project: Project,
19 | private val variant: Variant
20 | ) : ArtifactsRepository, VariantExtension {
21 |
22 | private val singleArtifactStorage = mutableMapOf, SingleArtifactContainer<*>>()
23 | private val multipleArtifactStorage = mutableMapOf, MultipleArtifactContainer<*>>()
24 |
25 | init {
26 | val ext = project.extensions.getByType(PolyfillExtension::class.java)
27 |
28 | ext.singleArtifactMap.forEach { (artifactType, _) ->
29 | if (artifactType.kind == ArtifactKind.FILE) {
30 | singleArtifactStorage[artifactType] = SingleArtifactContainer(
31 | artifactType, project, variant
32 | )
33 | } else if (artifactType.kind == ArtifactKind.DIRECTORY) {
34 | singleArtifactStorage[artifactType] = SingleArtifactContainer(
35 | artifactType, project, variant
36 | )
37 | }
38 | }
39 |
40 | ext.multipleArtifactMap.forEach { (artifactType, _) ->
41 | if (artifactType.kind == ArtifactKind.FILE) {
42 | multipleArtifactStorage[artifactType] = MultipleArtifactContainer(
43 | artifactType, project, variant
44 | )
45 | } else if (artifactType.kind == ArtifactKind.DIRECTORY) {
46 | multipleArtifactStorage[artifactType] = MultipleArtifactContainer(
47 | artifactType, project, variant
48 | )
49 | }
50 | }
51 | }
52 |
53 |
54 | override fun get(
55 | type: PolyfilledSingleArtifact,
56 | ): Provider = getSingleArtifactContainer(type).get()
57 |
58 | override fun get(
59 | type: Artifact.Single
60 | ): Provider = variant.artifacts.toImplementation().get(type)
61 |
62 | override fun getAll(
63 | type: PolyfilledMultipleArtifact
64 | ): Provider> = getMultipleArtifactContainer(type).get()
65 |
66 | override fun getAll(
67 | type: Artifact.Multiple
68 | ): Provider> = variant.artifacts.toImplementation().getAll(type)
69 |
70 | override fun use(
71 | action: PolyfillAction,
72 | toInPlaceUpdate: PolyfilledSingleArtifact
73 | ) {
74 | getSingleArtifactContainer(toInPlaceUpdate).inPlaceUpdate(action)
75 | }
76 |
77 | override fun use(
78 | action: PolyfillAction>,
79 | toInPlaceUpdate: PolyfilledMultipleArtifact
80 | ) {
81 | getMultipleArtifactContainer(toInPlaceUpdate).inPlaceUpdate(action)
82 | }
83 |
84 | @Suppress("UNCHECKED_CAST")
85 | private fun getSingleArtifactContainer(
86 | artifactType: PolyfilledSingleArtifact
87 | ): SingleArtifactContainer = singleArtifactStorage[artifactType] as SingleArtifactContainer
88 |
89 | @Suppress("UNCHECKED_CAST")
90 | private fun getMultipleArtifactContainer(
91 | artifactType: PolyfilledMultipleArtifact
92 | ): MultipleArtifactContainer =
93 | multipleArtifactStorage[artifactType] as MultipleArtifactContainer
94 |
95 | }
96 |
97 | class ApplicationArtifactsRepository(p: Project, v: Variant) :
98 | DefaultArtifactsRepository(p, v)
99 |
100 | class LibraryArtifactsRepository(p: Project, v: Variant) :
101 | DefaultArtifactsRepository(p, v)
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/jar/JavaResourceMergeOfExtProjectsPreHookConfiguration.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.jar
2 |
3 | import com.android.build.api.variant.ApplicationVariant
4 | import com.android.build.gradle.internal.scope.getRegularFiles
5 | import com.android.build.gradle.internal.tasks.MergeJavaResourceTask
6 | import me.xx2bab.polyfill.PolyfillAction
7 | import me.xx2bab.polyfill.getCapitalizedName
8 | import me.xx2bab.polyfill.task.MultipleArtifactTaskExtendConfiguration
9 | import org.gradle.api.Project
10 | import org.gradle.api.file.RegularFile
11 | import org.gradle.api.provider.ListProperty
12 | import org.gradle.api.provider.Provider
13 | import org.gradle.kotlin.dsl.listProperty
14 | import org.gradle.kotlin.dsl.withType
15 |
16 | /**
17 | * To retrieve all java resources for external projects
18 | * that will participate the resource merge process.
19 | */
20 | class JavaResourceMergeOfExtProjectsPreHookConfiguration(
21 | project: Project,
22 | appVariant: ApplicationVariant,
23 | actionList: () -> List>>
24 | ) : MultipleArtifactTaskExtendConfiguration(project, appVariant, actionList) {
25 |
26 | override val data: Provider> = project.objects.listProperty() // A placeholder
27 |
28 | override fun orchestrate() {
29 | val variantCapitalizedName = variant.getCapitalizedName()
30 | project.afterEvaluate {
31 | val mergeTask = project.tasks.withType().first {
32 | it.name.contains(variantCapitalizedName, true)
33 | && it.name.contains("test", true).not()
34 | }
35 |
36 | // Setup data
37 | (data as ListProperty).set(mergeTask.externalLibJavaRes
38 | .getRegularFiles(project.rootProject.layout.projectDirectory))
39 |
40 | val localData = data
41 | // Setup in-place-update
42 | actionList().forEachIndexed { index, action ->
43 | action.onTaskConfigure(mergeTask)
44 | mergeTask.doFirst("JavaResourceMergePreHookByPolyfill$index") {
45 | action.onExecute(localData)
46 | }
47 | }
48 | }
49 | }
50 |
51 | }
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/jar/JavaResourceMergeOfSubProjectsPreHookConfiguration.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.jar
2 |
3 | import com.android.build.api.variant.ApplicationVariant
4 | import com.android.build.gradle.internal.scope.getDirectories
5 | import com.android.build.gradle.internal.tasks.MergeJavaResourceTask
6 | import me.xx2bab.polyfill.PolyfillAction
7 | import me.xx2bab.polyfill.getCapitalizedName
8 | import me.xx2bab.polyfill.task.MultipleArtifactTaskExtendConfiguration
9 | import org.gradle.api.Project
10 | import org.gradle.api.file.Directory
11 | import org.gradle.api.provider.ListProperty
12 | import org.gradle.api.provider.Provider
13 | import org.gradle.kotlin.dsl.listProperty
14 | import org.gradle.kotlin.dsl.withType
15 |
16 | /**
17 | * To retrieve all java resources for sub-projects (except current module)
18 | * that will participate the resource merge process.
19 | */
20 | class JavaResourceMergeOfSubProjectsPreHookConfiguration(
21 | project: Project,
22 | appVariant: ApplicationVariant,
23 | actionList: () -> List>>
24 | ) : MultipleArtifactTaskExtendConfiguration(project, appVariant, actionList) {
25 |
26 | override val data: Provider> = project.objects.listProperty() // A placeholder
27 |
28 | override fun orchestrate() {
29 | val variantCapitalizedName = variant.getCapitalizedName()
30 | project.afterEvaluate {
31 | val mergeTask = project.tasks.withType().first {
32 | it.name.contains(variantCapitalizedName, true)
33 | && it.name.contains("test", true).not()
34 | }
35 |
36 | // Setup data }
37 | (data as ListProperty).set(mergeTask.subProjectJavaRes
38 | .getDirectories(project.rootProject.layout.projectDirectory))
39 |
40 | val localData = data
41 | // Setup in-place-update
42 | actionList().forEachIndexed { index, action ->
43 | action.onTaskConfigure(mergeTask)
44 | mergeTask.doFirst("JavaResourceMergePreHookByPolyfill$index") {
45 | action.onExecute(localData)
46 | }
47 | }
48 | }
49 | }
50 |
51 |
52 | }
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/jar/JavaResourceMergePreHookConfiguration.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.jar
2 |
3 | import com.android.build.api.variant.ApplicationVariant
4 | import com.android.build.gradle.internal.scope.getRegularFiles
5 | import com.android.build.gradle.internal.tasks.MergeJavaResourceTask
6 | import me.xx2bab.polyfill.PolyfillAction
7 | import me.xx2bab.polyfill.getCapitalizedName
8 | import me.xx2bab.polyfill.task.MultipleArtifactTaskExtendConfiguration
9 | import org.gradle.api.Project
10 | import org.gradle.api.file.RegularFile
11 | import org.gradle.api.provider.ListProperty
12 | import org.gradle.api.provider.Provider
13 | import org.gradle.kotlin.dsl.listProperty
14 | import org.gradle.kotlin.dsl.withType
15 |
16 | /**
17 | * To retrieve all java resources (except current module)
18 | * that will participate the merge process.
19 | */
20 | @Deprecated(message = "Since AGP 8.1, the `subProjectJavaRes` become Directory type, " +
21 | "we need to separate them into different artifacts.")
22 | class JavaResourceMergePreHookConfiguration(
23 | project: Project,
24 | appVariant: ApplicationVariant,
25 | actionList: () -> List>>
26 | ) : MultipleArtifactTaskExtendConfiguration(project, appVariant, actionList) {
27 |
28 | override val data: Provider> = project.objects.listProperty() // A placeholder
29 |
30 | override fun orchestrate() {
31 | val variantCapitalizedName = variant.getCapitalizedName()
32 | project.afterEvaluate {
33 | val mergeTask = project.tasks.withType().first {
34 | it.name.contains(variantCapitalizedName)
35 | && it.name.contains("test", true).not()
36 | }
37 |
38 | // Setup data
39 | val subProjectsJavaResList = mergeTask.subProjectJavaRes
40 | .getRegularFiles(project.rootProject.layout.projectDirectory)
41 | val externalDepJavaResList = mergeTask.externalLibJavaRes
42 | .getRegularFiles(project.rootProject.layout.projectDirectory)
43 | val all = subProjectsJavaResList.zip(externalDepJavaResList) { a, b -> a + b }
44 | (data as ListProperty).set(all)
45 |
46 | val localData = data
47 | // Setup in-place-update
48 | actionList().forEachIndexed { index, action ->
49 | action.onTaskConfigure(mergeTask)
50 | mergeTask.doFirst("JavaResourceMergePreHookByPolyfill$index") {
51 | action.onExecute(localData)
52 | }
53 | }
54 | }
55 | }
56 |
57 |
58 | }
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/manifest/ManifestMergePreHookConfiguration.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.manifest
2 |
3 | import com.android.build.api.variant.ApplicationVariant
4 | import com.android.build.gradle.internal.publishing.AndroidArtifacts
5 | import me.xx2bab.polyfill.PolyfillAction
6 | import me.xx2bab.polyfill.getCapitalizedName
7 | import me.xx2bab.polyfill.getVariantDependenciesImpl
8 | import me.xx2bab.polyfill.task.MultipleArtifactTaskExtendConfiguration
9 | import org.gradle.api.Project
10 | import org.gradle.api.artifacts.result.ResolvedArtifactResult
11 | import org.gradle.api.file.RegularFile
12 | import org.gradle.api.model.ObjectFactory
13 | import org.gradle.api.provider.Provider
14 | import javax.inject.Inject
15 |
16 | /**
17 | * Configurations for fetching required data and set up dependencies
18 | * through both explicit/implicit approaches.
19 | */
20 | class ManifestMergePreHookConfiguration(
21 | project: Project,
22 | private val appVariant: ApplicationVariant,
23 | actionList: () -> List>>
24 | ) : MultipleArtifactTaskExtendConfiguration
25 | (project, appVariant, actionList) {
26 |
27 | override val data: Provider> = project.objects.newInstance(
28 | CreateAction::class.java,
29 | appVariant.getVariantDependenciesImpl()
30 | .getArtifactCollection(
31 | AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
32 | AndroidArtifacts.ArtifactScope.ALL,
33 | AndroidArtifacts.ArtifactType.MANIFEST
34 | )
35 | .resolvedArtifacts
36 | ).transform()
37 |
38 | override fun orchestrate() {
39 | // `variant.toTaskContainer().processManifestTask` can not guarantee the impl class
40 | val variantCapitalizedName = variant.getCapitalizedName()
41 | project.tasks.whenTaskAdded {
42 | // > if (this is ProcessApplicationManifest)
43 | // Can not use above logic since it includes more unwanted tasks
44 | if (this.name == "process${variantCapitalizedName}MainManifest") {
45 | // Create a local copy to
46 | // 1. Avoid referring the *TaskConfiguration class with Project instance
47 | // 2. Avoid referring any Project instance from task.doFirst()/doLast()
48 | // that help us comply the Configuration Cache rules.
49 | val localData = data
50 | actionList().forEachIndexed { index, action ->
51 | action.onTaskConfigure(this)
52 | doFirst("ManifestMergePreHookByPolyfill$index") {
53 | action.onExecute(localData)
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
60 | /**
61 | * To avoid referring project instance directly, we need to create a wrapper,
62 | * then inject/gather those build services & data into it.
63 | *
64 | * @see https://docs.gradle.org/current/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution
65 | */
66 | abstract class CreateAction @Inject constructor(
67 | private val inputCollection: Provider>
68 | ) {
69 |
70 | @get:Inject
71 | abstract val objectFactory: ObjectFactory
72 |
73 | fun transform(): Provider> {
74 | return inputCollection.map { set ->
75 | set.map {
76 | val rp = objectFactory.fileProperty()
77 | rp.fileValue(it.file)
78 | rp.get()
79 | }
80 | }
81 | }
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/res/ResourceMergePostHookConfiguration.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.res
2 |
3 | import com.android.build.api.variant.ApplicationVariant
4 | import com.android.build.gradle.internal.scope.InternalArtifactType
5 | import com.android.build.gradle.tasks.MergeResources
6 | import me.xx2bab.polyfill.PolyfillAction
7 | import me.xx2bab.polyfill.getArtifactsImpl
8 | import me.xx2bab.polyfill.task.SingleArtifactTaskExtendConfiguration
9 | import org.gradle.api.Project
10 | import org.gradle.api.file.Directory
11 | import org.gradle.api.provider.Provider
12 | import org.gradle.kotlin.dsl.withType
13 |
14 | /**
15 | * Configurations for fetching required data and set up dependencies
16 | * through both explicit/implicit approaches.
17 | */
18 | class ResourceMergePostHookConfiguration(
19 | project: Project,
20 | private val appVariant: ApplicationVariant,
21 | actionList: () -> List>
22 | ) : SingleArtifactTaskExtendConfiguration(project, appVariant, actionList) {
23 |
24 | override val data: Provider
25 | get() = CreationAction(appVariant).extractMergedRes()
26 |
27 | override fun orchestrate() {
28 | // We try to avoid using afterEvaluate{},
29 | // but here it looks like the best workaround...
30 | project.afterEvaluate {
31 | val localData = data
32 |
33 | // To consume the task instance here is ok,
34 | // since the merge task must run in a clean build,
35 | // it's not an avoidance task actually...
36 | // val mergeTask = mergeTaskProvider.get()
37 | val mergeTask = project.tasks.withType().first {
38 | it.name.let { taskName ->
39 | taskName.equals("merge${appVariant.name}Resources", true)
40 | && taskName.contains("test").not()
41 | }
42 | }
43 | actionList().forEachIndexed { index, action ->
44 | action.onTaskConfigure(mergeTask)
45 | mergeTask.doLast("ResourceMergePostHookByPolyfill$index") {
46 | action.onExecute(localData)
47 | }
48 | }
49 | }
50 |
51 | // If the getTaskContainer() does not work anymore,
52 | // we can fall back to below solution instead.
53 | // However, we should be aware of that
54 | // the `whenTaskAdded` is executed after `afterEavluate`.
55 | // val variantCapitalizedName = variant.getCapitalizedName()
56 | // project.tasks.whenTaskAdded {
57 | // if (name == "merge${variantCapitalizedName}Resources") {
58 | // val localData = data
59 | // actionList().forEachIndexed { index, action ->
60 | // action.onTaskConfigure(this)
61 | // doLast("ResourceMergePostHookByPolyfill$index") {
62 | // action.onExecute(localData)
63 | // }
64 | // }
65 | // }
66 | // }
67 | }
68 |
69 | class CreationAction(private val appVariant: ApplicationVariant) {
70 | fun extractMergedRes(): Provider {
71 | return appVariant.getArtifactsImpl()
72 | .get(InternalArtifactType.MERGED_RES)
73 | }
74 | }
75 |
76 | }
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/res/ResourceMergePreHookConfiguration.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.res
2 |
3 | import com.android.build.api.variant.ApplicationVariant
4 | import com.android.build.gradle.tasks.MergeResources
5 | import me.xx2bab.polyfill.PolyfillAction
6 | import me.xx2bab.polyfill.getTaskContainer
7 | import me.xx2bab.polyfill.task.MultipleArtifactTaskExtendConfiguration
8 | import org.gradle.api.Project
9 | import org.gradle.api.file.Directory
10 | import org.gradle.api.provider.Provider
11 | import org.gradle.kotlin.dsl.withType
12 | import java.io.File
13 |
14 | /**
15 | * Configurations for fetching required data and set up dependencies
16 | * through both explicit/implicit approaches.
17 | */
18 | class ResourceMergePreHookConfiguration(
19 | project: Project,
20 | private val appVariant: ApplicationVariant,
21 | actionList: () -> List>>
22 | ) : MultipleArtifactTaskExtendConfiguration(
23 | project, appVariant, actionList
24 | ) {
25 |
26 | override val data: Provider>
27 | get() {
28 | return project.provider {
29 | // mergeDebugResources
30 | val mergeTask = project.tasks.withType().first {
31 | it.name.let { taskName ->
32 | taskName.equals("merge${appVariant.name}Resources", true)
33 | && taskName.contains("test").not()
34 | }
35 | }
36 | // val mergeTask = appVariant.getTaskContainer().mergeResourcesTask.get()
37 | val resourcesComputer = mergeTask.resourcesComputer
38 | val resourceSets = resourcesComputer.compute(
39 | false,
40 | null,
41 | mergeTask.renderscriptGeneratedResDir
42 | )
43 | val resourceFiles = resourceSets.mapNotNull { resourceSet ->
44 | val getSourceFiles = resourceSet.javaClass.methods.find {
45 | it.name == "getSourceFiles" && it.parameterCount == 0
46 | }
47 | @Suppress("UNCHECKED_CAST")
48 | getSourceFiles?.invoke(resourceSet) as? Iterable
49 | }.flatten()
50 | resourceFiles.map { file ->
51 | // A hacky way to transform File -> RegularFile
52 | val rp = project.objects.directoryProperty()
53 | rp.fileValue(file)
54 | rp.get()
55 | }
56 | }
57 | }
58 |
59 | override fun orchestrate() {
60 | project.afterEvaluate {
61 | val mergeTaskProvider = appVariant.getTaskContainer().mergeResourcesTask
62 | val localData = data
63 | actionList().forEachIndexed { index, action ->
64 | mergeTaskProvider.configure {
65 | action.onTaskConfigure(this)
66 | doFirst("ResourceMergePreHookByPolyfill$index") {
67 | action.onExecute(localData)
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 |
75 | }
--------------------------------------------------------------------------------
/polyfill/src/main/kotlin/me/xx2bab/polyfill/tools/CommandLineKit.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.tools
2 |
3 | import java.io.File
4 | import java.io.IOException
5 | import java.util.concurrent.TimeUnit
6 |
7 | object CommandLineKit {
8 |
9 | private var workingDir = File("./")
10 |
11 | fun runCommand(
12 | command: String,
13 | workingDir: File = CommandLineKit.workingDir,
14 | timeoutInMilliseconds: Long = 10 * 1000
15 | ): String? {
16 | return try {
17 | val parts = command.split("\\s".toRegex())
18 | val proc = ProcessBuilder(*parts.toTypedArray())
19 | .directory(workingDir)
20 | .redirectOutput(ProcessBuilder.Redirect.PIPE)
21 | .redirectError(ProcessBuilder.Redirect.PIPE)
22 | .start()
23 | proc.waitFor(timeoutInMilliseconds, TimeUnit.MILLISECONDS)
24 | proc.inputStream.bufferedReader().readText()
25 | } catch (e: IOException) {
26 | e.printStackTrace()
27 | null
28 | }
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/polyfill/src/test/kotlin/me/xx2bab/polyfill/PolyfillTest.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill
2 |
3 | class PolyfillTest {
4 |
5 |
6 |
7 | }
--------------------------------------------------------------------------------
/publish.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Keep the order as followed one may depend on previous one
4 | MODULE_ARRAY=('android-arsc-parser' 'android-manifest-parser' 'polyfill-backport')
5 | for module in "${MODULE_ARRAY[@]}"
6 | do
7 | ./gradlew :"$module":publishPolyfillArtifactPublicationToSonatypeRepository
8 | done
9 | ./gradlew :polyfill:publishPluginMavenPublicationToSonatypeRepository
10 | ./gradlew aggregateJars releaseArtifactsToGithub
--------------------------------------------------------------------------------
/publish_to_local.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Keep the order as followed one may depend on previous one
4 | MODULE_ARRAY=('android-arsc-parser' 'android-manifest-parser' 'polyfill-backport')
5 | for module in "${MODULE_ARRAY[@]}"
6 | do
7 | ./gradlew clean :"$module":publishAllPublicationsToMyMavenlocalRepository
8 | done
9 |
10 | ./gradlew clean :polyfill:publishPluginMavenPublicationToMavenLocalRepository
11 | ./gradlew clean :polyfill-test-plugin:publishAllPublicationsToMyMavenlocalRepository
--------------------------------------------------------------------------------
/scripts/all-test.sh:
--------------------------------------------------------------------------------
1 | ./gradlew clean check
--------------------------------------------------------------------------------
/scripts/function-test.sh:
--------------------------------------------------------------------------------
1 | # Currently we are working on alpha/beta/rc versions,
2 | # because the Polyfill project is under incubating.
3 |
4 | # One for current min support version
5 | ./gradlew clean functionalTest -PagpVersion=8.0.1
6 | # One for latest version
7 | ./gradlew clean functionalTest -PagpVersion=8.1.2
--------------------------------------------------------------------------------
/scripts/unit-and-integration-test.sh:
--------------------------------------------------------------------------------
1 | ./gradlew clean test
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "polyfill-parent"
2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
3 |
4 | pluginManagement {
5 |
6 | val versions = file("deps.versions.toml").readText()
7 | val regexPlaceHolder = "%s\\s\\=\\s\\\"([A-Za-z0-9\\.\\-]+)\\\""
8 | val getVersion = { s: String -> regexPlaceHolder.format(s).toRegex().find(versions)!!.groupValues[1] }
9 |
10 | plugins {
11 | kotlin("jvm") version getVersion("kotlinVer")
12 | id("com.github.gmazzo.buildconfig") version getVersion("buildConfigVer") apply false
13 | kotlin("plugin.serialization") version getVersion("kotlinVer") apply false
14 | }
15 | repositories {
16 | mavenCentral()
17 | google()
18 | gradlePluginPortal()
19 | }
20 | }
21 | dependencyResolutionManagement {
22 | repositories {
23 | google()
24 | mavenCentral()
25 | mavenLocal()
26 | }
27 | versionCatalogs {
28 | create("deps") {
29 | from(files("./deps.versions.toml"))
30 | }
31 | }
32 | }
33 |
34 | include(":polyfill")
35 | include(":polyfill-backport")
36 | include(":android-arsc-parser") // resource.arsc parser
37 | include(":android-manifest-parser") // AndroidManifest.xml parser
38 | include(":polyfill-test-plugin") // A test plugin for testing polyfill function
39 | include(":functional-test")
40 |
--------------------------------------------------------------------------------
/test-app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/test-app/android-lib/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/test-app/android-lib/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | kotlin("android")
4 | }
5 |
6 | android {
7 | namespace = "me.xx2bab.polyfill.sample.android"
8 | compileSdk = 34
9 | defaultConfig {
10 | minSdk = 21
11 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
12 | }
13 |
14 | sourceSets["main"].java.srcDir("src/main/kotlin")
15 |
16 | compileOptions {
17 | sourceCompatibility = JavaVersion.VERSION_11
18 | targetCompatibility = JavaVersion.VERSION_11
19 | }
20 |
21 | kotlinOptions {
22 | jvmTarget = "11"
23 | }
24 | }
25 |
26 | dependencies {
27 |
28 | }
29 |
30 | java {
31 | toolchain {
32 | languageVersion.set(JavaLanguageVersion.of(11))
33 | }
34 | }
--------------------------------------------------------------------------------
/test-app/android-lib/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/test-app/android-lib/src/main/java/me/xx2bab/polyfill/sample/android/ExportedAndroidLibraryRunnable.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.sample.android
2 |
3 | class ExportedAndroidLibraryRunnable: Runnable {
4 | override fun run() {
5 | println("ExportedAndroidLibraryAPI is running")
6 | }
7 | }
--------------------------------------------------------------------------------
/test-app/android-lib/src/main/resources/android-lib-java-res.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2BAB/Polyfill/86b198afbd5616c80eb54063cc11034a3577865e/test-app/android-lib/src/main/resources/android-lib-java-res.txt
--------------------------------------------------------------------------------
/test-app/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | kotlin("android")
4 | id("polyfill-test-plugin")
5 | }
6 |
7 | android {
8 | namespace = "me.xx2bab.polyfill.sample"
9 | defaultConfig {
10 | applicationId = "me.xx2bab.polyfill.sample"
11 | minSdk = 21
12 | targetSdk = 34
13 | compileSdkVersion = "android-34"
14 | versionCode = 1
15 | versionName = "1.0"
16 | }
17 | buildTypes {
18 | getByName("debug") {
19 | isMinifyEnabled = false
20 | }
21 | }
22 |
23 | sourceSets["main"].java.srcDir("src/main/kotlin")
24 | compileOptions {
25 | sourceCompatibility = JavaVersion.VERSION_11
26 | targetCompatibility = JavaVersion.VERSION_11
27 | }
28 | kotlinOptions {
29 | jvmTarget = "11"
30 | }
31 | }
32 |
33 | dependencies {
34 | implementation("androidx.appcompat:appcompat:1.2.0")
35 | implementation(projects.androidLib)
36 | }
37 |
38 | java {
39 | sourceCompatibility = JavaVersion.VERSION_11
40 | targetCompatibility = JavaVersion.VERSION_11
41 | toolchain {
42 | languageVersion.set(JavaLanguageVersion.of(11))
43 | }
44 | }
--------------------------------------------------------------------------------
/test-app/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/test-app/app/src/main/kotlin/me/xx2bab/polyfill/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package me.xx2bab.polyfill.sample
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import me.xx2bab.polyfill.sample.android.ExportedAndroidLibraryRunnable
6 |
7 | class MainActivity : AppCompatActivity() {
8 |
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 | setContentView(R.layout.activity_main)
12 | ExportedAndroidLibraryRunnable().run()
13 | }
14 |
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/test-app/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/test-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2BAB/Polyfill/86b198afbd5616c80eb54063cc11034a3577865e/test-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/test-app/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/test-app/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Polyfill Test
3 |
4 |
--------------------------------------------------------------------------------
/test-app/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/test-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | task("clean") {
2 | delete(rootProject.buildDir)
3 | }
4 |
--------------------------------------------------------------------------------
/test-app/gradle.properties:
--------------------------------------------------------------------------------
1 | android.useAndroidX=true
2 | org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m
3 | org.gradle.unsafe.configuration-cache=true
--------------------------------------------------------------------------------
/test-app/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2BAB/Polyfill/86b198afbd5616c80eb54063cc11034a3577865e/test-app/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/test-app/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/test-app/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/test-app/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto init
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto init
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :init
68 | @rem Get command-line arguments, handling Windows variants
69 |
70 | if not "%OS%" == "Windows_NT" goto win9xME_args
71 |
72 | :win9xME_args
73 | @rem Slurp the command line arguments.
74 | set CMD_LINE_ARGS=
75 | set _SKIP=2
76 |
77 | :win9xME_args_slurp
78 | if "x%~1" == "x" goto execute
79 |
80 | set CMD_LINE_ARGS=%*
81 |
82 | :execute
83 | @rem Setup the command line
84 |
85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
86 |
87 |
88 | @rem Execute Gradle
89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
90 |
91 | :end
92 | @rem End local scope for the variables with windows NT shell
93 | if "%ERRORLEVEL%"=="0" goto mainEnd
94 |
95 | :fail
96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
97 | rem the _cmd.exe /c_ return code!
98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
99 | exit /b 1
100 |
101 | :mainEnd
102 | if "%OS%"=="Windows_NT" endlocal
103 |
104 | :omega
105 |
--------------------------------------------------------------------------------
/test-app/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "polyfill-func-test-project"
2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
3 |
4 | val externalDependencyBaseDir = extra["externalDependencyBaseDir"].toString()
5 | val enabledCompositionBuild = true
6 |
7 | pluginManagement {
8 | extra["externalDependencyBaseDir"] = "../"
9 | val versions = file(extra["externalDependencyBaseDir"].toString() + "deps.versions.toml").readText()
10 | val regexPlaceHolder = "%s\\s\\=\\s\\\"([A-Za-z0-9\\.\\-]+)\\\""
11 | val getVersion = { s: String -> regexPlaceHolder.format(s).toRegex().find(versions)!!.groupValues[1] }
12 |
13 | plugins {
14 | id("com.android.application") version getVersion("agpVer") apply false
15 | id("com.android.library") version getVersion("agpVer") apply false
16 | kotlin("android") version getVersion("kotlinVer") apply false
17 | }
18 | repositories {
19 | mavenLocal()
20 | google()
21 | gradlePluginPortal()
22 | }
23 | resolutionStrategy {
24 | eachPlugin {
25 | when (requested.id.id) {
26 | "polyfill-test-plugin" -> useModule("me.2bab:polyfill-test-plugin:+")
27 | }
28 | }
29 | }
30 | }
31 | dependencyResolutionManagement {
32 | repositories {
33 | mavenLocal()
34 | google()
35 | mavenCentral()
36 | }
37 | versionCatalogs {
38 | create("deps") {
39 | from(files(externalDependencyBaseDir + "deps.versions.toml"))
40 | }
41 | }
42 | }
43 |
44 | // Main test app
45 | include(":app", ":android-lib")
46 |
47 | // Substitute the test plugin with a project(":polyfill-test-plugin"),
48 | // also check ./build.gradle.kts
49 | if (enabledCompositionBuild) {
50 | includeBuild(externalDependencyBaseDir) {
51 | dependencySubstitution {
52 | substitute(module("me.2bab:polyfill-test-plugin"))
53 | .using(project(":polyfill-test-plugin"))
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------