├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── core ├── .gitignore ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ ├── main │ └── java │ │ └── com │ │ └── bytedance │ │ └── android │ │ └── aabresguard │ │ ├── AabResGuardMain.java │ │ ├── android │ │ ├── AndroidDebugKeyStoreHelper.java │ │ ├── AndroidLocation.java │ │ ├── JarSigner.java │ │ ├── OpenJDKJarSigner.java │ │ └── SdkConstants.java │ │ ├── bundle │ │ ├── AppBundleAnalyzer.java │ │ ├── AppBundlePackager.java │ │ ├── AppBundleSigner.java │ │ ├── AppBundleUtils.java │ │ ├── NativeLibrariesOperation.java │ │ ├── ResourcesTableBuilder.java │ │ └── ResourcesTableOperation.java │ │ ├── commands │ │ ├── CommandHelp.java │ │ ├── DuplicatedResourcesMergerCommand.java │ │ ├── FileFilterCommand.java │ │ ├── ObfuscateBundleCommand.java │ │ └── StringFilterCommand.java │ │ ├── executors │ │ ├── BundleFileFilter.java │ │ ├── BundleStringFilter.java │ │ ├── DuplicatedResourcesMerger.java │ │ └── ResourcesObfuscator.java │ │ ├── model │ │ ├── ResourcesMapping.java │ │ ├── version │ │ │ └── AabResGuardVersion.java │ │ └── xml │ │ │ ├── AabResGuardConfig.java │ │ │ ├── FileFilterConfig.java │ │ │ └── StringFilterConfig.java │ │ ├── obfuscation │ │ └── ResGuardStringBuilder.java │ │ ├── parser │ │ ├── AabResGuardXmlParser.java │ │ ├── FileFilterXmlParser.java │ │ ├── ResourcesMappingParser.java │ │ └── StringFilterXmlParser.java │ │ └── utils │ │ ├── DrawNinePatchUtils.java │ │ ├── FileOperation.java │ │ ├── FileUtils.java │ │ ├── Pair.groovy │ │ ├── TimeClock.java │ │ ├── Utils.java │ │ └── exception │ │ └── CommandExceptionPreconditions.java │ └── test │ ├── java │ └── com │ │ └── bytedance │ │ └── android │ │ └── aabresguard │ │ ├── .gitignore │ │ ├── AabResGuardMainTest.java │ │ ├── BaseTest.java │ │ ├── TestData.java │ │ ├── bundle │ │ ├── AppBundlePackagerTest.java │ │ ├── AppBundleSignerTest.java │ │ └── AppBundleUtilsTest.java │ │ ├── commands │ │ ├── DuplicatedResourcesMergerCommandTest.java │ │ ├── FileFilterCommandTest.java │ │ ├── ObfuscateBundleCommandTest.java │ │ └── StringFilterCommandTest.java │ │ ├── executors │ │ ├── BundleFileFilterTest.java │ │ ├── BundleStringFilterTest.java │ │ ├── DuplicatedResourcesMergerTest.java │ │ └── ResourcesObfuscatorTest.java │ │ ├── issues │ │ └── i1 │ │ │ └── Issue1Test.java │ │ ├── parser │ │ ├── AabResGuardXmlParserTest.java │ │ ├── FileFilterXmlParserTest.java │ │ ├── ResourcesMappingParserTest.java │ │ └── StringFilterXmlParserTest.java │ │ ├── testing │ │ ├── Aapt2Helper.java │ │ ├── BundleToolOperation.java │ │ └── ProcessThread.java │ │ └── utils │ │ └── FileOperationTest.java │ └── resources │ └── com │ └── bytedance │ └── android │ └── aabresguard │ ├── .gitignore │ ├── demo │ ├── config-filter.xml │ ├── config.xml │ ├── demo.aab │ ├── mapping.txt │ ├── test.apk │ └── unused.txt │ ├── device-spec │ └── armeabi-v7a_sdk16.json │ └── issues │ └── i1 │ ├── config.xml │ ├── mapping.txt │ └── raw.aab ├── gradle.properties ├── gradle ├── aabresguard.gradle ├── config.gradle ├── ext.gradle ├── gradle-chrome-trace.gradle ├── publish-bintray.gradle ├── publish-shadow.gradle ├── publish.gradle ├── tools │ └── gradle-chrome-trace.jar ├── versions.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── plugin ├── .gitignore ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── kotlin │ └── com │ │ └── bytedance │ │ └── android │ │ └── plugin │ │ ├── AabResGuardPlugin.kt │ │ ├── extensions │ │ └── AabResGuardExtension.kt │ │ ├── internal │ │ ├── AGPVersionResolution.kt │ │ ├── BundleResolution.kt │ │ └── SigningConfigResolution.kt │ │ ├── model │ │ └── SigningConfig.kt │ │ └── tasks │ │ └── AabResGuardTask.kt │ └── resources │ └── META-INF │ └── gradle-plugins │ └── com.bytedance.android.aabResGuard.properties ├── samples ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── res │ │ ├── drawable │ │ ├── ic_abc.png │ │ ├── ic_bcd.png │ │ └── ic_keep.png │ │ ├── values-ml-rIN │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values │ │ └── strings.xml │ │ └── xml │ │ └── actions.xml ├── dynamic-features │ ├── df_module1 │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── bytedance │ │ │ │ └── android │ │ │ │ └── df │ │ │ │ └── module1 │ │ │ │ └── DfModule1.java │ │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ └── df_module2 │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── bytedance │ │ │ └── android │ │ │ └── df │ │ │ └── module2 │ │ │ └── DfModule2.java │ │ └── res │ │ └── values │ │ └── strings.xml ├── mapping.txt └── unused.txt ├── script └── publish.sh ├── settings.gradle └── wiki ├── en ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── COMMAND.md ├── CONTRIBUTOR.md ├── DATA.md ├── OUTPUT.md ├── PULL_REQUEST_TEMPLATE.md └── WHITELIST.md ├── images ├── logo.png └── output.png └── zh-cn ├── CHANGELOG.md ├── COMMAND.md ├── CONTRIBUTOR.md ├── DATA.md ├── OUTPUT.md └── README.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Gradle issue] " 5 | labels: bug 6 | assignees: JingYeoh 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Info (please complete the following information):** 17 | - Device: [e.g. iPhone6] 18 | - OS: [e.g. iOS8.1] 19 | - AabResGuard version [e.g. 0.1.1] 20 | - Android studio version: [e.g. Android Studio 4.0 Canary 7] 21 | - AGP version: [e.g. com.android.tools.build:bundletool:3.2.1] 22 | - Gradle version: [e.g. gradle-5.1.1] 23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | /repo -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | script: "./gradlew check" 3 | jdk: 4 | - oraclejdk8 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AabResGuard 2 |

3 | 4 |

The tool of obfuscated aab resources

5 |

