├── .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 | Polyfill 2 | 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/me.2bab/polyfill/badge.svg)](https://search.maven.org/artifact/me.2bab/polyfill) 4 | [![Actions Status](https://github.com/2bab/Polyfill/workflows/CI/badge.svg)](https://github.com/2bab/Polyfill/actions) 5 | [![Apache 2](https://img.shields.io/badge/License-Apache%202-brightgreen.svg)](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 | --------------------------------------------------------------------------------