├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── core ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── main │ └── java │ │ └── org │ │ └── lsposed │ │ └── lspollution │ │ ├── android │ │ ├── AndroidDebugKeyStoreHelper.java │ │ ├── AndroidLocation.java │ │ ├── JarSigner.java │ │ ├── OpenJDKJarSigner.kt │ │ └── 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 │ │ ├── executors │ │ ├── BundleFileFilter.java │ │ ├── BundleStringFilter.java │ │ ├── DuplicatedResourcesMerger.java │ │ └── ResourcesObfuscator.java │ │ ├── model │ │ ├── ResourcesMapping.java │ │ └── xml │ │ │ ├── FileFilterConfig.java │ │ │ └── StringFilterConfig.java │ │ ├── obfuscation │ │ └── ResGuardStringBuilder.java │ │ ├── parser │ │ └── ResourcesMappingParser.java │ │ └── utils │ │ ├── FileOperation.java │ │ ├── FileUtils.java │ │ ├── Pair.groovy │ │ ├── TimeClock.java │ │ ├── Utils.java │ │ └── exception │ │ └── CommandExceptionPreconditions.java │ └── test │ ├── java │ └── org │ │ └── lsposed │ │ └── lspollution │ │ ├── BaseTest.java │ │ ├── TestData.java │ │ ├── bundle │ │ ├── AppBundlePackagerTest.java │ │ ├── AppBundleSignerTest.java │ │ └── AppBundleUtilsTest.java │ │ ├── commands │ │ ├── DuplicatedResourcesMergerCommandTest.java │ │ └── FileFilterCommandTest.java │ │ ├── executors │ │ ├── BundleFileFilterTest.java │ │ ├── BundleStringFilterTest.java │ │ ├── DuplicatedResourcesMergerTest.java │ │ └── ResourcesObfuscatorTest.java │ │ ├── parser │ │ └── ResourcesMappingParserTest.java │ │ ├── testing │ │ └── ProcessThread.java │ │ └── utils │ │ └── FileOperationTest.java │ └── resources │ └── com │ └── bytedance │ └── android │ └── aabresguard │ ├── demo │ ├── config-filter.xml │ ├── config.xml │ ├── demo.aab │ ├── mapping.txt │ ├── test.apk │ └── unused.txt │ └── device-spec │ └── armeabi-v7a_sdk16.json ├── gradle-plugin ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── org │ │ └── lsposed │ │ └── lspollution │ │ └── plugin │ │ ├── LSPollutionExtension.kt │ │ ├── LSPollutionPlugin.kt │ │ ├── LSPollutionTask.kt │ │ ├── internal │ │ ├── BundleResolution.kt │ │ └── SigningConfigResolution.kt │ │ └── model │ │ └── SigningConfig.kt │ └── resources │ └── META-INF │ └── gradle-plugins │ └── com.bytedance.android.aabResGuard.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── 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 ├── settings.gradle.kts └── 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 | .gradle 2 | .idea 3 | build 4 | generated 5 | local.properties 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/LSPosed/LSPollution/actions/workflows/build.yml/badge.svg)](https://github.com/LSPosed/LSPollution/actions/workflows/build.yml) 2 | 3 | LSPollution 4 | ======== 5 | 6 | Resource obfuscator for Android applications. LSPollution supports [configuration cache](https://docs.gradle.org/current/userguide/configuration_cache.html). 7 | 8 | Features 9 | -------- 10 | > The tool of obfuscated aab resources. 11 | 12 | - **Merge duplicated resources:** Consolidate duplicate resource files to reduce package size. 13 | - **Filter bundle files:** Support for filtering files in the `bundle` package. Currently only supports filtering in the `MATE-INFO/` and `lib/` paths. 14 | - **White list:** The resources in the whitelist are not to be obfuscated. 15 | - **Incremental obfuscation:** Input the `mapping` file to support incremental obfuscation. 16 | - **Remove string:** Input the unused file splits by lines to support remove strings. 17 | - **???:** Looking ahead, there will be more feature support, welcome to submit PR & issue. 18 | 19 | Usage 20 | ----- 21 | In order to make LSPollution work with your project you have to apply the LSPollution Gradle plugin 22 | to the project. 23 | 24 | The following is an example `settings.gradle.kts` to apply LSPollution. 25 | ```kotlin 26 | pluginManagement { 27 | repositories { 28 | mavenCentral() 29 | } 30 | plugins { 31 | id("org.lsposed.lspollution") version "0.2.0" 32 | } 33 | } 34 | ``` 35 | 36 | **Note that you should use at least Java 17 to launch the gradle daemon for this plugin (this is also required by AGP 8+).** 37 | The project that uses this plugin on the other hand does not necessarily to target Java 17. 38 | 39 | Configuration 40 | ------------- 41 | LSPollution plugin can be configured using `lspollution` extension object. 42 | 43 | The following is an example `build.gradle.kts` that configures `lspollution` extension object with default values. 44 | ```kotlin 45 | plugins { 46 | id("org.lsposed.lspollution") 47 | // other plugins... 48 | } 49 | 50 | lspollution { 51 | mappingFile = file("mapping.txt").toPath() // Mapping file used for incremental obfuscation 52 | whiteList = [ // White list rules 53 | "*.R.raw.*", 54 | "*.R.drawable.icon" 55 | ] 56 | obfuscatedBundleFileName = "duplicated-app.aab" // Obfuscated file name, must end with '.aab' 57 | mergeDuplicatedRes = true // Whether to allow the merge of duplicate resources 58 | enableFilterFiles = true // Whether to allow filter files 59 | filterList = [ // file filter rules 60 | "*/arm64-v8a/*", 61 | "META-INF/*" 62 | ] 63 | 64 | enableFilterStrings = false // switch of filter strings 65 | unusedStringPath = file("unused.txt").toPath() // strings will be filtered in this file 66 | languageWhiteList = ["en", "zh"] // keep en,en-xx,zh,zh-xx etc. remove others. 67 | } 68 | ``` 69 | 70 | The `lspollution plugin` intrudes the `bundle` packaging process and can be obfuscated by executing the original packaging commands. 71 | ```cmd 72 | ./gradlew clean :app:bundleDebug --stacktrace 73 | ``` 74 | 75 | Get the obfuscated `bundle` file path by `gradle` . 76 | ```kotlin 77 | val lspollutionPlugin = project.tasks.getByName("lspollution${VARIANT_NAME}") 78 | val bundlePath = lspollutionPlugin.getObfuscatedBundlePath() 79 | ``` 80 | 81 | ### [Whitelist](wiki/en/WHITELIST.md) 82 | The resources that can not be confused. Welcome PR your configs which is not included in [WHITELIST](wiki/en/WHITELIST.md) 83 | 84 | ### [Command line](wiki/en/COMMAND.md) 85 | **LSPollution** provides a `jar` file that can be executed directly from the command line. More details, please go to **[Command Line](wiki/en/COMMAND.md)**. 86 | 87 | ### [Output](wiki/en/OUTPUT.md) 88 | 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)**. 89 | - **resources-mapping.txt:** Resource obfuscation mapping, which can be used as the next obfuscation input to achieve incremental obfuscate. 90 | - **aab:** Optimized aab file. 91 | - **-duplicated.txt:** duplicated file logging. 92 | 93 | ## [Change log](wiki/en/CHANGELOG.md) 94 | Version change log. More details, please go to **[Change Log](wiki/en/CHANGELOG.md)** . 95 | 96 | Credit 97 | ------ 98 | LSPollution was forked from https://github.com/bytedance/AabResGuard. Credits to its original authors. 99 | 100 | License 101 | ======= 102 | Copyright 2019-2021 AabResGuard Authors 103 | Copyright 2023 LSPosed 104 | 105 | Licensed under the Apache License, Version 2.0 (the "License"); 106 | you may not use this file except in compliance with the License. 107 | You may obtain a copy of the License at 108 | 109 | http://www.apache.org/licenses/LICENSE-2.0 110 | 111 | Unless required by applicable law or agreed to in writing, software 112 | distributed under the License is distributed on an "AS IS" BASIS, 113 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 114 | See the License for the specific language governing permissions and 115 | limitations under the License. 116 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapper 2 | import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension 3 | 4 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 5 | plugins { 6 | alias(libs.plugins.lsplugin.publish) 7 | alias(libs.plugins.kotlin) apply false 8 | } 9 | 10 | allprojects { 11 | group = "org.lsposed.lspollution" 12 | version = "0.2.0" 13 | 14 | plugins.withType(JavaPlugin::class.java) { 15 | extensions.configure(JavaPluginExtension::class.java) { 16 | sourceCompatibility = JavaVersion.VERSION_17 17 | targetCompatibility = JavaVersion.VERSION_17 18 | } 19 | } 20 | 21 | plugins.withType(KotlinPluginWrapper::class.java) { 22 | extensions.configure(KotlinJvmProjectExtension::class.java) { 23 | jvmToolchain(17) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 2 | plugins { 3 | alias(libs.plugins.kotlin) 4 | `java-library` 5 | `maven-publish` 6 | signing 7 | } 8 | 9 | dependencies { 10 | annotationProcessor(libs.auto.value) 11 | compileOnly(libs.aapt2.proto) 12 | compileOnly(libs.androidx.annotation) 13 | compileOnly(libs.auto.value.annotations) 14 | compileOnly(libs.guava) 15 | implementation(libs.bundletool) 16 | implementation(libs.commons.codec) 17 | implementation(libs.protobuf.java) 18 | 19 | testCompileOnly(libs.guava) 20 | testImplementation(libs.junit) 21 | } 22 | 23 | publish { 24 | githubRepo = "LSPosed/LSPollution" 25 | publications { 26 | register(rootProject.name) { 27 | artifactId = project.name 28 | group = group 29 | version = version 30 | from(components.getByName("java")) 31 | pom { 32 | name.set(project.name) 33 | description.set("Resource obfuscator for Android applications") 34 | url.set("https://github.com/LSPosed/LSPollution") 35 | licenses { 36 | license { 37 | name.set("Apache License 2.0") 38 | url.set("https://github.com/LSPosed/LSPollution/blob/master/LICENSE.txt") 39 | } 40 | } 41 | developers { 42 | developer { 43 | name.set("LSPosed") 44 | url.set("https://lsposed.org") 45 | } 46 | } 47 | scm { 48 | connection.set("scm:git:https://github.com/LSPosed/LSPollution.git") 49 | url.set("https://github.com/LSPosed/LSPollution") 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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/org/lsposed/lspollution/android/AndroidDebugKeyStoreHelper.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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; 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/org/lsposed/lspollution/android/AndroidLocation.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.android; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * Manages the location of the android files (including emulator files, ddms config, debug keystore) 7 | */ 8 | public class AndroidLocation { 9 | 10 | /** 11 | * The name of the .android folder returned by {@link #getFolder}. 12 | */ 13 | public static final String FOLDER_DOT_ANDROID = ".android"; 14 | 15 | /** 16 | * Virtual Device folder inside the path returned by {@link #getFolder} 17 | */ 18 | public static final String FOLDER_AVD = "avd"; 19 | private static String sPrefsLocation = null; 20 | private static String sAvdLocation = null; 21 | 22 | /** 23 | * Returns the folder used to store android related files. 24 | * If the folder is not created yet, it will be created here. 25 | * 26 | * @return an OS specific path, terminated by a separator. 27 | * @throws AndroidLocationException 28 | */ 29 | public static String getFolder() throws AndroidLocationException { 30 | if (sPrefsLocation == null) { 31 | sPrefsLocation = findHomeFolder(); 32 | } 33 | 34 | // make sure the folder exists! 35 | File f = new File(sPrefsLocation); 36 | if (!f.exists()) { 37 | try { 38 | f.mkdirs(); 39 | } catch (SecurityException e) { 40 | AndroidLocationException e2 = new AndroidLocationException(String.format( 41 | "Unable to create folder '%1$s'. " + 42 | "This is the path of preference folder expected by the Android tools.", 43 | sPrefsLocation)); 44 | e2.initCause(e); 45 | throw e2; 46 | } 47 | } else if (f.isFile()) { 48 | throw new AndroidLocationException(String.format( 49 | "%1$s is not a directory!\n" + 50 | "This is the path of preference folder expected by the Android tools.", sPrefsLocation)); 51 | } 52 | return sPrefsLocation; 53 | } 54 | 55 | /** 56 | * Returns the folder used to store android related files. 57 | * This method will not create the folder if it doesn't exist yet.\ 58 | * 59 | * @return an OS specific path, terminated by a separator or null 60 | * if no path is found or an error occurred. 61 | */ 62 | public static String getFolderWithoutWrites() { 63 | if (sPrefsLocation == null) { 64 | try { 65 | sPrefsLocation = findHomeFolder(); 66 | } catch (AndroidLocationException e) { 67 | return null; 68 | } 69 | } 70 | return sPrefsLocation; 71 | } 72 | 73 | /** 74 | * Check the if ANDROID_SDK_HOME variable points to a SDK. 75 | * If it points to an SDK 76 | * 77 | * @throws AndroidLocationException 78 | */ 79 | public static void checkAndroidSdkHome() throws AndroidLocationException { 80 | Global.ANDROID_SDK_HOME.validatePath(false); 81 | } 82 | 83 | /** 84 | * Returns the folder where the users AVDs are stored. 85 | * 86 | * @return an OS specific path, terminated by a separator. 87 | * @throws AndroidLocationException 88 | */ 89 | public static String getAvdFolder() throws AndroidLocationException { 90 | if (sAvdLocation == null) { 91 | String home = findValidPath(Global.ANDROID_AVD_HOME); 92 | if (home == null) { 93 | home = getFolder() + FOLDER_AVD; 94 | } 95 | sAvdLocation = home; 96 | if (!sAvdLocation.endsWith(File.separator)) { 97 | sAvdLocation += File.separator; 98 | } 99 | } 100 | return sAvdLocation; 101 | } 102 | 103 | public static String getUserHomeFolder() throws AndroidLocationException { 104 | return findValidPath(Global.TEST_TMPDIR, Global.USER_HOME, Global.HOME); 105 | } 106 | 107 | private static String findHomeFolder() throws AndroidLocationException { 108 | String home = findValidPath(Global.ANDROID_SDK_HOME, Global.TEST_TMPDIR, Global.USER_HOME, Global.HOME); 109 | 110 | // if the above failed, we throw an exception. 111 | if (home == null) { 112 | throw new AndroidLocationException("prop: " + System.getProperty("ANDROID_SDK_HOME")); 113 | } 114 | if (!home.endsWith(File.separator)) { 115 | home += File.separator; 116 | } 117 | return home + FOLDER_DOT_ANDROID + File.separator; 118 | } 119 | 120 | /** 121 | * Resets the folder used to store android related files. For testing. 122 | */ 123 | public static void resetFolder() { 124 | sPrefsLocation = null; 125 | sAvdLocation = null; 126 | } 127 | 128 | /** 129 | * Checks a list of system properties and/or system environment variables for validity, 130 | * and returns the first one. 131 | * 132 | * @param vars The variables to check. Order does matter. 133 | * @return the content of the first property/variable that is a valid directory. 134 | */ 135 | private static String findValidPath(Global... vars) throws AndroidLocationException { 136 | for (Global var : vars) { 137 | String path = var.validatePath(true); 138 | if (path != null) { 139 | return path; 140 | } 141 | } 142 | return null; 143 | } 144 | 145 | /** 146 | * Enum describing which variables to check and whether they should 147 | * be checked via {@link System#getProperty(String)} or {@link System#getenv()} or both. 148 | */ 149 | private enum Global { 150 | ANDROID_AVD_HOME("ANDROID_AVD_HOME", true, true), // both sys prop and env var 151 | ANDROID_SDK_HOME("ANDROID_SDK_HOME", true, true), // both sys prop and env var 152 | TEST_TMPDIR("TEST_TMPDIR", false, true), // Bazel kludge 153 | USER_HOME("user.home", true, false), // sys prop only 154 | HOME("HOME", false, true); // env var only 155 | 156 | final String mName; 157 | final boolean mIsSysProp; 158 | final boolean mIsEnvVar; 159 | 160 | Global(String name, boolean isSysProp, boolean isEnvVar) { 161 | mName = name; 162 | mIsSysProp = isSysProp; 163 | mIsEnvVar = isEnvVar; 164 | } 165 | 166 | private static boolean isSdkRootWithoutDotAndroid(File folder) { 167 | return subFolderExist(folder, "platforms") && 168 | subFolderExist(folder, "platform-tools") && 169 | !subFolderExist(folder, FOLDER_DOT_ANDROID); 170 | } 171 | 172 | private static boolean subFolderExist(File folder, String subFolder) { 173 | return new File(folder, subFolder).isDirectory(); 174 | } 175 | 176 | public String validatePath(boolean silent) throws AndroidLocationException { 177 | String path; 178 | if (mIsSysProp) { 179 | path = checkPath(System.getProperty(mName), silent); 180 | if (path != null) { 181 | return path; 182 | } 183 | } 184 | 185 | if (mIsEnvVar) { 186 | path = checkPath(System.getenv(mName), silent); 187 | if (path != null) { 188 | return path; 189 | } 190 | } 191 | return null; 192 | } 193 | 194 | private String checkPath(String path, boolean silent) 195 | throws AndroidLocationException { 196 | if (path == null) { 197 | return null; 198 | } 199 | File file = new File(path); 200 | if (!file.isDirectory()) { 201 | return null; 202 | } 203 | if (!(this == ANDROID_SDK_HOME && isSdkRootWithoutDotAndroid(file))) { 204 | return path; 205 | } 206 | if (!silent) { 207 | throw new AndroidLocationException(String.format( 208 | "ANDROID_SDK_HOME is set to the root of your SDK: %1$s\n" + 209 | "This is the path of the preference folder expected by the Android tools.\n" + 210 | "It should NOT be set to the same as the root of your SDK.\n" + 211 | "Please set it to a different folder or do not set it at all.\n" + 212 | "If this is not set we default to: %2$s", 213 | path, findValidPath(TEST_TMPDIR, USER_HOME, HOME))); 214 | } 215 | return null; 216 | } 217 | } 218 | 219 | /** 220 | * Throw when the location of the android folder couldn't be found. 221 | */ 222 | public static final class AndroidLocationException extends Exception { 223 | private static final long serialVersionUID = 1L; 224 | 225 | public AndroidLocationException(String string) { 226 | super(string); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/android/JarSigner.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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 org.lsposed.lspollution.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/org/lsposed/lspollution/android/OpenJDKJarSigner.kt: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.android 2 | 3 | import com.android.tools.build.bundletool.model.utils.files.FilePreconditions 4 | import org.lsposed.lspollution.utils.FileUtils 5 | import java.io.File 6 | import java.io.IOException 7 | import java.util.logging.Logger 8 | 9 | /** 10 | * Created by YangJing on 2019/10/18 . 11 | * Email: yangjing.yeoh@bytedance.com 12 | */ 13 | class OpenJDKJarSigner { 14 | @Throws(IOException::class, InterruptedException::class) 15 | fun sign(toBeSigned: File, signature: JarSigner.Signature) { 16 | FilePreconditions.checkFileExistsAndReadable(toBeSigned.toPath()) 17 | FilePreconditions.checkFileExistsAndReadable(signature.storeFile) 18 | val jarSigner = locatedJarSigner() 19 | val args: MutableList = ArrayList() 20 | if (jarSigner != null) { 21 | args.add(jarSigner.absolutePath) 22 | } else { 23 | args.add(jarSignerExecutable) 24 | } 25 | args.add("-keystore") 26 | args.add(signature.storeFile.toFile().absolutePath) 27 | var keyStorePasswordFile: File? = null 28 | var aliasPasswordFile: File? = null 29 | 30 | // write passwords to a file so it cannot be spied on. 31 | if (signature.storePassword != null) { 32 | keyStorePasswordFile = File.createTempFile("store", "prv") 33 | FileUtils.writeToFile(keyStorePasswordFile, signature.storePassword) 34 | args.add("-storepass:file") 35 | args.add(keyStorePasswordFile.absolutePath) 36 | } 37 | if (signature.keyPassword != null) { 38 | aliasPasswordFile = File.createTempFile("alias", "prv") 39 | FileUtils.writeToFile(aliasPasswordFile, signature.keyPassword) 40 | args.add("--keypass:file") 41 | args.add(aliasPasswordFile.absolutePath) 42 | } 43 | args.add(toBeSigned.absolutePath) 44 | if (signature.keyAlias != null) { 45 | args.add(signature.keyAlias) 46 | } 47 | val errorLog = File.createTempFile("error", ".log") 48 | val outputLog = File.createTempFile("output", ".log") 49 | logger.fine("Invoking " + args.joinToString(" ")) 50 | val process = start(ProcessBuilder(args).redirectError(errorLog).redirectOutput(outputLog)) 51 | val exitCode = process.waitFor() 52 | if (exitCode != 0) { 53 | val errors = FileUtils.loadFileWithUnixLineSeparators(errorLog) 54 | val output = FileUtils.loadFileWithUnixLineSeparators(outputLog) 55 | throw RuntimeException( 56 | String.format( 57 | "%s failed with exit code %d: \n %s", 58 | jarSignerExecutable, exitCode, 59 | if (errors.trim { it <= ' ' }.isEmpty()) output else errors 60 | ) 61 | ) 62 | } 63 | keyStorePasswordFile?.delete() 64 | aliasPasswordFile?.delete() 65 | } 66 | 67 | @Throws(IOException::class) 68 | private fun start(builder: ProcessBuilder): Process { 69 | return builder.start() 70 | } 71 | 72 | /** 73 | * Return the "jarsigner" tool location or null if it cannot be determined. 74 | */ 75 | private fun locatedJarSigner(): File? { 76 | // Look in the java.home bin folder, on jdk installations or Mac OS X, this is where the 77 | // javasigner will be located. 78 | val javaHome = File(System.getProperty("java.home")) 79 | var jarSigner = getJarSigner(javaHome) 80 | return if (jarSigner.exists()) { 81 | jarSigner 82 | } else { 83 | // if not in java.home bin, it's probable that the java.home points to a JRE 84 | // installation, we should then look one folder up and in the bin folder. 85 | jarSigner = getJarSigner(javaHome.parentFile) 86 | // if still cant' find it, give up. 87 | if (jarSigner.exists()) jarSigner else null 88 | } 89 | } 90 | 91 | /** 92 | * Returns the jarsigner tool location with the bin folder. 93 | */ 94 | private fun getJarSigner(parentDir: File): File { 95 | return File(File(parentDir, "bin"), jarSignerExecutable) 96 | } 97 | 98 | companion object { 99 | private val jarSignerExecutable = if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) "jarsigner.exe" else "jarsigner" 100 | private val logger = Logger.getLogger(OpenJDKJarSigner::class.java.name) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/android/SdkConstants.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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/org/lsposed/lspollution/bundle/AppBundleAnalyzer.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.bundle; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import org.lsposed.lspollution.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.printf("analyze bundle file done, const %s%n", timeClock.getCoast()); 32 | return appBundle; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/bundle/AppBundlePackager.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.bundle; 2 | 3 | import com.android.tools.build.bundletool.io.AppBundleSerializer; 4 | import com.android.tools.build.bundletool.model.AppBundle; 5 | import org.lsposed.lspollution.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.printf("package bundle done, coast: %s%n", timeClock.getCoast()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/bundle/AppBundleSigner.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.bundle; 2 | 3 | import org.lsposed.lspollution.android.JarSigner; 4 | import org.lsposed.lspollution.utils.TimeClock; 5 | 6 | import java.io.IOException; 7 | import java.nio.file.Path; 8 | 9 | /** 10 | * Created by YangJing on 2019/10/11 . 11 | * Email: yangjing.yeoh@bytedance.com 12 | */ 13 | public class AppBundleSigner { 14 | 15 | private final Path bundleFile; 16 | private JarSigner.Signature bundleSignature = JarSigner.Signature.DEBUG_SIGNATURE; 17 | 18 | public AppBundleSigner(Path bundleFile, JarSigner.Signature signature) { 19 | this.bundleFile = bundleFile; 20 | this.bundleSignature = signature; 21 | } 22 | 23 | public AppBundleSigner(Path bundleFile) { 24 | this.bundleFile = bundleFile; 25 | } 26 | 27 | public void setBundleSignature(JarSigner.Signature bundleSignature) { 28 | this.bundleSignature = bundleSignature; 29 | } 30 | 31 | public void execute() throws IOException, InterruptedException { 32 | if (bundleSignature == null) { 33 | return; 34 | } 35 | TimeClock timeClock = new TimeClock(); 36 | JarSigner.Signature signature = new JarSigner.Signature( 37 | bundleSignature.storeFile, 38 | bundleSignature.storePassword, 39 | bundleSignature.keyAlias, 40 | bundleSignature.keyPassword 41 | ); 42 | new JarSigner().sign(bundleFile.toFile(), signature); 43 | System.out.printf("[sign] sign done, coast: %s%n", timeClock.getCoast()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/bundle/AppBundleUtils.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.bundle; 2 | 3 | import static org.lsposed.lspollution.utils.FileOperation.getZipPathFileSize; 4 | 5 | import com.android.tools.build.bundletool.model.BundleModule; 6 | import com.android.tools.build.bundletool.model.ModuleEntry; 7 | import com.android.tools.build.bundletool.model.ResourceTableEntry; 8 | import com.android.tools.build.bundletool.model.ZipPath; 9 | 10 | import org.apache.commons.codec.digest.DigestUtils; 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 | /** 18 | * Created by YangJing on 2019/10/14 . 19 | * Email: yangjing.yeoh@bytedance.com 20 | */ 21 | public class AppBundleUtils { 22 | 23 | public static long getZipEntrySize(ZipFile bundleZipFile, ModuleEntry entry, BundleModule bundleModule) { 24 | String path = String.format("%s/%s", bundleModule.getName().getName(), entry.getPath().toString()); 25 | ZipEntry bundleConfigEntry = bundleZipFile.getEntry(path); 26 | return getZipPathFileSize(bundleZipFile, bundleConfigEntry); 27 | } 28 | 29 | public static long getZipEntrySize(ZipFile bundleZipFile, ZipPath zipPath) { 30 | String path = zipPath.toString(); 31 | ZipEntry bundleConfigEntry = bundleZipFile.getEntry(path); 32 | return getZipPathFileSize(bundleZipFile, bundleConfigEntry); 33 | } 34 | 35 | 36 | public static String getEntryMd5(ZipFile bundleZipFile, ModuleEntry entry, BundleModule bundleModule) { 37 | String path = String.format("%s/%s", bundleModule.getName().getName(), entry.getPath().toString()); 38 | ZipEntry bundleConfigEntry = bundleZipFile.getEntry(path); 39 | try (InputStream is = bundleZipFile.getInputStream(bundleConfigEntry)) { 40 | return bytesToHexString(DigestUtils.md5(is)); 41 | } catch (IOException e) { 42 | throw new RuntimeException(e); 43 | } 44 | } 45 | 46 | public static byte[] readBytes(ZipFile bundleZipFile, ModuleEntry entry, BundleModule bundleModule) throws IOException { 47 | String path = String.format("%s/%s", bundleModule.getName().getName(), entry.getPath().toString()); 48 | ZipEntry bundleConfigEntry = bundleZipFile.getEntry(path); 49 | try(InputStream is = bundleZipFile.getInputStream(bundleConfigEntry)) { 50 | return is.readAllBytes(); 51 | } 52 | } 53 | 54 | public static String bytesToHexString(byte[] src) { 55 | if (src.length <= 0) { 56 | return ""; 57 | } 58 | StringBuilder stringBuilder = new StringBuilder(src.length); 59 | for (byte b : src) { 60 | int v = b & 0xFF; 61 | String hv = Integer.toHexString(v); 62 | if (hv.length() < 2) { 63 | stringBuilder.append(0); 64 | } 65 | stringBuilder.append(hv); 66 | } 67 | return stringBuilder.toString(); 68 | } 69 | 70 | public static String getEntryNameByResourceName(String resourceName) { 71 | int index = resourceName.indexOf(".R."); 72 | String value = resourceName.substring(index + 3); 73 | String[] values = value.replace(".", "/").split("/"); 74 | if (values.length != 2) { 75 | throw new RuntimeException("Invalid resource format, it should be package.type.entry, yours: " + resourceName); 76 | } 77 | return values[values.length - 1]; 78 | } 79 | 80 | public static String getTypeNameByResourceName(String resourceName) { 81 | int index = resourceName.indexOf(".R."); 82 | String value = resourceName.substring(index + 3); 83 | String[] values = value.replace(".", "/").split("/"); 84 | if (values.length != 2) { 85 | throw new RuntimeException("Invalid resource format, it should be package.type.entry, yours: " + resourceName); 86 | } 87 | return values[0]; 88 | } 89 | 90 | public static String getResourceFullName(ResourceTableEntry entry) { 91 | return getResourceFullName(entry.getPackage().getPackageName(), entry.getType().getName(), entry.getEntry().getName()); 92 | } 93 | 94 | public static String getResourceFullName(String packageName, String typeName, String entryName) { 95 | return String.format("%s.R.%s.%s", packageName, typeName, entryName); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/bundle/NativeLibrariesOperation.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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/org/lsposed/lspollution/bundle/ResourcesTableBuilder.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.bundle; 2 | 3 | import com.android.aapt.Resources; 4 | 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import static com.google.common.base.Preconditions.checkArgument; 11 | import static com.google.common.base.Preconditions.checkState; 12 | 13 | import androidx.annotation.NonNull; 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 final Resources.ResourceTable.Builder table; 25 | private final Map resPackageMap; 26 | private final 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.forEach((key, value) -> table.addPackage(value.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/org/lsposed/lspollution/bundle/ResourcesTableOperation.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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/org/lsposed/lspollution/commands/DuplicatedResourcesMergerCommand.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.commands; 2 | 3 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; 4 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 5 | import static org.lsposed.lspollution.utils.FileOperation.getNetFileSizeDescription; 6 | import static org.lsposed.lspollution.utils.exception.CommandExceptionPreconditions.checkFlagPresent; 7 | 8 | import com.android.tools.build.bundletool.flags.Flag; 9 | import com.android.tools.build.bundletool.flags.ParsedFlags; 10 | import com.android.tools.build.bundletool.model.AppBundle; 11 | import com.android.tools.build.bundletool.model.exceptions.CommandExecutionException; 12 | import com.google.auto.value.AutoValue; 13 | 14 | import org.lsposed.lspollution.android.JarSigner; 15 | import org.lsposed.lspollution.bundle.AppBundleAnalyzer; 16 | import org.lsposed.lspollution.bundle.AppBundlePackager; 17 | import org.lsposed.lspollution.bundle.AppBundleSigner; 18 | import org.lsposed.lspollution.executors.DuplicatedResourcesMerger; 19 | import org.lsposed.lspollution.utils.FileOperation; 20 | import org.lsposed.lspollution.utils.TimeClock; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Path; 24 | import java.util.Optional; 25 | import java.util.logging.Logger; 26 | 27 | /** 28 | * Created by YangJing on 2019/10/10 . 29 | * Email: yangjing.yeoh@bytedance.com 30 | */ 31 | @AutoValue 32 | public abstract class DuplicatedResourcesMergerCommand { 33 | 34 | public static final String COMMAND_NAME = "merge-duplicated-res"; 35 | private static final Logger logger = Logger.getLogger(DuplicatedResourcesMergerCommand.class.getName()); 36 | 37 | private static final Flag BUNDLE_LOCATION_FLAG = Flag.path("bundle"); 38 | private static final Flag OUTPUT_FILE_FLAG = Flag.path("output"); 39 | 40 | private static final Flag DISABLE_SIGN_FLAG = Flag.booleanFlag("disable-sign"); 41 | private static final Flag STORE_FILE_FLAG = Flag.path("storeFile"); 42 | private static final Flag STORE_PASSWORD_FLAG = Flag.string("storePassword"); 43 | private static final Flag KEY_ALIAS_FLAG = Flag.string("keyAlias"); 44 | private static final Flag KEY_PASSWORD_FLAG = Flag.string("keyPassword"); 45 | 46 | public static CommandHelp help() { 47 | return CommandHelp.builder() 48 | .setCommandName(COMMAND_NAME) 49 | .setCommandDescription( 50 | CommandHelp.CommandDescription.builder() 51 | .setShortDescription("Remove duplicated resources files from an bundle file.") 52 | .build()) 53 | .addFlag( 54 | CommandHelp.FlagDescription.builder() 55 | .setFlagName(BUNDLE_LOCATION_FLAG.getName()) 56 | .setExampleValue("bundle.aab") 57 | .setDescription("Path of the Android App Bundle to remove duplicated resources from.") 58 | .build()) 59 | .addFlag( 60 | CommandHelp.FlagDescription.builder() 61 | .setFlagName(OUTPUT_FILE_FLAG.getName()) 62 | .setExampleValue("shrink.aab") 63 | .setDescription("Path to where the file should be created after remove duplicated resources.") 64 | .build()) 65 | .addFlag( 66 | CommandHelp.FlagDescription.builder() 67 | .setFlagName(DISABLE_SIGN_FLAG.getName()) 68 | .setExampleValue("disable-sign=true") 69 | .setOptional(true) 70 | .setDescription("If set true, the bundle file will not be signed after package.") 71 | .build()) 72 | .addFlag( 73 | CommandHelp.FlagDescription.builder() 74 | .setFlagName(STORE_FILE_FLAG.getName()) 75 | .setExampleValue("store.keystore") 76 | .setOptional(true) 77 | .setDescription("Path of the keystore file.") 78 | .build()) 79 | .addFlag( 80 | CommandHelp.FlagDescription.builder() 81 | .setFlagName(STORE_PASSWORD_FLAG.getName()) 82 | .setOptional(true) 83 | .setDescription("Path of the keystore password.") 84 | .build()) 85 | .addFlag( 86 | CommandHelp.FlagDescription.builder() 87 | .setFlagName(KEY_ALIAS_FLAG.getName()) 88 | .setOptional(true) 89 | .setDescription("Path of the key alias name.") 90 | .build()) 91 | .addFlag( 92 | CommandHelp.FlagDescription.builder() 93 | .setFlagName(KEY_PASSWORD_FLAG.getName()) 94 | .setOptional(true) 95 | .setDescription("Path of the key password.") 96 | .build()) 97 | .build(); 98 | } 99 | 100 | public static Builder build() { 101 | return new AutoValue_DuplicatedResourcesMergerCommand.Builder(); 102 | } 103 | 104 | public static DuplicatedResourcesMergerCommand fromFlags(ParsedFlags flags) { 105 | Path bundleLocationPath = BUNDLE_LOCATION_FLAG.getRequiredValue(flags); 106 | Path outputFilePath = OUTPUT_FILE_FLAG.getRequiredValue(flags); 107 | 108 | Builder builder = build(); 109 | builder.setBundlePath(bundleLocationPath); 110 | builder.setOutputPath(outputFilePath); 111 | 112 | DISABLE_SIGN_FLAG.getValue(flags).ifPresent(builder::setDisableSign); 113 | STORE_FILE_FLAG.getValue(flags).ifPresent(builder::setStoreFile); 114 | STORE_PASSWORD_FLAG.getValue(flags).ifPresent(builder::setStorePassword); 115 | KEY_ALIAS_FLAG.getValue(flags).ifPresent(builder::setKeyAlias); 116 | KEY_PASSWORD_FLAG.getValue(flags).ifPresent(builder::setKeyPassword); 117 | 118 | return builder.build(); 119 | } 120 | 121 | public Path execute() throws IOException, InterruptedException { 122 | TimeClock timeClock = new TimeClock(); 123 | 124 | AppBundle appBundle = new AppBundleAnalyzer(getBundlePath()).analyze(); 125 | // merge duplicated resources file 126 | DuplicatedResourcesMerger merger = new DuplicatedResourcesMerger(getBundlePath(), appBundle, getOutputPath().getParent()); 127 | appBundle = merger.merge(); 128 | // package bundle 129 | AppBundlePackager packager = new AppBundlePackager(appBundle, getOutputPath()); 130 | packager.execute(); 131 | // sign bundle 132 | if (getDisableSign().isEmpty() || !getDisableSign().get()) { 133 | AppBundleSigner signer = new AppBundleSigner(getOutputPath()); 134 | getStoreFile().ifPresent(storeFile -> signer.setBundleSignature(new JarSigner.Signature( 135 | storeFile, getStorePassword().get(), getKeyAlias().get(), getKeyPassword().get() 136 | ))); 137 | signer.execute(); 138 | } 139 | 140 | long rawSize = FileOperation.getFileSizes(getBundlePath().toFile()); 141 | long filteredSize = FileOperation.getFileSizes(getOutputPath().toFile()); 142 | System.out.printf(""" 143 | duplicate resources done, coast %s 144 | ----------------------------------------- 145 | Reduce bundle file size: %s, %s -> %s 146 | -----------------------------------------%n""", 147 | timeClock.getCoast(), 148 | getNetFileSizeDescription(rawSize - filteredSize), 149 | getNetFileSizeDescription(rawSize), 150 | getNetFileSizeDescription(filteredSize) 151 | ); 152 | return getOutputPath(); 153 | } 154 | 155 | public abstract Path getBundlePath(); 156 | 157 | public abstract Path getOutputPath(); 158 | 159 | public abstract Optional getStoreFile(); 160 | 161 | public abstract Optional getStorePassword(); 162 | 163 | public abstract Optional getKeyAlias(); 164 | 165 | public abstract Optional getKeyPassword(); 166 | 167 | public abstract Optional getDisableSign(); 168 | 169 | @AutoValue.Builder 170 | public abstract static class Builder { 171 | public abstract Builder setBundlePath(Path bundlePath); 172 | 173 | public abstract Builder setOutputPath(Path outputPath); 174 | 175 | public abstract Builder setDisableSign(Boolean disableSign); 176 | 177 | public abstract Builder setStoreFile(Path storeFile); 178 | 179 | public abstract Builder setStorePassword(String storePassword); 180 | 181 | public abstract Builder setKeyAlias(String keyAlias); 182 | 183 | public abstract Builder setKeyPassword(String keyPassword); 184 | 185 | public abstract DuplicatedResourcesMergerCommand autoBuilder(); 186 | 187 | public DuplicatedResourcesMergerCommand build() { 188 | DuplicatedResourcesMergerCommand command = autoBuilder(); 189 | checkFileExistsAndReadable(command.getBundlePath()); 190 | checkFileDoesNotExist(command.getOutputPath()); 191 | 192 | if (!command.getBundlePath().toFile().getName().endsWith(".aab")) { 193 | throw CommandExecutionException.builder() 194 | .withInternalMessage("Wrong properties: %s must end with '.aab'.", 195 | BUNDLE_LOCATION_FLAG) 196 | .build(); 197 | } 198 | 199 | if (!command.getOutputPath().toFile().getName().endsWith(".aab")) { 200 | throw CommandExecutionException.builder() 201 | .withInternalMessage("Wrong properties: %s must end with '.aab'.", 202 | OUTPUT_FILE_FLAG) 203 | .build(); 204 | } 205 | 206 | if (command.getStoreFile().isPresent()) { 207 | checkFlagPresent(command.getKeyAlias(), KEY_ALIAS_FLAG); 208 | checkFlagPresent(command.getKeyPassword(), KEY_PASSWORD_FLAG); 209 | checkFlagPresent(command.getStorePassword(), STORE_PASSWORD_FLAG); 210 | } 211 | return command; 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/executors/BundleFileFilter.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.executors; 2 | 3 | import static com.android.tools.build.bundletool.model.AppBundle.METADATA_DIRECTORY; 4 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 5 | import static org.lsposed.lspollution.utils.FileOperation.getNetFileSizeDescription; 6 | 7 | import com.android.bundle.Files; 8 | import com.android.tools.build.bundletool.model.AppBundle; 9 | import com.android.tools.build.bundletool.model.BundleMetadata; 10 | import com.android.tools.build.bundletool.model.BundleModule; 11 | import com.android.tools.build.bundletool.model.BundleModuleName; 12 | import com.android.tools.build.bundletool.model.ModuleEntry; 13 | import com.android.tools.build.bundletool.model.ZipPath; 14 | import com.google.common.collect.ImmutableMap; 15 | import com.google.common.collect.ImmutableSet; 16 | 17 | import org.lsposed.lspollution.bundle.AppBundleUtils; 18 | import org.lsposed.lspollution.bundle.NativeLibrariesOperation; 19 | import org.lsposed.lspollution.utils.TimeClock; 20 | import org.lsposed.lspollution.utils.Utils; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Path; 24 | import java.rmi.UnexpectedException; 25 | import java.util.ArrayList; 26 | import java.util.Collection; 27 | import java.util.HashMap; 28 | import java.util.HashSet; 29 | import java.util.List; 30 | import java.util.Map; 31 | import java.util.Set; 32 | import java.util.regex.Pattern; 33 | import java.util.stream.Collectors; 34 | import java.util.stream.Stream; 35 | import java.util.zip.ZipFile; 36 | 37 | /** 38 | * Created by YangJing on 2019/10/12 . 39 | * Email: yangjing.yeoh@bytedance.com 40 | */ 41 | public class BundleFileFilter { 42 | 43 | private static final Set FILE_SIGN = new HashSet<>( 44 | ImmutableSet.of( 45 | "META-INF/*.RSA", 46 | "META-INF/*.SF", 47 | "META-INF/*.MF" 48 | ) 49 | ); 50 | private final ZipFile bundleZipFile; 51 | private final AppBundle rawAppBundle; 52 | private final Set filterRules; 53 | 54 | private int filterTotalSize = 0; 55 | private int filterTotalCount = 0; 56 | 57 | public BundleFileFilter(Path bundlePath, AppBundle rawAppBundle, Set filterRules) throws IOException { 58 | checkFileExistsAndReadable(bundlePath); 59 | this.bundleZipFile = new ZipFile(bundlePath.toFile()); 60 | this.rawAppBundle = rawAppBundle; 61 | if (filterRules == null) { 62 | filterRules = new HashSet<>(); 63 | } 64 | this.filterRules = filterRules; 65 | 66 | filterRules.addAll(FILE_SIGN); 67 | } 68 | 69 | public AppBundle filter() throws IOException { 70 | TimeClock timeClock = new TimeClock(); 71 | 72 | // filter bundle module file 73 | Map bundleModules = new HashMap<>(); 74 | for (Map.Entry entry : rawAppBundle.getModules().entrySet()) { 75 | bundleModules.put(entry.getKey(), filterBundleModule(entry.getValue())); 76 | } 77 | AppBundle appBundle = rawAppBundle.toBuilder() 78 | .setBundleMetadata(filterMetaData()) 79 | .setModules(ImmutableMap.copyOf(bundleModules)) 80 | .build(); 81 | System.out.printf(""" 82 | filter bundle files done, coast %s 83 | ----------------------------------------- 84 | Reduce file count: %s 85 | Reduce file size: %s 86 | -----------------------------------------%n""", 87 | timeClock.getCoast(), 88 | filterTotalCount, 89 | getNetFileSizeDescription(filterTotalSize) 90 | ); 91 | return appBundle; 92 | } 93 | 94 | private BundleModule filterBundleModule(BundleModule bundleModule) throws IOException { 95 | BundleModule.Builder builder = bundleModule.toBuilder(); 96 | List filteredModuleEntries = new ArrayList<>(); 97 | List entries = bundleModule.getEntries().stream() 98 | .filter(entry -> { 99 | String filterRule = getMatchedFilterRule(entry.getPath()); 100 | if (filterRule != null) { 101 | checkFilteredEntry(entry, filterRule); 102 | System.out.printf("[filter] metadata file is filtered, path: %s%n", entry.getPath()); 103 | filteredModuleEntries.add(entry); 104 | filterTotalSize += AppBundleUtils.getZipEntrySize(bundleZipFile, entry, bundleModule); 105 | return false; 106 | } 107 | return true; 108 | }) 109 | .collect(Collectors.toList()); 110 | builder.setRawEntries(entries); 111 | filterTotalCount += filteredModuleEntries.size(); 112 | // update pb 113 | Files.NativeLibraries nativeLibraries = updateLibDirectory(bundleModule, filteredModuleEntries); 114 | if (nativeLibraries != null) { 115 | builder.setNativeConfig(nativeLibraries); 116 | } 117 | return builder.build(); 118 | } 119 | 120 | private Files.NativeLibraries updateLibDirectory(BundleModule bundleModule, List entries) throws UnexpectedException { 121 | List libEntries = entries.stream() 122 | .filter(entry -> entry.getPath().startsWith(BundleModule.LIB_DIRECTORY)) 123 | .toList(); 124 | Files.NativeLibraries nativeLibraries = bundleModule.getNativeConfig().orElse(null); 125 | if (libEntries.isEmpty()) { 126 | return nativeLibraries; 127 | } 128 | if (nativeLibraries == null) { 129 | throw new UnexpectedException(String.format("can not find nativeLibraries file `native.pb` in %s module", bundleModule.getName().getName())); 130 | } 131 | 132 | Files.NativeLibraries filteredNativeLibraries = nativeLibraries; 133 | for (Files.TargetedNativeDirectory directory : nativeLibraries.getDirectoryList()) { 134 | int directoryNativeSize = libEntries.stream() 135 | .filter(entry -> entry.getPath().startsWith(directory.getPath())) 136 | .toList().size(); 137 | if (directoryNativeSize > 0) { 138 | int moduleNativeSize = bundleModule.getEntries().stream() 139 | .filter(entry -> entry.getPath().startsWith(directory.getPath())) 140 | .toList().size(); 141 | if (directoryNativeSize == moduleNativeSize) { 142 | filteredNativeLibraries = NativeLibrariesOperation.removeDirectory(filteredNativeLibraries, directory.getPath()); 143 | } 144 | } 145 | } 146 | return filteredNativeLibraries; 147 | } 148 | 149 | /** 150 | * Filter meta data dir and return filtered list. 151 | */ 152 | private BundleMetadata filterMetaData() { 153 | BundleMetadata.Builder builder = BundleMetadata.builder(); 154 | Stream.of(rawAppBundle.getBundleMetadata()) 155 | .map(BundleMetadata::getFileContentMap) 156 | .map(ImmutableMap::entrySet) 157 | .flatMap(Collection::stream) 158 | .filter(entry -> { 159 | ZipPath entryZipPath = ZipPath.create(AppBundle.METADATA_DIRECTORY + "/" + entry.getKey()); 160 | if (getMatchedFilterRule(entryZipPath) != null) { 161 | System.out.printf("[filter] metadata file is filtered, path: %s%n", entryZipPath); 162 | filterTotalCount += 1; 163 | filterTotalSize += AppBundleUtils.getZipEntrySize(bundleZipFile, entryZipPath); 164 | return false; 165 | } 166 | return true; 167 | }) 168 | .forEach(entry -> builder.addFile(entry.getKey(), entry.getValue())); 169 | return builder.build(); 170 | } 171 | 172 | private void checkFilteredEntry(ModuleEntry entry, String filterRule) { 173 | if (!entry.getPath().startsWith(BundleModule.LIB_DIRECTORY) && 174 | !entry.getPath().startsWith(METADATA_DIRECTORY.toString())) { 175 | throw new UnsupportedOperationException(String.format("%s entry can not be filtered, please check the filter rule [%s].", entry.getPath(), filterRule)); 176 | } 177 | } 178 | 179 | private String getMatchedFilterRule(ZipPath zipPath) { 180 | for (String rule : filterRules) { 181 | Pattern filterPattern = Pattern.compile(Utils.convertToPatternString(rule)); 182 | if (filterPattern.matcher(zipPath.toString()).matches()) { 183 | return rule; 184 | } 185 | } 186 | return null; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/executors/BundleStringFilter.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.executors; 2 | 3 | import com.android.aapt.Resources; 4 | import com.android.tools.build.bundletool.model.AppBundle; 5 | import com.android.tools.build.bundletool.model.BundleModule; 6 | import com.android.tools.build.bundletool.model.BundleModuleName; 7 | import org.lsposed.lspollution.bundle.ResourcesTableBuilder; 8 | import org.lsposed.lspollution.utils.TimeClock; 9 | import com.google.common.collect.ImmutableMap; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.util.ArrayList; 17 | import java.util.HashMap; 18 | import java.util.HashSet; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Objects; 22 | import java.util.Set; 23 | import java.util.stream.Collectors; 24 | import java.util.zip.ZipFile; 25 | 26 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 27 | 28 | /** 29 | * Created by jiangzilai on 2019-10-20. 30 | */ 31 | public class BundleStringFilter { 32 | private final ZipFile bundleZipFile; 33 | private final AppBundle rawAppBundle; 34 | private final String unusedStrPath; 35 | private final Set languageWhiteList; 36 | private final Set unUsedNameSet = new HashSet<>(5000); 37 | 38 | private static final String replaceValue = "[value removed]"; 39 | 40 | public BundleStringFilter(Path bundlePath, AppBundle rawAppBundle, String unusedStrPath, Set languageWhiteList) 41 | throws IOException { 42 | checkFileExistsAndReadable(bundlePath); 43 | this.bundleZipFile = new ZipFile(bundlePath.toFile()); 44 | this.rawAppBundle = rawAppBundle; 45 | this.unusedStrPath = unusedStrPath; 46 | this.languageWhiteList = languageWhiteList; 47 | } 48 | 49 | public AppBundle filter() throws IOException { 50 | TimeClock timeClock = new TimeClock(); 51 | 52 | File unusedStrFile = new File(unusedStrPath); 53 | Map obfuscatedModules = new HashMap<>(); 54 | 55 | if (unusedStrFile.exists()) { 56 | //shrink结果 57 | unUsedNameSet.addAll(Files.readAllLines(Paths.get(unusedStrPath))); 58 | System.out.println("无用字符串 : " + unUsedNameSet.size()); 59 | } 60 | 61 | if (!unUsedNameSet.isEmpty() || !languageWhiteList.isEmpty()) { 62 | for (Map.Entry entry : rawAppBundle.getModules().entrySet()) { 63 | BundleModule bundleModule = entry.getValue(); 64 | BundleModuleName bundleModuleName = entry.getKey(); 65 | // obfuscate bundle module 66 | BundleModule obfuscatedModule = obfuscateBundleModule(bundleModule); 67 | obfuscatedModules.put(bundleModuleName, obfuscatedModule); 68 | } 69 | } else { 70 | return rawAppBundle; 71 | } 72 | 73 | AppBundle appBundle = rawAppBundle.toBuilder() 74 | .setModules(ImmutableMap.copyOf(obfuscatedModules)) 75 | .build(); 76 | 77 | System.out.printf("filtering strings done, coast %s\n%n", timeClock.getCoast()); 78 | 79 | return appBundle; 80 | } 81 | 82 | private BundleModule obfuscateBundleModule(BundleModule bundleModule) { 83 | BundleModule.Builder builder = bundleModule.toBuilder(); 84 | 85 | // obfuscate resourceTable 86 | Resources.ResourceTable obfuscatedResTable = obfuscateResourceTable(bundleModule); 87 | if (obfuscatedResTable != null) { 88 | builder.setResourceTable(obfuscatedResTable); 89 | } 90 | return builder.build(); 91 | } 92 | 93 | private Resources.ResourceTable obfuscateResourceTable(BundleModule bundleModule) { 94 | if (bundleModule.getResourceTable().isEmpty()) { 95 | return null; 96 | } 97 | Resources.ResourceTable rawTable = bundleModule.getResourceTable().get(); 98 | 99 | ResourcesTableBuilder tableBuilder = new ResourcesTableBuilder(); 100 | List packageList = rawTable.getPackageList(); 101 | 102 | 103 | if (packageList.isEmpty()) { 104 | return tableBuilder.build(); 105 | } 106 | 107 | for (Resources.Package resPackage : packageList) { 108 | if (resPackage == null) { 109 | continue; 110 | } 111 | ResourcesTableBuilder.PackageBuilder packageBuilder = tableBuilder.addPackage(resPackage); 112 | List typeList = resPackage.getTypeList(); 113 | Set languageFilterSet = new HashSet<>(100); 114 | List nameFilterList = new ArrayList<>(3000); 115 | for (Resources.Type resType : typeList) { 116 | if (resType == null) { 117 | continue; 118 | } 119 | List entryList = resType.getEntryList(); 120 | for (Resources.Entry resEntry : entryList) { 121 | if (resEntry == null) { 122 | continue; 123 | } 124 | 125 | if (resPackage.getPackageId().getId() == 127 && resType.getName().equals("string") && 126 | languageWhiteList != null && !languageWhiteList.isEmpty()) { 127 | //删除语言 128 | List languageValue = resEntry.getConfigValueList().stream() 129 | .filter(Objects::nonNull) 130 | .filter(configValue -> { 131 | String locale = configValue.getConfig().getLocale(); 132 | if (keepLanguage(locale)) { 133 | return true; 134 | } 135 | languageFilterSet.add(locale); 136 | return false; 137 | }).collect(Collectors.toList()); 138 | resEntry = resEntry.toBuilder().clearConfigValue().addAllConfigValue(languageValue).build(); 139 | } 140 | 141 | // 删除shrink扫描出的无用字符串 142 | if (resPackage.getPackageId().getId() == 127 && resType.getName().equals("string") 143 | && unUsedNameSet.size() > 0 && unUsedNameSet.contains(resEntry.getName())) { 144 | List proguardConfigValue = resEntry.getConfigValueList().stream() 145 | .filter(Objects::nonNull) 146 | .map(configValue -> { 147 | Resources.ConfigValue.Builder rcb = configValue.toBuilder(); 148 | Resources.Value.Builder rvb = rcb.getValueBuilder(); 149 | Resources.Item.Builder rib = rvb.getItemBuilder(); 150 | Resources.String.Builder rfb = rib.getStrBuilder(); 151 | return rcb.setValue( 152 | rvb.setItem(rib.setStr(rfb.setValue(replaceValue).build()).build()).build() 153 | ).build(); 154 | }).collect(Collectors.toList()); 155 | nameFilterList.add(resEntry.getName()); 156 | resEntry = resEntry.toBuilder().clearConfigValue().addAllConfigValue(proguardConfigValue).build(); 157 | } 158 | packageBuilder.addResource(resType, resEntry); 159 | } 160 | } 161 | System.out.println("filtering " + resPackage.getPackageName() + " id:" + resPackage.getPackageId().getId()); 162 | StringBuilder l = new StringBuilder(); 163 | for (String lan : languageFilterSet) { 164 | l.append("[remove language] : ").append(lan).append("\n"); 165 | } 166 | System.out.println(l); 167 | l = new StringBuilder(); 168 | for (String name : nameFilterList) { 169 | l.append("[delete name] ").append(name).append("\n"); 170 | } 171 | System.out.println(l); 172 | System.out.println("-----------"); 173 | packageBuilder.build(); 174 | } 175 | return tableBuilder.build(); 176 | } 177 | 178 | private boolean keepLanguage(String lan) { 179 | if (lan == null || lan.equals(" ") || lan.isEmpty()) { 180 | return true; 181 | } 182 | if (lan.contains("-")) { 183 | int index = lan.indexOf("-"); 184 | if (index != -1) { 185 | String language = lan.substring(0, index); 186 | return languageWhiteList.contains(language); 187 | } 188 | } else { 189 | return languageWhiteList.contains(lan); 190 | } 191 | return false; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/executors/DuplicatedResourcesMerger.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.executors; 2 | 3 | import com.android.aapt.Resources; 4 | import com.android.tools.build.bundletool.model.AppBundle; 5 | import com.android.tools.build.bundletool.model.BundleModule; 6 | import com.android.tools.build.bundletool.model.BundleModuleName; 7 | import com.android.tools.build.bundletool.model.ModuleEntry; 8 | import com.android.tools.build.bundletool.model.ZipPath; 9 | import com.android.tools.build.bundletool.model.utils.ResourcesUtils; 10 | 11 | import org.lsposed.lspollution.bundle.AppBundleUtils; 12 | import org.lsposed.lspollution.bundle.ResourcesTableBuilder; 13 | import org.lsposed.lspollution.bundle.ResourcesTableOperation; 14 | import org.lsposed.lspollution.utils.TimeClock; 15 | 16 | import java.io.BufferedWriter; 17 | import java.io.File; 18 | import java.io.FileWriter; 19 | import java.io.IOException; 20 | import java.io.Writer; 21 | import java.nio.file.Path; 22 | import java.util.ArrayList; 23 | import java.util.Collection; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.logging.Logger; 28 | import java.util.stream.Collectors; 29 | import java.util.stream.Stream; 30 | import java.util.zip.ZipFile; 31 | 32 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileDoesNotExist; 33 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 34 | import static org.lsposed.lspollution.utils.FileOperation.getNetFileSizeDescription; 35 | import static com.google.common.collect.ImmutableList.toImmutableList; 36 | 37 | /** 38 | * Created by YangJing on 2019/10/10 . 39 | * Email: yangjing.yeoh@bytedance.com 40 | */ 41 | public class DuplicatedResourcesMerger { 42 | public static final String SUFFIX_FILE_DUPLICATED_LOGGER = "-duplicated.txt"; 43 | private static final Logger logger = Logger.getLogger(DuplicatedResourcesMerger.class.getName()); 44 | private final Path outputLogLocationDir; 45 | private final ZipFile bundleZipFile; 46 | private final AppBundle rawAppBundle; 47 | 48 | private final Map md5FileList = new HashMap<>(); 49 | private final Map duplicatedFileList = new HashMap<>(); 50 | private int mergeDuplicatedTotalSize = 0; 51 | private int mergeDuplicatedTotalCount = 0; 52 | 53 | public DuplicatedResourcesMerger(Path bundlePath, AppBundle appBundle, Path outputLogLocationDir) throws IOException { 54 | checkFileExistsAndReadable(bundlePath); 55 | this.outputLogLocationDir = outputLogLocationDir; 56 | 57 | bundleZipFile = new ZipFile(bundlePath.toFile()); 58 | rawAppBundle = appBundle; 59 | } 60 | 61 | public AppBundle merge() throws IOException { 62 | TimeClock timeClock = new TimeClock(); 63 | 64 | List mergedBundleModuleList = new ArrayList<>(); 65 | for (Map.Entry moduleEntry : rawAppBundle.getModules().entrySet()) { 66 | mergedBundleModuleList.add(mergeBundleModule(moduleEntry.getValue())); 67 | } 68 | AppBundle mergedAppBundle = AppBundle.buildFromModules( 69 | mergedBundleModuleList.stream().collect(toImmutableList()), 70 | rawAppBundle.getBundleConfig(), 71 | rawAppBundle.getBundleMetadata() 72 | ); 73 | 74 | System.out.printf(""" 75 | merge duplicated resources done, coast %s 76 | ----------------------------------------- 77 | Reduce file count: %s 78 | Reduce file size: %s 79 | -----------------------------------------%n""", 80 | timeClock.getCoast(), 81 | mergeDuplicatedTotalCount, 82 | getNetFileSizeDescription(mergeDuplicatedTotalSize) 83 | ); 84 | return mergedAppBundle; 85 | } 86 | 87 | /** 88 | * merge duplicated resources. 89 | */ 90 | private BundleModule mergeBundleModule(BundleModule bundleModule) throws IOException { 91 | File logFile = new File(outputLogLocationDir.toFile(), bundleModule.getName().getName() + SUFFIX_FILE_DUPLICATED_LOGGER); 92 | checkFileDoesNotExist(logFile.toPath()); 93 | 94 | Resources.ResourceTable table = bundleModule.getResourceTable().orElse(Resources.ResourceTable.getDefaultInstance()); 95 | if (table.getPackageList().isEmpty() || bundleModule.getEntries().isEmpty()) { 96 | return bundleModule; 97 | } 98 | 99 | md5FileList.clear(); 100 | duplicatedFileList.clear(); 101 | 102 | List mergedModuleEntry = new ArrayList<>(); 103 | for (ModuleEntry entry : bundleModule.getEntries()) { 104 | if (!entry.getPath().startsWith(BundleModule.RESOURCES_DIRECTORY)) { 105 | mergedModuleEntry.add(entry); 106 | continue; 107 | } 108 | String md5 = AppBundleUtils.getEntryMd5(bundleZipFile, entry, bundleModule); 109 | if (md5FileList.containsKey(md5)) { 110 | duplicatedFileList.put(entry.getPath(), md5); 111 | } else { 112 | md5FileList.put(md5, entry.getPath()); 113 | mergedModuleEntry.add(entry); 114 | } 115 | } 116 | generateDuplicatedLog(logFile, bundleModule); 117 | 118 | Resources.ResourceTable mergedTable = mergeResourcesTable(table); 119 | return bundleModule.toBuilder() 120 | .setResourceTable(mergedTable) 121 | .setRawEntries(mergedModuleEntry) 122 | .build(); 123 | } 124 | 125 | /** 126 | * merge resourcesTable, remove duplicated resources. 127 | */ 128 | private Resources.ResourceTable mergeResourcesTable(Resources.ResourceTable resourceTable) { 129 | ResourcesTableBuilder resourcesTableBuilder = new ResourcesTableBuilder(); 130 | ResourcesUtils.entries(resourceTable).forEach(entry -> { 131 | ResourcesTableBuilder.PackageBuilder packageBuilder = resourcesTableBuilder.addPackage(entry.getPackage()); 132 | // replace the duplicated path 133 | List configValues = getDuplicatedMergedConfigValues(entry.getEntry()); 134 | Resources.Entry mergedEntry = ResourcesTableOperation.updateEntryConfigValueList(entry.getEntry(), configValues); 135 | packageBuilder.addResource(entry.getType(), mergedEntry); 136 | }); 137 | return resourcesTableBuilder.build(); 138 | } 139 | 140 | private List getDuplicatedMergedConfigValues(Resources.Entry entry) { 141 | return Stream.of(entry.getConfigValueList()) 142 | .flatMap(Collection::stream) 143 | .map(configValue -> { 144 | if (!configValue.getValue().getItem().hasFile()) { 145 | return configValue; 146 | } 147 | ZipPath zipPath = ZipPath.create(configValue.getValue().getItem().getFile().getPath()); 148 | if (duplicatedFileList.containsKey(zipPath)) { 149 | zipPath = md5FileList.get(duplicatedFileList.get(zipPath)); 150 | } 151 | return ResourcesTableOperation.replaceEntryPath(configValue, zipPath.toString()); 152 | }).collect(Collectors.toList()); 153 | } 154 | 155 | private void generateDuplicatedLog(File logFile, BundleModule bundleModule) throws IOException { 156 | int duplicatedSize = 0; 157 | checkFileDoesNotExist(logFile.toPath()); 158 | Writer writer = new BufferedWriter(new FileWriter(logFile, false)); 159 | writer.write("res filter path mapping:\n"); 160 | writer.flush(); 161 | for (Map.Entry entry : duplicatedFileList.entrySet()) { 162 | ZipPath keepPath = md5FileList.get(entry.getValue()); 163 | System.out.printf("[merge duplicated] found duplicated file, path: %s%n", bundleModule.getName().getName() + "/" + entry.getKey().toString()); 164 | ModuleEntry moduleEntry = bundleModule.getEntry(entry.getKey()).get(); 165 | long fileSize = AppBundleUtils.getZipEntrySize(bundleZipFile, moduleEntry, bundleModule); 166 | duplicatedSize += fileSize; 167 | writer.write( 168 | "\t" + entry.getKey().toString() 169 | + " -> " 170 | + keepPath.toString() 171 | + " (size " + getNetFileSizeDescription(fileSize) + ")" 172 | + "\n" 173 | ); 174 | } 175 | writer.write( 176 | "removed: count(" + duplicatedFileList.size() + "), totalSize(" 177 | + getNetFileSizeDescription(duplicatedSize) + ")" 178 | ); 179 | writer.close(); 180 | System.out.printf( 181 | "[merge duplicated] duplicated count %s, total size: %s%n", 182 | duplicatedFileList.size(), 183 | getNetFileSizeDescription(duplicatedSize) 184 | ); 185 | mergeDuplicatedTotalSize += duplicatedSize; 186 | mergeDuplicatedTotalCount += duplicatedFileList.size(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/model/ResourcesMapping.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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 final Map dirMapping = new HashMap<>(); 20 | private final Map resourceMapping = new HashMap<>(); 21 | private final Map entryFilesMapping = new HashMap<>(); 22 | 23 | private final Map resourcesNameToIdMapping = new HashMap<>(); 24 | private final 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.containsValue(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/org/lsposed/lspollution/model/xml/FileFilterConfig.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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 final 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/org/lsposed/lspollution/model/xml/StringFilterConfig.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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 final 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/org/lsposed/lspollution/obfuscation/ResGuardStringBuilder.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.obfuscation; 2 | 3 | import org.lsposed.lspollution.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 final 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 final 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 final 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/org/lsposed/lspollution/parser/ResourcesMappingParser.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.parser; 2 | 3 | import org.lsposed.lspollution.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/org/lsposed/lspollution/utils/FileOperation.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.utils; 2 | 3 | import static com.android.tools.build.bundletool.model.utils.files.FilePreconditions.checkFileExistsAndReadable; 4 | 5 | import com.android.tools.build.bundletool.model.ZipPath; 6 | import com.android.tools.build.bundletool.model.utils.files.FileUtils; 7 | 8 | import java.io.BufferedInputStream; 9 | import java.io.BufferedOutputStream; 10 | import java.io.File; 11 | import java.io.FileInputStream; 12 | import java.io.FileOutputStream; 13 | import java.io.IOException; 14 | import java.io.InputStream; 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | import java.text.DecimalFormat; 18 | import java.util.Enumeration; 19 | import java.util.zip.ZipEntry; 20 | import java.util.zip.ZipFile; 21 | 22 | /** 23 | * Created by YangJing on 2019/10/09 . 24 | * Email: yangjing.yeoh@bytedance.com 25 | */ 26 | public class FileOperation { 27 | 28 | private static final int BUFFER = 8192; 29 | 30 | public static boolean deleteDir(File file) { 31 | if (file == null || (!file.exists())) { 32 | return false; 33 | } 34 | if (file.isFile()) { 35 | file.delete(); 36 | } else if (file.isDirectory()) { 37 | File[] files = file.listFiles(); 38 | for (File value : files) { 39 | deleteDir(value); 40 | } 41 | } 42 | file.delete(); 43 | return true; 44 | } 45 | 46 | public static void uncompress(Path uncompressedFile, Path targetDir) throws IOException { 47 | checkFileExistsAndReadable(uncompressedFile); 48 | if (Files.exists(targetDir)) { 49 | targetDir.toFile().delete(); 50 | } else { 51 | FileUtils.createDirectories(targetDir); 52 | } 53 | try (ZipFile zipFile = new ZipFile(uncompressedFile.toFile())) { 54 | Enumeration emu = zipFile.entries(); 55 | while (emu.hasMoreElements()) { 56 | ZipEntry entry = (ZipEntry) emu.nextElement(); 57 | if (entry.isDirectory()) { 58 | FileUtils.createDirectories(new File(targetDir.toFile(), entry.getName()).toPath()); 59 | continue; 60 | } 61 | BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry)); 62 | 63 | File file = new File(targetDir.toFile() + File.separator + entry.getName()); 64 | 65 | File parent = file.getParentFile(); 66 | if (parent != null && (!parent.exists())) { 67 | FileUtils.createDirectories(parent.toPath()); 68 | } 69 | 70 | FileOutputStream fos = new FileOutputStream(file); 71 | BufferedOutputStream bos = new BufferedOutputStream(fos, BUFFER); 72 | 73 | byte[] buf = new byte[BUFFER]; 74 | int len; 75 | while ((len = bis.read(buf, 0, BUFFER)) != -1) { 76 | fos.write(buf, 0, len); 77 | } 78 | bos.flush(); 79 | bos.close(); 80 | bis.close(); 81 | } 82 | } 83 | } 84 | 85 | public static String getNetFileSizeDescription(long size) { 86 | StringBuilder bytes = new StringBuilder(); 87 | DecimalFormat format = new DecimalFormat("###.0"); 88 | if (size >= 1024 * 1024 * 1024) { 89 | double i = (size / (1024.0 * 1024.0 * 1024.0)); 90 | bytes.append(format.format(i)).append("GB"); 91 | } else if (size >= 1024 * 1024) { 92 | double i = (size / (1024.0 * 1024.0)); 93 | bytes.append(format.format(i)).append("MB"); 94 | } else if (size >= 1024) { 95 | double i = (size / (1024.0)); 96 | bytes.append(format.format(i)).append("KB"); 97 | } else { 98 | if (size <= 0) { 99 | bytes.append("0B"); 100 | } else { 101 | bytes.append((int) size).append("B"); 102 | } 103 | } 104 | return bytes.toString(); 105 | } 106 | 107 | public static long getFileSizes(File f) { 108 | long size = 0; 109 | if (f.exists() && f.isFile()) { 110 | FileInputStream fis = null; 111 | try { 112 | fis = new FileInputStream(f); 113 | size = fis.available(); 114 | } catch (IOException e) { 115 | e.printStackTrace(); 116 | } finally { 117 | try { 118 | if (fis != null) { 119 | fis.close(); 120 | } 121 | } catch (IOException e) { 122 | e.printStackTrace(); 123 | } 124 | } 125 | } 126 | return size; 127 | } 128 | 129 | public static long getZipPathFileSize(ZipFile zipFile, ZipEntry zipEntry) { 130 | long size = 0; 131 | try(InputStream is = zipFile.getInputStream(zipEntry)) { 132 | size = is.available(); 133 | } catch (IOException e) { 134 | e.printStackTrace(); 135 | } 136 | return size; 137 | } 138 | 139 | public static void copyFileUsingStream(File source, File dest) throws IOException { 140 | FileInputStream is = null; 141 | FileOutputStream os = null; 142 | File parent = dest.getParentFile(); 143 | if (parent != null && (!parent.exists())) { 144 | parent.mkdirs(); 145 | } 146 | try { 147 | is = new FileInputStream(source); 148 | os = new FileOutputStream(dest, false); 149 | 150 | byte[] buffer = new byte[BUFFER]; 151 | int length; 152 | while ((length = is.read(buffer)) > 0) { 153 | os.write(buffer, 0, length); 154 | } 155 | } finally { 156 | if (is != null) { 157 | is.close(); 158 | } 159 | if (os != null) { 160 | os.close(); 161 | } 162 | } 163 | } 164 | 165 | public static String getFileSimpleName(ZipPath zipPath) { 166 | return zipPath.getFileName().toString(); 167 | } 168 | 169 | public static String getFileSuffix(ZipPath zipPath) { 170 | String fileName = zipPath.getName(zipPath.getNameCount() - 1).toString(); 171 | if (!fileName.contains(".")) { 172 | return fileName; 173 | } 174 | String[] values = fileName.replace(".", "/").split("/"); 175 | return fileName.substring(values[0].length()); 176 | } 177 | 178 | public static String getParentFromZipFilePath(String zipPath) { 179 | if (!zipPath.contains("/")) { 180 | throw new IllegalArgumentException("invalid zipPath: " + zipPath); 181 | } 182 | String[] values = zipPath.split("/"); 183 | return zipPath.substring(0, zipPath.indexOf(values[values.length - 1]) - 1); 184 | } 185 | 186 | public static String getNameFromZipFilePath(String zipPath) { 187 | if (!zipPath.contains("/")) { 188 | throw new IllegalArgumentException("invalid zipPath: " + zipPath); 189 | } 190 | String[] values = zipPath.split("/"); 191 | return values[values.length - 1]; 192 | } 193 | 194 | public static String getFilePrefixByFileName(String fileName) { 195 | if (!fileName.contains(".")) { 196 | throw new IllegalArgumentException("invalid file name: " + fileName); 197 | } 198 | String[] values = fileName.replace(".", "/").split("/"); 199 | return values[0]; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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/org/lsposed/lspollution/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 org.lsposed.lspollution.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/org/lsposed/lspollution/utils/TimeClock.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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 final 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/org/lsposed/lspollution/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.utils; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.io.LineNumberReader; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.HashSet; 13 | import java.util.Iterator; 14 | import java.util.Map; 15 | import java.util.Set; 16 | import java.util.regex.Pattern; 17 | 18 | public class Utils { 19 | public static boolean isPresent(String str) { 20 | return str != null && str.length() > 0; 21 | } 22 | 23 | public static boolean isBlank(String str) { 24 | return !isPresent(str); 25 | } 26 | 27 | public static boolean isPresent(Iterator iterator) { 28 | return iterator != null && iterator.hasNext(); 29 | } 30 | 31 | public static boolean isBlank(Iterator iterator) { 32 | return !isPresent(iterator); 33 | } 34 | 35 | public static String convertToPatternString(String input) { 36 | // ? Zero or one character 37 | // * Zero or more of character 38 | // + One or more of character 39 | final String[] searchList = new String[]{".", "?", "*", "+"}; 40 | final String[] replacementList = new String[]{"\\.", ".?", ".*", ".+"}; 41 | return replaceEach(input, searchList, replacementList); 42 | } 43 | 44 | public static boolean match(String str, HashSet patterns) { 45 | if (patterns == null) { 46 | return false; 47 | } 48 | for (Pattern p : patterns) { 49 | Boolean isMatch = p.matcher(str).matches(); 50 | if (isMatch) return true; 51 | } 52 | return false; 53 | } 54 | 55 | public static void cleanDir(File dir) { 56 | if (dir.exists()) { 57 | FileOperation.deleteDir(dir); 58 | dir.mkdirs(); 59 | } 60 | } 61 | 62 | public static String readInputStream(InputStream inputStream) throws IOException { 63 | ByteArrayOutputStream result = new ByteArrayOutputStream(); 64 | byte[] buffer = new byte[4096]; 65 | int length; 66 | while ((length = inputStream.read(buffer)) != -1) { 67 | result.write(buffer, 0, length); 68 | } 69 | return result.toString(StandardCharsets.UTF_8); 70 | } 71 | 72 | public static String runCmd(String... cmd) throws IOException, InterruptedException { 73 | String output; 74 | Process process = null; 75 | try { 76 | process = new ProcessBuilder(cmd).start(); 77 | output = readInputStream(process.getInputStream()); 78 | process.waitFor(); 79 | if (process.exitValue() != 0) { 80 | System.err.printf("%s Failed! Please check your signature file.\n%n", cmd[0]); 81 | throw new RuntimeException(readInputStream(process.getErrorStream())); 82 | } 83 | } finally { 84 | if (process != null) { 85 | process.destroy(); 86 | } 87 | } 88 | return output; 89 | } 90 | 91 | public static String runExec(String[] argv) throws IOException, InterruptedException { 92 | Process process = null; 93 | String output; 94 | try { 95 | process = Runtime.getRuntime().exec(argv); 96 | output = readInputStream(process.getInputStream()); 97 | process.waitFor(); 98 | if (process.exitValue() != 0) { 99 | System.err.printf("%s Failed! Please check your signature file.\n%n", argv[0]); 100 | throw new RuntimeException(readInputStream(process.getErrorStream())); 101 | } 102 | } finally { 103 | if (process != null) { 104 | process.destroy(); 105 | } 106 | } 107 | return output; 108 | } 109 | 110 | private static void processOutputStreamInThread(Process process) throws IOException { 111 | InputStreamReader ir = new InputStreamReader(process.getInputStream()); 112 | LineNumberReader input = new LineNumberReader(ir); 113 | //如果不读会有问题,被阻塞 114 | while (input.readLine() != null) { 115 | } 116 | } 117 | 118 | private static String replaceEach(String text, String[] searchList, String[] replacementList) { 119 | // TODO: throw new IllegalArgumentException() if any param doesn't make sense 120 | //validateParams(text, searchList, replacementList); 121 | 122 | SearchTracker tracker = new SearchTracker(text, searchList, replacementList); 123 | if (!tracker.hasNextMatch(0)) { 124 | return text; 125 | } 126 | 127 | StringBuilder buf = new StringBuilder(text.length() * 2); 128 | int start = 0; 129 | 130 | do { 131 | SearchTracker.MatchInfo matchInfo = tracker.matchInfo; 132 | int textIndex = matchInfo.textIndex; 133 | String pattern = matchInfo.pattern; 134 | String replacement = matchInfo.replacement; 135 | 136 | buf.append(text, start, textIndex); 137 | buf.append(replacement); 138 | 139 | start = textIndex + pattern.length(); 140 | } while (tracker.hasNextMatch(start)); 141 | 142 | return buf.append(text.substring(start)).toString(); 143 | } 144 | 145 | static class SearchTracker { 146 | 147 | final String text; 148 | 149 | final Map patternToReplacement = new HashMap<>(); 150 | final Set pendingPatterns = new HashSet<>(); 151 | 152 | MatchInfo matchInfo = null; 153 | 154 | SearchTracker(String text, String[] searchList, String[] replacementList) { 155 | this.text = text; 156 | for (int i = 0; i < searchList.length; ++i) { 157 | String pattern = searchList[i]; 158 | patternToReplacement.put(pattern, replacementList[i]); 159 | pendingPatterns.add(pattern); 160 | } 161 | } 162 | 163 | boolean hasNextMatch(int start) { 164 | int textIndex = -1; 165 | String nextPattern = null; 166 | 167 | for (String pattern : new ArrayList<>(pendingPatterns)) { 168 | int matchIndex = text.indexOf(pattern, start); 169 | if (matchIndex == -1) { 170 | pendingPatterns.remove(pattern); 171 | } else { 172 | if (textIndex == -1 || matchIndex < textIndex) { 173 | textIndex = matchIndex; 174 | nextPattern = pattern; 175 | } 176 | } 177 | } 178 | 179 | if (nextPattern != null) { 180 | matchInfo = new MatchInfo(nextPattern, patternToReplacement.get(nextPattern), textIndex); 181 | return true; 182 | } 183 | return false; 184 | } 185 | 186 | private static class MatchInfo { 187 | final String pattern; 188 | final String replacement; 189 | final int textIndex; 190 | 191 | MatchInfo(String pattern, String replacement, int textIndex) { 192 | this.pattern = pattern; 193 | this.replacement = replacement; 194 | this.textIndex = textIndex; 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /core/src/main/java/org/lsposed/lspollution/utils/exception/CommandExceptionPreconditions.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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 | .withInternalMessage("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/org/lsposed/lspollution/BaseTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution; 2 | 3 | 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.rules.TemporaryFolder; 9 | import org.junit.runner.RunWith; 10 | import org.junit.runners.JUnit4; 11 | import org.lsposed.lspollution.testing.ProcessThread; 12 | 13 | import java.io.File; 14 | import java.io.InputStream; 15 | import java.io.Reader; 16 | import java.nio.file.Path; 17 | 18 | /** 19 | * Created by YangJing on 2019/04/14 . 20 | * Email: yangjing.yeoh@bytedance.com 21 | */ 22 | @RunWith(JUnit4.class) 23 | public class BaseTest { 24 | 25 | private static final boolean DEBUG = true; 26 | 27 | @Rule 28 | public final TemporaryFolder tmp = new TemporaryFolder(); // 注意,临时文件夹在执行完毕之后会被自动删除! 29 | 30 | private Path tmpDir; 31 | private long startTime; 32 | 33 | protected static File loadResourceFile(String path) { 34 | return TestData.openFile(path); 35 | } 36 | 37 | protected static Reader loadResourceReader(String path) { 38 | return TestData.openReader(path); 39 | } 40 | 41 | protected static InputStream loadResourceStream(String path) { 42 | return TestData.openStream(path); 43 | } 44 | 45 | protected static String loadResourcePath(String path) { 46 | return TestData.resourcePath(path); 47 | } 48 | 49 | protected static boolean executeCmd(String cmd, Object... objects) { 50 | return ProcessThread.execute(cmd, objects); 51 | } 52 | 53 | protected static void openDir(String dir, Object... objects) { 54 | if (!DEBUG) { 55 | return; 56 | } 57 | ProcessThread.execute("open " + dir, objects); 58 | } 59 | 60 | @Before 61 | public void setUp() { 62 | tmpDir = tmp.getRoot().toPath(); 63 | startTime = System.currentTimeMillis(); 64 | } 65 | 66 | @After 67 | public void tearDown() { 68 | System.out.println(System.currentTimeMillis() - startTime); 69 | } 70 | 71 | /** 72 | * 返回临时路径 73 | */ 74 | protected Path getTempDirPath() { 75 | return tmpDir; 76 | } 77 | 78 | protected String getTempDirFilePath() { 79 | return tmpDir.toFile().toString(); 80 | } 81 | 82 | @Test 83 | public void emptyTest() { 84 | assert true; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /core/src/test/java/org/lsposed/lspollution/TestData.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution;/* 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(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 | static byte[] readBytes(String fileName) { 83 | try (InputStream inputStream = openStream(fileName)) { 84 | return ByteStreams.toByteArray(inputStream); 85 | } catch (IOException e) { 86 | // Throw an unchecked exception to allow usage in lambda expressions. 87 | throw new UncheckedIOException( 88 | String.format("Failed to read contents of com.bytedance.android.aabresguard file '%s'.", fileName), e); 89 | } 90 | } 91 | 92 | static File openFile(String fileName) { 93 | String filePath = getUrl(fileName).getFile(); 94 | checkArgument(filePath != null, "Testdata file '%s' not found.", fileName); 95 | return new File(filePath); 96 | } 97 | 98 | static String resourcePath(String resourceName) { 99 | return openFile(resourceName).getPath(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /core/src/test/java/org/lsposed/lspollution/bundle/AppBundlePackagerTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.bundle; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import org.lsposed.lspollution.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/org/lsposed/lspollution/bundle/AppBundleSignerTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.bundle; 2 | 3 | import org.lsposed.lspollution.BaseTest; 4 | import org.lsposed.lspollution.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/org/lsposed/lspollution/bundle/AppBundleUtilsTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.bundle; 2 | 3 | import org.lsposed.lspollution.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/org/lsposed/lspollution/commands/DuplicatedResourcesMergerCommandTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.commands; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertThrows; 5 | 6 | import com.android.tools.build.bundletool.flags.Flag; 7 | import com.android.tools.build.bundletool.flags.FlagParser; 8 | import org.lsposed.lspollution.BaseTest; 9 | 10 | import org.junit.Test; 11 | import org.lsposed.lspollution.utils.FileOperation; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 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 | assertEquals("Missing the required --bundle flag.", flagsException.getMessage()); 31 | } 32 | 33 | @Test 34 | public void test_no_bundle() { 35 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 36 | () -> DuplicatedResourcesMergerCommand.fromFlags( 37 | new FlagParser().parse( 38 | "--output=" + getTempDirFilePath() 39 | ) 40 | ).execute()); 41 | assertEquals("Missing the required --bundle flag.", flagsException.getMessage()); 42 | } 43 | 44 | @Test 45 | public void test_no_Output() { 46 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 47 | () -> DuplicatedResourcesMergerCommand.fromFlags( 48 | new FlagParser().parse( 49 | "--bundle=" + loadResourcePath("demo/demo.aab") 50 | ) 51 | ).execute()); 52 | assertEquals("Missing the required --output flag.", flagsException.getMessage()); 53 | } 54 | 55 | @Test 56 | public void test_notExists_bundle() { 57 | String tempPath = getTempDirFilePath(); 58 | File apkFile = new File(tempPath + "abc.apk"); 59 | IllegalArgumentException flagsException = assertThrows(IllegalArgumentException.class, 60 | () -> DuplicatedResourcesMergerCommand.fromFlags( 61 | new FlagParser().parse( 62 | "--bundle=" + apkFile.getAbsolutePath(), 63 | "--output=" + getTempDirFilePath() 64 | ) 65 | ).execute()); 66 | assertEquals(String.format("File '%s' was not found.", apkFile.getAbsolutePath()), flagsException.getMessage()); 67 | } 68 | 69 | @Test 70 | public void test_wrong_params() { 71 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 72 | () -> DuplicatedResourcesMergerCommand.fromFlags( 73 | new FlagParser().parse( 74 | "--abc=a" 75 | ) 76 | ).execute()); 77 | assertEquals("Missing the required --bundle flag.", flagsException.getMessage()); 78 | } 79 | 80 | @Test 81 | public void test_disableSign() throws IOException, InterruptedException { 82 | File rawAabFile = loadResourceFile("demo/demo.aab"); 83 | File outputFile = new File(getTempDirPath().toFile(), "duplicated.aab"); 84 | DuplicatedResourcesMergerCommand.fromFlags( 85 | new FlagParser().parse( 86 | "--bundle=" + rawAabFile.getAbsolutePath(), 87 | "--output=" + outputFile.getAbsolutePath(), 88 | "--disable-sign=true" 89 | ) 90 | ).execute(); 91 | assert outputFile.exists(); 92 | } 93 | 94 | @Test 95 | public void testMergeDuplicatedRes() throws IOException, InterruptedException { 96 | File rawAabFile = loadResourceFile("demo/demo.aab"); 97 | File outputFile = new File(getTempDirPath().toFile(), "duplicated.aab"); 98 | DuplicatedResourcesMergerCommand.fromFlags( 99 | new FlagParser().parse( 100 | "--bundle=" + rawAabFile.getAbsolutePath(), 101 | "--output=" + outputFile.getAbsolutePath() 102 | ) 103 | ).execute(); 104 | assert outputFile.exists(); 105 | assert FileOperation.getFileSizes(rawAabFile) > FileOperation.getFileSizes(outputFile); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /core/src/test/java/org/lsposed/lspollution/commands/FileFilterCommandTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.commands; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertThrows; 5 | 6 | import com.android.tools.build.bundletool.flags.Flag; 7 | import com.android.tools.build.bundletool.flags.FlagParser; 8 | import org.lsposed.lspollution.BaseTest; 9 | 10 | import org.junit.Test; 11 | import org.lsposed.lspollution.utils.FileOperation; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | 16 | /** 17 | * Created by YangJing on 2019/10/14 . 18 | * Email: yangjing.yeoh@bytedance.com 19 | */ 20 | public class FileFilterCommandTest extends BaseTest { 21 | @Test 22 | public void test_noFlag() { 23 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 24 | () -> FileFilterCommand.fromFlags( 25 | new FlagParser().parse( 26 | "" 27 | ) 28 | ).execute()); 29 | assertEquals("Missing the required --bundle flag.", flagsException.getMessage()); 30 | } 31 | 32 | @Test 33 | public void test_no_bundle() { 34 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 35 | () -> FileFilterCommand.fromFlags( 36 | new FlagParser().parse( 37 | "--output=" + getTempDirFilePath() 38 | ) 39 | ).execute()); 40 | assertEquals("Missing the required --bundle flag.", flagsException.getMessage()); 41 | } 42 | 43 | @Test 44 | public void test_no_Config() { 45 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 46 | () -> FileFilterCommand.fromFlags( 47 | new FlagParser().parse( 48 | "--bundle=" + loadResourcePath("demo/demo.aab") 49 | ) 50 | ).execute()); 51 | assertEquals("Missing the required --config flag.", flagsException.getMessage()); 52 | } 53 | 54 | 55 | @Test 56 | public void test_wrong_params() { 57 | Flag.RequiredFlagNotSetException flagsException = assertThrows(Flag.RequiredFlagNotSetException.class, 58 | () -> FileFilterCommand.fromFlags( 59 | new FlagParser().parse( 60 | "--abc=a" 61 | ) 62 | ).execute()); 63 | assertEquals("Missing the required --bundle flag.", flagsException.getMessage()); 64 | } 65 | 66 | @Test 67 | public void test_disableSign() throws IOException, InterruptedException { 68 | File rawAabFile = loadResourceFile("demo/demo.aab"); 69 | File outputFile = new File(getTempDirPath().toFile(), "filtered.aab"); 70 | FileFilterCommand.fromFlags( 71 | new FlagParser().parse( 72 | "--bundle=" + rawAabFile.getAbsolutePath(), 73 | "--output=" + outputFile.getAbsolutePath(), 74 | "--config="+loadResourcePath("demo/config-filter.xml"), 75 | "--disable-sign=true" 76 | ) 77 | ).execute(); 78 | assert outputFile.exists(); 79 | } 80 | 81 | @Test 82 | public void testPass() throws IOException, InterruptedException { 83 | File rawAabFile = loadResourceFile("demo/demo.aab"); 84 | File outputFile = new File(getTempDirPath().toFile(), "filtered.aab"); 85 | FileFilterCommand.fromFlags( 86 | new FlagParser().parse( 87 | "--bundle=" + rawAabFile.getAbsolutePath(), 88 | "--output=" + outputFile.getAbsolutePath(), 89 | "--config="+loadResourcePath("demo/config-filter.xml") 90 | ) 91 | ).execute(); 92 | assert outputFile.exists(); 93 | assert FileOperation.getFileSizes(rawAabFile) > FileOperation.getFileSizes(outputFile); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /core/src/test/java/org/lsposed/lspollution/executors/BundleFileFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.executors; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import org.lsposed.lspollution.BaseTest; 5 | import org.lsposed.lspollution.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/org/lsposed/lspollution/executors/BundleStringFilterTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.executors; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import org.lsposed.lspollution.BaseTest; 5 | import org.lsposed.lspollution.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/org/lsposed/lspollution/executors/DuplicatedResourcesMergerTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.executors; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import org.lsposed.lspollution.BaseTest; 5 | import org.lsposed.lspollution.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/org/lsposed/lspollution/executors/ResourcesObfuscatorTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.executors; 2 | 3 | import com.android.tools.build.bundletool.model.AppBundle; 4 | import com.android.tools.build.bundletool.model.BundleModule; 5 | import org.lsposed.lspollution.BaseTest; 6 | import org.lsposed.lspollution.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/org/lsposed/lspollution/parser/ResourcesMappingParserTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.parser; 2 | 3 | import org.lsposed.lspollution.BaseTest; 4 | import org.lsposed.lspollution.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/org/lsposed/lspollution/testing/ProcessThread.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.testing; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.util.Objects; 8 | 9 | /** 10 | * Used to execute shell cmd 11 | *

12 | * Created by YangJing on 2019/04/11 . 13 | * Email: yangjing.yeoh@bytedance.com 14 | */ 15 | public class ProcessThread extends Thread { 16 | 17 | 18 | private final InputStream is; 19 | private final String printType; 20 | 21 | ProcessThread(InputStream is, String printType) { 22 | this.is = is; 23 | this.printType = printType; 24 | } 25 | 26 | public static boolean execute(String cmd) { 27 | try { 28 | Process process = Runtime.getRuntime().exec(cmd); 29 | new ProcessThread(process.getInputStream(), "INFO").start(); 30 | new ProcessThread(process.getErrorStream(), "ERR").start(); 31 | int value = process.waitFor(); 32 | return value == 0; 33 | } catch (IOException | InterruptedException e) { 34 | e.printStackTrace(); 35 | } 36 | return false; 37 | } 38 | 39 | public static boolean execute(String cmd, Object... objects) { 40 | return execute(String.format(cmd, objects)); 41 | } 42 | 43 | @Override 44 | public void run() { 45 | try { 46 | InputStreamReader isr = new InputStreamReader(is); 47 | BufferedReader br = new BufferedReader(isr); 48 | String line; 49 | while ((line = br.readLine()) != null) { 50 | System.out.println(printType + ">" + line); 51 | } 52 | } catch (IOException ioe) { 53 | ioe.printStackTrace(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/test/java/org/lsposed/lspollution/utils/FileOperationTest.java: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.utils; 2 | 3 | 4 | import org.lsposed.lspollution.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/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/LSPosed/LSPollution/07e8c99a7bfa17d165cfaaf62040b69f621b5400/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/LSPosed/LSPollution/07e8c99a7bfa17d165cfaaf62040b69f621b5400/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 | -------------------------------------------------------------------------------- /gradle-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed 4 | plugins { 5 | idea 6 | alias(libs.plugins.kotlin) 7 | `java-gradle-plugin` 8 | `maven-publish` 9 | signing 10 | } 11 | 12 | dependencies { 13 | // TODO: Replace with `libs.agp` 14 | compileOnly(libs.agp.impl) 15 | implementation(projects.core) 16 | } 17 | 18 | val generatedDir = File(projectDir, "generated") 19 | val generatedJavaSourcesDir = File(generatedDir, "main/java") 20 | 21 | val genTask = tasks.register("generateBuildClass") { 22 | inputs.property("version", version) 23 | outputs.dir(generatedDir) 24 | doLast { 25 | val buildClassFile = 26 | File(generatedJavaSourcesDir, "org/lsposed/lspollution/plugin/Build.java") 27 | buildClassFile.parentFile.mkdirs() 28 | buildClassFile.writeText( 29 | """ 30 | package org.lsposed.lspollution.plugin; 31 | /** 32 | * The type Build. 33 | */ 34 | public class Build { 35 | /** 36 | * The constant VERSION. 37 | */ 38 | public static final String VERSION = "$version"; 39 | }""".trimIndent() 40 | ) 41 | } 42 | } 43 | 44 | sourceSets { 45 | main { 46 | java { 47 | srcDir(generatedJavaSourcesDir) 48 | } 49 | } 50 | } 51 | 52 | tasks.withType(KotlinCompile::class.java) { 53 | dependsOn(genTask) 54 | } 55 | 56 | tasks.withType(Jar::class.java) { 57 | dependsOn(genTask) 58 | } 59 | 60 | idea { 61 | module { 62 | generatedSourceDirs.add(generatedJavaSourcesDir) 63 | } 64 | } 65 | 66 | publish { 67 | githubRepo = "LSPosed/LSPollution" 68 | publishPlugin("$group", rootProject.name, "org.lsposed.lspollution.plugin.LSPollutionPlugin") { 69 | name.set(rootProject.name) 70 | description.set("Resource obfuscator for Android applications") 71 | url.set("https://github.com/LSPosed/LSPollution") 72 | licenses { 73 | license { 74 | name.set("Apache License 2.0") 75 | url.set("https://github.com/LSPosed/LSPollution/blob/master/LICENSE.txt") 76 | } 77 | } 78 | developers { 79 | developer { 80 | name.set("LSPosed") 81 | url.set("https://lsposed.org") 82 | } 83 | } 84 | scm { 85 | connection.set("scm:git:https://github.com/LSPosed/LSPollution.git") 86 | url.set("https://github.com/LSPosed/LSPollution") 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/org/lsposed/lspollution/plugin/LSPollutionExtension.kt: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.plugin 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 LSPollutionExtension { 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 "LSPollutionExtension\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 | } 35 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/org/lsposed/lspollution/plugin/LSPollutionPlugin.kt: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.plugin 2 | 3 | import com.android.build.gradle.AppExtension 4 | import com.android.build.gradle.api.ApplicationVariant 5 | import org.gradle.api.GradleException 6 | import org.gradle.api.Plugin 7 | import org.gradle.api.Project 8 | import org.gradle.api.Task 9 | import org.gradle.configurationcache.extensions.capitalized 10 | 11 | /** 12 | * Created by YangJing on 2019/10/15 . 13 | * Email: yangjing.yeoh@bytedance.com 14 | */ 15 | class LSPollutionPlugin : Plugin { 16 | 17 | override fun apply(project: Project) { 18 | checkApplicationPlugin(project) 19 | project.extensions.create("lspollution", LSPollutionExtension::class.java) 20 | 21 | val android = project.extensions.getByName("android") as AppExtension 22 | project.afterEvaluate { 23 | android.applicationVariants.all { variant -> 24 | createLSPollutionTask(project, variant) 25 | } 26 | } 27 | } 28 | 29 | private fun createLSPollutionTask(project: Project, variant: ApplicationVariant) { 30 | val variantName = variant.name.capitalized() 31 | val bundleTaskName = "bundle$variantName" 32 | if (project.tasks.findByName(bundleTaskName) == null) { 33 | return 34 | } 35 | val lspollutionTaskName = "lspollution$variantName" 36 | val lspollutionTask = if (project.tasks.findByName(lspollutionTaskName) == null) { 37 | project.tasks.create(lspollutionTaskName, LSPollutionTask::class.java) 38 | } else { 39 | project.tasks.getByName(lspollutionTaskName) as LSPollutionTask 40 | } 41 | lspollutionTask.setVariantScope(variant) 42 | 43 | val bundleTask: Task = project.tasks.getByName(bundleTaskName) 44 | val bundlePackageTask: Task = project.tasks.getByName("package${variantName}Bundle") 45 | bundleTask.dependsOn(lspollutionTask) 46 | lspollutionTask.dependsOn(bundlePackageTask) 47 | // AGP-4.0.0-alpha07: use FinalizeBundleTask to sign bundle file 48 | // FinalizeBundleTask is executed after PackageBundleTask 49 | val finalizeBundleTaskName = "sign${variantName}Bundle" 50 | if (project.tasks.findByName(finalizeBundleTaskName) != null) { 51 | lspollutionTask.dependsOn(project.tasks.getByName(finalizeBundleTaskName)) 52 | } 53 | } 54 | 55 | private fun checkApplicationPlugin(project: Project) { 56 | if (!project.plugins.hasPlugin("com.android.application")) { 57 | throw GradleException("Android Application plugin required") 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/org/lsposed/lspollution/plugin/LSPollutionTask.kt: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.plugin 2 | 3 | import com.android.build.gradle.api.ApplicationVariant 4 | import org.gradle.api.DefaultTask 5 | import org.gradle.api.tasks.TaskAction 6 | import org.lsposed.lspollution.commands.ObfuscateBundleCommand 7 | import org.lsposed.lspollution.plugin.internal.getBundleFilePath 8 | import org.lsposed.lspollution.plugin.internal.getSigningConfig 9 | import org.lsposed.lspollution.plugin.model.SigningConfig 10 | import java.io.File 11 | import java.nio.file.Path 12 | 13 | /** 14 | * Created by YangJing on 2019/10/15 . 15 | * Email: yangjing.yeoh@bytedance.com 16 | */ 17 | abstract class LSPollutionTask : DefaultTask() { 18 | 19 | private lateinit var variant: ApplicationVariant 20 | 21 | private lateinit var signingConfig: SigningConfig 22 | 23 | private lateinit var bundlePath: Path 24 | 25 | private lateinit var obfuscatedBundlePath: Path 26 | 27 | private val lspollution = project.extensions.getByName("lspollution") as LSPollutionExtension 28 | 29 | init { 30 | description = "Assemble resource proguard for bundle file" 31 | group = "bundle" 32 | outputs.upToDateWhen { false } 33 | } 34 | 35 | fun setVariantScope(variant: ApplicationVariant) { 36 | this.variant = variant 37 | // init bundleFile, obfuscatedBundlePath must init before task action. 38 | bundlePath = getBundleFilePath(project, variant) 39 | obfuscatedBundlePath = File(bundlePath.toFile().parentFile, lspollution.obfuscatedBundleFileName).toPath() 40 | } 41 | 42 | @TaskAction 43 | private fun execute() { 44 | println(lspollution.toString()) 45 | // init signing config 46 | signingConfig = getSigningConfig(variant) 47 | printSignConfiguration() 48 | 49 | prepareUnusedFile() 50 | 51 | val command = ObfuscateBundleCommand.builder() 52 | .setEnableObfuscate(lspollution.enableObfuscate) 53 | .setBundlePath(bundlePath) 54 | .setOutputPath(obfuscatedBundlePath) 55 | .setMergeDuplicatedResources(lspollution.mergeDuplicatedRes) 56 | .setWhiteList(lspollution.whiteList) 57 | .setFilterFile(lspollution.enableFilterFiles) 58 | .setFileFilterRules(lspollution.filterList) 59 | .setRemoveStr(lspollution.enableFilterStrings) 60 | .setUnusedStrPath(lspollution.unusedStringPath) 61 | .setLanguageWhiteList(lspollution.languageWhiteList) 62 | if (lspollution.mappingFile != null) { 63 | command.setMappingPath(lspollution.mappingFile) 64 | } 65 | 66 | if (signingConfig.storeFile != null && signingConfig.storeFile!!.exists()) { 67 | command.setStoreFile(signingConfig.storeFile!!.toPath()) 68 | .setKeyAlias(signingConfig.keyAlias) 69 | .setKeyPassword(signingConfig.keyPassword) 70 | .setStorePassword(signingConfig.storePassword) 71 | } 72 | command.build().execute() 73 | } 74 | 75 | private fun prepareUnusedFile() { 76 | val simpleName = variant.name.replace("Release", "") 77 | val name = simpleName[0].lowercaseChar() + simpleName.substring(1) 78 | val resourcePath = "${project.buildDir}/outputs/mapping/$name/release/unused.txt" 79 | val usedFile = File(resourcePath) 80 | if (usedFile.exists()) { 81 | println("find unused.txt : ${usedFile.absolutePath}") 82 | if (lspollution.enableFilterStrings) { 83 | if (lspollution.unusedStringPath == null || lspollution.unusedStringPath!!.isBlank()) { 84 | lspollution.unusedStringPath = usedFile.absolutePath 85 | println("replace unused.txt!") 86 | } 87 | } 88 | } else { 89 | println( 90 | "not exists unused.txt : ${usedFile.absolutePath}\n" + 91 | "use default path : ${lspollution.unusedStringPath}" 92 | ) 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 | } 113 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/org/lsposed/lspollution/plugin/internal/BundleResolution.kt: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.plugin.internal 2 | 3 | import com.android.build.gradle.api.ApplicationVariant 4 | import org.gradle.api.Project 5 | import org.gradle.configurationcache.extensions.capitalized 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 variantCapped = variant.name.capitalized() 15 | // use FinalizeBundleTask to sign bundle file 16 | val finalizeBundleTask = project.tasks.getByName("sign${variantCapped}Bundle") 17 | // FinalizeBundleTask.finalBundleFile is the final bundle path 18 | val bundleFile = finalizeBundleTask.property("finalBundleFile") 19 | val regularFile = bundleFile!!::class.java.getMethod("get").invoke(bundleFile) 20 | val path = regularFile::class.java.getMethod("getAsFile").invoke(regularFile) as File 21 | return path.toPath() 22 | } 23 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/org/lsposed/lspollution/plugin/internal/SigningConfigResolution.kt: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.plugin.internal 2 | 3 | import com.android.build.gradle.api.ApplicationVariant 4 | import org.lsposed.lspollution.plugin.model.SigningConfig 5 | 6 | /** 7 | * Created by YangJing on 2020/01/06 . 8 | * Email: yangjing.yeoh@bytedance.com 9 | */ 10 | internal fun getSigningConfig(variant: ApplicationVariant): SigningConfig { 11 | return SigningConfig( 12 | variant.signingConfig.storeFile, 13 | variant.signingConfig.storePassword, 14 | variant.signingConfig.keyAlias, 15 | variant.signingConfig.keyPassword 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/org/lsposed/lspollution/plugin/model/SigningConfig.kt: -------------------------------------------------------------------------------- 1 | package org.lsposed.lspollution.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 | ) 15 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/resources/META-INF/gradle-plugins/com.bytedance.android.aabResGuard.properties: -------------------------------------------------------------------------------- 1 | implementation-class=org.lsposed.lspollution.plugin.LSPollutionPlugin -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSPosed/LSPollution/07e8c99a7bfa17d165cfaaf62040b69f621b5400/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/LSPosed/LSPollution/07e8c99a7bfa17d165cfaaf62040b69f621b5400/samples/app/src/main/res/drawable/ic_abc.png -------------------------------------------------------------------------------- /samples/app/src/main/res/drawable/ic_bcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSPosed/LSPollution/07e8c99a7bfa17d165cfaaf62040b69f621b5400/samples/app/src/main/res/drawable/ic_bcd.png -------------------------------------------------------------------------------- /samples/app/src/main/res/drawable/ic_keep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSPosed/LSPollution/07e8c99a7bfa17d165cfaaf62040b69f621b5400/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 -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | gradlePluginPortal() 6 | google() 7 | mavenCentral() 8 | } 9 | } 10 | 11 | dependencyResolutionManagement { 12 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | 18 | versionCatalogs { 19 | create("libs") { 20 | library("aapt2-proto", "com.android.tools.build:aapt2-proto:7.4.2-8841542") 21 | library("agp", "com.android.tools.build:gradle-api:7.4.2") 22 | library("agp-impl", "com.android.tools.build:gradle:7.4.2") 23 | library("androidx-annotation", "androidx.annotation:annotation:1.6.0") 24 | library("auto-value", "com.google.auto.value:auto-value:1.8.2") 25 | library("auto-value-annotations", "com.google.auto.value:auto-value-annotations:1.10.1") 26 | library("bundletool", "com.android.tools.build:bundletool:1.14.0") 27 | library("commons-codec", "commons-codec:commons-codec:1.5") 28 | library("guava", "com.google.guava:guava:31.1-jre") 29 | library("junit", "junit:junit:4.13.2") 30 | library("protobuf-java", "com.google.protobuf:protobuf-java:3.21.12") 31 | plugin("lsplugin-publish", "org.lsposed.lsplugin.publish").version("1.1") 32 | plugin("kotlin", "org.jetbrains.kotlin.jvm").version("1.8.10") 33 | } 34 | } 35 | } 36 | rootProject.name = "lspollution" 37 | 38 | include(":core", ":gradle-plugin") 39 | 40 | //include ':core', ':plugin', ':app', ':df_module1', ':df_module2' 41 | // 42 | //settings.project(":app").projectDir = file("$rootDir/samples/app") 43 | //settings.project(":df_module1").projectDir = file("$rootDir/samples/dynamic-features/df_module1") 44 | //settings.project(":df_module2").projectDir = file("$rootDir/samples/dynamic-features/df_module2") 45 | // 46 | -------------------------------------------------------------------------------- /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/LSPosed/LSPollution/07e8c99a7bfa17d165cfaaf62040b69f621b5400/wiki/images/logo.png -------------------------------------------------------------------------------- /wiki/images/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LSPosed/LSPollution/07e8c99a7bfa17d165cfaaf62040b69f621b5400/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 | --------------------------------------------------------------------------------