6 | 7 | [ ![Download](https://api.bintray.com/packages/yeoh/maven/aabresguard-core/images/download.svg) ](https://bintray.com/yeoh/maven/aabresguard-plugin/) 8 | [![License](https://img.shields.io/badge/license-Apache2.0-brightgreen)](LICENSE) 9 | [![Bundletool](https://img.shields.io/badge/Dependency-Bundletool/0.10.0-blue)](https://github.com/google/bundletool) 10 | 11 | **[English](README.md)** | [简体中文](wiki/zh-cn/README.md) 12 | 13 | > Powered by bytedance douyin android team. 14 | 15 | ## Features 16 | > The tool of obfuscated aab resources. 17 | 18 | - **Merge duplicated resources:** Consolidate duplicate resource files to reduce package size. 19 | - **Filter bundle files:** Support for filtering files in the `bundle` package. Currently only supports filtering in the `MATE-INFO/` and `lib/` paths. 20 | - **White list:** The resources in the whitelist are not to be obfuscated. 21 | - **Incremental obfuscation:** Input the `mapping` file to support incremental obfuscation. 22 | - **Remove string:** Input the unused file splits by lines to support remove strings. 23 | - **???:** Looking ahead, there will be more feature support, welcome to submit PR & issue. 24 | 25 | ## [Data of size savings](wiki/en/DATA.md) 26 | **AabResGuard** is a resource obfuscation tool powered by the douyin Android team. It has been launched at the end of July 2018 in several overseas products, such as **Tiktok, Vigo**, etc. 27 | There is no feedback on related resource issues. 28 | For more data details, please go to **[Data of size savings](wiki/en/DATA.md)**. 29 | 30 | ## Quick start 31 | - **Command tool:** Support command line. 32 | - **Gradle plugin:** Support for `gradle plugin`, using the original packaging command to execute obfuscation. 33 | 34 | ### Gradle plugin 35 | Configured in `build.gradle(root project)` 36 | ```gradle 37 | buildscript { 38 | repositories { 39 | mavenCentral() 40 | jcenter() 41 | google() 42 | } 43 | dependencies { 44 | classpath "com.bytedance.android:aabresguard-plugin:0.1.0" 45 | } 46 | } 47 | ``` 48 | 49 | Configured in `build.gradle(application)` 50 | ```gradle 51 | apply plugin: "com.bytedance.android.aabResGuard" 52 | aabResGuard { 53 | mappingFile = file("mapping.txt").toPath() // Mapping file used for incremental obfuscation 54 | whiteList = [ // White list rules 55 | "*.R.raw.*", 56 | "*.R.drawable.icon" 57 | ] 58 | obfuscatedBundleFileName = "duplicated-app.aab" // Obfuscated file name, must end with '.aab' 59 | mergeDuplicatedRes = true // Whether to allow the merge of duplicate resources 60 | enableFilterFiles = true // Whether to allow filter files 61 | filterList = [ // file filter rules 62 | "*/arm64-v8a/*", 63 | "META-INF/*" 64 | ] 65 | 66 | enableFilterStrings = false // switch of filter strings 67 | unusedStringPath = file("unused.txt").toPath() // strings will be filtered in this file 68 | languageWhiteList = ["en", "zh"] // keep en,en-xx,zh,zh-xx etc. remove others. 69 | } 70 | ``` 71 | 72 | The `aabResGuard plugin` intrudes the `bundle` packaging process and can be obfuscated by executing the original packaging commands. 73 | ```cmd 74 | ./gradlew clean :app:bundleDebug --stacktrace 75 | ``` 76 | 77 | Get the obfuscated `bundle` file path by `gradle` . 78 | ```groovy 79 | def aabResGuardPlugin = project.tasks.getByName("aabresguard${VARIANT_NAME}") 80 | Path bundlePath = aabResGuardPlugin.getObfuscatedBundlePath() 81 | ``` 82 | 83 | ### [Whitelist](wiki/en/WHITELIST.md) 84 | The resources that can not be confused. Welcome PR your configs which is not included in [WHITELIST](wiki/en/WHITELIST.md) 85 | 86 | ### [Command line](wiki/en/COMMAND.md) 87 | **AabResGuard** provides a `jar` file that can be executed directly from the command line. More details, please go to **[Command Line](wiki/en/COMMAND.md)**. 88 | 89 | ### [Output](wiki/en/OUTPUT.md) 90 | After the packaging is completed, the obfuscated file and the log files will be output. More details, please go to **[Output File](wiki/en/OUTPUT.md)**. 91 | - **resources-mapping.txt:** Resource obfuscation mapping, which can be used as the next obfuscation input to achieve incremental obfuscate. 92 | - **aab:** Optimized aab file. 93 | - **-duplicated.txt:** duplicated file logging. 94 | 95 | ## [Change log](wiki/en/CHANGELOG.md) 96 | Version change log. More details, please go to **[Change Log](wiki/en/CHANGELOG.md)** . 97 | 98 | ## [Contribute](wiki/en/CONTRIBUTOR.md) 99 | Read the details to learn how to participate in the improvement **AabResGuard**. 100 | 101 | ### Contributor 102 | * [JingYeoh](https://github.com/JingYeoh) 103 | * [Jun Li]() 104 | * [Zilai Jiang](https://github.com/Zzzia) 105 | * [Zhiqian Yang](https://github.com/yangzhiqian) 106 | * [Xiaoshuang Bai (Designer)](https://www.behance.net/shawnpai) 107 | 108 | ## Thanks 109 | * [AndResGuard](https://github.com/shwenzhang/AndResGuard/) 110 | * [BundleTool](https://github.com/google/bundletool) 111 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | apply from: rootProject.file("gradle/config.gradle") 5 | repositories { 6 | google() 7 | jcenter() 8 | if ("true".equalsIgnoreCase(System.getProperty('useLocalMaven', "false"))) { 9 | println "use local repo" 10 | mavenLocal() 11 | } else { 12 | println "use remove repo" 13 | maven { url findProperty("RELEASE_REPOSITORY_URL") } 14 | maven { url findProperty("SNAPSHOT_REPOSITORY_URL") } 15 | } 16 | maven { 17 | url "https://plugins.gradle.org/m2/" 18 | } 19 | } 20 | dependencies { 21 | classpath deps.plugin.digital 22 | classpath deps.plugin.shadow 23 | classpath deps.plugin['bintray-plugin'] 24 | classpath deps.gradle.agp 25 | classpath deps.kotlin.plugin 26 | 27 | if ("true".equalsIgnoreCase(System.getProperty("enableAabResGuardPlugin", "false"))) { 28 | classpath deps.aabresguard.plugin 29 | } 30 | } 31 | } 32 | 33 | allprojects { 34 | repositories { 35 | google() 36 | jcenter() 37 | if ("true".equalsIgnoreCase(System.getProperty('useLocalMaven', "false"))) { 38 | mavenLocal() 39 | } else { 40 | maven { url findProperty("RELEASE_REPOSITORY_URL") } 41 | maven { url findProperty("SNAPSHOT_REPOSITORY_URL") } 42 | } 43 | } 44 | } 45 | 46 | task clean(type: Delete) { 47 | delete rootProject.buildDir 48 | } 49 | -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | maven { 5 | url "https://plugins.gradle.org/m2/" 6 | } 7 | } 8 | dependencies { 9 | classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.8" 10 | } 11 | } 12 | 13 | apply plugin: 'java-library' 14 | apply plugin: "com.google.protobuf" 15 | apply plugin: "com.github.johnrengelman.shadow" 16 | apply from: rootProject.file('gradle/publish-shadow.gradle') 17 | 18 | [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' 19 | 20 | configurations.all { 21 | resolutionStrategy.dependencySubstitution { 22 | resolutionStrategy { 23 | force deps.bundletool 24 | } 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | 31 | compileOnly "com.android.tools.build:aapt2-proto:0.4.0" 32 | compileOnly gradleApi() 33 | // compile deps.gradle.agp 34 | 35 | implementation "com.android.support:support-annotations:24.2.0" 36 | shadow 'commons-io:commons-io:2.6' 37 | shadow 'commons-codec:commons-codec:1.5' 38 | shadow "com.google.guava:guava:27.0.1-jre" 39 | 40 | annotationProcessor "com.google.auto.value:auto-value:1.5.2" 41 | implementation "com.google.auto.value:auto-value:1.5.2" 42 | shadow group: 'org.dom4j', name: 'dom4j', version: '2.1.0' 43 | shadow deps.bundletool 44 | 45 | testImplementation deps.gradle.agp 46 | testImplementation "com.android.tools.build:aapt2-proto:0.4.0" 47 | testImplementation group: 'org.dom4j', name: 'dom4j', version: '2.1.0' 48 | testImplementation "junit:junit:4.12" 49 | testImplementation "org.junit.jupiter:junit-jupiter-api:5.2.0" 50 | testImplementation "com.google.guava:guava:27.0.1-jre" 51 | testImplementation "org.mockito:mockito-core:2.18.3" 52 | testImplementation "com.google.truth.extensions:truth-java8-extension:0.45" 53 | testImplementation "com.google.truth.extensions:truth-proto-extension:0.45" 54 | testImplementation "com.google.auto.value:auto-value:1.5.2" 55 | testAnnotationProcessor "com.google.auto.value:auto-value:1.5.2" 56 | } 57 | 58 | protobuf { 59 | protoc { 60 | artifact = "com.google.protobuf:protoc:3.4.0" 61 | } 62 | } 63 | 64 | shadowJar { 65 | baseName = "AabResGuard" 66 | version = versions[ARTIFACT_ID] 67 | classifier = '' 68 | 69 | minimize() 70 | 71 | from sourceSets.main.output 72 | configurations = [ 73 | project.configurations.shadow 74 | ] 75 | manifest { 76 | attributes 'Main-Class': 'com.bytedance.android.aabresguard.AabResGuardMain' 77 | } 78 | exclude 'META-INF/*.SF' 79 | exclude 'META-INF/*.DSA' 80 | exclude 'META-INF/*.RSA' 81 | 82 | relocate 'com.android', 'shadow.bytedance.com.android' 83 | relocate 'com.google', 'shadow.bytedance.com.google' 84 | } 85 | -------------------------------------------------------------------------------- /core/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | #publish 15 | ARTIFACT_ID=aabresguard-core -------------------------------------------------------------------------------- /core/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # This is a configuration file for ProGuard. 2 | # http://proguard.sourceforge.net/index.html#manual/usage.html 3 | # 4 | # Starting with version 2.2 of the Android plugin for Gradle, these files are no longer used. Newer 5 | # versions are distributed with the plugin and unpacked at build time. Files in this directory are 6 | # no longer maintained. 7 | 8 | #表示混淆时不使用大小写混合类名 9 | -dontusemixedcaseclassnames 10 | #表示不跳过library中的非public的类 11 | -dontskipnonpubliclibraryclasses 12 | #打印混淆的详细信息 13 | -verbose 14 | 15 | # Optimization is turned off by default. Dex does not like code run 16 | # through the ProGuard optimize and preverify steps (and performs some 17 | # of these optimizations on its own). 18 | -dontoptimize 19 | ##表示不进行校验,这个校验作用 在java平台上的 20 | -dontpreverify 21 | # Note that if you want to enable optimization, you cannot just 22 | # include optimization flags in your own project configuration file; 23 | # instead you will need to point to the 24 | # "proguard-android-optimize.txt" file instead of this one from your 25 | # project.properties file. 26 | 27 | -keepattributes *Annotation* 28 | -keep public class com.google.vending.licensing.ILicensingService 29 | -keep public class com.android.vending.licensing.ILicensingService 30 | 31 | # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native 32 | -keepclasseswithmembernames class * { 33 | native ; 34 | } 35 | 36 | # keep setters in Views so that animations can still work. 37 | # see http://proguard.sourceforge.net/manual/examples.html#beans 38 | -keepclassmembers public class * extends android.view.View { 39 | void set*(***); 40 | *** get*(); 41 | } 42 | 43 | # We want to keep methods in Activity that could be used in the XML attribute onClick 44 | -keepclassmembers class * extends android.app.Activity { 45 | public void *(android.view.View); 46 | } 47 | 48 | # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations 49 | -keepclassmembers enum * { 50 | public static **[] values(); 51 | public static ** valueOf(java.lang.String); 52 | } 53 | 54 | -keepclassmembers class * implements android.os.Parcelable { 55 | public static final android.os.Parcelable$Creator CREATOR; 56 | } 57 | 58 | -keepclassmembers class **.R$* { 59 | public static ; 60 | } 61 | 62 | # The support library contains references to newer platform versions. 63 | # Don't warn about those in case this app is linking against an older 64 | # platform version. We know about them, and they are safe. 65 | -dontwarn android.support.** 66 | 67 | # Understand the @Keep support annotation. 68 | -keep class android.support.annotation.Keep 69 | 70 | -keep @android.support.annotation.Keep class * {*;} 71 | 72 | -keepclasseswithmembers class * { 73 | @android.support.annotation.Keep ; 74 | } 75 | 76 | -keepclasseswithmembers class * { 77 | @android.support.annotation.Keep ; 78 | } 79 | 80 | -keepclasseswithmembers class * { 81 | @android.support.annotation.Keep (...); 82 | } 83 | 84 | #忽略警告 85 | -ignorewarnings 86 | #保证是独立的jar,没有任何项目引用,如果不写就会认为我们所有的代码是无用的,从而把所有的代码压缩掉,导出一个空的jar 87 | -dontshrink 88 | #保护泛型 89 | -keepattributes Signature 90 | -keep class com.bytedance.android.aabresguard.AabResGuardMain { *; } 91 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/AabResGuardMain.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard; 2 | 3 | 4 | import com.android.tools.build.bundletool.flags.FlagParser; 5 | import com.android.tools.build.bundletool.flags.ParsedFlags; 6 | import com.bytedance.android.aabresguard.commands.CommandHelp; 7 | import com.bytedance.android.aabresguard.commands.DuplicatedResourcesMergerCommand; 8 | import com.bytedance.android.aabresguard.commands.FileFilterCommand; 9 | import com.bytedance.android.aabresguard.commands.ObfuscateBundleCommand; 10 | import com.bytedance.android.aabresguard.commands.StringFilterCommand; 11 | import com.bytedance.android.aabresguard.model.version.AabResGuardVersion; 12 | import com.google.common.collect.ImmutableList; 13 | 14 | import java.util.Optional; 15 | 16 | /** 17 | * Main entry point of the AabResGuard. 18 | *

19 | * Created by YangJing on 2019/10/09 . 20 | * Email: yangjing.yeoh@bytedance.com 21 | */ 22 | public class AabResGuardMain { 23 | private static final String HELP_CMD = "help"; 24 | 25 | public static void main(String[] args) { 26 | main(args, Runtime.getRuntime()); 27 | } 28 | 29 | /** 30 | * Parses the flags and routes to the appropriate commands handler. 31 | */ 32 | private static void main(String[] args, Runtime runtime) { 33 | final ParsedFlags flags; 34 | try { 35 | flags = new FlagParser().parse(args); 36 | } catch (FlagParser.FlagParseException e) { 37 | System.err.println("Error while parsing the flags: " + e.getMessage()); 38 | runtime.exit(1); 39 | return; 40 | } 41 | Optional command = flags.getMainCommand(); 42 | if (!command.isPresent()) { 43 | System.err.println("Error: You have to specify a commands."); 44 | help(); 45 | runtime.exit(1); 46 | return; 47 | } 48 | try { 49 | switch (command.get()) { 50 | case ObfuscateBundleCommand.COMMAND_NAME: 51 | ObfuscateBundleCommand.fromFlags(flags).execute(); 52 | break; 53 | case DuplicatedResourcesMergerCommand.COMMAND_NAME: 54 | DuplicatedResourcesMergerCommand.fromFlags(flags).execute(); 55 | break; 56 | case FileFilterCommand.COMMAND_NAME: 57 | FileFilterCommand.fromFlags(flags).execute(); 58 | break; 59 | case StringFilterCommand.COMMAND_NAME: 60 | StringFilterCommand.fromFlags(flags).execute(); 61 | break; 62 | case HELP_CMD: 63 | if (flags.getSubCommand().isPresent()) { 64 | help(flags.getSubCommand().get(), runtime); 65 | } else { 66 | help(); 67 | } 68 | break; 69 | default: 70 | System.err.printf("Error: Unrecognized command '%s'.%n%n%n", command.get()); 71 | help(); 72 | runtime.exit(1); 73 | return; 74 | } 75 | } catch (Exception e) { 76 | System.err.println( 77 | "[BT:" + AabResGuardVersion.getCurrentVersion() + "] Error: " + e.getMessage()); 78 | e.printStackTrace(); 79 | runtime.exit(1); 80 | return; 81 | } 82 | runtime.exit(0); 83 | } 84 | 85 | /** 86 | * Displays a general help. 87 | */ 88 | public static void help() { 89 | ImmutableList commandHelps = 90 | ImmutableList.of( 91 | ObfuscateBundleCommand.help(), 92 | DuplicatedResourcesMergerCommand.help(), 93 | FileFilterCommand.help(), 94 | StringFilterCommand.help() 95 | ); 96 | System.out.println("Synopsis: aabResGuard ..."); 97 | System.out.println(); 98 | System.out.println("Use 'aabResGuard help ' to learn more about the given command."); 99 | System.out.println(); 100 | commandHelps.forEach(commandHelp -> commandHelp.printSummary(System.out)); 101 | } 102 | 103 | /** 104 | * Displays help about a given commands. 105 | */ 106 | public static void help(String commandName, Runtime runtime) { 107 | CommandHelp commandHelp; 108 | switch (commandName) { 109 | case ObfuscateBundleCommand.COMMAND_NAME: 110 | commandHelp = ObfuscateBundleCommand.help(); 111 | break; 112 | case DuplicatedResourcesMergerCommand.COMMAND_NAME: 113 | commandHelp = DuplicatedResourcesMergerCommand.help(); 114 | break; 115 | case FileFilterCommand.COMMAND_NAME: 116 | commandHelp = FileFilterCommand.help(); 117 | break; 118 | case StringFilterCommand.COMMAND_NAME: 119 | commandHelp = StringFilterCommand.help(); 120 | break; 121 | default: 122 | System.err.printf("Error: Unrecognized command '%s'.%n%n%n", commandName); 123 | help(); 124 | runtime.exit(1); 125 | return; 126 | } 127 | commandHelp.printDetails(System.out); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/android/AndroidDebugKeyStoreHelper.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.android; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * Created by YangJing on 2019/10/17 . 7 | * Email: yangjing.yeoh@bytedance.com 8 | */ 9 | public class AndroidDebugKeyStoreHelper { 10 | 11 | public static final String DEFAULT_PASSWORD = "android"; 12 | public static final String DEFAULT_ALIAS = "AndroidDebugKey"; 13 | 14 | public static JarSigner.Signature debugSigningConfig() { 15 | String debugKeystoreLocation = defaultDebugKeystoreLocation(); 16 | if (debugKeystoreLocation == null || !new File(debugKeystoreLocation).exists()) { 17 | return null; 18 | } 19 | return new JarSigner.Signature( 20 | new File(debugKeystoreLocation).toPath(), 21 | DEFAULT_PASSWORD, 22 | DEFAULT_ALIAS, 23 | DEFAULT_PASSWORD 24 | ); 25 | } 26 | 27 | /** 28 | * Returns the location of the default debug keystore. 29 | * 30 | * @return The location of the default debug keystore 31 | */ 32 | private static String defaultDebugKeystoreLocation() { 33 | //this is guaranteed to either return a non null value (terminated with a platform 34 | // specific separator), or throw. 35 | String folder = null; 36 | try { 37 | folder = AndroidLocation.getFolder(); 38 | } catch (AndroidLocation.AndroidLocationException e) { 39 | e.printStackTrace(); 40 | return null; 41 | } 42 | return folder + "debug.keystore"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/android/JarSigner.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.android; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.Path; 6 | 7 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 8 | import static com.bytedance.android.aabresguard.utils.exception.CommandExceptionPreconditions.checkStringIsEmpty; 9 | 10 | /** 11 | * Created by YangJing on 2019/10/18 . 12 | * Email: yangjing.yeoh@bytedance.com 13 | */ 14 | public class JarSigner { 15 | 16 | public void sign(File toBeSigned, Signature signature) throws IOException, InterruptedException { 17 | new OpenJDKJarSigner().sign(toBeSigned, signature); 18 | } 19 | 20 | public static class Signature { 21 | public static final Signature DEBUG_SIGNATURE = AndroidDebugKeyStoreHelper.debugSigningConfig(); 22 | public final Path storeFile; 23 | public final String storePassword; 24 | public final String keyAlias; 25 | public final String keyPassword; 26 | 27 | 28 | public Signature(Path storeFile, String storePassword, String keyAlias, String keyPassword) { 29 | this.storeFile = storeFile; 30 | this.storePassword = storePassword; 31 | this.keyAlias = keyAlias; 32 | this.keyPassword = keyPassword; 33 | checkFileExistsAndReadable(storeFile); 34 | checkStringIsEmpty(storePassword, "storePassword"); 35 | checkStringIsEmpty(keyAlias, "keyAlias"); 36 | checkStringIsEmpty(keyPassword, "keyPassword"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/android/OpenJDKJarSigner.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.android; 2 | 3 | import com.bytedance.android.aabresguard.utils.FileUtils; 4 | import com.google.common.base.Joiner; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.logging.Logger; 11 | 12 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 13 | 14 | /** 15 | * Created by YangJing on 2019/10/18 . 16 | * Email: yangjing.yeoh@bytedance.com 17 | */ 18 | public class OpenJDKJarSigner { 19 | 20 | private static String jarSignerExecutable = SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS 21 | ? "jarsigner.exe" : "jarsigner"; 22 | private static Logger logger = Logger.getLogger(OpenJDKJarSigner.class.getName()); 23 | 24 | 25 | public void sign(File toBeSigned, JarSigner.Signature signature) throws IOException, InterruptedException { 26 | checkFileExistsAndReadable(toBeSigned.toPath()); 27 | checkFileExistsAndReadable(signature.storeFile); 28 | File jarSigner = locatedJarSigner(); 29 | List args = new ArrayList<>(); 30 | if (jarSigner != null) { 31 | args.add(jarSigner.getAbsolutePath()); 32 | } else { 33 | args.add(jarSignerExecutable); 34 | } 35 | args.add("-keystore"); 36 | args.add(signature.storeFile.toFile().getAbsolutePath()); 37 | 38 | File keyStorePasswordFile = null; 39 | File aliasPasswordFile = null; 40 | 41 | // write passwords to a file so it cannot be spied on. 42 | if (signature.storePassword != null) { 43 | keyStorePasswordFile = File.createTempFile("store", "prv"); 44 | FileUtils.writeToFile(keyStorePasswordFile, signature.storePassword); 45 | args.add("-storepass:file"); 46 | args.add(keyStorePasswordFile.getAbsolutePath()); 47 | } 48 | 49 | if (signature.keyPassword != null) { 50 | aliasPasswordFile = File.createTempFile("alias", "prv"); 51 | FileUtils.writeToFile(aliasPasswordFile, signature.keyPassword); 52 | args.add("--keypass:file"); 53 | args.add(aliasPasswordFile.getAbsolutePath()); 54 | } 55 | 56 | args.add(toBeSigned.getAbsolutePath()); 57 | 58 | if (signature.keyAlias != null) { 59 | args.add(signature.keyAlias); 60 | } 61 | 62 | File errorLog = File.createTempFile("error", ".log"); 63 | File outputLog = File.createTempFile("output", ".log"); 64 | 65 | logger.fine("Invoking " + Joiner.on(" ").join(args)); 66 | Process process = start(new ProcessBuilder(args).redirectError(errorLog).redirectOutput(outputLog)); 67 | int exitCode = process.waitFor(); 68 | if (exitCode != 0) { 69 | String errors = FileUtils.loadFileWithUnixLineSeparators(errorLog); 70 | String output = FileUtils.loadFileWithUnixLineSeparators(outputLog); 71 | throw new RuntimeException( 72 | String.format("%s failed with exit code %d: \n %s", 73 | jarSignerExecutable, exitCode, 74 | errors.trim().isEmpty() ? output : errors 75 | ) 76 | ); 77 | } 78 | if (keyStorePasswordFile != null) { 79 | keyStorePasswordFile.delete(); 80 | } 81 | if (aliasPasswordFile != null) { 82 | aliasPasswordFile.delete(); 83 | } 84 | } 85 | 86 | private Process start(ProcessBuilder builder) throws IOException { 87 | return builder.start(); 88 | } 89 | 90 | /** 91 | * Return the "jarsigner" tool location or null if it cannot be determined. 92 | */ 93 | private File locatedJarSigner() { 94 | // Look in the java.home bin folder, on jdk installations or Mac OS X, this is where the 95 | // javasigner will be located. 96 | File javaHome = new File(System.getProperty("java.home")); 97 | File jarSigner = getJarSigner(javaHome); 98 | if (jarSigner.exists()) { 99 | return jarSigner; 100 | } else { 101 | // if not in java.home bin, it's probable that the java.home points to a JRE 102 | // installation, we should then look one folder up and in the bin folder. 103 | jarSigner = getJarSigner(javaHome.getParentFile()); 104 | // if still cant' find it, give up. 105 | return jarSigner.exists() ? jarSigner : null; 106 | } 107 | } 108 | 109 | /** 110 | * Returns the jarsigner tool location with the bin folder. 111 | */ 112 | private File getJarSigner(File parentDir) { 113 | return new File(new File(parentDir, "bin"), jarSignerExecutable); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/android/SdkConstants.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.android; 2 | 3 | /** 4 | * Created by YangJing on 2019/10/18 . 5 | * Email: yangjing.yeoh@bytedance.com 6 | */ 7 | public class SdkConstants { 8 | public static final int PLATFORM_UNKNOWN = 0; 9 | public static final int PLATFORM_LINUX = 1; 10 | public static final int PLATFORM_WINDOWS = 2; 11 | public static final int PLATFORM_DARWIN = 3; 12 | 13 | public static int currentPlatform() { 14 | String os = System.getProperty("os.name"); //$NON-NLS-1$ 15 | if (os.startsWith("Mac OS")) { //$NON-NLS-1$ 16 | return PLATFORM_DARWIN; 17 | } else if (os.startsWith("Windows")) { //$NON-NLS-1$ 18 | return PLATFORM_WINDOWS; 19 | } else if (os.startsWith("Linux")) { //$NON-NLS-1$ 20 | return PLATFORM_LINUX; 21 | } 22 | 23 | return PLATFORM_UNKNOWN; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/bundle/AppBundleAnalyzer.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.bundle; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import com.bytedance.android.aabresguard.utils.TimeClock; 5 | 6 | import java.io.IOException; 7 | import java.nio.file.Path; 8 | import java.util.logging.Logger; 9 | import java.util.zip.ZipFile; 10 | 11 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 12 | 13 | /** 14 | * Created by YangJing on 2019/10/10 . 15 | * Email: yangjing.yeoh@bytedance.com 16 | */ 17 | public class AppBundleAnalyzer { 18 | 19 | private static final Logger logger = Logger.getLogger(AppBundleAnalyzer.class.getName()); 20 | private final Path bundlePath; 21 | 22 | public AppBundleAnalyzer(Path bundlePath) { 23 | checkFileExistsAndReadable(bundlePath); 24 | this.bundlePath = bundlePath; 25 | } 26 | 27 | public AppBundle analyze() throws IOException { 28 | TimeClock timeClock = new TimeClock(); 29 | ZipFile bundleZip = new ZipFile(bundlePath.toFile()); 30 | AppBundle appBundle = AppBundle.buildFromZip(bundleZip); 31 | System.out.println(String.format("analyze bundle file done, const %s", timeClock.getCoast())); 32 | return appBundle; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/bundle/AppBundlePackager.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.bundle; 2 | 3 | import com.android.tools.build.bundletool.io.AppBundleSerializer; 4 | import com.android.tools.build.bundletool.model.AppBundle; 5 | import com.bytedance.android.aabresguard.utils.TimeClock; 6 | 7 | import java.io.IOException; 8 | import java.nio.file.Path; 9 | import java.util.logging.Logger; 10 | 11 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; 12 | 13 | /** 14 | * Created by YangJing on 2019/10/11 . 15 | * Email: yangjing.yeoh@bytedance.com 16 | */ 17 | public class AppBundlePackager { 18 | private static final Logger logger = Logger.getLogger(AppBundlePackager.class.getName()); 19 | 20 | private final Path output; 21 | private final AppBundle appBundle; 22 | 23 | public AppBundlePackager(AppBundle appBundle, Path output) { 24 | this.output = output; 25 | this.appBundle = appBundle; 26 | checkFileDoesNotExist(output); 27 | } 28 | 29 | public void execute() throws IOException { 30 | TimeClock timeClock = new TimeClock(); 31 | AppBundleSerializer appBundleSerializer = new AppBundleSerializer(); 32 | appBundleSerializer.writeToDisk(appBundle, output); 33 | 34 | System.out.println(String.format("package bundle done, coast: %s", timeClock.getCoast())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/bundle/AppBundleSigner.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.bundle; 2 | 3 | import com.bytedance.android.aabresguard.android.JarSigner; 4 | import com.bytedance.android.aabresguard.utils.TimeClock; 5 | 6 | import java.io.IOException; 7 | import java.nio.file.Path; 8 | import java.util.logging.Logger; 9 | 10 | /** 11 | * Created by YangJing on 2019/10/11 . 12 | * Email: yangjing.yeoh@bytedance.com 13 | */ 14 | public class AppBundleSigner { 15 | 16 | private Path bundleFile; 17 | private JarSigner.Signature bundleSignature = JarSigner.Signature.DEBUG_SIGNATURE; 18 | 19 | public AppBundleSigner(Path bundleFile, JarSigner.Signature signature) { 20 | this.bundleFile = bundleFile; 21 | this.bundleSignature = signature; 22 | } 23 | 24 | public AppBundleSigner(Path bundleFile) { 25 | this.bundleFile = bundleFile; 26 | } 27 | 28 | public void setBundleSignature(JarSigner.Signature bundleSignature) { 29 | this.bundleSignature = bundleSignature; 30 | } 31 | 32 | public void execute() throws IOException, InterruptedException { 33 | if (bundleSignature == null) { 34 | return; 35 | } 36 | TimeClock timeClock = new TimeClock(); 37 | JarSigner.Signature signature = new JarSigner.Signature( 38 | bundleSignature.storeFile, 39 | bundleSignature.storePassword, 40 | bundleSignature.keyAlias, 41 | bundleSignature.keyPassword 42 | ); 43 | new JarSigner().sign(bundleFile.toFile(), signature); 44 | System.out.println(String.format("[sign] sign done, coast: %s", timeClock.getCoast())); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/bundle/AppBundleUtils.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.bundle; 2 | 3 | import com.android.tools.build.bundletool.model.BundleModule; 4 | import com.android.tools.build.bundletool.model.ModuleEntry; 5 | import com.android.tools.build.bundletool.model.ResourceTableEntry; 6 | import com.android.tools.build.bundletool.model.ZipPath; 7 | import com.android.tools.build.bundletool.model.utils.files.BufferedIo; 8 | 9 | import org.apache.commons.codec.digest.DigestUtils; 10 | import org.apache.commons.io.IOUtils; 11 | 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.util.zip.ZipEntry; 15 | import java.util.zip.ZipFile; 16 | 17 | import static com.bytedance.android.aabresguard.utils.FileOperation.getZipPathFileSize; 18 | 19 | /** 20 | * Created by YangJing on 2019/10/14 . 21 | * Email: yangjing.yeoh@bytedance.com 22 | */ 23 | public class AppBundleUtils { 24 | 25 | public static long getZipEntrySize(ZipFile bundleZipFile, ModuleEntry entry, BundleModule bundleModule) { 26 | String path = String.format("%s/%s", bundleModule.getName().getName(), entry.getPath().toString()); 27 | ZipEntry bundleConfigEntry = bundleZipFile.getEntry(path); 28 | return getZipPathFileSize(bundleZipFile, bundleConfigEntry); 29 | } 30 | 31 | public static long getZipEntrySize(ZipFile bundleZipFile, ZipPath zipPath) { 32 | String path = zipPath.toString(); 33 | ZipEntry bundleConfigEntry = bundleZipFile.getEntry(path); 34 | return getZipPathFileSize(bundleZipFile, bundleConfigEntry); 35 | } 36 | 37 | 38 | public static String getEntryMd5(ZipFile bundleZipFile, ModuleEntry entry, BundleModule bundleModule) { 39 | String path = String.format("%s/%s", bundleModule.getName().getName(), entry.getPath().toString()); 40 | ZipEntry bundleConfigEntry = bundleZipFile.getEntry(path); 41 | try { 42 | InputStream is = BufferedIo.inputStream(bundleZipFile, bundleConfigEntry); 43 | String md5 = bytesToHexString(DigestUtils.md5(is)); 44 | is.close(); 45 | return md5; 46 | } catch (IOException e) { 47 | throw new RuntimeException(e); 48 | } 49 | } 50 | 51 | public static byte[] readByte(ZipFile bundleZipFile, ModuleEntry entry, BundleModule bundleModule) throws IOException { 52 | String path = String.format("%s/%s", bundleModule.getName().getName(), entry.getPath().toString()); 53 | ZipEntry bundleConfigEntry = bundleZipFile.getEntry(path); 54 | InputStream is = BufferedIo.inputStream(bundleZipFile, bundleConfigEntry); 55 | byte[] bytes = IOUtils.toByteArray(is); 56 | is.close(); 57 | return bytes; 58 | } 59 | 60 | public static String bytesToHexString(byte[] src) { 61 | if (src.length <= 0) { 62 | return ""; 63 | } 64 | StringBuilder stringBuilder = new StringBuilder(src.length); 65 | for (byte b : src) { 66 | int v = b & 0xFF; 67 | String hv = Integer.toHexString(v); 68 | if (hv.length() < 2) { 69 | stringBuilder.append(0); 70 | } 71 | stringBuilder.append(hv); 72 | } 73 | return stringBuilder.toString(); 74 | } 75 | 76 | public static String getEntryNameByResourceName(String resourceName) { 77 | int index = resourceName.indexOf(".R."); 78 | String value = resourceName.substring(index + 3); 79 | String[] values = value.replace(".", "/").split("/"); 80 | if (values.length != 2) { 81 | throw new RuntimeException("Invalid resource format, it should be package.type.entry, yours: " + resourceName); 82 | } 83 | return values[values.length - 1]; 84 | } 85 | 86 | public static String getTypeNameByResourceName(String resourceName) { 87 | int index = resourceName.indexOf(".R."); 88 | String value = resourceName.substring(index + 3); 89 | String[] values = value.replace(".", "/").split("/"); 90 | if (values.length != 2) { 91 | throw new RuntimeException("Invalid resource format, it should be package.type.entry, yours: " + resourceName); 92 | } 93 | return values[0]; 94 | } 95 | 96 | public static String getResourceFullName(ResourceTableEntry entry) { 97 | return getResourceFullName(entry.getPackage().getPackageName(), entry.getType().getName(), entry.getEntry().getName()); 98 | } 99 | 100 | public static String getResourceFullName(String packageName, String typeName, String entryName) { 101 | return String.format("%s.R.%s.%s", packageName, typeName, entryName); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/bundle/NativeLibrariesOperation.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.bundle; 2 | 3 | import com.android.bundle.Files; 4 | 5 | /** 6 | * Created by YangJing on 2019/10/14 . 7 | * Email: yangjing.yeoh@bytedance.com 8 | */ 9 | public class NativeLibrariesOperation { 10 | 11 | public static Files.NativeLibraries removeDirectory(Files.NativeLibraries nativeLibraries, String zipPath) { 12 | int index = -1; 13 | for (int i = 0; i < nativeLibraries.getDirectoryList().size(); i++) { 14 | Files.TargetedNativeDirectory directory = nativeLibraries.getDirectoryList().get(i); 15 | if (directory.getPath().equals(zipPath)) { 16 | index = i; 17 | break; 18 | } 19 | } 20 | if (index == -1) { 21 | return nativeLibraries; 22 | } 23 | return nativeLibraries.toBuilder().removeDirectory(index).build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/bundle/ResourcesTableBuilder.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.bundle; 2 | 3 | import android.support.annotation.NonNull; 4 | 5 | import com.android.aapt.Resources; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import static com.google.common.base.Preconditions.checkArgument; 13 | import static com.google.common.base.Preconditions.checkState; 14 | 15 | 16 | /** 17 | * 用于生成 {@link com.android.aapt.Resources.ResourceTable} 类 18 | *

19 | * Created by YangJing on 2019/04/15 . 20 | * Email: yangjing.yeoh@bytedance.com 21 | */ 22 | public class ResourcesTableBuilder { 23 | 24 | private Resources.ResourceTable.Builder table; 25 | private Map resPackageMap; 26 | private List resPackages; 27 | 28 | 29 | public ResourcesTableBuilder() { 30 | table = Resources.ResourceTable.newBuilder(); 31 | resPackageMap = new HashMap<>(); 32 | resPackages = new ArrayList<>(); 33 | } 34 | 35 | /** 36 | * 添加 package 37 | */ 38 | public PackageBuilder addPackage(Resources.Package resPackage) { 39 | if (resPackageMap.containsKey(resPackage.getPackageName())) { 40 | return resPackageMap.get(resPackage.getPackageName()); 41 | } 42 | PackageBuilder packageBuilder = new PackageBuilder(resPackage); 43 | resPackageMap.put(resPackage.getPackageName(), packageBuilder); 44 | return packageBuilder; 45 | } 46 | 47 | /** 48 | * 生成 ResourceTable 49 | */ 50 | public Resources.ResourceTable build() { 51 | resPackageMap.entrySet().forEach(entry -> table.addPackage(entry.getValue().resPackageBuilder.build())); 52 | return table.build(); 53 | } 54 | 55 | public class PackageBuilder { 56 | 57 | Resources.Package.Builder resPackageBuilder; 58 | 59 | private PackageBuilder(Resources.Package resPackage) { 60 | addPackage(resPackage); 61 | } 62 | 63 | public ResourcesTableBuilder build() { 64 | // resPackages.add(resPackageBuilder.build()); 65 | return ResourcesTableBuilder.this; 66 | } 67 | 68 | /** 69 | * 添加 package 70 | */ 71 | private void addPackage(Resources.Package resPackage) { 72 | int id = resPackage.getPackageId().getId(); 73 | checkArgument( 74 | table.getPackageList().stream().noneMatch(pkg -> pkg.getPackageId().getId() == id), 75 | "Package ID %s already in use.", 76 | id); 77 | 78 | resPackageBuilder = Resources.Package.newBuilder() 79 | .setPackageId(resPackage.getPackageId()) 80 | .setPackageName(resPackage.getPackageName()); 81 | } 82 | 83 | /** 84 | * 在当前的 package 中寻找 type,如果不存在则添加指 package 中 85 | */ 86 | @NonNull 87 | Resources.Type.Builder getResourceType(@NonNull Resources.Type resType) { 88 | return resPackageBuilder.getTypeBuilderList().stream() 89 | .filter(type -> type.getTypeId().getId() == resType.getTypeId().getId()) 90 | .findFirst() 91 | .orElseGet(() -> addResourceType(resType)); 92 | } 93 | 94 | @NonNull 95 | Resources.Type.Builder addResourceType(@NonNull Resources.Type resType) { 96 | Resources.Type.Builder typeBuilder = Resources.Type.newBuilder() 97 | .setName(resType.getName()) 98 | .setTypeId(resType.getTypeId()); 99 | resPackageBuilder.addType(typeBuilder); 100 | return getResourceType(resType); 101 | } 102 | 103 | /** 104 | * 添加资源 105 | *

106 | * 如果 Entry 中不指定 id 的话,该资源不会被添加进 ResourceTable ,此处强行指定 id 为 0. 107 | * 108 | * @param resType 资源类型 109 | * @param resEntry entry 110 | */ 111 | @SuppressWarnings("UnusedReturnValue") 112 | public PackageBuilder addResource(@NonNull Resources.Type resType, 113 | @NonNull Resources.Entry resEntry) { 114 | // 如果 package 中不存在 package 则先添加 type 115 | Resources.Type.Builder type = getResourceType(resType); 116 | // 添加 entry 资源 117 | checkState(resPackageBuilder != null, "A package must be created before a resource can be added."); 118 | if (!resEntry.getEntryId().isInitialized()) { 119 | resEntry = resEntry.toBuilder().setEntryId( 120 | resEntry.getEntryId().toBuilder().setId(0).build() 121 | ).build(); 122 | } 123 | type.addEntry(resEntry.toBuilder()); 124 | return this; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/bundle/ResourcesTableOperation.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.bundle; 2 | 3 | import com.android.aapt.Resources; 4 | 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | /** 10 | * Created by YangJing on 2019/10/10 . 11 | * Email: yangjing.yeoh@bytedance.com 12 | */ 13 | public class ResourcesTableOperation { 14 | 15 | public static Resources.ConfigValue replaceEntryPath(Resources.ConfigValue configValue, String path) { 16 | Resources.ConfigValue.Builder entryBuilder = configValue.toBuilder(); 17 | entryBuilder.setValue( 18 | configValue.getValue().toBuilder().setItem( 19 | configValue.getValue().getItem().toBuilder().setFile( 20 | configValue.getValue().getItem().getFile().toBuilder().setPath(path).build() 21 | ).build() 22 | ).build() 23 | ); 24 | return entryBuilder.build(); 25 | } 26 | 27 | public static Resources.Entry updateEntryConfigValueList(Resources.Entry entry, List configValueList) { 28 | Resources.Entry.Builder entryBuilder = entry.toBuilder(); 29 | entryBuilder.clearConfigValue(); 30 | entryBuilder.addAllConfigValue(configValueList); 31 | return entryBuilder.build(); 32 | } 33 | 34 | public static Resources.Entry updateEntryName(Resources.Entry entry, String name) { 35 | Resources.Entry.Builder builder = entry.toBuilder(); 36 | builder.setName(name); 37 | return builder.build(); 38 | } 39 | 40 | public static void checkConfiguration(Resources.Entry entry) { 41 | if (entry.getConfigValueCount() == 0) return; 42 | Set configValues = new HashSet<>(); 43 | for (Resources.ConfigValue configValue : entry.getConfigValueList()) { 44 | if (configValues.contains(configValue)) { 45 | throw new IllegalArgumentException("duplicate configuration for entry: " + entry.getName()); 46 | } 47 | configValues.add(configValue); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/model/ResourcesMapping.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.model; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.FileWriter; 5 | import java.io.IOException; 6 | import java.io.Writer; 7 | import java.nio.file.Path; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.stream.Collectors; 12 | 13 | /** 14 | * Created by YangJing on 2019/10/14 . 15 | * Email: yangjing.yeoh@bytedance.com 16 | */ 17 | public class ResourcesMapping { 18 | 19 | private Map dirMapping = new HashMap<>(); 20 | private Map resourceMapping = new HashMap<>(); 21 | private Map entryFilesMapping = new HashMap<>(); 22 | 23 | private Map resourcesNameToIdMapping = new HashMap<>(); 24 | private Map resourcesPathToIdMapping = new HashMap<>(); 25 | 26 | public ResourcesMapping() { 27 | } 28 | 29 | public static String getResourceSimpleName(String resourceName) { 30 | String[] values = resourceName.split("/"); 31 | return values[values.length - 1]; 32 | } 33 | 34 | public Map getDirMapping() { 35 | return dirMapping; 36 | } 37 | 38 | public Map getResourceMapping() { 39 | return resourceMapping; 40 | } 41 | 42 | public Map getEntryFilesMapping() { 43 | return entryFilesMapping; 44 | } 45 | 46 | public void putDirMapping(String rawPath, String obfuscatePath) { 47 | dirMapping.put(rawPath, obfuscatePath); 48 | } 49 | 50 | public void putResourceMapping(String rawResource, String obfuscateResource) { 51 | if (resourceMapping.values().contains(obfuscateResource)) { 52 | throw new IllegalArgumentException( 53 | String.format("Multiple entries: %s -> %s", 54 | rawResource, obfuscateResource) 55 | ); 56 | } 57 | resourceMapping.put(rawResource, obfuscateResource); 58 | } 59 | 60 | public void putEntryFileMapping(String rawPath, String obfuscatedPath) { 61 | entryFilesMapping.put(rawPath, obfuscatedPath); 62 | } 63 | 64 | public List getPathMappingNameList() { 65 | return dirMapping.values().stream() 66 | .map(value -> { 67 | String[] values = value.split("/"); 68 | if (value.length() == 0) return value; 69 | return values[values.length - 1]; 70 | }) 71 | .collect(Collectors.toList()); 72 | } 73 | 74 | public void addResourceNameAndId(String name, String id) { 75 | resourcesNameToIdMapping.put(name, id); 76 | } 77 | 78 | public void addResourcePathAndId(String path, String id) { 79 | resourcesPathToIdMapping.put(path, id); 80 | } 81 | 82 | /** 83 | * Write mapping rules to file. 84 | */ 85 | public void writeMappingToFile(Path mappingPath) throws IOException { 86 | Writer writer = new BufferedWriter(new FileWriter(mappingPath.toFile(), false)); 87 | 88 | // write resources dir 89 | writer.write("res dir mapping:\n"); 90 | for (Map.Entry entry : dirMapping.entrySet()) { 91 | writer.write(String.format( 92 | "\t%s -> %s\n", 93 | entry.getKey(), 94 | entry.getValue() 95 | )); 96 | } 97 | writer.write("\n\n"); 98 | writer.flush(); 99 | 100 | // write resources name 101 | writer.write("res id mapping:\n"); 102 | for (Map.Entry entry : resourceMapping.entrySet()) { 103 | writer.write(String.format( 104 | "\t%s : %s -> %s\n", 105 | resourcesNameToIdMapping.get(entry.getKey()), 106 | entry.getKey(), 107 | entry.getValue() 108 | )); 109 | } 110 | writer.write("\n\n"); 111 | writer.flush(); 112 | 113 | // write resources entries path 114 | writer.write("res entries path mapping:\n"); 115 | for (Map.Entry entry : entryFilesMapping.entrySet()) { 116 | writer.write(String.format( 117 | "\t%s : %s -> %s\n", 118 | resourcesPathToIdMapping.get(entry.getKey()), 119 | entry.getKey(), 120 | entry.getValue() 121 | )); 122 | } 123 | writer.write("\n\n"); 124 | writer.flush(); 125 | 126 | writer.close(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/model/version/AabResGuardVersion.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.model.version; 2 | 3 | import com.android.tools.build.bundletool.model.version.Version; 4 | 5 | /** 6 | * Versions of AabResGuard. 7 | *

8 | * Created by YangJing on 2019/10/09 . 9 | * Email: yangjing.yeoh@bytedance.com 10 | */ 11 | public class AabResGuardVersion { 12 | 13 | private static final String CURRENT_VERSION = "0.9.0"; 14 | 15 | /** 16 | * Returns the version of BundleTool being run. 17 | */ 18 | public static Version getCurrentVersion() { 19 | return Version.of(CURRENT_VERSION); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/model/xml/AabResGuardConfig.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.model.xml; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | /** 7 | * Created by YangJing on 2019/10/14 . 8 | * Email: yangjing.yeoh@bytedance.com 9 | */ 10 | public class AabResGuardConfig { 11 | private FileFilterConfig fileFilter; 12 | private StringFilterConfig stringFilterConfig; 13 | private boolean useWhiteList; 14 | private Set whiteList = new HashSet<>(); 15 | 16 | 17 | public FileFilterConfig getFileFilter() { 18 | return fileFilter; 19 | } 20 | 21 | public void setFileFilter(FileFilterConfig fileFilter) { 22 | this.fileFilter = fileFilter; 23 | } 24 | 25 | public boolean isUseWhiteList() { 26 | return useWhiteList; 27 | } 28 | 29 | public void setUseWhiteList(boolean useWhiteList) { 30 | this.useWhiteList = useWhiteList; 31 | } 32 | 33 | public Set getWhiteList() { 34 | return whiteList; 35 | } 36 | 37 | public void addWhiteList(String whiteRule) { 38 | this.whiteList.add(whiteRule); 39 | } 40 | 41 | public StringFilterConfig getStringFilterConfig() { 42 | return stringFilterConfig; 43 | } 44 | 45 | public void setStringFilterConfig(StringFilterConfig stringFilterConfig) { 46 | this.stringFilterConfig = stringFilterConfig; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/model/xml/FileFilterConfig.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.model.xml; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | /** 7 | * Created by YangJing on 2019/10/14 . 8 | * Email: yangjing.yeoh@bytedance.com 9 | */ 10 | public class FileFilterConfig { 11 | private boolean isActive; 12 | private Set rules = new HashSet<>(); 13 | 14 | public boolean isActive() { 15 | return isActive; 16 | } 17 | 18 | public void setActive(boolean active) { 19 | isActive = active; 20 | } 21 | 22 | public Set getRules() { 23 | return rules; 24 | } 25 | 26 | public void addRule(String rule) { 27 | rules.add(rule); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/model/xml/StringFilterConfig.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.model.xml; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | /** 7 | * Created by jiangzilai on 2019-10-20. 8 | */ 9 | public class StringFilterConfig { 10 | private boolean isActive; 11 | private String path = ""; 12 | private Set languageWhiteList = new HashSet<>(); 13 | 14 | 15 | public boolean isActive() { 16 | return isActive; 17 | } 18 | 19 | public void setActive(boolean active) { 20 | isActive = active; 21 | } 22 | 23 | public String getPath() { 24 | return path; 25 | } 26 | 27 | public void setPath(String path) { 28 | this.path = path; 29 | } 30 | 31 | @Override public String toString() { 32 | return "StringFilterConfig{" + 33 | "isActive=" + isActive + 34 | ", path='" + path + '\'' + 35 | ", languageWhiteList=" + languageWhiteList + 36 | '}'; 37 | } 38 | 39 | public Set getLanguageWhiteList() { 40 | return languageWhiteList; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/obfuscation/ResGuardStringBuilder.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.obfuscation; 2 | 3 | import com.bytedance.android.aabresguard.utils.Utils; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.HashSet; 8 | import java.util.List; 9 | import java.util.Set; 10 | import java.util.regex.Pattern; 11 | 12 | /** 13 | * 混淆字典. 14 | *

15 | * Copied from: https://github.com/shwenzhang/AndResGuard 16 | */ 17 | public class ResGuardStringBuilder { 18 | 19 | private final List mReplaceStringBuffer; 20 | private final Set mIsReplaced; 21 | private final Set mIsWhiteList; 22 | private String[] mAToZ = { 23 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", 24 | "w", "x", "y", "z" 25 | }; 26 | private String[] mAToAll = { 27 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "_", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", 28 | "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" 29 | }; 30 | /** 31 | * 在window上面有些关键字是不能作为文件名的 32 | * CON, PRN, AUX, CLOCK$, NUL 33 | * COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9 34 | * LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9. 35 | */ 36 | private HashSet mFileNameBlackList; 37 | 38 | public ResGuardStringBuilder() { 39 | mFileNameBlackList = new HashSet<>(); 40 | mFileNameBlackList.add("con"); 41 | mFileNameBlackList.add("prn"); 42 | mFileNameBlackList.add("aux"); 43 | mFileNameBlackList.add("nul"); 44 | mReplaceStringBuffer = new ArrayList<>(); 45 | mIsReplaced = new HashSet<>(); 46 | mIsWhiteList = new HashSet<>(); 47 | } 48 | 49 | public void reset(HashSet blacklistPatterns) { 50 | mReplaceStringBuffer.clear(); 51 | mIsReplaced.clear(); 52 | mIsWhiteList.clear(); 53 | 54 | for (String str : mAToZ) { 55 | if (!Utils.match(str, blacklistPatterns)) { 56 | mReplaceStringBuffer.add(str); 57 | } 58 | } 59 | 60 | for (String first : mAToZ) { 61 | for (String aMAToAll : mAToAll) { 62 | String str = first + aMAToAll; 63 | if (!Utils.match(str, blacklistPatterns)) { 64 | mReplaceStringBuffer.add(str); 65 | } 66 | } 67 | } 68 | 69 | for (String first : mAToZ) { 70 | for (String second : mAToAll) { 71 | for (String third : mAToAll) { 72 | String str = first + second + third; 73 | if (!mFileNameBlackList.contains(str) && !Utils.match(str, blacklistPatterns)) { 74 | mReplaceStringBuffer.add(str); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | // 对于某种类型用过的mapping,全部不能再用了 82 | public void removeStrings(Collection collection) { 83 | if (collection == null) return; 84 | mReplaceStringBuffer.removeAll(collection); 85 | } 86 | 87 | public boolean isReplaced(int id) { 88 | return mIsReplaced.contains(id); 89 | } 90 | 91 | public boolean isInWhiteList(int id) { 92 | return mIsWhiteList.contains(id); 93 | } 94 | 95 | public void setInWhiteList(int id) { 96 | mIsWhiteList.add(id); 97 | } 98 | 99 | public void setInReplaceList(int id) { 100 | mIsReplaced.add(id); 101 | } 102 | 103 | public String getReplaceString(Collection names) throws IllegalArgumentException { 104 | if (mReplaceStringBuffer.isEmpty()) { 105 | throw new IllegalArgumentException("now can only obfuscation less than 35594 in a single type\n"); 106 | } 107 | if (names != null) { 108 | for (int i = 0; i < mReplaceStringBuffer.size(); i++) { 109 | String name = mReplaceStringBuffer.get(i); 110 | if (names.contains(name)) { 111 | continue; 112 | } 113 | return mReplaceStringBuffer.remove(i); 114 | } 115 | } 116 | return mReplaceStringBuffer.remove(0); 117 | } 118 | 119 | public String getReplaceString() { 120 | return getReplaceString(null); 121 | } 122 | } -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/parser/AabResGuardXmlParser.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.parser; 2 | 3 | import com.bytedance.android.aabresguard.model.xml.AabResGuardConfig; 4 | 5 | import org.dom4j.Document; 6 | import org.dom4j.DocumentException; 7 | import org.dom4j.Element; 8 | import org.dom4j.io.SAXReader; 9 | 10 | import java.nio.file.Path; 11 | import java.util.Iterator; 12 | 13 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 14 | 15 | /** 16 | * Created by YangJing on 2019/10/14 . 17 | * Email: yangjing.yeoh@bytedance.com 18 | */ 19 | public class AabResGuardXmlParser { 20 | private final Path configPath; 21 | 22 | public AabResGuardXmlParser(Path configPath) { 23 | checkFileExistsAndReadable(configPath); 24 | this.configPath = configPath; 25 | } 26 | 27 | public AabResGuardConfig parse() throws DocumentException { 28 | AabResGuardConfig aabResGuardConfig = new AabResGuardConfig(); 29 | SAXReader reader = new SAXReader(); 30 | Document doc = reader.read(configPath.toFile()); 31 | Element root = doc.getRootElement(); 32 | for (Iterator i = root.elementIterator("issue"); i.hasNext(); ) { 33 | Element element = (Element) i.next(); 34 | String id = element.attributeValue("id"); 35 | if (id == null || !id.equals("whitelist")) { 36 | continue; 37 | } 38 | String isActive = element.attributeValue("isactive"); 39 | if (isActive != null && isActive.equals("true")) { 40 | aabResGuardConfig.setUseWhiteList(true); 41 | } 42 | for (Iterator rules = element.elementIterator("path"); rules.hasNext(); ) { 43 | Element ruleElement = (Element) rules.next(); 44 | String rule = ruleElement.attributeValue("value"); 45 | if (rule != null) { 46 | aabResGuardConfig.addWhiteList(rule); 47 | } 48 | } 49 | } 50 | 51 | // file filter 52 | aabResGuardConfig.setFileFilter(new FileFilterXmlParser(configPath).parse()); 53 | 54 | // string filter 55 | aabResGuardConfig.setStringFilterConfig(new StringFilterXmlParser(configPath).parse()); 56 | 57 | return aabResGuardConfig; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/parser/FileFilterXmlParser.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.parser; 2 | 3 | import com.bytedance.android.aabresguard.model.xml.FileFilterConfig; 4 | 5 | import org.dom4j.Document; 6 | import org.dom4j.DocumentException; 7 | import org.dom4j.Element; 8 | import org.dom4j.io.SAXReader; 9 | 10 | import java.nio.file.Path; 11 | import java.util.Iterator; 12 | 13 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 14 | 15 | /** 16 | * Created by YangJing on 2019/10/14 . 17 | * Email: yangjing.yeoh@bytedance.com 18 | */ 19 | public class FileFilterXmlParser { 20 | private final Path configPath; 21 | 22 | public FileFilterXmlParser(Path configPath) { 23 | checkFileExistsAndReadable(configPath); 24 | this.configPath = configPath; 25 | } 26 | 27 | public FileFilterConfig parse() throws DocumentException { 28 | FileFilterConfig fileFilter = new FileFilterConfig(); 29 | 30 | SAXReader reader = new SAXReader(); 31 | Document doc = reader.read(configPath.toFile()); 32 | Element root = doc.getRootElement(); 33 | for (Iterator i = root.elementIterator("filter"); i.hasNext(); ) { 34 | Element element = (Element) i.next(); 35 | String isActiveValue = element.attributeValue("isactive"); 36 | if (isActiveValue != null && isActiveValue.equals("true")) { 37 | fileFilter.setActive(true); 38 | } 39 | for (Iterator rules = element.elementIterator("rule"); rules.hasNext(); ) { 40 | Element ruleElement = (Element) rules.next(); 41 | String rule = ruleElement.attributeValue("value"); 42 | if (rule != null) { 43 | fileFilter.addRule(rule); 44 | } 45 | } 46 | } 47 | return fileFilter; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/parser/ResourcesMappingParser.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.parser; 2 | 3 | import com.bytedance.android.aabresguard.model.ResourcesMapping; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.FileReader; 7 | import java.io.IOException; 8 | import java.nio.file.Path; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 13 | 14 | /** 15 | * Created by YangJing on 2019/10/14 . 16 | * Email: yangjing.yeoh@bytedance.com 17 | */ 18 | public class ResourcesMappingParser { 19 | private static final Pattern MAP_DIR_PATTERN = Pattern.compile("\\s+(.*)->(.*)"); 20 | private static final Pattern MAP_RES_PATTERN = Pattern.compile("\\s+(.*):(.*)->(.*)"); 21 | private final Path mappingPath; 22 | 23 | public ResourcesMappingParser(Path mappingPath) { 24 | checkFileExistsAndReadable(mappingPath); 25 | this.mappingPath = mappingPath; 26 | } 27 | 28 | public ResourcesMapping parse() throws IOException { 29 | ResourcesMapping mapping = new ResourcesMapping(); 30 | 31 | FileReader fr = new FileReader(mappingPath.toFile()); 32 | BufferedReader br = new BufferedReader(fr); 33 | String line = br.readLine(); 34 | while (line != null) { 35 | if (line.length() <= 0) { 36 | line = br.readLine(); 37 | continue; 38 | } 39 | if (!line.contains(":")) { 40 | Matcher mat = MAP_DIR_PATTERN.matcher(line); 41 | if (mat.find()) { 42 | String rawName = mat.group(1).trim(); 43 | String obfuscateName = mat.group(2).trim(); 44 | if (!line.contains("/") || line.contains(".")) { 45 | throw new IllegalArgumentException("Unexpected resource dir: " + line); 46 | } 47 | mapping.putDirMapping(rawName, obfuscateName); 48 | } 49 | } else { 50 | Matcher mat = MAP_RES_PATTERN.matcher(line); 51 | if (mat.find()) { 52 | String rawName = mat.group(2).trim(); 53 | String obfuscateName = mat.group(3).trim(); 54 | if (line.contains("/")) { 55 | mapping.putEntryFileMapping(rawName, obfuscateName); 56 | } else { 57 | int packagePos = rawName.indexOf(".R."); 58 | if (packagePos == -1) { 59 | throw new IllegalArgumentException(String.format("the mapping file packageName is malformed, " 60 | + "it should be like com.bytedance.android.ugc.R.attr.test, yours %s\n", 61 | rawName 62 | )); 63 | } 64 | mapping.putResourceMapping(rawName, obfuscateName); 65 | } 66 | } 67 | } 68 | line = br.readLine(); 69 | } 70 | 71 | br.close(); 72 | fr.close(); 73 | return mapping; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/parser/StringFilterXmlParser.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.parser; 2 | 3 | import com.bytedance.android.aabresguard.model.xml.StringFilterConfig; 4 | 5 | import org.dom4j.Document; 6 | import org.dom4j.DocumentException; 7 | import org.dom4j.Element; 8 | import org.dom4j.io.SAXReader; 9 | 10 | import java.nio.file.Path; 11 | import java.util.Iterator; 12 | 13 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 14 | 15 | /** 16 | * Created by jiangzilai on 2019-10-20. 17 | */ 18 | public class StringFilterXmlParser { 19 | private final Path configPath; 20 | 21 | public StringFilterXmlParser(Path configPath) { 22 | checkFileExistsAndReadable(configPath); 23 | this.configPath = configPath; 24 | } 25 | 26 | public StringFilterConfig parse() throws DocumentException { 27 | StringFilterConfig config = new StringFilterConfig(); 28 | SAXReader reader = new SAXReader(); 29 | Document doc = reader.read(configPath.toFile()); 30 | Element root = doc.getRootElement(); 31 | 32 | for (Iterator i = root.elementIterator("filter-str"); i.hasNext(); ) { 33 | Element element = (Element) i.next(); 34 | String isActive = element.attributeValue("isactive"); 35 | if (isActive != null && isActive.toLowerCase().equals("true")) { 36 | config.setActive(true); 37 | } 38 | for (Iterator rules = element.elementIterator("path"); rules.hasNext(); ) { 39 | Element ruleElement = (Element) rules.next(); 40 | String path = ruleElement.attributeValue("value"); 41 | config.setPath(path); 42 | } 43 | for (Iterator rules = element.elementIterator("language"); rules.hasNext(); ) { 44 | Element ruleElement = (Element) rules.next(); 45 | String path = ruleElement.attributeValue("value"); 46 | config.getLanguageWhiteList().add(path); 47 | } 48 | } 49 | return config; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/utils/DrawNinePatchUtils.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.utils; 2 | 3 | import com.android.tools.build.bundletool.model.BundleModule; 4 | import com.android.tools.build.bundletool.model.ZipPath; 5 | 6 | 7 | /** 8 | * .9 文件的帮助类 9 | *

10 | * Created by YangJing on 2019/04/19 . 11 | * Email: yangjing.yeoh@bytedance.com 12 | */ 13 | public class DrawNinePatchUtils { 14 | 15 | public static final String RESOURCE_SUFFIX_9_PATCH = ".9"; 16 | public static final String[] RESOURCE_TYPE_IMG = new String[]{ 17 | "drawable", 18 | "mipmap" 19 | }; 20 | 21 | /** 22 | * 是否是 .9 的图片资源 23 | * 24 | * @param typeName 资源类型名称,如: drawable 25 | * @param simpleName 资源名称(不包含文件后缀),如 a.9 aa 26 | * @return 是否是 .9 的图片资源 27 | */ 28 | public static boolean is9PatchResource(String typeName, String simpleName) { 29 | if (!simpleName.endsWith(RESOURCE_SUFFIX_9_PATCH)) { 30 | return false; 31 | } 32 | for (String type : RESOURCE_TYPE_IMG) { 33 | if (typeName.equals(type)) { 34 | return true; 35 | } 36 | } 37 | return false; 38 | } 39 | 40 | public static boolean is9PatchResource(ZipPath zipPath) { 41 | if (!zipPath.startsWith(BundleModule.RESOURCES_DIRECTORY)) { 42 | return false; 43 | } 44 | return zipPath.toString().endsWith(RESOURCE_SUFFIX_9_PATCH); 45 | } 46 | 47 | /** 48 | * 获取 .9 资源的名称 49 | * 50 | * @param simpleName 资源名称(不包含文件后缀),如 a.9 aa 51 | * @return 如:a.9 => a 52 | */ 53 | public static String get9PatchSimpleName(String simpleName) { 54 | return simpleName.substring(0, simpleName.length() - RESOURCE_SUFFIX_9_PATCH.length()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.utils; 2 | 3 | import com.google.common.base.Charsets; 4 | import com.google.common.base.Joiner; 5 | import com.google.common.io.Files; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.charset.StandardCharsets; 10 | 11 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 12 | 13 | /** 14 | * Created by YangJing on 2019/10/18 . 15 | * Email: yangjing.yeoh@bytedance.com 16 | */ 17 | public class FileUtils { 18 | private static final Joiner UNIX_NEW_LINE_JOINER = Joiner.on('\n'); 19 | 20 | /** 21 | * Loads a text file forcing the line separator to be of Unix style '\n' rather than being 22 | * Windows style '\r\n'. 23 | */ 24 | public static String loadFileWithUnixLineSeparators(File file) throws IOException { 25 | checkFileExistsAndReadable(file.toPath()); 26 | return UNIX_NEW_LINE_JOINER.join(Files.readLines(file, Charsets.UTF_8)); 27 | } 28 | 29 | /** 30 | * Creates a new text file or replaces content of an existing file. 31 | * 32 | * @param file the file to write to 33 | * @param content the new content of the file 34 | */ 35 | public static void writeToFile(File file, String content) throws IOException { 36 | Files.createParentDirs(file); 37 | Files.asCharSink(file, StandardCharsets.UTF_8).write(content); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/utils/Pair.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.bytedance.android.aabresguard.utils; 18 | 19 | /** 20 | * A Pair class is simply a 2-tuple for use in this package. We might want to 21 | * think about adding something like this to a more central utility place, or 22 | * replace it by a common tuple class if one exists, or even rewrite the layout 23 | * classes using this Pair by a more dedicated data structure (so we don't have 24 | * to pass around generic signatures as is currently done, though at least the 25 | * construction is helped a bit by the {@link #of} factory method. 26 | * 27 | * @param < S > The type of the first value 28 | * @param < T > The type of the second value 29 | */ 30 | public final class Pair { 31 | private final S mFirst; 32 | private final T mSecond; 33 | 34 | // Use {@link Pair#of} factory instead since it infers generic types 35 | private Pair(S first, T second) { 36 | this.mFirst = first; 37 | this.mSecond = second; 38 | } 39 | 40 | /** 41 | * Return the first item in the pair 42 | * 43 | * @return the first item in the pair 44 | */ 45 | public S getFirst() { 46 | return mFirst; 47 | } 48 | 49 | /** 50 | * Return the second item in the pair 51 | * 52 | * @return the second item in the pair 53 | */ 54 | public T getSecond() { 55 | return mSecond; 56 | } 57 | 58 | /** 59 | * Constructs a new pair of the given two objects, inferring generic types. 60 | * 61 | * @param first the first item to store in the pair 62 | * @param second the second item to store in the pair 63 | * @param < S > the type of the first item 64 | * @param < T > the type of the second item 65 | * @return a new pair wrapping the two items 66 | */ 67 | public static Pair of(S first, T second) { 68 | return new Pair(first, second); 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | return "Pair [first=" + mFirst + ", second=" + mSecond + "]"; 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | final int prime = 31; 79 | int result = 1; 80 | result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); 81 | result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); 82 | return result; 83 | } 84 | 85 | @SuppressWarnings("unchecked") 86 | @Override 87 | public boolean equals(Object obj) { 88 | if (this == obj) 89 | return true; 90 | if (obj == null) 91 | return false; 92 | if (getClass() != obj.getClass()) 93 | return false; 94 | Pair other = (Pair) obj; 95 | if (mFirst == null) { 96 | if (other.mFirst != null) 97 | return false; 98 | } else if (!mFirst.equals(other.mFirst)) 99 | return false; 100 | if (mSecond == null) { 101 | if (other.mSecond != null) 102 | return false; 103 | } else if (!mSecond.equals(other.mSecond)) 104 | return false; 105 | return true; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/utils/TimeClock.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.utils; 2 | 3 | /** 4 | * Created by YangJing on 2019/04/19 . 5 | * Email: yangjing.yeoh@bytedance.com 6 | */ 7 | public class TimeClock { 8 | 9 | private long startTime; 10 | 11 | public TimeClock() { 12 | startTime = System.currentTimeMillis(); 13 | } 14 | 15 | public String getCoast() { 16 | return (System.currentTimeMillis() - startTime) + ""; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/java/com/bytedance/android/aabresguard/utils/exception/CommandExceptionPreconditions.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.utils.exception; 2 | 3 | import com.android.tools.build.bundletool.flags.Flag; 4 | import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * Created by YangJing on 2019/10/10 . 10 | * Email: yangjing.yeoh@bytedance.com 11 | */ 12 | public final class CommandExceptionPreconditions { 13 | 14 | public static void checkFlagPresent(Object object, Flag flag) { 15 | if (object instanceof Optional) { 16 | object = ((Optional) object).get(); 17 | } 18 | Optional.of(object).orElseThrow(() -> CommandExecutionException.builder() 19 | .withMessage("Wrong properties: %s can not be empty", flag) 20 | .build()); 21 | } 22 | 23 | public static void checkStringIsEmpty(String value, String name) { 24 | if (value == null || value.trim().isEmpty()) { 25 | throw new IllegalArgumentException(String.format("Wrong properties: %s can not be empty", name)); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/.gitignore: -------------------------------------------------------------------------------- 1 | issues/bytedance/ -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/AabResGuardMainTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard; 2 | 3 | import org.junit.Test; 4 | 5 | /** 6 | * Created by YangJing on 2019/10/16 . 7 | * Email: yangjing.yeoh@bytedance.com 8 | */ 9 | public class AabResGuardMainTest extends BaseTest { 10 | 11 | @Test 12 | public void test_help() { 13 | AabResGuardMain.main( 14 | new String[]{ 15 | "help" 16 | } 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/BaseTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard; 2 | 3 | 4 | import com.bytedance.android.aabresguard.testing.ProcessThread; 5 | 6 | import org.junit.After; 7 | import org.junit.Before; 8 | import org.junit.Rule; 9 | import org.junit.Test; 10 | import org.junit.rules.TemporaryFolder; 11 | import org.junit.runner.RunWith; 12 | import org.junit.runners.JUnit4; 13 | 14 | import java.io.File; 15 | import java.io.InputStream; 16 | import java.io.Reader; 17 | import java.nio.file.Path; 18 | 19 | /** 20 | * Created by YangJing on 2019/04/14 . 21 | * Email: yangjing.yeoh@bytedance.com 22 | */ 23 | @RunWith(JUnit4.class) 24 | public class BaseTest { 25 | 26 | private static final boolean DEBUG = true; 27 | 28 | @Rule 29 | public final TemporaryFolder tmp = new TemporaryFolder(); // 注意,临时文件夹在执行完毕之后会被自动删除! 30 | 31 | private Path tmpDir; 32 | private long startTime; 33 | 34 | protected static File loadResourceFile(String path) { 35 | return TestData.openFile(path); 36 | } 37 | 38 | protected static Reader loadResourceReader(String path) { 39 | return TestData.openReader(path); 40 | } 41 | 42 | protected static InputStream loadResourceStream(String path) { 43 | return TestData.openStream(path); 44 | } 45 | 46 | protected static String loadResourcePath(String path) { 47 | return TestData.resourcePath(path); 48 | } 49 | 50 | protected static boolean executeCmd(String cmd, Object... objects) { 51 | return ProcessThread.execute(cmd, objects); 52 | } 53 | 54 | protected static void openDir(String dir, Object... objects) { 55 | if (!DEBUG) { 56 | return; 57 | } 58 | ProcessThread.execute("open " + dir, objects); 59 | } 60 | 61 | @Before 62 | public void setUp() throws Exception { 63 | tmpDir = tmp.getRoot().toPath(); 64 | startTime = System.currentTimeMillis(); 65 | } 66 | 67 | @After 68 | public void tearDown() { 69 | System.out.println(System.currentTimeMillis() - startTime); 70 | } 71 | 72 | /** 73 | * 返回临时路径 74 | */ 75 | protected Path getTempDirPath() { 76 | return tmpDir; 77 | } 78 | 79 | protected String getTempDirFilePath() { 80 | return tmpDir.toFile().toString(); 81 | } 82 | 83 | @Test 84 | public void emptyTest() { 85 | assert true; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/TestData.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard;/* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | 17 | import com.google.common.io.ByteStreams; 18 | 19 | import java.io.File; 20 | import java.io.FileInputStream; 21 | import java.io.FileNotFoundException; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.io.InputStreamReader; 25 | import java.io.Reader; 26 | import java.io.UncheckedIOException; 27 | import java.net.MalformedURLException; 28 | import java.net.URL; 29 | 30 | import static com.google.common.base.Preconditions.checkArgument; 31 | import static java.nio.charset.StandardCharsets.UTF_8; 32 | 33 | /** 34 | * Provides centralized access to testdata files. 35 | * 36 | *

The {@code fileName} argument always starts with the "testdata/" directory. 37 | */ 38 | @SuppressWarnings("WeakerAccess") 39 | class TestData { 40 | 41 | private static final String PACKAGE = "com/bytedance/android/aabresguard/"; 42 | 43 | private TestData() { 44 | } 45 | 46 | static URL getUrl(String path) { 47 | URL dirURL = TestData.class.getResource(path); 48 | if (dirURL != null && dirURL.getProtocol().equals("file")) { 49 | return dirURL; 50 | } 51 | if (dirURL == null) { 52 | String className = TestData.class.getName().replace(".", "/") + ".class"; 53 | dirURL = TestData.class.getClassLoader().getResource(className); 54 | String classPath = dirURL.getFile(); 55 | classPath = classPath.substring(0, classPath.indexOf("/build/")); 56 | classPath = "file:" + classPath + "/src/test/resources/" + PACKAGE + path; 57 | try { 58 | dirURL = new URL(classPath); 59 | } catch (MalformedURLException e) { 60 | e.printStackTrace(); 61 | } 62 | } 63 | return dirURL; 64 | } 65 | 66 | static InputStream openStream(String fileName) { 67 | InputStream is = null; 68 | try { 69 | is = new FileInputStream(new File(getUrl(fileName).getFile())); 70 | 71 | } catch (FileNotFoundException e) { 72 | e.printStackTrace(); 73 | } 74 | checkArgument(is != null, "Testdata file '%s' not found.", fileName); 75 | return is; 76 | } 77 | 78 | static Reader openReader(String fileName) { 79 | return new InputStreamReader(openStream(fileName), UTF_8); 80 | } 81 | 82 | @SuppressWarnings("UnstableApiUsage") 83 | static byte[] readBytes(String fileName) { 84 | try (InputStream inputStream = openStream(fileName)) { 85 | return ByteStreams.toByteArray(inputStream); 86 | } catch (IOException e) { 87 | // Throw an unchecked exception to allow usage in lambda expressions. 88 | throw new UncheckedIOException( 89 | String.format("Failed to read contents of com.bytedance.android.aabresguard file '%s'.", fileName), e); 90 | } 91 | } 92 | 93 | static File openFile(String fileName) { 94 | String filePath = getUrl(fileName).getFile(); 95 | checkArgument(filePath != null, "Testdata file '%s' not found.", fileName); 96 | return new File(filePath); 97 | } 98 | 99 | static String resourcePath(String resourceName) { 100 | return openFile(resourceName).getPath(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/bundle/AppBundlePackagerTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.bundle; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import com.bytedance.android.aabresguard.BaseTest; 5 | 6 | import org.junit.Test; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.util.zip.ZipFile; 11 | 12 | /** 13 | * Created by YangJing on 2019/10/11 . 14 | * Email: yangjing.yeoh@bytedance.com 15 | */ 16 | public class AppBundlePackagerTest extends BaseTest { 17 | 18 | @Test 19 | public void testPackageAppBundle() throws IOException { 20 | File output = new File(getTempDirPath().toFile(), "package.aab"); 21 | AppBundle appBundle = AppBundle.buildFromZip(new ZipFile(loadResourceFile("demo/demo.aab"))); 22 | AppBundlePackager packager = new AppBundlePackager(appBundle, output.toPath()); 23 | packager.execute(); 24 | assert output.exists(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/bundle/AppBundleSignerTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.bundle; 2 | 3 | import com.bytedance.android.aabresguard.BaseTest; 4 | import com.bytedance.android.aabresguard.utils.FileOperation; 5 | 6 | import org.junit.Test; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | 11 | /** 12 | * Created by YangJing on 2019/10/11 . 13 | * Email: yangjing.yeoh@bytedance.com 14 | */ 15 | public class AppBundleSignerTest extends BaseTest { 16 | 17 | @Test 18 | public void testAppBundleSigner() throws IOException, InterruptedException { 19 | File output = new File(getTempDirPath().toFile(), "signed.aab"); 20 | FileOperation.copyFileUsingStream(loadResourceFile("demo/demo.aab"), output); 21 | AppBundleSigner signer = new AppBundleSigner(output.toPath()); 22 | signer.execute(); 23 | assert output.exists(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/bundle/AppBundleUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.bundle; 2 | 3 | import com.bytedance.android.aabresguard.BaseTest; 4 | 5 | import org.junit.Test; 6 | 7 | import static junit.framework.TestCase.assertEquals; 8 | 9 | /** 10 | * Created by YangJing on 2019/11/03 . 11 | * Email: yangjing.yeoh@bytedance.com 12 | */ 13 | public class AppBundleUtilsTest extends BaseTest { 14 | 15 | @Test 16 | public void test_getEntryNameByResourceName() { 17 | assertEquals(AppBundleUtils.getEntryNameByResourceName("a.b.c.R.drawable.a"), "a"); 18 | } 19 | 20 | @Test 21 | public void test_getTypeNameByResourceName() { 22 | assertEquals(AppBundleUtils.getTypeNameByResourceName("a.b.c.R.drawable.a"), "drawable"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/commands/DuplicatedResourcesMergerCommandTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.commands; 2 | 3 | import com.android.tools.build.bundletool.flags.Flag; 4 | import com.android.tools.build.bundletool.flags.FlagParser; 5 | import com.bytedance.android.aabresguard.BaseTest; 6 | import com.bytedance.android.aabresguard.utils.FileOperation; 7 | 8 | import org.junit.Test; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | 13 | import static com.google.common.truth.Truth.assertThat; 14 | import static org.junit.jupiter.api.Assertions.assertThrows; 15 | 16 | /** 17 | * Created by YangJing on 2019/10/11 . 18 | * Email: yangjing.yeoh@bytedance.com 19 | */ 20 | public class DuplicatedResourcesMergerCommandTest extends BaseTest { 21 | 22 | @Test 23 | public void test_noFlag() { 24 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 25 | () -> DuplicatedResourcesMergerCommand.fromFlags( 26 | new FlagParser().parse( 27 | "" 28 | ) 29 | ).execute()); 30 | assertThat(flagsException) 31 | .hasMessageThat() 32 | .matches("Missing the required --bundle flag."); 33 | } 34 | 35 | @Test 36 | public void test_no_bundle() { 37 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 38 | () -> DuplicatedResourcesMergerCommand.fromFlags( 39 | new FlagParser().parse( 40 | "--output=" + getTempDirFilePath() 41 | ) 42 | ).execute()); 43 | assertThat(flagsException) 44 | .hasMessageThat() 45 | .matches("Missing the required --bundle flag."); 46 | } 47 | 48 | @Test 49 | public void test_no_Output() { 50 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 51 | () -> DuplicatedResourcesMergerCommand.fromFlags( 52 | new FlagParser().parse( 53 | "--bundle=" + loadResourcePath("demo/demo.aab") 54 | ) 55 | ).execute()); 56 | assertThat(flagsException) 57 | .hasMessageThat() 58 | .matches("Missing the required --output flag."); 59 | } 60 | 61 | @Test 62 | public void test_notExists_bundle() { 63 | String tempPath = getTempDirFilePath(); 64 | File apkFile = new File(tempPath + "abc.apk"); 65 | IllegalArgumentException flagsException = assertThrows(IllegalArgumentException.class, 66 | () -> DuplicatedResourcesMergerCommand.fromFlags( 67 | new FlagParser().parse( 68 | "--bundle=" + apkFile.getAbsolutePath(), 69 | "--output=" + getTempDirFilePath() 70 | ) 71 | ).execute()); 72 | assertThat(flagsException) 73 | .hasMessageThat() 74 | .matches(String.format("File '%s' was not found.", apkFile.getAbsolutePath())); 75 | } 76 | 77 | @Test 78 | public void test_wrong_params() { 79 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 80 | () -> DuplicatedResourcesMergerCommand.fromFlags( 81 | new FlagParser().parse( 82 | "--abc=a" 83 | ) 84 | ).execute()); 85 | assertThat(flagsException) 86 | .hasMessageThat() 87 | .matches("Missing the required --bundle flag."); 88 | } 89 | 90 | @Test 91 | public void test_disableSign() throws IOException, InterruptedException { 92 | File rawAabFile = loadResourceFile("demo/demo.aab"); 93 | File outputFile = new File(getTempDirPath().toFile(), "duplicated.aab"); 94 | DuplicatedResourcesMergerCommand.fromFlags( 95 | new FlagParser().parse( 96 | "--bundle=" + rawAabFile.getAbsolutePath(), 97 | "--output=" + outputFile.getAbsolutePath(), 98 | "--disable-sign=true" 99 | ) 100 | ).execute(); 101 | assert outputFile.exists(); 102 | } 103 | 104 | @Test 105 | public void testMergeDuplicatedRes() throws IOException, InterruptedException { 106 | File rawAabFile = loadResourceFile("demo/demo.aab"); 107 | File outputFile = new File(getTempDirPath().toFile(), "duplicated.aab"); 108 | DuplicatedResourcesMergerCommand.fromFlags( 109 | new FlagParser().parse( 110 | "--bundle=" + rawAabFile.getAbsolutePath(), 111 | "--output=" + outputFile.getAbsolutePath() 112 | ) 113 | ).execute(); 114 | assert outputFile.exists(); 115 | assert FileOperation.getFileSizes(rawAabFile) > FileOperation.getFileSizes(outputFile); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/commands/FileFilterCommandTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.commands; 2 | 3 | import com.android.tools.build.bundletool.flags.Flag; 4 | import com.android.tools.build.bundletool.flags.FlagParser; 5 | import com.bytedance.android.aabresguard.BaseTest; 6 | import com.bytedance.android.aabresguard.utils.FileOperation; 7 | 8 | import org.dom4j.DocumentException; 9 | import org.junit.Test; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | 14 | import static com.google.common.truth.Truth.assertThat; 15 | import static org.junit.jupiter.api.Assertions.assertThrows; 16 | 17 | /** 18 | * Created by YangJing on 2019/10/14 . 19 | * Email: yangjing.yeoh@bytedance.com 20 | */ 21 | public class FileFilterCommandTest extends BaseTest { 22 | @Test 23 | public void test_noFlag() { 24 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 25 | () -> FileFilterCommand.fromFlags( 26 | new FlagParser().parse( 27 | "" 28 | ) 29 | ).execute()); 30 | assertThat(flagsException) 31 | .hasMessageThat() 32 | .matches("Missing the required --bundle flag."); 33 | } 34 | 35 | @Test 36 | public void test_no_bundle() { 37 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 38 | () -> FileFilterCommand.fromFlags( 39 | new FlagParser().parse( 40 | "--output=" + getTempDirFilePath() 41 | ) 42 | ).execute()); 43 | assertThat(flagsException) 44 | .hasMessageThat() 45 | .matches("Missing the required --bundle flag."); 46 | } 47 | 48 | @Test 49 | public void test_no_Config() { 50 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 51 | () -> FileFilterCommand.fromFlags( 52 | new FlagParser().parse( 53 | "--bundle=" + loadResourcePath("demo/demo.aab") 54 | ) 55 | ).execute()); 56 | assertThat(flagsException) 57 | .hasMessageThat() 58 | .matches("Missing the required --config flag."); 59 | } 60 | 61 | 62 | @Test 63 | public void test_wrong_params() { 64 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 65 | () -> FileFilterCommand.fromFlags( 66 | new FlagParser().parse( 67 | "--abc=a" 68 | ) 69 | ).execute()); 70 | assertThat(flagsException) 71 | .hasMessageThat() 72 | .matches("Missing the required --bundle flag."); 73 | } 74 | 75 | @Test 76 | public void test_disableSign() throws IOException, DocumentException, InterruptedException { 77 | File rawAabFile = loadResourceFile("demo/demo.aab"); 78 | File outputFile = new File(getTempDirPath().toFile(), "filtered.aab"); 79 | FileFilterCommand.fromFlags( 80 | new FlagParser().parse( 81 | "--bundle=" + rawAabFile.getAbsolutePath(), 82 | "--output=" + outputFile.getAbsolutePath(), 83 | "--config="+loadResourcePath("demo/config-filter.xml"), 84 | "--disable-sign=true" 85 | ) 86 | ).execute(); 87 | assert outputFile.exists(); 88 | } 89 | 90 | @Test 91 | public void testPass() throws IOException, DocumentException, InterruptedException { 92 | File rawAabFile = loadResourceFile("demo/demo.aab"); 93 | File outputFile = new File(getTempDirPath().toFile(), "filtered.aab"); 94 | FileFilterCommand.fromFlags( 95 | new FlagParser().parse( 96 | "--bundle=" + rawAabFile.getAbsolutePath(), 97 | "--output=" + outputFile.getAbsolutePath(), 98 | "--config="+loadResourcePath("demo/config-filter.xml") 99 | ) 100 | ).execute(); 101 | assert outputFile.exists(); 102 | assert FileOperation.getFileSizes(rawAabFile) > FileOperation.getFileSizes(outputFile); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/commands/ObfuscateBundleCommandTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.commands; 2 | 3 | import com.android.tools.build.bundletool.flags.Flag; 4 | import com.android.tools.build.bundletool.flags.FlagParser; 5 | import com.bytedance.android.aabresguard.BaseTest; 6 | import com.bytedance.android.aabresguard.testing.BundleToolOperation; 7 | import com.bytedance.android.aabresguard.utils.FileOperation; 8 | 9 | import org.dom4j.DocumentException; 10 | import org.junit.Test; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.nio.file.Path; 15 | 16 | import static com.google.common.truth.Truth.assertThat; 17 | import static org.junit.jupiter.api.Assertions.assertThrows; 18 | 19 | /** 20 | * Created by YangJing on 2019/10/15 . 21 | * Email: yangjing.yeoh@bytedance.com 22 | */ 23 | public class ObfuscateBundleCommandTest extends BaseTest { 24 | @Test 25 | public void test_noFlag() { 26 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 27 | () -> ObfuscateBundleCommand.fromFlags( 28 | new FlagParser().parse( 29 | "" 30 | ) 31 | ).execute()); 32 | assertThat(flagsException) 33 | .hasMessageThat() 34 | .matches("Missing the required --bundle flag."); 35 | } 36 | 37 | @Test 38 | public void test_no_bundle() { 39 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 40 | () -> ObfuscateBundleCommand.fromFlags( 41 | new FlagParser().parse( 42 | "--output=" + getTempDirFilePath() 43 | ) 44 | ).execute()); 45 | assertThat(flagsException) 46 | .hasMessageThat() 47 | .matches("Missing the required --bundle flag."); 48 | } 49 | 50 | @Test 51 | public void test_no_Config() { 52 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 53 | () -> ObfuscateBundleCommand.fromFlags( 54 | new FlagParser().parse( 55 | "--bundle=" + loadResourcePath("demo/demo.aab") 56 | ) 57 | ).execute()); 58 | assertThat(flagsException) 59 | .hasMessageThat() 60 | .matches("Missing the required --config flag."); 61 | } 62 | 63 | 64 | @Test 65 | public void test_wrong_params() { 66 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 67 | () -> ObfuscateBundleCommand.fromFlags( 68 | new FlagParser().parse( 69 | "--abc=a" 70 | ) 71 | ).execute()); 72 | assertThat(flagsException) 73 | .hasMessageThat() 74 | .matches("Missing the required --bundle flag."); 75 | } 76 | 77 | @Test 78 | public void test_DisableSign() throws DocumentException, IOException, InterruptedException { 79 | File rawAabFile = loadResourceFile("demo/demo.aab"); 80 | File outputFile = new File(getTempDirPath().toFile(), "obfuscated.aab"); 81 | ObfuscateBundleCommand.fromFlags( 82 | new FlagParser().parse( 83 | "--bundle=" + rawAabFile.getAbsolutePath(), 84 | "--output=" + outputFile.getAbsolutePath(), 85 | "--config=" + loadResourcePath("demo/config.xml"), 86 | "--merge-duplicated-res=true", 87 | "--mapping=" + loadResourcePath("demo/mapping.txt"), 88 | "--disable-sign=true" 89 | ) 90 | ).execute(); 91 | assert outputFile.exists(); 92 | } 93 | 94 | @Test 95 | public void testPass() throws IOException, DocumentException, InterruptedException { 96 | File rawAabFile = loadResourceFile("demo/demo.aab"); 97 | File outputFile = new File(getTempDirPath().toFile(), "obfuscated.aab"); 98 | ObfuscateBundleCommand.fromFlags( 99 | new FlagParser().parse( 100 | "--bundle=" + rawAabFile.getAbsolutePath(), 101 | "--output=" + outputFile.getAbsolutePath(), 102 | "--config=" + loadResourcePath("demo/config.xml"), 103 | "--merge-duplicated-res=true", 104 | "--mapping=" + loadResourcePath("demo/mapping.txt") 105 | ) 106 | ).execute(); 107 | assert outputFile.exists(); 108 | assert FileOperation.getFileSizes(rawAabFile) > FileOperation.getFileSizes(outputFile); 109 | 110 | Path apkPath = BundleToolOperation.buildApkByBundle(outputFile.toPath(), getTempDirPath()); 111 | assert apkPath.toFile().exists(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/commands/StringFilterCommandTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.commands; 2 | 3 | import com.android.tools.build.bundletool.flags.Flag; 4 | import com.android.tools.build.bundletool.flags.FlagParser; 5 | import com.bytedance.android.aabresguard.BaseTest; 6 | import com.bytedance.android.aabresguard.utils.FileOperation; 7 | 8 | import org.dom4j.DocumentException; 9 | import org.junit.Test; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | 14 | import static com.google.common.truth.Truth.assertThat; 15 | import static org.junit.jupiter.api.Assertions.assertThrows; 16 | 17 | /** 18 | * Created by jiangzilai on 2019-10-20. 19 | */ 20 | public class StringFilterCommandTest extends BaseTest { 21 | @Test 22 | public void test_noFlag() { 23 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 24 | () -> StringFilterCommand.fromFlags( 25 | new FlagParser().parse( 26 | "" 27 | ) 28 | ).execute()); 29 | assertThat(flagsException) 30 | .hasMessageThat() 31 | .matches("Missing the required --bundle flag."); 32 | } 33 | 34 | @Test 35 | public void test_no_bundle() { 36 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 37 | () -> StringFilterCommand.fromFlags( 38 | new FlagParser().parse( 39 | "--output=" + getTempDirFilePath() 40 | ) 41 | ).execute()); 42 | assertThat(flagsException) 43 | .hasMessageThat() 44 | .matches("Missing the required --bundle flag."); 45 | } 46 | 47 | @Test 48 | public void test_no_Config() { 49 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 50 | () -> StringFilterCommand.fromFlags( 51 | new FlagParser().parse( 52 | "--bundle=" + loadResourcePath("demo/app.aab") 53 | ) 54 | ).execute()); 55 | assertThat(flagsException) 56 | .hasMessageThat() 57 | .matches("Missing the required --config flag."); 58 | } 59 | 60 | 61 | @Test 62 | public void test_wrong_params() { 63 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 64 | () -> StringFilterCommand.fromFlags( 65 | new FlagParser().parse( 66 | "--abc=a" 67 | ) 68 | ).execute()); 69 | assertThat(flagsException) 70 | .hasMessageThat() 71 | .matches("Missing the required --bundle flag."); 72 | } 73 | 74 | @Test 75 | public void testPass() throws IOException, DocumentException, InterruptedException { 76 | File rawAabFile = loadResourceFile("demo/app.aab"); 77 | File outputFile = new File(getTempDirPath().toFile(), "filtered.aab"); 78 | StringFilterCommand.fromFlags( 79 | new FlagParser().parse( 80 | "--bundle=" + rawAabFile.getAbsolutePath(), 81 | "--output=" + outputFile.getAbsolutePath(), 82 | "--config="+loadResourcePath("demo/config.xml") 83 | ) 84 | ).execute(); 85 | assert outputFile.exists(); 86 | assert FileOperation.getFileSizes(rawAabFile) >= FileOperation.getFileSizes(outputFile); 87 | } 88 | } -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/executors/BundleFileFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.executors; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import com.bytedance.android.aabresguard.BaseTest; 5 | import com.bytedance.android.aabresguard.bundle.AppBundleAnalyzer; 6 | import com.google.common.collect.ImmutableSet; 7 | 8 | import org.junit.Test; 9 | 10 | import java.io.IOException; 11 | import java.nio.file.Path; 12 | import java.util.HashSet; 13 | 14 | /** 15 | * Created by YangJing on 2019/10/14 . 16 | * Email: yangjing.yeoh@bytedance.com 17 | */ 18 | public class BundleFileFilterTest extends BaseTest { 19 | 20 | @Test 21 | public void test() throws IOException { 22 | Path bundlePath = loadResourceFile("demo/demo.aab").toPath(); 23 | AppBundleAnalyzer analyzer = new AppBundleAnalyzer(bundlePath); 24 | AppBundle appBundle = analyzer.analyze(); 25 | ImmutableSet filterRules = ImmutableSet.of( 26 | "*/arm64-v8a/*" 27 | ); 28 | BundleFileFilter fileFilter = new BundleFileFilter(loadResourceFile("demo/demo.aab").toPath(), appBundle, new HashSet<>(filterRules)); 29 | AppBundle filteredAppBundle = fileFilter.filter(); 30 | assert filteredAppBundle != null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/executors/BundleStringFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.executors; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import com.bytedance.android.aabresguard.BaseTest; 5 | import com.bytedance.android.aabresguard.bundle.AppBundleAnalyzer; 6 | 7 | import org.junit.Test; 8 | 9 | import java.io.IOException; 10 | import java.nio.file.Path; 11 | import java.util.HashSet; 12 | 13 | /** 14 | * Created by jiangzilai on 2019-10-20. 15 | */ 16 | public class BundleStringFilterTest extends BaseTest { 17 | 18 | @Test 19 | public void test() throws IOException { 20 | Path bundlePath = loadResourceFile("demo/demo.aab").toPath(); 21 | AppBundleAnalyzer analyzer = new AppBundleAnalyzer(bundlePath); 22 | AppBundle appBundle = analyzer.analyze(); 23 | BundleStringFilter filter = new BundleStringFilter(loadResourceFile("demo/demo.aab").toPath(), appBundle, 24 | loadResourceFile("demo/unused.txt").toPath().toString(), new HashSet<>()); 25 | AppBundle filteredAppBundle = filter.filter(); 26 | assert filteredAppBundle != null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/executors/DuplicatedResourcesMergerTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.executors; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import com.bytedance.android.aabresguard.BaseTest; 5 | import com.bytedance.android.aabresguard.bundle.AppBundleAnalyzer; 6 | 7 | import org.junit.Test; 8 | 9 | import java.io.IOException; 10 | import java.nio.file.Path; 11 | 12 | /** 13 | * Created by YangJing on 2019/10/10 . 14 | * Email: yangjing.yeoh@bytedance.com 15 | */ 16 | public class DuplicatedResourcesMergerTest extends BaseTest { 17 | 18 | @Test 19 | public void test() throws IOException { 20 | Path outputDirPath = getTempDirPath(); 21 | Path bundlePath = loadResourceFile("demo/demo.aab").toPath(); 22 | AppBundle rawAppBundle = new AppBundleAnalyzer(bundlePath).analyze(); 23 | DuplicatedResourcesMerger merger = new DuplicatedResourcesMerger(bundlePath, rawAppBundle, outputDirPath); 24 | AppBundle appBundle = merger.merge(); 25 | assert appBundle != null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/executors/ResourcesObfuscatorTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.executors; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import com.android.tools.build.bundletool.model.BundleModule; 5 | import com.bytedance.android.aabresguard.BaseTest; 6 | import com.bytedance.android.aabresguard.bundle.AppBundleAnalyzer; 7 | import com.google.common.collect.ImmutableSet; 8 | 9 | import org.junit.Test; 10 | 11 | import java.io.IOException; 12 | import java.nio.file.Path; 13 | import java.util.HashSet; 14 | import java.util.Set; 15 | 16 | /** 17 | * Created by YangJing on 2019/10/14 . 18 | * Email: yangjing.yeoh@bytedance.com 19 | */ 20 | public class ResourcesObfuscatorTest extends BaseTest { 21 | 22 | @Test 23 | public void test() throws IOException { 24 | Set whiteList = new HashSet<>( 25 | ImmutableSet.of( 26 | "com.bytedance.android.ugc.aweme.R.raw.*", 27 | "*.R.drawable.icon", 28 | "*.R.anim.ab*" 29 | ) 30 | ); 31 | Path bundlePath = loadResourceFile("demo/demo.aab").toPath(); 32 | Path outputDir = getTempDirPath(); 33 | AppBundleAnalyzer analyzer = new AppBundleAnalyzer(bundlePath); 34 | AppBundle appBundle = analyzer.analyze(); 35 | ResourcesObfuscator obfuscator = new ResourcesObfuscator(bundlePath, appBundle, whiteList, outputDir, loadResourceFile("demo/mapping.txt").toPath()); 36 | AppBundle obfuscateAppBundle = obfuscator.obfuscate(); 37 | assert obfuscateAppBundle != null; 38 | assert obfuscateAppBundle.getModules().size() == appBundle.getModules().size(); 39 | appBundle.getModules().forEach((bundleModuleName, bundleModule) -> { 40 | BundleModule obfuscatedModule = obfuscateAppBundle.getModule(bundleModuleName); 41 | assert obfuscatedModule.getEntries().size() == bundleModule.getEntries().size(); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/issues/i1/Issue1Test.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.issues.i1; 2 | 3 | import com.android.tools.build.bundletool.flags.FlagParser; 4 | import com.bytedance.android.aabresguard.BaseTest; 5 | import com.bytedance.android.aabresguard.commands.ObfuscateBundleCommand; 6 | import com.bytedance.android.aabresguard.testing.BundleToolOperation; 7 | 8 | import org.dom4j.DocumentException; 9 | import org.junit.Test; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.nio.file.Path; 14 | 15 | /** 16 | * Created by YangJing on 2019/10/31 . 17 | * Email: yangjing.yeoh@bytedance.com 18 | */ 19 | public class Issue1Test extends BaseTest { 20 | 21 | private Path loadIssueResourcePath(String name) { 22 | return loadResourceFile("issues/i1/" + name).toPath(); 23 | } 24 | 25 | /** 26 | * 增量混淆后生成的 aab 无法被解析,错误信息: 27 | * Caused by: java.util.concurrent.ExecutionException: com.android.tools.build.bundletool.model.Aapt2Command$Aapt2Exception: 28 | * Command '[/var/folders/sc/1qy4dyz527vf0j1qg5h2r0b40000gn/T/2712062020451125499/output/macos/aapt2, convert, --output-format, binary, -o, /var/folders/sc/1qy4dyz527vf0j1qg5h2r0b40000gn/T/5211613263510034859/binary.apk, /var/folders/sc/1qy4dyz527vf0j1qg5h2r0b40000gn/T/5211613263510034859/proto.apk]' 29 | * didn't terminate successfully (exit code: 1). Check the logs. 30 | *

31 | * aapt2 convert --output-format binary -o binary.apk proto.apk 32 | * proto.apk: error: failed to deserialize resources.pb: duplicate configuration in resource table. 33 | * proto.apk: error: failed to load APK. 34 | */ 35 | @Test 36 | public void test() throws DocumentException, IOException, InterruptedException { 37 | Path bundlePath = loadIssueResourcePath("raw.aab"); 38 | File outputFile = new File(getTempDirPath().toFile(), "obfuscated.aab"); 39 | ObfuscateBundleCommand.fromFlags( 40 | new FlagParser().parse( 41 | "--bundle=" + bundlePath.toFile().getAbsolutePath(), 42 | "--output=" + outputFile.getAbsolutePath(), 43 | "--config=" + loadIssueResourcePath("config.xml"), 44 | "--merge-duplicated-res=true", 45 | "--mapping=" + loadIssueResourcePath("mapping.txt") 46 | ) 47 | ).execute(); 48 | assert outputFile.exists(); 49 | 50 | Path apkPath = BundleToolOperation.buildApkByBundle(outputFile.toPath(), getTempDirPath()); 51 | assert apkPath.toFile().exists(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/parser/AabResGuardXmlParserTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.parser; 2 | 3 | import com.bytedance.android.aabresguard.BaseTest; 4 | import com.bytedance.android.aabresguard.model.xml.AabResGuardConfig; 5 | 6 | import org.dom4j.DocumentException; 7 | import org.junit.Test; 8 | 9 | /** 10 | * Created by YangJing on 2019/10/14 . 11 | * Email: yangjing.yeoh@bytedance.com 12 | */ 13 | public class AabResGuardXmlParserTest extends BaseTest { 14 | 15 | @Test 16 | public void test() throws DocumentException { 17 | AabResGuardXmlParser parser = new AabResGuardXmlParser(loadResourceFile("demo/config.xml").toPath()); 18 | AabResGuardConfig config = parser.parse(); 19 | assert config != null; 20 | assert config.isUseWhiteList(); 21 | assert config.getFileFilter() != null; 22 | assert config.getFileFilter().getRules().size() == 2; 23 | assert config.getWhiteList().size() == 1; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/parser/FileFilterXmlParserTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.parser; 2 | 3 | import com.bytedance.android.aabresguard.BaseTest; 4 | import com.bytedance.android.aabresguard.model.xml.FileFilterConfig; 5 | 6 | import org.dom4j.DocumentException; 7 | import org.junit.Test; 8 | 9 | /** 10 | * Created by YangJing on 2019/10/14 . 11 | * Email: yangjing.yeoh@bytedance.com 12 | */ 13 | public class FileFilterXmlParserTest extends BaseTest { 14 | 15 | @Test 16 | public void test() throws DocumentException { 17 | FileFilterXmlParser parser = new FileFilterXmlParser(loadResourceFile("demo/config-filter.xml").toPath()); 18 | FileFilterConfig fileFilter = parser.parse(); 19 | assert fileFilter != null; 20 | assert fileFilter.isActive(); 21 | assert fileFilter.getRules().size() == 2; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/parser/ResourcesMappingParserTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.parser; 2 | 3 | import com.bytedance.android.aabresguard.BaseTest; 4 | import com.bytedance.android.aabresguard.model.ResourcesMapping; 5 | 6 | import org.junit.Test; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * Created by YangJing on 2019/10/14 . 12 | * Email: yangjing.yeoh@bytedance.com 13 | */ 14 | public class ResourcesMappingParserTest extends BaseTest { 15 | 16 | @Test 17 | public void test() throws IOException { 18 | ResourcesMappingParser parser = new ResourcesMappingParser(loadResourceFile("demo/mapping.txt").toPath()); 19 | ResourcesMapping mapping = parser.parse(); 20 | 21 | assert !mapping.getDirMapping().isEmpty(); 22 | assert !mapping.getResourceMapping().isEmpty(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/parser/StringFilterXmlParserTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.parser; 2 | 3 | import com.bytedance.android.aabresguard.BaseTest; 4 | import com.bytedance.android.aabresguard.model.xml.AabResGuardConfig; 5 | 6 | import org.dom4j.DocumentException; 7 | import org.junit.Test; 8 | 9 | /** 10 | * Created by jiangzilai on 2019-10-20. 11 | */ 12 | public class StringFilterXmlParserTest extends BaseTest { 13 | 14 | @Test 15 | public void test() throws DocumentException { 16 | AabResGuardXmlParser parser = new AabResGuardXmlParser(loadResourceFile("demo/config.xml").toPath()); 17 | AabResGuardConfig config = parser.parse(); 18 | System.out.println(config.getStringFilterConfig().toString()); 19 | // assert config != null; 20 | // assert config.isUseWhiteList(); 21 | // assert config.getFileFilter() != null; 22 | // assert config.getFileFilter().getRules().size() == 2; 23 | // assert config.getWhiteList().size() == 1; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/testing/Aapt2Helper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package com.bytedance.android.aabresguard.testing; 17 | 18 | import com.android.tools.build.bundletool.model.Aapt2Command; 19 | import com.google.common.collect.ObjectArrays; 20 | 21 | import java.nio.file.Path; 22 | import java.nio.file.Paths; 23 | 24 | /** Helper for tests using aapt2. */ 25 | public final class Aapt2Helper { 26 | 27 | public static final String AAPT2_PATH = 28 | System.getenv("AAPT2_PATH"); 29 | 30 | public static Aapt2Command getAapt2Command() { 31 | return Aapt2Command.createFromExecutablePath(Paths.get(AAPT2_PATH)); 32 | } 33 | 34 | public static void convertBinaryApkToProtoApk(Path binaryApk, Path protoApk) { 35 | runAapt2( 36 | "convert", "--output-format", "proto", "-o", protoApk.toString(), binaryApk.toString()); 37 | } 38 | 39 | private static void runAapt2(String... command) { 40 | new Aapt2Command.CommandExecutor().execute(ObjectArrays.concat(AAPT2_PATH, command)); 41 | } 42 | 43 | private Aapt2Helper() {} 44 | } 45 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/testing/BundleToolOperation.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.testing; 2 | 3 | import com.android.tools.build.bundletool.commands.BuildApksCommand; 4 | import com.android.tools.build.bundletool.commands.ExtractApksCommand; 5 | import com.android.tools.build.bundletool.device.AdbServer; 6 | import com.android.tools.build.bundletool.device.DdmlibAdbServer; 7 | import com.android.tools.build.bundletool.flags.FlagParser; 8 | import com.bytedance.android.aabresguard.BaseTest; 9 | 10 | import java.io.File; 11 | import java.nio.file.Path; 12 | 13 | import static com.bytedance.android.aabresguard.testing.Aapt2Helper.AAPT2_PATH; 14 | 15 | /** 16 | * Created by YangJing on 2019/10/15 . 17 | * Email: yangjing.yeoh@bytedance.com 18 | */ 19 | public class BundleToolOperation extends BaseTest { 20 | 21 | public static Path buildApkByBundle(Path bundlePath, Path apkDirPath) { 22 | // build apks 23 | Path apksPath = new File(apkDirPath.toFile(), "app.apks").toPath(); 24 | Path deviceSpecPath = loadResourceFile("device-spec/armeabi-v7a_sdk16.json").toPath(); 25 | AdbServer adbServer = DdmlibAdbServer.getInstance(); 26 | BuildApksCommand.fromFlags( 27 | new FlagParser().parse( 28 | "--bundle=" + bundlePath.toFile().getAbsolutePath(), 29 | "--output=" + apksPath.toFile(), 30 | "--device-spec=" + deviceSpecPath.toFile(), 31 | "--aapt2=" + AAPT2_PATH 32 | ), 33 | adbServer 34 | ).execute(); 35 | assert apksPath.toFile().exists(); 36 | // extract apks 37 | ExtractApksCommand.fromFlags( 38 | new FlagParser().parse( 39 | "--apks=" + apksPath.toFile().getAbsolutePath(), 40 | "--output-dir=" + apkDirPath.toFile(), 41 | "--device-spec=" + deviceSpecPath.toFile() 42 | ) 43 | ).execute(); 44 | File[] apkList = apkDirPath.toFile().listFiles((file, s) -> s.endsWith(".apk")); 45 | assert apkList != null; 46 | assert apkList.length > 0; 47 | return apkList[0].toPath(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/testing/ProcessThread.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.testing; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | 8 | /** 9 | * Used to execute shell cmd 10 | *

11 | * Created by YangJing on 2019/04/11 . 12 | * Email: yangjing.yeoh@bytedance.com 13 | */ 14 | public class ProcessThread extends Thread { 15 | 16 | 17 | private InputStream is; 18 | private String printType; 19 | 20 | ProcessThread(InputStream is, String printType) { 21 | this.is = is; 22 | this.printType = printType; 23 | } 24 | 25 | public static boolean execute(String cmd) { 26 | try { 27 | Process process = Runtime.getRuntime().exec(cmd); 28 | new ProcessThread(process.getInputStream(), "INFO").start(); 29 | new ProcessThread(process.getErrorStream(), "ERR").start(); 30 | int value = process.waitFor(); 31 | return value == 0; 32 | } catch (IOException e) { 33 | e.printStackTrace(); 34 | } catch (InterruptedException e) { 35 | e.printStackTrace(); 36 | } 37 | return false; 38 | } 39 | 40 | public static boolean execute(String cmd, Object... objects) { 41 | return execute(String.format(cmd, objects)); 42 | } 43 | 44 | @Override 45 | public void run() { 46 | try { 47 | InputStreamReader isr = new InputStreamReader(is); 48 | BufferedReader br = new BufferedReader(isr); 49 | String line = null; 50 | while ((line = br.readLine()) != null) { 51 | if (printType == "ERR") { 52 | // System.out.println(printType + ">" + line); 53 | } 54 | System.out.println(printType + ">" + line); 55 | } 56 | } catch (IOException ioe) { 57 | ioe.printStackTrace(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/src/test/java/com/bytedance/android/aabresguard/utils/FileOperationTest.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.aabresguard.utils; 2 | 3 | 4 | import com.bytedance.android.aabresguard.BaseTest; 5 | 6 | import org.junit.Test; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.nio.file.Path; 11 | 12 | import static junit.framework.TestCase.assertEquals; 13 | 14 | /** 15 | * Created by YangJing on 2019/04/10 . 16 | * Email: yangjing.yeoh@bytedance.com 17 | */ 18 | public class FileOperationTest extends BaseTest { 19 | 20 | @Test 21 | public void testUnZip() throws IOException { 22 | File aabFile = loadResourceFile("demo/demo.aab"); 23 | Path unzipDirPath = getTempDirPath(); 24 | Path targetDir = new File(getTempDirPath().toFile(), "/aab").toPath(); 25 | FileOperation.uncompress(aabFile.toPath(), targetDir); 26 | System.out.println("testUnZip method coast:"); 27 | FileOperation.uncompress(aabFile.toPath(), unzipDirPath); 28 | } 29 | 30 | @Test 31 | public void testDrawNinePatchName() { 32 | assertEquals(FileOperation.getParentFromZipFilePath("res/a/a.9.png"), "res/a"); 33 | assertEquals(FileOperation.getNameFromZipFilePath("res/a/a.9.png"), "a.9.png"); 34 | assertEquals(FileOperation.getFilePrefixByFileName("a.9.png"), "a"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/.gitignore: -------------------------------------------------------------------------------- 1 | issues/bytedance/ -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/demo/config-filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/demo/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/demo/demo.aab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance/AabResGuard/e8f3a5d361ce61a3d4fa8bafb9d030bbe459c400/core/src/test/resources/com/bytedance/android/aabresguard/demo/demo.aab -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/demo/mapping.txt: -------------------------------------------------------------------------------- 1 | res path mapping: 2 | res/anim -> res/a 3 | res/drawable-v23 -> res/b 4 | res/drawable-ldrtl-xxxhdpi-v17 -> res/c 5 | res/drawable-xxhdpi-v4 -> res/d 6 | res/drawable-ldrtl-mdpi-v17 -> res/e 7 | res/transition -> res/f 8 | res/color-v23 -> res/g 9 | res/drawable-night-xhdpi-v8 -> res/h 10 | res/drawable-nodpi-v4 -> res/i 11 | res/layout-v22 -> res/j 12 | res/layout-land-v17 -> res/k 13 | res/layout-land -> res/l 14 | res/interpolator -> res/m 15 | res/mipmap-xxxhdpi-v4 -> res/n 16 | res/drawable -> res/o 17 | res/mipmap-hdpi-v4 -> res/p 18 | res/layout-watch-v20 -> res/q 19 | res/mipmap-xhdpi-v4 -> res/r 20 | res/drawable-ldrtl-xxhdpi-v17 -> res/s 21 | res/layout -> res/t 22 | res/drawable-xhdpi-v4 -> res/u 23 | res/color -> res/v 24 | res/layout-sw600dp-v13 -> res/w 25 | res/animator-v21 -> res/x 26 | res/animator-v19 -> res/y 27 | res/anim-land -> res/z 28 | res/animator -> res/a0 29 | res/drawable-xxxhdpi-v4 -> res/a1 30 | res/xml -> res/a2 31 | res/drawable-ldrtl-xhdpi-v17 -> res/a3 32 | res/color-v21 -> res/a4 33 | res/drawable-v21 -> res/a5 34 | res/drawable-night-v8 -> res/a6 35 | res/drawable-night-xxhdpi-v8 -> res/a7 36 | res/drawable-anydpi-v21 -> res/a8 37 | res/drawable-mdpi-v4 -> res/a9 38 | res/layout-v26 -> res/a_ 39 | res/layout-v19 -> res/aa 40 | res/layout-v21 -> res/ab 41 | res/menu -> res/ac 42 | res/layout-v17 -> res/ad 43 | res/drawable-watch-v20 -> res/ae 44 | res/mipmap-xxhdpi-v4 -> res/af 45 | res/anim-v21 -> res/ag 46 | res/layout-v16 -> res/ah 47 | res/drawable-hdpi-v4 -> res/ai 48 | res/interpolator-v21 -> res/aj 49 | res/raw -> res/ak 50 | res/drawable-ldpi-v4 -> res/al 51 | res/drawable-ldrtl-hdpi-v17 -> res/am 52 | 53 | 54 | res id mapping: 55 | 0x7e01000a : com.bytedance.android.ugc.dynamic_feature.R.attr.pressedStateOverlayImage -> com.bytedance.android.ugc.dynamic_feature.R.attr.k 56 | 0x7e01000e : com.bytedance.android.ugc.dynamic_feature.R.attr.retryImage -> com.bytedance.android.ugc.dynamic_feature.R.attr.o 57 | 0x7f0c0099 : com.bytedance.android.ugc.R.style.Base.Widget.AppCompat.SearchView.ActionBar -> com.bytedance.android.ugc.R.style.df 58 | 0x7f070061 : com.bytedance.android.ugc.R.id.right_icon -> com.bytedance.android.ugc.R.id.bx 59 | 0x7f05000f : com.bytedance.android.ugc.R.dimen.abc_action_button_min_width_overflow_material -> com.bytedance.android.ugc.R.dimen.p 60 | 0x7f0c00fc : com.bytedance.android.ugc.R.style.Theme.AppCompat.DayNight.NoActionBar -> com.bytedance.android.ugc.R.style.g4 61 | 62 | 63 | res entries path mapping: 64 | 0x7f010000 : base/res/anim/abc_fade_in.xml -> res/a/a.xml 65 | 0x7f010001 : base/res/anim/abc_fade_out.xml -> res/a/b.xml 66 | 0x7f040002 : base/res/color-v21/abc_btn_colored_borderless_text_material.xml -> res/a4/a.xml 67 | 0x7f060036 : base/res/drawable-xxhdpi-v4/abc_popup_background_mtrl_mult.9.png -> res/d/a2.9.png 68 | 0x7e020000 : dynamic_feature/res/drawable-xhdpi-v4/df_xh.png -> res/u/a.png 69 | 0x7e020002 : dynamic_feature/res/drawable-xxxhdpi-v4/df_xxxh.png -> res/a1/a.png 70 | 71 | -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/demo/test.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance/AabResGuard/e8f3a5d361ce61a3d4fa8bafb9d030bbe459c400/core/src/test/resources/com/bytedance/android/aabresguard/demo/test.apk -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/demo/unused.txt: -------------------------------------------------------------------------------- 1 | abc_action_bar_home_description 2 | abc_action_bar_up_description 3 | abc_action_menu_overflow_description 4 | abc_action_mode_done -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/device-spec/armeabi-v7a_sdk16.json: -------------------------------------------------------------------------------- 1 | { 2 | "supportedAbis": ["armeabi-v7a"], 3 | "supportedLocales": ["zh-CN", "en-US", "ja-JP", "zh-HK", "zh-TW"], 4 | "screenDensity": 480, 5 | "sdkVersion": 16 6 | } 7 | -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/issues/i1/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/issues/i1/mapping.txt: -------------------------------------------------------------------------------- 1 | res dir mapping: 2 | res/layout -> res/v 3 | res/drawable-xxhdpi-v4 -> res/i 4 | res/layout-v17 -> res/w 5 | res/drawable-ldpi-v4 -> res/t 6 | res/mipmap-mdpi-v4 -> res/a3 7 | res/layout-v16 -> res/a1 8 | res/drawable -> res/k 9 | res/drawable-ldrtl-mdpi-v17 -> res/o 10 | res/mipmap-anydpi-v26 -> res/a2 11 | res/mipmap-hdpi-v4 -> res/a4 12 | res/mipmap-xxxhdpi-v4 -> res/a7 13 | res/drawable-ldrtl-xxxhdpi-v17 -> res/s 14 | res/drawable-watch-v20 -> res/n 15 | res/mipmap-xhdpi-v4 -> res/a5 16 | res/drawable-xxxhdpi-v4 -> res/l 17 | res/drawable-ldrtl-hdpi-v17 -> res/p 18 | res/drawable-ldrtl-xxhdpi-v17 -> res/r 19 | res/drawable-anydpi-v21 -> res/u 20 | res/drawable-hdpi-v4 -> res/g 21 | res/drawable-v24 -> res/e 22 | res/color -> res/b 23 | res/layout-watch-v20 -> res/x 24 | res/layout-v22 -> res/y 25 | res/layout-v21 -> res/z 26 | res/drawable-mdpi-v4 -> res/f 27 | res/drawable-v23 -> res/m 28 | res/layout-v26 -> res/a0 29 | res/drawable-v21 -> res/j 30 | res/drawable-ldrtl-xhdpi-v17 -> res/q 31 | res/drawable-xhdpi-v4 -> res/h 32 | res/color-v21 -> res/c 33 | res/color-v23 -> res/d 34 | res/anim -> res/a 35 | res/mipmap-xxhdpi-v4 -> res/a6 36 | 37 | 38 | res id mapping: 39 | 0x7e01000a : com.ss.android.ugc.dynamic_feature.R.attr.pressedStateOverlayImage -> com.ss.android.ugc.dynamic_feature.R.attr.k 40 | 0x7f02011c : com.ss.android.ugc.R.attr.textAppearanceSearchResultTitle -> com.ss.android.ugc.R.attr.gz 41 | 0x7f0c00d6 : com.ss.android.ugc.R.style.TextAppearance.AppCompat.Title -> com.ss.android.ugc.R.style.f3 42 | 0x7f060062 : com.ss.android.ugc.R.drawable.notification_bg_normal -> com.ss.android.ugc.R.drawable.by 43 | 44 | 45 | res entries path mapping: 46 | 0x7f060031 : base/res/drawable-xxhdpi-v4/abc_list_selector_disabled_holo_dark.9.png -> res/i/z.9.png 47 | 0x7f060023 : base/res/drawable-xxxhdpi-v4/abc_ic_star_half_black_16dp.png -> res/l/o.png 48 | 0x7f09001a : base/res/layout/abc_select_dialog_material.xml -> res/v/a0.xml 49 | 0x7f01000a : base/res/anim/abc_tooltip_enter.xml -> res/a/k.xml 50 | -------------------------------------------------------------------------------- /core/src/test/resources/com/bytedance/android/aabresguard/issues/i1/raw.aab: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance/AabResGuard/e8f3a5d361ce61a3d4fa8bafb9d030bbe459c400/core/src/test/resources/com/bytedance/android/aabresguard/issues/i1/raw.aab -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | #maven 15 | # maven 16 | NEXUS_USERNAME= 17 | USERNAME= 18 | NEXUS_PASSWORD= 19 | PASSWORD= 20 | RELEASE_REPOSITORY_URL= 21 | SNAPSHOT_REPOSITORY_URL= 22 | PUBLISH_LOCAL_REPO=../repo 23 | # 是否使用本地 maven 依赖 24 | useLocalMaven=false 25 | # 是否源码依赖 26 | useSource=false 27 | enableAabResGuardPlugin=true 28 | # bintray 29 | uploadToBintray=true 30 | bintrayInfo.user=**** 31 | bintrayInfo.apiKey=*************** 32 | -------------------------------------------------------------------------------- /gradle/aabresguard.gradle: -------------------------------------------------------------------------------- 1 | if (!"true".equalsIgnoreCase(System.getProperty("enableAabResGuardPlugin", "false"))) { 2 | return 3 | } 4 | 5 | apply plugin: "com.bytedance.android.aabResGuard" 6 | aabResGuard { 7 | enableObfuscate = true 8 | mappingFile = file("../mapping.txt").toPath() 9 | whiteList = [ 10 | // keep resources 11 | "*.R.raw.*", 12 | "*.R.drawable.icon", 13 | "*.R.drawable.ic_*", 14 | "*.R.anim.abc*", 15 | "*.R.xml.actions", 16 | // keep resource file 17 | "*/res/xml/actions.xml", 18 | ] 19 | obfuscatedBundleFileName = "obfuscated-app.aab" 20 | mergeDuplicatedRes = false 21 | enableFilterFiles = true 22 | filterList = [ 23 | "*/arm64-v8a/*" 24 | ] 25 | enableFilterStrings = true 26 | unusedStringPath = "core/src/test/resources/com/bytedance/android/aabresguard/demo/unused.txt" 27 | languageWhiteList = ["en", "zh"] 28 | } 29 | -------------------------------------------------------------------------------- /gradle/config.gradle: -------------------------------------------------------------------------------- 1 | loadSystemProperties(rootProject.file("gradle.properties")) 2 | loadSystemProperties(rootProject.file("local.properties")) 3 | 4 | apply from: 'gradle/ext.gradle' 5 | apply from: 'gradle/versions.gradle' 6 | 7 | allprojects { 8 | tasks.withType(Javadoc) { 9 | options.addStringOption('Xdoclint:none', '-quiet') 10 | options.addStringOption('encoding', 'UTF-8') 11 | } 12 | } 13 | 14 | static void loadSystemProperties(File path) { 15 | if (!path.exists()) { 16 | return 17 | } 18 | Properties properties = new Properties() 19 | properties.load(path.newDataInputStream()) 20 | System.properties.putAll(properties) 21 | } 22 | -------------------------------------------------------------------------------- /gradle/ext.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | GROUP_ID = "com.bytedance.android" 3 | bintrayInfo = [ 4 | repo : [ 5 | name : "AabResGuard", 6 | groupId : GROUP_ID, 7 | artifactId : "aabresguard", 8 | description: "The tool of obfuscated aab resources", 9 | packaging : "jar", 10 | siteUrl : "https://github.com/bytedance/AabResGuard", 11 | gitUrl : "https://github.com/bytedance/AabResGuard.git", 12 | userOrg : System.getProperty("bintrayInfo.userOrg", "****"), 13 | ], 14 | developer: [ 15 | id : "JingYeoh", 16 | name : "JingYeoh", 17 | email : "yangjing.yeoh@gmail.com", 18 | user : System.getProperty("bintrayInfo.user", "****"), 19 | apikey: System.getProperty("bintrayInfo.apiKey", "*************") 20 | ], 21 | javadoc : [ 22 | name: "AabResGuard" 23 | ] 24 | ] 25 | } 26 | 27 | // 读取 local.properties 文件内容 28 | def localFile = project.rootProject.file("local.properties") 29 | if (localFile.exists()) { 30 | println "======== read local.properties ========" 31 | Properties properties = new Properties() 32 | properties.load(localFile.newDataInputStream()) 33 | // 逐行读取文件 34 | localFile.eachLine { line -> 35 | if (line.startsWith("#")) { 36 | return 37 | } 38 | def arr = line.split("=") 39 | ext.set(arr[0], arr[1]) 40 | System.setProperty(arr[0], arr[1]) 41 | } 42 | ext.properties.keySet().forEach { 43 | println "\t${it} : ${ext.get(it)}" 44 | } 45 | println "======== read local.properties ========" 46 | } 47 | -------------------------------------------------------------------------------- /gradle/gradle-chrome-trace.gradle: -------------------------------------------------------------------------------- 1 | initscript { 2 | dependencies { 3 | classpath files("tools/gradle-chrome-trace.jar") 4 | } 5 | } 6 | 7 | rootProject { 8 | ext.chromeTraceFile = new File(rootProject.buildDir, "trace.html") 9 | } 10 | 11 | apply plugin: org.gradle.trace.GradleTracingPlugin 12 | -------------------------------------------------------------------------------- /gradle/publish-bintray.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.jfrog.bintray' 2 | if (System.getProperty("uploadToBintray", "false").equalsIgnoreCase("true")) { 3 | bintray { 4 | user = bintrayInfo.developer.user 5 | key = bintrayInfo.developer.apikey 6 | pkg { 7 | repo = 'maven' 8 | name = ARTIFACT_ID 9 | websiteUrl = bintrayInfo.repo.siteUrl 10 | vcsUrl = bintrayInfo.repo.gitUrl 11 | licenses = ["Apache-2.0"] 12 | publish = true 13 | userOrg = bintrayInfo.repo.userOrg 14 | 15 | version { 16 | name = versions[ARTIFACT_ID] 17 | released = new Date() 18 | vcsTag = versions[ARTIFACT_ID] 19 | } 20 | } 21 | publications = ['MyPublication'] 22 | } 23 | } -------------------------------------------------------------------------------- /gradle/publish-shadow.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'digital.wup.android-maven-publish' 2 | 3 | task sourcesJar(type: Jar) { 4 | from sourceSets.main.java.srcDirs 5 | classifier = 'sources' 6 | version = versions[ARTIFACT_ID] 7 | } 8 | 9 | task javadocJar(type: Jar, dependsOn: javadoc) { 10 | classifier = 'javadoc' 11 | from javadoc.destinationDir 12 | version = versions[ARTIFACT_ID] 13 | } 14 | 15 | def releasesRepoUrl = System.getProperty("RELEASE_REPOSITORY_URL") 16 | def snapshotsRepoUrl = System.getProperty("SNAPSHOT_REPOSITORY_URL") 17 | def publishUrl = versions[ARTIFACT_ID].endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl 18 | def publishUserName = System.getProperty("NEXUS_USERNAME") 19 | def publishPassword = System.getProperty("NEXUS_PASSWORD") 20 | 21 | println "-------publish info---------" 22 | println "url: $publishUrl" 23 | println "username: $publishUserName" 24 | println "password: $publishPassword" 25 | 26 | apply plugin: 'maven-publish' 27 | 28 | def pomConfig = { 29 | licenses { 30 | license { 31 | name "The Apache Software License, Version 2.0" 32 | url "http://www.apache.org/licenses/LICENSE-2.0.txt" 33 | distribution "repo" 34 | } 35 | } 36 | developers { 37 | developer { 38 | id bintrayInfo.developer.id 39 | name bintrayInfo.developer.name 40 | email bintrayInfo.developer.email 41 | } 42 | } 43 | scm { 44 | url bintrayInfo.repo.siteUrl 45 | } 46 | } 47 | 48 | publishing { 49 | publications { 50 | MyPublication(MavenPublication) { publication -> 51 | project.shadow.component(publication) 52 | groupId GROUP_ID 53 | artifactId ARTIFACT_ID 54 | version versions[ARTIFACT_ID] 55 | 56 | artifact sourcesJar 57 | artifact javadocJar 58 | 59 | pom.withXml { 60 | def root = asNode() 61 | root.appendNode('description', bintrayInfo.repo.description) 62 | root.appendNode('name', bintrayInfo.repo.name) 63 | root.appendNode('url', bintrayInfo.repo.siteUrl) 64 | root.children().last() + pomConfig 65 | } 66 | } 67 | } 68 | 69 | if (!System.getProperty("uploadToBintray", "false").equalsIgnoreCase("true")) { 70 | repositories { 71 | maven { 72 | url = publishUrl 73 | credentials { 74 | username = publishUserName 75 | password = publishPassword 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | apply from: "$rootDir/gradle/publish-bintray.gradle" 83 | -------------------------------------------------------------------------------- /gradle/publish.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'digital.wup.android-maven-publish' 2 | 3 | task sourcesJar(type: Jar) { 4 | from sourceSets.main.java.srcDirs 5 | classifier = 'sources' 6 | version = versions[ARTIFACT_ID] 7 | } 8 | 9 | task javadocJar(type: Jar, dependsOn: javadoc) { 10 | classifier = 'javadoc' 11 | from javadoc.destinationDir 12 | version = versions[ARTIFACT_ID] 13 | } 14 | 15 | def releasesRepoUrl = System.getProperty("RELEASE_REPOSITORY_URL") 16 | def snapshotsRepoUrl = System.getProperty("SNAPSHOT_REPOSITORY_URL") 17 | def publishUrl = versions[ARTIFACT_ID].endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl 18 | def publishUserName = System.getProperty("NEXUS_USERNAME") 19 | def publishPassword = System.getProperty("NEXUS_PASSWORD") 20 | 21 | println "-------publish info---------" 22 | println "url: $publishUrl" 23 | println "username: $publishUserName" 24 | println "password: $publishPassword" 25 | 26 | publishing { 27 | if (!System.getProperty("uploadToBintray", "false").equalsIgnoreCase("true")) { 28 | repositories { 29 | maven { 30 | url = publishUrl 31 | credentials { 32 | username = publishUserName 33 | password = publishPassword 34 | } 35 | } 36 | if (System.getProperty("useLocalMaven", "false").equalsIgnoreCase("true")) { 37 | maven { 38 | url = "${rootProject.projectDir}/repo" 39 | } 40 | } 41 | } 42 | } 43 | 44 | if (project.plugins.hasPlugin("java-library") || project.plugins.hasPlugin("java")) { 45 | publications { 46 | MyPublication(MavenPublication) { 47 | from components.java 48 | artifact sourcesJar 49 | artifact javadocJar 50 | 51 | groupId GROUP_ID 52 | artifactId ARTIFACT_ID 53 | version versions[ARTIFACT_ID] 54 | } 55 | } 56 | } else { 57 | throw new GradleScriptException("java-library plugin required") 58 | } 59 | } 60 | 61 | apply from: "$rootDir/gradle/publish-bintray.gradle" -------------------------------------------------------------------------------- /gradle/tools/gradle-chrome-trace.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance/AabResGuard/e8f3a5d361ce61a3d4fa8bafb9d030bbe459c400/gradle/tools/gradle-chrome-trace.jar -------------------------------------------------------------------------------- /gradle/versions.gradle: -------------------------------------------------------------------------------- 1 | def versions = [:] 2 | versions.agp = "4.1.0" 3 | versions.kotlin = "1.3.61" 4 | versions.java = "8" 5 | versions.aabresguard = "0.1.9" 6 | // android 7 | versions.compileSdkVersion = 28 8 | versions.minSdkVersion = 15 9 | versions.targetSdkVersion = 28 10 | versions.support = "28.0.0" 11 | 12 | versions.bundletool = "0.10.0" 13 | 14 | versions["aabresguard-core"] = versions.aabresguard 15 | versions["aabresguard-plugin"] = versions.aabresguard 16 | 17 | // plugin 18 | versions["bintray-plugin"] = "1.8.4" 19 | versions.shadow = "4.0.4" 20 | versions.digital = "3.4.0" 21 | 22 | ext.versions = versions 23 | 24 | ext.deps = [:] 25 | // gradle 26 | def gradle = [:] 27 | gradle.agp = "com.android.tools.build:gradle:${versions.agp}" 28 | deps.gradle = gradle 29 | // kotlin 30 | def kotlin = [:] 31 | kotlin.stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin" 32 | kotlin.plugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" 33 | kotlin.allopen = "org.jetbrains.kotlin:kotlin-allopen:$versions.kotlin" 34 | kotlin.embeddable = "org.jetbrains.kotlin:kotlin-compiler-embeddable:$versions.kotlin" 35 | deps.kotlin = kotlin 36 | // plugin 37 | def plugin = [:] 38 | plugin.digital = "digital.wup:android-maven-publish:${versions.digital}" 39 | plugin.shadow = "com.github.jengelman.gradle.plugins:shadow:${versions.shadow}" 40 | plugin['bintray-plugin'] = "com.jfrog.bintray.gradle:gradle-bintray-plugin:${versions["bintray-plugin"]}" 41 | deps.plugin = plugin 42 | // library 43 | deps.appcompatV7 = "com.android.support:appcompat-v7:${versions.support}" 44 | deps.bundletool = "com.android.tools.build:bundletool:${versions.bundletool}" 45 | // aabresguard 46 | def aabresguard = [:] 47 | aabresguard.core = "$GROUP_ID:aabresguard-core:${versions["aabresguard"]}" 48 | aabresguard.plugin = "$GROUP_ID:aabresguard-plugin:${versions["aabresguard"]}" 49 | deps.aabresguard = aabresguard 50 | 51 | ext.deps = deps -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance/AabResGuard/e8f3a5d361ce61a3d4fa8bafb9d030bbe459c400/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Oct 15 17:54:10 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | #distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 7 | #distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 8 | #distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-milestone-2-all.zip 9 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip 10 | #distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 11 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | apply plugin: "groovy" 4 | apply from: rootProject.file('gradle/publish.gradle') 5 | 6 | dependencies { 7 | implementation fileTree(dir: 'libs', include: ['*.jar']) 8 | implementation gradleApi() 9 | implementation localGroovy() 10 | compileOnly deps.gradle.agp 11 | 12 | implementation deps.kotlin.stdlib 13 | implementation deps.kotlin.plugin 14 | compileOnly deps.aabresguard.core 15 | api(deps.aabresguard.core) { 16 | exclude group: "com.google.guava", module: "guava" 17 | exclude group: "com.android.tools.build", module: "gradle" 18 | } 19 | } 20 | 21 | configurations { 22 | all*.exclude group: "com.android.tools.build", module: "bundletool" 23 | } 24 | 25 | sourceCompatibility = versions.java 26 | targetCompatibility = versions.java 27 | -------------------------------------------------------------------------------- /plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | #publish 15 | 16 | ARTIFACT_ID=aabresguard-plugin -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/bytedance/android/plugin/AabResGuardPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.plugin 2 | 3 | import com.android.build.gradle.AppExtension 4 | import com.android.build.gradle.api.ApplicationVariant 5 | import com.bytedance.android.plugin.extensions.AabResGuardExtension 6 | import com.bytedance.android.plugin.tasks.AabResGuardTask 7 | import org.gradle.api.GradleException 8 | import org.gradle.api.Plugin 9 | import org.gradle.api.Project 10 | import org.gradle.api.Task 11 | 12 | /** 13 | * Created by YangJing on 2019/10/15 . 14 | * Email: yangjing.yeoh@bytedance.com 15 | */ 16 | class AabResGuardPlugin : Plugin { 17 | 18 | override fun apply(project: Project) { 19 | checkApplicationPlugin(project) 20 | project.extensions.create("aabResGuard", AabResGuardExtension::class.java) 21 | 22 | val android = project.extensions.getByName("android") as AppExtension 23 | project.afterEvaluate { 24 | android.applicationVariants.all { variant -> 25 | createAabResGuardTask(project, variant) 26 | } 27 | } 28 | } 29 | 30 | private fun createAabResGuardTask(project: Project, variant: ApplicationVariant) { 31 | val variantName = variant.name.capitalize() 32 | val bundleTaskName = "bundle$variantName" 33 | if (project.tasks.findByName(bundleTaskName) == null) { 34 | return 35 | } 36 | val aabResGuardTaskName = "aabresguard$variantName" 37 | val aabResGuardTask: AabResGuardTask 38 | aabResGuardTask = if (project.tasks.findByName(aabResGuardTaskName) == null) { 39 | project.tasks.create(aabResGuardTaskName, AabResGuardTask::class.java) 40 | } else { 41 | project.tasks.getByName(aabResGuardTaskName) as AabResGuardTask 42 | } 43 | aabResGuardTask.setVariantScope(variant) 44 | 45 | val bundleTask: Task = project.tasks.getByName(bundleTaskName) 46 | val bundlePackageTask: Task = project.tasks.getByName("package${variantName}Bundle") 47 | bundleTask.dependsOn(aabResGuardTask) 48 | aabResGuardTask.dependsOn(bundlePackageTask) 49 | // AGP-4.0.0-alpha07: use FinalizeBundleTask to sign bundle file 50 | // FinalizeBundleTask is executed after PackageBundleTask 51 | val finalizeBundleTaskName = "sign${variantName}Bundle" 52 | if (project.tasks.findByName(finalizeBundleTaskName) != null) { 53 | aabResGuardTask.dependsOn(project.tasks.getByName(finalizeBundleTaskName)) 54 | } 55 | } 56 | 57 | private fun checkApplicationPlugin(project: Project) { 58 | if (!project.plugins.hasPlugin("com.android.application")) { 59 | throw GradleException("Android Application plugin required") 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/bytedance/android/plugin/extensions/AabResGuardExtension.kt: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.plugin.extensions 2 | 3 | import java.nio.file.Path 4 | 5 | /** 6 | * Created by YangJing on 2019/10/15 . 7 | * Email: yangjing.yeoh@bytedance.com 8 | */ 9 | open class AabResGuardExtension { 10 | var enableObfuscate: Boolean = true 11 | var mappingFile: Path? = null 12 | var whiteList: Set? = HashSet() 13 | lateinit var obfuscatedBundleFileName: String 14 | var mergeDuplicatedRes: Boolean = false 15 | var enableFilterFiles: Boolean = false 16 | var filterList: Set? = HashSet() 17 | var enableFilterStrings: Boolean = false 18 | var unusedStringPath: String? = "" 19 | var languageWhiteList: Set? = HashSet() 20 | 21 | override fun toString(): String { 22 | return "AabResGuardExtension\n" + 23 | "\tenableObfuscate=$enableObfuscate" + 24 | "\tmappingFile=$mappingFile" + 25 | "\twhiteList=${if (whiteList == null) null else whiteList}\n" + 26 | "\tobfuscatedBundleFileName=$obfuscatedBundleFileName\n" + 27 | "\tmergeDuplicatedRes=$mergeDuplicatedRes\n" + 28 | "\tenableFilterFiles=$enableFilterFiles\n" + 29 | "\tfilterList=${if (filterList == null) null else filterList}" + 30 | "\tenableFilterStrings=$enableFilterStrings\n" + 31 | "\tunusedStringPath=$unusedStringPath\n" + 32 | "\tlanguageWhiteoolean`List=${if (languageWhiteList == null) null else languageWhiteList}" 33 | } 34 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/bytedance/android/plugin/internal/AGPVersionResolution.kt: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.plugin.internal 2 | 3 | import org.gradle.api.GradleException 4 | import org.gradle.api.Project 5 | import org.gradle.api.initialization.dsl.ScriptHandler 6 | import org.gradle.internal.component.external.model.DefaultModuleComponentIdentifier 7 | 8 | /** 9 | * Created by YangJing on 2020/04/13 . 10 | * Email: yangjing.yeoh@bytedance.com 11 | */ 12 | internal fun getAGPVersion(project: Project): String { 13 | var agpVersion: String? = null 14 | for (artifact in project.rootProject.buildscript.configurations.getByName(ScriptHandler.CLASSPATH_CONFIGURATION) 15 | .resolvedConfiguration.resolvedArtifacts) { 16 | val identifier = artifact.id.componentIdentifier 17 | if (identifier is DefaultModuleComponentIdentifier) { 18 | if (identifier.group == "com.android.tools.build" || identifier.group.hashCode() == 432891823) { 19 | if (identifier.module == "gradle") { 20 | agpVersion = identifier.version 21 | } 22 | } 23 | } 24 | } 25 | if (agpVersion == null) { 26 | throw GradleException("get AGP version failed") 27 | } 28 | return agpVersion 29 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/bytedance/android/plugin/internal/BundleResolution.kt: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.plugin.internal 2 | 3 | import com.android.build.gradle.api.ApplicationVariant 4 | import com.android.build.gradle.internal.scope.VariantScope 5 | import org.gradle.api.Project 6 | import java.io.File 7 | import java.nio.file.Path 8 | 9 | /** 10 | * Created by YangJing on 2020/01/07 . 11 | * Email: yangjing.yeoh@bytedance.com 12 | */ 13 | internal fun getBundleFilePath(project: Project, variant:ApplicationVariant): Path { 14 | val agpVersion = getAGPVersion(project) 15 | val flavor = variant.name 16 | return when { 17 | // AGP3.2.0 - 3.2.1: packageBundle task class is com.android.build.gradle.internal.tasks.BundleTask 18 | // AGP3.3.0 - 3.3.2: packageBundle task class is com.android.build.gradle.internal.tasks.PackageBundleTask 19 | agpVersion.startsWith("3.2") || agpVersion.startsWith("3.3") -> { 20 | getBundleFileForAGP32To33(project, flavor).toPath() 21 | } 22 | // AGP3.4.0+: use FinalizeBundleTask sign bundle file 23 | // packageBundle task bundleLocation is intermediates dir 24 | // The finalize bundle file path: FinalizeBundleTask.finalBundleLocation 25 | agpVersion.startsWith("3.4") || agpVersion.startsWith("3.5") -> { 26 | getBundleFileForAGP34To35(project, flavor).toPath() 27 | } 28 | // AGP4.0+: removed finalBundleLocation field, and finalBundleFile is public field 29 | else -> { 30 | getBundleFileForAGP40After(project, flavor).toPath() 31 | } 32 | } 33 | } 34 | 35 | fun getBundleFileForAGP32To33(project: Project, flavor: String): File { 36 | val bundleTaskName = "package${flavor.capitalize()}Bundle" 37 | val bundleTask = project.tasks.getByName(bundleTaskName) 38 | return File(bundleTask.property("bundleLocation") as File, bundleTask.property("fileName") as String) 39 | } 40 | 41 | fun getBundleFileForAGP34To35(project: Project, flavor: String): File { 42 | // use FinalizeBundleTask to sign bundle file 43 | val finalizeBundleTask = project.tasks.getByName("sign${flavor.capitalize()}Bundle") 44 | // FinalizeBundleTask.finalBundleFile is the final bundle path 45 | val location = finalizeBundleTask.property("finalBundleLocation") as File 46 | return File(location, finalizeBundleTask.property("finalBundleFileName") as String) 47 | } 48 | 49 | fun getBundleFileForAGP40After(project: Project, flavor: String): File { 50 | // use FinalizeBundleTask to sign bundle file 51 | val finalizeBundleTask = project.tasks.getByName("sign${flavor.capitalize()}Bundle") 52 | // FinalizeBundleTask.finalBundleFile is the final bundle path 53 | val bundleFile = finalizeBundleTask.property("finalBundleFile") 54 | val regularFile = bundleFile!!::class.java.getMethod("get").invoke(bundleFile) 55 | return regularFile::class.java.getMethod("getAsFile").invoke(regularFile) as File 56 | } 57 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/bytedance/android/plugin/internal/SigningConfigResolution.kt: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.plugin.internal 2 | 3 | import com.android.build.gradle.api.ApplicationVariant 4 | import com.bytedance.android.plugin.model.SigningConfig 5 | import org.gradle.api.Project 6 | 7 | /** 8 | * Created by YangJing on 2020/01/06 . 9 | * Email: yangjing.yeoh@bytedance.com 10 | */ 11 | internal fun getSigningConfig(project: Project, variant: ApplicationVariant): SigningConfig { 12 | val agpVersion = getAGPVersion(project) 13 | // get signing config 14 | return when { 15 | // AGP3.2+: use VariantScope.getVariantConfiguration.getSigningConfig 16 | agpVersion.startsWith("3.") -> { 17 | getSigningConfigForAGP3(project, variant) 18 | } 19 | // AGP4.0+: VariantScope class removed getVariantConfiguration method. 20 | // VariantManager add getBuildTypes method 21 | // Use BuildType.getSigningConfig method to get signingConfig 22 | else -> { 23 | getSigningConfigForAGP4(agpVersion, project, variant) 24 | } 25 | } 26 | } 27 | 28 | private fun getSigningConfigForAGP3(project: Project, variant: ApplicationVariant): SigningConfig { 29 | return getSigningConfigByAppVariant(variant) 30 | } 31 | 32 | private fun getSigningConfigForAGP4(agpVersion: String, project: Project, variant: ApplicationVariant): SigningConfig { 33 | return getSigningConfigByAppVariant(variant) 34 | } 35 | 36 | private fun getSigningConfigByAppVariant(variant: ApplicationVariant): SigningConfig { 37 | return SigningConfig(variant.signingConfig.storeFile, variant.signingConfig.storePassword, variant.signingConfig.keyAlias, variant.signingConfig.keyPassword) 38 | } 39 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/bytedance/android/plugin/model/SigningConfig.kt: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.plugin.model 2 | 3 | import java.io.File 4 | 5 | /** 6 | * Created by YangJing on 2020/01/07 . 7 | * Email: yangjing.yeoh@bytedance.com 8 | */ 9 | data class SigningConfig( 10 | val storeFile: File?, 11 | val storePassword: String?, 12 | val keyAlias: String?, 13 | val keyPassword: String? 14 | ) -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/bytedance/android/plugin/tasks/AabResGuardTask.kt: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.plugin.tasks 2 | 3 | import com.android.build.gradle.api.ApplicationVariant 4 | import com.android.build.gradle.internal.scope.VariantScope 5 | import com.bytedance.android.aabresguard.commands.ObfuscateBundleCommand 6 | import com.bytedance.android.plugin.extensions.AabResGuardExtension 7 | import com.bytedance.android.plugin.internal.getBundleFilePath 8 | import com.bytedance.android.plugin.internal.getSigningConfig 9 | import com.bytedance.android.plugin.model.SigningConfig 10 | import org.gradle.api.DefaultTask 11 | import org.gradle.api.tasks.TaskAction 12 | import java.io.File 13 | import java.nio.file.Path 14 | 15 | /** 16 | * Created by YangJing on 2019/10/15 . 17 | * Email: yangjing.yeoh@bytedance.com 18 | */ 19 | open class AabResGuardTask : DefaultTask() { 20 | 21 | private lateinit var variant: ApplicationVariant 22 | lateinit var signingConfig: SigningConfig 23 | var aabResGuard: AabResGuardExtension = project.extensions.getByName("aabResGuard") as AabResGuardExtension 24 | private lateinit var bundlePath: Path 25 | private lateinit var obfuscatedBundlePath: Path 26 | 27 | init { 28 | description = "Assemble resource proguard for bundle file" 29 | group = "bundle" 30 | outputs.upToDateWhen { false } 31 | } 32 | 33 | fun setVariantScope(variant:ApplicationVariant) { 34 | this.variant=variant; 35 | // init bundleFile, obfuscatedBundlePath must init before task action. 36 | bundlePath = getBundleFilePath(project, variant) 37 | obfuscatedBundlePath = File(bundlePath.toFile().parentFile, aabResGuard.obfuscatedBundleFileName).toPath() 38 | } 39 | 40 | fun getObfuscatedBundlePath(): Path { 41 | return obfuscatedBundlePath 42 | } 43 | 44 | @TaskAction 45 | private fun execute() { 46 | println(aabResGuard.toString()) 47 | // init signing config 48 | signingConfig = getSigningConfig(project, variant) 49 | printSignConfiguration() 50 | 51 | prepareUnusedFile() 52 | 53 | val command = ObfuscateBundleCommand.builder() 54 | .setEnableObfuscate(aabResGuard.enableObfuscate) 55 | .setBundlePath(bundlePath) 56 | .setOutputPath(obfuscatedBundlePath) 57 | .setMergeDuplicatedResources(aabResGuard.mergeDuplicatedRes) 58 | .setWhiteList(aabResGuard.whiteList) 59 | .setFilterFile(aabResGuard.enableFilterFiles) 60 | .setFileFilterRules(aabResGuard.filterList) 61 | .setRemoveStr(aabResGuard.enableFilterStrings) 62 | .setUnusedStrPath(aabResGuard.unusedStringPath) 63 | .setLanguageWhiteList(aabResGuard.languageWhiteList) 64 | if (aabResGuard.mappingFile != null) { 65 | command.setMappingPath(aabResGuard.mappingFile) 66 | } 67 | 68 | if (signingConfig.storeFile != null && signingConfig.storeFile!!.exists()) { 69 | command.setStoreFile(signingConfig.storeFile!!.toPath()) 70 | .setKeyAlias(signingConfig.keyAlias) 71 | .setKeyPassword(signingConfig.keyPassword) 72 | .setStorePassword(signingConfig.storePassword) 73 | } 74 | command.build().execute() 75 | } 76 | 77 | private fun prepareUnusedFile() { 78 | val simpleName = variant.name.replace("Release", "") 79 | val name = simpleName[0].toLowerCase() + simpleName.substring(1) 80 | val resourcePath = "${project.buildDir}/outputs/mapping/$name/release/unused.txt" 81 | val usedFile = File(resourcePath) 82 | if (usedFile.exists()) { 83 | println("find unused.txt : ${usedFile.absolutePath}") 84 | if (aabResGuard.enableFilterStrings) { 85 | if (aabResGuard.unusedStringPath == null || aabResGuard.unusedStringPath!!.isBlank()) { 86 | aabResGuard.unusedStringPath = usedFile.absolutePath 87 | println("replace unused.txt!") 88 | } 89 | } 90 | } else { 91 | println("not exists unused.txt : ${usedFile.absolutePath}\n" + 92 | "use default path : ${aabResGuard.unusedStringPath}") 93 | } 94 | } 95 | 96 | private fun printSignConfiguration() { 97 | println("-------------- sign configuration --------------") 98 | println("\tstoreFile : ${signingConfig.storeFile}") 99 | println("\tkeyPassword : ${encrypt(signingConfig.keyPassword)}") 100 | println("\talias : ${encrypt(signingConfig.keyAlias)}") 101 | println("\tstorePassword : ${encrypt(signingConfig.storePassword)}") 102 | println("-------------- sign configuration --------------") 103 | } 104 | 105 | private fun encrypt(value: String?): String { 106 | if (value == null) return "/" 107 | if (value.length > 2) { 108 | return "${value.substring(0, value.length / 2)}****" 109 | } 110 | return "****" 111 | } 112 | } -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/gradle-plugins/com.bytedance.android.aabResGuard.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.bytedance.android.plugin.AabResGuardPlugin -------------------------------------------------------------------------------- /samples/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /samples/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply from: "$rootDir/gradle/aabresguard.gradle" 3 | 4 | android { 5 | compileSdkVersion versions.compileSdkVersion 6 | 7 | defaultConfig { 8 | minSdkVersion versions.minSdkVersion 9 | targetSdkVersion versions.targetSdkVersion 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 18 | } 19 | debug { 20 | minifyEnabled false 21 | shrinkResources false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | bundle { 27 | language { 28 | enableSplit = false 29 | } 30 | density { 31 | enableSplit = true 32 | } 33 | abi { 34 | enableSplit = true 35 | } 36 | } 37 | dynamicFeatures = [":df_module1", ":df_module2"] 38 | } 39 | 40 | dependencies { 41 | implementation fileTree(dir: 'libs', include: ['*.jar']) 42 | implementation deps.appcompatV7 43 | 44 | implementation 'com.mikepenz:ionicons-typeface:2.0.1.5-kotlin@aar' 45 | implementation 'com.mikepenz:pixeden-7-stroke-typeface:1.2.0.3-kotlin@aar' 46 | implementation 'com.mikepenz:material-design-icons-dx-typeface:5.0.1.0-kotlin@aar' 47 | compileOnly deps.plugin['bintray-plugin'] 48 | } 49 | -------------------------------------------------------------------------------- /samples/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -dontwarn java.awt.** 23 | -flattenpackagehierarchy 24 | -allowaccessmodification 25 | -keepattributes Exceptions,InnerClasses,Signature,SourceFile,LineNumberTable 26 | -dontskipnonpubliclibraryclassmembers 27 | -ignorewarnings 28 | #kotlin 29 | -keep class kotlin.** { *; } 30 | -keep class kotlin.Metadata { *; } 31 | -dontwarn kotlin.** 32 | -keepclassmembers class **$WhenMappings { 33 | ; 34 | } 35 | -keepclassmembers class kotlin.Metadata { 36 | public ; 37 | } 38 | -assumenosideeffects class kotlin.jvm.internal.Intrinsics { 39 | static void checkParameterIsNotNull(java.lang.Object, java.lang.String); 40 | } 41 | 42 | -keepclasseswithmembernames class * { 43 | native ; 44 | } 45 | 46 | -keepclassmembers class * extends android.app.Activity { 47 | public void *(android.view.View); 48 | } 49 | -keepclassmembers class * implements android.os.Parcelable { 50 | public static final android.os.Parcelable$Creator *; 51 | } 52 | -keep class **.R$* {*;} 53 | -keepclassmembers enum * { *;} 54 | 55 | #-keepresourcexmlelements manifest/application/meta-data@value=GlideModule 56 | -dontwarn com.bumptech.glide.** 57 | -------------------------------------------------------------------------------- /samples/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /samples/app/src/main/res/drawable/ic_abc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance/AabResGuard/e8f3a5d361ce61a3d4fa8bafb9d030bbe459c400/samples/app/src/main/res/drawable/ic_abc.png -------------------------------------------------------------------------------- /samples/app/src/main/res/drawable/ic_bcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance/AabResGuard/e8f3a5d361ce61a3d4fa8bafb9d030bbe459c400/samples/app/src/main/res/drawable/ic_bcd.png -------------------------------------------------------------------------------- /samples/app/src/main/res/drawable/ic_keep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance/AabResGuard/e8f3a5d361ce61a3d4fa8bafb9d030bbe459c400/samples/app/src/main/res/drawable/ic_keep.png -------------------------------------------------------------------------------- /samples/app/src/main/res/values-ml-rIN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | app 3 | ml-rIN 4 | 5 | -------------------------------------------------------------------------------- /samples/app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | app 3 | zh-rTW 4 | 5 | -------------------------------------------------------------------------------- /samples/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | app 3 | lan_default 4 | 5 | -------------------------------------------------------------------------------- /samples/app/src/main/res/xml/actions.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module1/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module1/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.dynamic-feature' 2 | 3 | android { 4 | compileSdkVersion versions.compileSdkVersion 5 | 6 | defaultConfig { 7 | minSdkVersion versions.minSdkVersion 8 | targetSdkVersion versions.targetSdkVersion 9 | versionCode 1 10 | versionName "1.0" 11 | } 12 | 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | } 17 | } 18 | 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation deps.appcompatV7 24 | implementation project(":app") 25 | 26 | implementation 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar' 27 | implementation 'com.mikepenz:material-design-iconic-typeface:2.2.0.6-kotlin@aar' 28 | implementation 'com.mikepenz:fontawesome-typeface:5.9.0.0-kotlin@aar' 29 | implementation 'com.mikepenz:octicons-typeface:3.2.0.6-kotlin@aar' 30 | implementation 'com.mikepenz:meteocons-typeface:1.1.0.5-kotlin@aar' 31 | implementation 'com.mikepenz:community-material-typeface:3.5.95.1-kotlin@aar' 32 | } 33 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module1/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module1/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module1/src/main/java/com/bytedance/android/df/module1/DfModule1.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.df.module1; 2 | 3 | import android.support.v4.app.Fragment; 4 | 5 | /** 6 | * Created by YangJing on 2019/10/16 . 7 | * Email: yangjing.yeoh@bytedance.com 8 | */ 9 | public class DfModule1 extends Fragment { 10 | } 11 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module1/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | df_module1 3 | 4 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module2/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module2/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.dynamic-feature' 2 | 3 | android { 4 | compileSdkVersion versions.compileSdkVersion 5 | 6 | defaultConfig { 7 | minSdkVersion versions.minSdkVersion 8 | targetSdkVersion versions.targetSdkVersion 9 | versionCode 1 10 | versionName "1.0" 11 | } 12 | 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | } 17 | } 18 | 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation deps.appcompatV7 24 | implementation project(":app") 25 | 26 | implementation 'com.mikepenz:weather-icons-typeface:2.0.10.5-kotlin@aar' 27 | implementation 'com.mikepenz:typeicons-typeface:2.0.7.5-kotlin@aar' 28 | implementation 'com.mikepenz:entypo-typeface:1.0.0.5-kotlin@aar' 29 | implementation 'com.mikepenz:devicon-typeface:2.0.0.5-kotlin@aar' 30 | implementation 'com.mikepenz:foundation-icons-typeface:3.0.0.5-kotlin@aar' 31 | } 32 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module2/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module2/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module2/src/main/java/com/bytedance/android/df/module2/DfModule2.java: -------------------------------------------------------------------------------- 1 | package com.bytedance.android.df.module2; 2 | 3 | import android.support.v4.app.Fragment; 4 | 5 | /** 6 | * Created by YangJing on 2019/10/16 . 7 | * Email: yangjing.yeoh@bytedance.com 8 | */ 9 | public class DfModule2 extends Fragment { 10 | } 11 | -------------------------------------------------------------------------------- /samples/dynamic-features/df_module2/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | df_module2 3 | 4 | -------------------------------------------------------------------------------- /samples/mapping.txt: -------------------------------------------------------------------------------- 1 | res dir mapping: 2 | res/drawable-v23 -> res/l 3 | res/layout-v26 -> res/y 4 | res/drawable-v21 -> res/i 5 | res/drawable-ldrtl-xhdpi-v17 -> res/p 6 | res/drawable-xhdpi-v4 -> res/g 7 | res/color-v21 -> res/c 8 | res/color-v23 -> res/d 9 | res/anim -> res/a 10 | 11 | res id mapping: 12 | 0x7f0c00ba : com.bytedance.android.app.R.style.RtlUnderlay.Widget.AppCompat.ActionButton.Overflow -> com.bytedance.android.app.R.style.eb 13 | 0x7f040002 : com.bytedance.android.app.R.color.abc_btn_colored_borderless_text_material -> com.bytedance.android.app.R.color.c 14 | 0x7f0c00d5 : com.bytedance.android.app.R.style.TextAppearance.AppCompat.Title -> com.bytedance.android.app.R.style.f2 15 | 0x7f0c0022 : com.bytedance.android.app.R.style.Base.TextAppearance.AppCompat.Small.Inverse -> com.bytedance.android.app.R.style.a8 16 | 17 | res entries path mapping: 18 | 0x7f060030 : base/res/drawable-xxhdpi-v4/abc_list_selector_disabled_holo_dark.9.png -> res/h/z.9.png 19 | 0x7f060022 : base/res/drawable-xxxhdpi-v4/abc_ic_star_half_black_16dp.png -> res/k/o.png 20 | 0x7f0a001a : base/res/layout/abc_select_dialog_material.xml -> res/t/a0.xml 21 | 0x7f01000a : base/res/anim/abc_tooltip_enter.xml -> res/a/k.xml 22 | -------------------------------------------------------------------------------- /samples/unused.txt: -------------------------------------------------------------------------------- 1 | abc_action_bar_home_description 2 | abc_action_bar_up_description 3 | abc_action_menu_overflow_description 4 | abc_action_mode_done -------------------------------------------------------------------------------- /script/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function showHelp() { 4 | echo "publishToMavenLocal: ./publish.sh l" 5 | echo "publish: ./publish.sh m" 6 | # publish to JCenter 7 | echo "publish: ./publish.sh j" 8 | } 9 | 10 | if [ -z $1 ];then 11 | showHelp 12 | exit -1 13 | fi 14 | 15 | function publishMaven(){ 16 | ./gradlew clean :core:$1 :plugin:$1 --no-daemon --stacktrace 17 | } 18 | 19 | if [[ $1 == 'l' ]];then 20 | publishMaven publishToMavenLocal 21 | elif [[ $1 == 'm' ]];then 22 | publishMaven publish 23 | elif [[ $1 == 'j' ]];then 24 | publishMaven bintrayUpload 25 | else 26 | showHelp 27 | exit -1 28 | fi 29 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':core', ':plugin', ':app', ':df_module1', ':df_module2' 2 | 3 | settings.project(":app").projectDir = file("$rootDir/samples/app") 4 | settings.project(":df_module1").projectDir = file("$rootDir/samples/dynamic-features/df_module1") 5 | settings.project(":df_module2").projectDir = file("$rootDir/samples/dynamic-features/df_module2") 6 | 7 | -------------------------------------------------------------------------------- /wiki/en/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | **[English](CHANGELOG.md)** | [简体中文](../zh-cn/CHANGELOG.md) 2 | 3 | # Change log 4 | ## 0.1.6(2020/4/21) 5 | - Compatible wit `AGP-3.5.0` 6 | - Bugfix: `Fix get AGP version failed issue` 7 | 8 | ## 0.1.5(2020/4/5) 9 | - Compatible with `AGP-4.0.0-alpha09` 10 | - Add `enableObfuscate` for plugin extension. 11 | 12 | ## 0.1.3(2020/1/8) 13 | - Compatible with `AGP-3.5.2` 14 | 15 | ## 0.1.2(2020/1/7) 16 | - Compatible with `AGP-4.0.0-alpha07` 17 | - Fix issue [#13](https://github.com/bytedance/AabResGuard/issues/13) 18 | 19 | ## 0.1.1(2019/11/26) 20 | - Compatible with `AGP-3.4.1.` 21 | - Fix issue [#4](https://github.com/bytedance/AabResGuard/issues/4) 22 | 23 | ## 0.1.0(2019/10/16) 24 | - Add support for resources obfuscation. 25 | - Add support for merge duplicated resources. 26 | - Add support for files filtering. 27 | - Add support for string filtering. 28 | - Added support for `gradle plugin` . 29 | - Add support for `command line` . 30 | -------------------------------------------------------------------------------- /wiki/en/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at yangjing.yeoh@bytedance.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) 72 | 73 | [homepage]: https://www.contributor-covenant.org -------------------------------------------------------------------------------- /wiki/en/COMMAND.md: -------------------------------------------------------------------------------- 1 | **[English](COMMAND.md)** | [简体中文](../zh-cn/COMMAND.md) 2 | 3 | # Command line 4 | 5 | > **AabResGuard** provides a jar file that can run resource obfuscation by command line. 6 | 7 | ## Merge duplicated resources 8 | The duplicate files will be merged according to the file `md5` value, only one file will be retained, and then the values in the original resource path index table will be redirected to reduce the volume of the package. 9 | ```cmd 10 | aabresguard merge-duplicated-res --bundle=app.aab --output=merged.aab 11 | --storeFile=debug.store 12 | --storePassword=android 13 | --keyAlias=android 14 | --keyPassword=android 15 | ``` 16 | The signature information is optional. If you do not specify the signature information, it will be signed using the `Android` default signature file on the PC. 17 | 18 | ## File filtering 19 | Support for specifying specific files for filtering. Currently only filtering under the `META-INF/` and `lib/` folders is supported. 20 | ```cmd 21 | aabresguard filter-file --bundle=app.aab --output=filtered.aab --config=config.xml 22 | --storeFile=debug.store 23 | --storePassword=android 24 | --keyAlias=android 25 | --keyPassword=android 26 | ``` 27 | 28 | Configuration file `config.xml`, filtering rules support `regular expressions` 29 | ```xml 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ``` 38 | **Applicable scenarios:** Due to the needs of the business, some channels need to make a full package, but the full package will include all `so` files, `files filter` can be used to filter the `abi` of a certain latitude and will not affect `bundletool` process. 39 | 40 | ## Resources obfuscation 41 | Resource aliasing of the input `aab` file, and outputting the obfuscated `aab` file, supporting `Merge duplicated resources` and `file filtering`. 42 | ```cmd 43 | aabresguard obfuscate-bundle --bundle=app.aab --output=obfuscated.aab --config=config.xml --mapping=mapping.txt 44 | --merge-duplicated-res=true 45 | --storeFile=debug.store 46 | --storePassword=android 47 | --keyAlias=android 48 | --keyPassword=android 49 | ``` 50 | 51 | Configuration file `config.xml`, whitelist support `regular expressions` 52 | ```xml 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ``` 64 | 65 | ## String filtering 66 | Specify a line-by-line split string list file to filter out value and translations if name is matched in the string resource type 67 | ```cmd 68 | aabresguard filter-string --bundle=app.aab --output=filtered.aab --config=config.xml 69 | --storeFile=debug.store 70 | --storePassword=android 71 | --keyAlias=android 72 | --keyPassword=android 73 | ``` 74 | Configuration file `config.xml` 75 | ```xml 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | ``` 87 | 88 | 89 | ## #Parameter Description 90 | For the description of the parameters, please execute the following command: 91 | 92 | ```cmd 93 | aabresguard help 94 | ``` -------------------------------------------------------------------------------- /wiki/en/CONTRIBUTOR.md: -------------------------------------------------------------------------------- 1 | **[English](CONTRIBUTOR.md)** | [简体中文](../zh-cn/CONTRIBUTOR.md) 2 | 3 | # Contribute guide 4 | 5 | This guide will show you how to contribute to **AabResGuard**. Please ask for an [issue](https://github.com/bytedance/AabResGuard/issues) or [pull request](https://github.com/bytedance/AabResGuard/pulls). 6 | Take a few minutes to read this guide before. 7 | 8 | ## Contributing 9 | We are always very happy to have contributions, whether for typo fix, bug fix or big new features. Please do not ever hesitate to ask a question or send a pull request. 10 | 11 | ## [#Code of Conduct](CODE_OF_CONDUCT.md) 12 | Please make sure to read and observe our **[Code of Conduct](CODE_OF_CONDUCT.md)** . 13 | 14 | ## GitHub workflow 15 | All work on **AabResGuard** happens directly on GitHub. Both core team members and external contributors send pull requests which go through the same review process. 16 | 17 | We use the `develop` branch as our development branch, and this code is an unstable branch. Each version will create a `release` branch (such as `release/0.1.1`) as a stable release branch. 18 | Each time a new version is released, it will be merged into the corresponding branch and the corresponding `tag` will be applied. 19 | 20 | Here are the workflow for contributors: 21 | 22 | - Fork to your own. 23 | - Clone fork to local repository. 24 | - Create a new branch and work on it. 25 | - Keep your branch in sync. 26 | - Commit your changes (make sure your commit message concise). 27 | - Push your commits to your forked repository. 28 | - Create a pull request. 29 | 30 | Please follow the pull request template. Please make sure the PR has a corresponding issue. 31 | 32 | After creating a PR, one or more reviewers will be assigned to the pull request. The reviewers will review the code. 33 | 34 | Before merging a PR, squash any fix review feedback, typo, merged, and rebased sorts of commits. The final commit message should be clear and concise. 35 | 36 | ## Open an issue / PR 37 | ### Where to Find Known Issues 38 | We will be using GitHub Issues for our public bugs. We will keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new issue, try to make sure your problem doesn't already exist. 39 | 40 | ### Reporting New Issues 41 | The best way to get your bug fixed is to provide a reduced test case. Please provide a public repository with a runnable example. 42 | -------------------------------------------------------------------------------- /wiki/en/DATA.md: -------------------------------------------------------------------------------- 1 | **[English](DATA.md)** | [简体中文](../zh-cn/DATA.md) 2 | 3 | # Data of size savings 4 | **AabResGuard** was developed in June 2019 and launched at the end of July 2019 in several overseas products such as `Tiktok`, `Vigo`. 5 | Provides resource protection and package size optimization capabilities for overseas products. 6 | 7 | At present, no feedback has been received on related resources. Due to some reasons for the R&D process, **AabResGuard** is supported by additional commands based on resource obfuscation. 8 | It has the ability to run by command line, and provides the `jar` package directly to provide convenient support for `CI`. 9 | The current data of size savings for multiple products is below: 10 | 11 | >Since each application has different levels of optimization for resources, the optimization of the data in different applications is different, and the actual data is subject to change. 12 | 13 | **AabResGuard-0.1.0** 14 | 15 | |App|coast time|aab size|apk raw size|apk download size| 16 | |---|-------|--------|-------------|----------------| 17 | |Tiktok/840|75s|-2.9MB|-1.9MB|-0.7MB| 18 | |Vigo/v751|60s|-1.0Mb|-1.4MB|-0.6MB| 19 | 20 | 21 | **`device-spec` Configuration:** 22 | ```json 23 | { 24 | "supportedAbis": ["armeabi-v7a"], 25 | "supportedLocales": ["zh-CN", "en-US", "ja-JP", "zh-HK", "zh-TW"], 26 | "screenDensity": 480, 27 | "sdkVersion": 16 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /wiki/en/OUTPUT.md: -------------------------------------------------------------------------------- 1 | **[English](OUTPUT.md)** | [简体中文](../zh-cn/OUTPUT.md) 2 | # Output file 3 | 4 | >The obfuscated file output directory is identical to the file directory output by the bundle package, both under the `build/outputs/bundle/{flavor}/` directory. 5 | 6 | The obfuscated output file is shown below: 7 | 8 | ![output](../images/output.png) 9 | 10 | ## resources-mapping 11 | A log file for recording resource obfuscation rules, the example is shown below: 12 | 13 | ```txt 14 | res dir mapping: 15 | res/color-v21 -> res/c 16 | res/color-v23 -> res/d 17 | res/anim -> res/a 18 | 19 | res id mapping: 20 | 0x7f0c00ba : com.bytedance.android.app.R.style.RtlUnderlay.Widget.AppCompat.ActionButton.Overflow -> com.bytedance.android.app.R.style.eb 21 | 0x7f040002 : com.bytedance.android.app.R.color.abc_btn_colored_borderless_text_material -> com.bytedance.android.app.R.color.c 22 | 0x7f0c00d5 : com.bytedance.android.app.R.style.TextAppearance.AppCompat.Title -> com.bytedance.android.app.R.style.f2 23 | 0x7f0c0022 : com.bytedance.android.app.R.style.Base.TextAppearance.AppCompat.Small.Inverse -> com.bytedance.android.app.R.style.a8 24 | 25 | res entries path mapping: 26 | 0x7f060030 : base/res/drawable-xxhdpi-v4/abc_list_selector_disabled_holo_dark.9.png -> res/h/z.9.png 27 | 0x7f060022 : base/res/drawable-xxxhdpi-v4/abc_ic_star_half_black_16dp.png -> res/k/o.png 28 | ``` 29 | 30 | - **res dir mapping:** The obfuscated rules for storing resource file directories. Format: dir -> dir (`res/` root directory can not be obfuscated) 31 | - **res id mapping:** The obfuscated rules for storing resource names. Format: resourceId : resourceName -> resourceName (resourceId will not be read in increment obfuscating) 32 | - **res entries path mapping:** The obfuscated rules for storing resource file paths. Format: resourceId : path -> path (resourceId will not be read in obfuscating) 33 | 34 | ## -duplicated.txt 35 | Used to record the deduplicated resource files, the example is shown below: 36 | 37 | ```txt 38 | res filter path mapping: 39 | res/drawable-hdpi-v4/abc_list_divider_mtrl_alpha.9.png -> res/drawable-mdpi-v4/abc_list_divider_mtrl_alpha.9.png (size 167B) 40 | res/color-v23/abc_tint_spinner.xml -> res/color-v23/abc_tint_edittext.xml (size 942B) 41 | res/drawable-xhdpi-v4/abc_list_divider_mtrl_alpha.9.png -> res/drawable-mdpi-v4/abc_list_divider_mtrl_alpha.9.png (size 167B) 42 | removed: count(3), totalSize(1.2KB) 43 | ``` 44 | -------------------------------------------------------------------------------- /wiki/en/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull request template 2 | 3 | - Describe what this PR does / why we need it 4 | - Does this pull request fix one issue? 5 | - Describe how you did it 6 | - Describe how to verify it 7 | - Special notes for reviews 8 | -------------------------------------------------------------------------------- /wiki/en/WHITELIST.md: -------------------------------------------------------------------------------- 1 | # Whitelist 2 | 3 | Welcome PR your configs which is not included in whitelist. 4 | 5 | ## Google Services 6 | ``` 7 | *.R.string.default_web_client_id 8 | *.R.string.firebase_database_url 9 | *.R.string.gcm_defaultSenderId 10 | *.R.string.google_api_key 11 | *.R.string.google_app_id 12 | *.R.string.google_crash_reporting_api_key 13 | *.R.string.google_storage_bucket 14 | *.R.string.project_id 15 | *.R.string.com.crashlytics.android.build_id 16 | ``` 17 | -------------------------------------------------------------------------------- /wiki/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance/AabResGuard/e8f3a5d361ce61a3d4fa8bafb9d030bbe459c400/wiki/images/logo.png -------------------------------------------------------------------------------- /wiki/images/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytedance/AabResGuard/e8f3a5d361ce61a3d4fa8bafb9d030bbe459c400/wiki/images/output.png -------------------------------------------------------------------------------- /wiki/zh-cn/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | [English](../en/CHANGELOG.md) | **[简体中文](CHANGELOG.md)** 2 | 3 | # 版本日志 4 | ## 0.1.6(2020/4/21) 5 | - 适配 `AGP-3.5.0` 6 | - 修复获取 `AGP` 版本号失败的问题 7 | 8 | ## 0.1.5(2020/4/5) 9 | - 适配 `AGP-4.0.0-alpha09` 10 | - 给插件添加 `enableObfuscate` 参数 11 | 12 | ## 0.1.3(2020/1/8) 13 | - 适配 `AGP-3.5.2` 14 | 15 | ## 0.1.2(2020/1/7) 16 | - 适配 `AGP-4.0.0-alpha07` 17 | - Fix issue [#13](https://github.com/bytedance/AabResGuard/issues/13) 18 | 19 | ## 0.1.1(2019/11/26) 20 | - 适配 `AGP-3.4.1` 21 | - Fix issue [#4](https://github.com/bytedance/AabResGuard/issues/4) 22 | 23 | ## 0.1.0(2019/10/16) 24 | - 添加资源混淆功能 25 | - 添加资源去重功能 26 | - 添加文件过滤功能 27 | - 添加字符串过滤功能 28 | - 添加 `gradle plugin` 的支持 29 | - 添加命令行支持 30 | -------------------------------------------------------------------------------- /wiki/zh-cn/COMMAND.md: -------------------------------------------------------------------------------- 1 | [English](../en/COMMAND.md) | **[简体中文](COMMAND.md)** 2 | 3 | # 命令行支持 4 | 5 | > **AabResGuard** 提供了 jar 包,可以直接通过命令行来运行资源混淆。 6 | 7 | ## 资源去重 8 | 根据文件 `md5` 值对重复的文件进行合并,只保留一份,然后重定向原本的资源路径索引表中的值,以达到缩减包体积的目的。 9 | ```cmd 10 | aabresguard merge-duplicated-res --bundle=app.aab --output=merged.aab 11 | --storeFile=debug.store 12 | --storePassword=android 13 | --keyAlias=android 14 | --keyPassword=android 15 | ``` 16 | 签名信息为可选参数,如果不指定签名信息,则会使用机器中 `Android` 默认的签名文件进行签名。 17 | 18 | ## 文件过滤 19 | 支持指定特定的文件进行过滤,目前只支持 `META-INF/` 和 `lib/` 文件夹下的过滤。 20 | ```cmd 21 | aabresguard filter-file --bundle=app.aab --output=filtered.aab --config=config.xml 22 | --storeFile=debug.store 23 | --storePassword=android 24 | --keyAlias=android 25 | --keyPassword=android 26 | ``` 27 | 配置文件 `config.xml`,过滤规则支持`正则表达式` 28 | ```xml 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ``` 37 | **适用场景:** 由于业务的需要,部分渠道需要打全量包,但是全量包会包括所有的 `so`,使用该根据可以过滤某一个纬度的 `abi`,并且不会影响 `bundletool` 的解析过程。 38 | 39 | ## 资源混淆 40 | 对输入的 `aab` 文件进行资源混淆,并输出混淆后的 `aab` 文件,支持 `资源去重` 和 `文件过滤`。 41 | ```cmd 42 | aabresguard obfuscate-bundle --bundle=app.aab --output=obfuscated.aab --config=config.xml --mapping=mapping.txt 43 | --merge-duplicated-res=true 44 | --storeFile=debug.store 45 | --storePassword=android 46 | --keyAlias=android 47 | --keyPassword=android 48 | ``` 49 | 配置文件 `config.xml`,白名单支持`正则表达式` 50 | ```xml 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ``` 62 | 63 | ## 文案过滤 64 | 指定一个按行分割的字符串列表文件,过滤掉string资源类型中name匹配的文案及翻译 65 | ```cmd 66 | aabresguard filter-string --bundle=app.aab --output=filtered.aab --config=config.xml 67 | --storeFile=debug.store 68 | --storePassword=android 69 | --keyAlias=android 70 | --keyPassword=android 71 | ``` 72 | 配置文件 `config.xml` 73 | ```xml 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ``` 85 | 86 | 87 | ## 参数说明 88 | 参数的说明请执行以下命令来进行查看: 89 | 90 | ```cmd 91 | aabresguard help 92 | ``` -------------------------------------------------------------------------------- /wiki/zh-cn/CONTRIBUTOR.md: -------------------------------------------------------------------------------- 1 | [English](../en/CONTRIBUTOR.md) | **[简体中文](../zh-cn/CONTRIBUTOR.md)** 2 | 3 | # 贡献指南 4 | 5 | 这篇指南会指导你如何为 **AabResGuard** 贡献一份自己的力量,请在你要提 [issue](https://github.com/bytedance/AabResGuard/issues) 或者 [pull request](https://github.com/bytedance/AabResGuard/pulls) 6 | 之前花几分钟来阅读一遍这篇指南。 7 | 8 | ## 贡献 9 | 我们随时都欢迎任何贡献,无论是简单的错别字修正,BUG 修复还是增加新功能。请踊跃提出问题或发起 PR。我们同样重视文档以及与其它开源项目的整合,欢迎在这方面做出贡献。 10 | 11 | ## [#行为准则](../en/CODE_OF_CONDUCT.md) 12 | 我们有一份[行为准则](../en/CODE_OF_CONDUCT.md),希望所有的贡献者都能遵守,请花时间阅读一遍全文以确保你能明白哪些是可以做的,哪些是不可以做的。 13 | 14 | ## 研发流程 15 | 我们所有的工作都会放在 GitHub 上。不管是核心团队的成员还是外部贡献者的 pull request 都需要经过同样流程的 review。 16 | 17 | 我们使用 `develop` 分支作为我们的开发分支,这代码它是不稳定的分支。每个版本都会创建一个 `release` 分支(如 `release/0.1`) 作为稳定的发布分支。 18 | 每发布一个新版本都会将其合并到对应的分支并打上对应的 `tag`。 19 | 20 | 下面是开源贡献者常用的工作流(workflow): 21 | 22 | - 将仓库 fork 到自己的 GitHub 下 23 | - 将 fork 后的仓库 clone 到本地 24 | - 创建新的分支,在新的分支上进行开发操作(请确保对应的变更都有测试用例或 demo 进行验证) 25 | - 保持分支与远程 master 分支一致(通过 fetch 和 rebase 操作) 26 | - 在本地提交变更(注意 commit log 保持简练、规范),注意提交的 email 需要和 GitHub 的 email 保持一致 27 | - 将提交 push 到 fork 的仓库下 28 | - 创建一个 pull request (PR) 29 | 30 | 提交 PR 的时候请参考 [PR 模板](../en/PULL_REQUEST_TEMPLATE.md)。在进行较大的变更的时候请确保 PR 有一个对应的 Issue。 31 | 32 | 33 | 在合并 PR 的时候,请把多余的提交记录都 squash 成一个。最终的提交信息需要保证简练、规范。 34 | 35 | ## 提交 bug 36 | ### 查找已知的 Issues 37 | 我们使用 GitHub Issues 来管理项目 bug。 我们将密切关注已知 bug,并尽快修复。 在提交新问题之前,请尝试确保您的问题尚不存在。 38 | 39 | ### 提交新的 Issues 40 | 请按照 Issues Template 的指示来提交新的 Issues。 41 | -------------------------------------------------------------------------------- /wiki/zh-cn/DATA.md: -------------------------------------------------------------------------------- 1 | [English](../en/DATA.md) | **[简体中文](DATA.md)** 2 | 3 | # 数据收益 4 | **AabResGuard** 于2019年六月研发完成,于2019年七月底在 `Tiktok`、`Vigo` 等多个海外产品上线, 5 | 为海外产品提供了资源保护和包大小优化的能力。 6 | 7 | 目前未收到相关资源方面的问题反馈,由于研发流程的一些原因,**AabResGuard** 在资源混淆的基础上由提供了额外的其他命令的支持, 8 | 做到了命令之间独立运行的能力,并且直接提供 `jar` 包,为 `CI` 提供便利支持。 9 | 目前在多个产品的收益数据如下所示:(解析 apk 的配置固定) 10 | 11 | >由于每个应用对资源的优化程度不同,所以该数据在不同的应用上的优化不同,以实际数据为准。 12 | 13 | **AabResGuard-0.1.0** 14 | 15 | |产品|运行时间|aab size|apk raw size|apk download size| 16 | |---|-------|--------|-------------|----------------| 17 | |Tiktok/840|75s|-2.9MB|-1.9MB|-0.7MB| 18 | |Vigo/v751|60s|-1.0Mb|-1.4MB|-0.6MB| 19 | 20 | 21 | **`device-spec` 配置:** 22 | ```json 23 | { 24 | "supportedAbis": ["armeabi-v7a"], 25 | "supportedLocales": ["zh-CN", "en-US", "ja-JP", "zh-HK", "zh-TW"], 26 | "screenDensity": 480, 27 | "sdkVersion": 16 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /wiki/zh-cn/OUTPUT.md: -------------------------------------------------------------------------------- 1 | [English](../OUTPUT.md) | **[简体中文](OUTPUT.md)** 2 | # 输出文件 3 | 4 | >混淆后的文件输出目录和 bundle 打包后输出的文件目录一致,均在 `build/outputs/bundle/{flavor}/` 目录下。 5 | 6 | 混淆后的输出文件如下图所示: 7 | 8 | ![output](../images/output.png) 9 | 10 | ## resources-mapping 11 | 用于记录资源混淆规则的日志文件,示例如下: 12 | 13 | ```txt 14 | res dir mapping: 15 | res/color-v21 -> res/c 16 | res/color-v23 -> res/d 17 | res/anim -> res/a 18 | 19 | res id mapping: 20 | 0x7f0c00ba : com.bytedance.android.app.R.style.RtlUnderlay.Widget.AppCompat.ActionButton.Overflow -> com.bytedance.android.app.R.style.eb 21 | 0x7f040002 : com.bytedance.android.app.R.color.abc_btn_colored_borderless_text_material -> com.bytedance.android.app.R.color.c 22 | 0x7f0c00d5 : com.bytedance.android.app.R.style.TextAppearance.AppCompat.Title -> com.bytedance.android.app.R.style.f2 23 | 0x7f0c0022 : com.bytedance.android.app.R.style.Base.TextAppearance.AppCompat.Small.Inverse -> com.bytedance.android.app.R.style.a8 24 | 25 | res entries path mapping: 26 | 0x7f060030 : base/res/drawable-xxhdpi-v4/abc_list_selector_disabled_holo_dark.9.png -> res/h/z.9.png 27 | 0x7f060022 : base/res/drawable-xxxhdpi-v4/abc_ic_star_half_black_16dp.png -> res/k/o.png 28 | ``` 29 | 30 | - **res dir mapping:** 存储资源文件目录的混淆规则。格式:dir -> dir(`res/` 根目录不可以被混淆) 31 | - **res id mapping:** 存储资源名称的混淆规则。格式:resourceId : resourceName -> resourceName(增量混淆时,resourceId 不会被读入) 32 | - **res entries path mapping:** 存储资源文件路径的混淆规则。格式:resourceId : path -> path(增量混淆时,resourceId 不会被读入) 33 | 34 | ## -duplicated.txt 35 | 用于记录被去重的资源文件,示例如下: 36 | 37 | ```txt 38 | res filter path mapping: 39 | res/drawable-hdpi-v4/abc_list_divider_mtrl_alpha.9.png -> res/drawable-mdpi-v4/abc_list_divider_mtrl_alpha.9.png (size 167B) 40 | res/color-v23/abc_tint_spinner.xml -> res/color-v23/abc_tint_edittext.xml (size 942B) 41 | res/drawable-xhdpi-v4/abc_list_divider_mtrl_alpha.9.png -> res/drawable-mdpi-v4/abc_list_divider_mtrl_alpha.9.png (size 167B) 42 | removed: count(3), totalSize(1.2KB) 43 | ``` 44 | -------------------------------------------------------------------------------- /wiki/zh-cn/README.md: -------------------------------------------------------------------------------- 1 | # AabResGuard 2 |

3 | 4 |

针对 aab 文件的资源混淆工具

5 |

6 | 7 | [ ![Download](https://api.bintray.com/packages/yeoh/maven/aabresguard-plugin/images/download.svg?version=0.1.4) ](https://bintray.com/yeoh/maven/aabresguard-plugin/0.1.4/link) 8 | [![License](https://img.shields.io/badge/license-Apache2.0-brightgreen)](../../LICENSE) 9 | [![Bundletool](https://img.shields.io/badge/Dependency-Bundletool/0.10.0-blue)](https://github.com/google/bundletool) 10 | 11 | [English](../../README.md) | **[简体中文](README.md)** 12 | 13 | > 本工具由字节跳动抖音 Android 团队提供。 14 | 15 | ## 特性 16 | > 针对 aab 文件的资源混淆工具 17 | 18 | - **资源去重:** 对重复资源文件进行合并,缩减包体积。 19 | - **文件过滤:** 支持对 `bundle` 包中的文件进行过滤,目前只支持 `MATE-INFO/`、`lib/` 路径下的过滤。 20 | - **白名单:** 白名单中的资源,名称不予混淆。 21 | - **增量混淆:** 输入 `mapping` 文件,支持增量混淆。 22 | - **文案删除:** 输入按行分割的字符串文件,移除文案及翻译。 23 | - **???:** 展望未来,会有更多的特性支持,欢迎提交 PR & issue。 24 | 25 | ## [数据收益](DATA.md) 26 | **AabResGuard** 是抖音Android团队完成的资源混淆工具,目前已经在 **Tiktok、Vigo** 等多个产品上线多月,目前无相关资源问题的反馈。 27 | 具体的数据详细信息请移步 **[数据收益](DATA.md)** 。 28 | 29 | ## 快速开始 30 | - **命令行工具:** 支持命令行一键输入输出。 31 | - **Gradle plugin:** 支持 `gradle plugin`,使用原始打包命令执行混淆。 32 | 33 | ### Gradle plugin 34 | 在 `build.gradle(root project)` 中进行配置 35 | ```gradle 36 | buildscript { 37 | repositories { 38 | mavenCentral() 39 | jcenter() 40 | google() 41 | } 42 | dependencies { 43 | classpath "com.bytedance.android:aabresguard-plugin:0.1.0" 44 | } 45 | } 46 | ``` 47 | 48 | 在 `build.gradle(application)` 中配置 49 | ```gradle 50 | apply plugin: "com.bytedance.android.aabResGuard" 51 | aabResGuard { 52 | mappingFile = file("mapping.txt").toPath() // 用于增量混淆的 mapping 文件 53 | whiteList = [ // 白名单规则 54 | "*.R.raw.*", 55 | "*.R.drawable.icon" 56 | ] 57 | obfuscatedBundleFileName = "duplicated-app.aab" // 混淆后的文件名称,必须以 `.aab` 结尾 58 | mergeDuplicatedRes = true // 是否允许去除重复资源 59 | enableFilterFiles = true // 是否允许过滤文件 60 | filterList = [ // 文件过滤规则 61 | "*/arm64-v8a/*", 62 | "META-INF/*" 63 | ] 64 | enableFilterStrings = false // 过滤文案 65 | unusedStringPath = file("unused.txt").toPath() // 过滤文案列表路径 默认在mapping同目录查找 66 | languageWhiteList = ["en", "zh"] // 保留en,en-xx,zh,zh-xx等语言,其余均删除 67 | } 68 | ``` 69 | 70 | `aabResGuard plugin` 侵入了 `bundle` 打包流程,可以直接执行原始打包命令进行混淆。 71 | ```cmd 72 | ./gradlew clean :app:bundleDebug --stacktrace 73 | ``` 74 | 75 | 通过 `gradle` 获取混淆后的 `bundle` 文件路径 76 | ```groovy 77 | def aabResGuardPlugin = project.tasks.getByName("aabresguard${VARIANT_NAME}") 78 | Path bundlePath = aabResGuardPlugin.getObfuscatedBundlePath() 79 | ``` 80 | 81 | ### [白名单](../en/WHITELIST.md) 82 | 不需要混淆的资源. 如果[白名单](../en/WHITELIST.md)中没有包含你的配置,欢迎提交 PR. 83 | 84 | ### [命令行支持](COMMAND.md) 85 | **AabResGuard** 提供了 `jar` 包,可以使用命令行直接执行,具体的使用请移步 **[命令行支持](COMMAND.md)** 。 86 | 87 | ### [输出文件](OUTPUT.md) 88 | 在打包完成后会输出混淆后的文件和相应的日志文件,详细信息请移步 **[输出文件](OUTPUT.md)** 。 89 | - **resources-mapping.txt:** 资源混淆 mapping,可作为下次混淆输入以达到增量混淆的目的。 90 | - **aab:** 优化后的 aab 文件。 91 | - **-duplicated.txt:** 被去重的文件日志记录。 92 | 93 | ## [版本日志](CHANGELOG.md) 94 | 版本变化日志记录,详细信息请移步 **[版本日志](CHANGELOG.md)** 。 95 | 96 | ## [代码贡献](CONTRIBUTOR.md) 97 | 阅读详细内容,了解如何参与改进 **AabResGuard**。 98 | 99 | ### 贡献者 100 | * [JingYeoh](https://github.com/JingYeoh) 101 | * [Jun Li]() 102 | * [Zilai Jiang](https://github.com/Zzzia) 103 | * [Zhiqian Yang](https://github.com/yangzhiqian) 104 | * [Xiaoshuang Bai (Designer)](https://www.behance.net/shawnpai) 105 | 106 | ## 感谢 107 | * [AndResGuard](https://github.com/shwenzhang/AndResGuard/) 108 | * [BundleTool](https://github.com/google/bundletool) 109 | --------------------------------------------------------------------------------