Gradle plugin for Android App Bundle resource obfuscation, duplicate resource merging, file filtering, and string cleanup.
ResChiper post-processes an Android .aab after the normal bundle task runs. It can:
- obfuscate resource file and directory names
- reuse a previous mapping file for stable obfuscation output
- merge duplicate bundled resources to reduce size
- filter selected files from
META-INF/andlib/ - remove unused string values and non-whitelisted locales
The plugin only supports Android application modules and works on App Bundles, not APKs.
- JDK 17
- Android Gradle Plugin 8.x
- An Android app module using
com.android.application - A build that produces an
.aabfor the target variant
Published artifact:
io.github.goldfish07.reschiper:plugin:<version>
Recommended Kotlin DSL usage:
plugins {
id("com.android.application")
id("io.github.goldfish07.reschiper") version "0.1.0-rc7.3"
}Legacy classpath usage is also supported. Add the dependency in the root build script:
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("io.github.goldfish07.reschiper:plugin:0.1.0-rc7.3")
}
}Then apply the plugin in the Android application module:
plugins {
id("com.android.application")
}
apply(plugin = "io.github.goldfish07.reschiper")Groovy DSL:
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "io.github.goldfish07.reschiper:plugin:0.1.0-rc7.3"
}
}
apply plugin: "com.android.application"
apply plugin: "io.github.goldfish07.reschiper"Full Kotlin DSL configuration:
configure<io.github.goldfish07.reschiper.plugin.Extension> {
enableObfuscation = true // by default res obfuscate is enabled
obfuscationMode = "default" // ["dir", "file", "default"]
obfuscatedBundleName = "reschiper-app.aab" // Obfuscated file name, must end with ".aab"
// mappingFile = file("path/to/your/mapping.txt").toPath() // Mapping file used for incremental obfuscation
whiteList = setOf(
// Whitelist rule (directory name to exclude)
"res/raw", // raw dir will not be obfuscated
"res/xml", // xml dir will not be obfuscated
// Whitelist rule (file name to exclude)
"res/raw/*", // all files inside raw directory will not be obfuscated
"res/raw/success_tick.json", // success_tick.json file will not be obfuscated
"res/xml/*", // all files inside xml directory will not be obfuscated
// Whitelist rules (resource name to exclude)
"*.R.raw.*",
"*.R.xml.*",
// for google-services
"*.R.string.google_api_key",
"*.R.string.google_app_id",
"*.R.string.default_web_client_id",
"*.R.string.gcm_defaultSenderId",
"*.R.string.ga_trackingId",
"*.R.string.firebase_database_url",
"*.R.string.google_crash_reporting_api_key",
"*.R.string.google_storage_bucket",
"*.R.integer.google_play_services_version",
// firebase
"*.R.string.project_id",
// firebase crashlytics
"*.R.string.com.google.firebase.crashlytics.mapping_file_id",
"*.R.bool.com.crashlytics.useFirebaseAppId",
"*.R.string.com.crashlytics.useFirebaseAppId",
"*.R.string.google_app_id",
"*.R.bool.com.crashlytics.CollectDeviceIdentifiers",
"*.R.string.com.crashlytics.CollectDeviceIdentifiers",
"*.R.bool.com.crashlytics.CollectUserIdentifiers",
"*.R.string.com.crashlytics.CollectUserIdentifiers",
"*.R.string.com.crashlytics.ApiEndpoint",
"*.R.string.com.crashlytics.android.build_id",
"*.R.bool.com.crashlytics.RequireBuildId",
"*.R.string.com.crashlytics.RequireBuildId",
"*.R.bool.com.crashlytics.CollectCustomLogs",
"*.R.string.com.crashlytics.CollectCustomLogs",
"*.R.bool.com.crashlytics.Trace",
"*.R.string.com.crashlytics.Trace",
"*.R.string.com.crashlytics.CollectCustomKeys"
)
mergeDuplicateResources = true // allow the merge of duplicate resources
enableFileFiltering = true
enableFilterStrings = true
fileFilterList = setOf( // file filter rules
"META-INF/*",
// "*/armeabi-v7a/*",
// "*/arm64-v8a/*",
// "*/x86/*",
// "*/x86_64/*"
)
unusedStringFile = "path/to/your/unused_strings.txt" // strings will be filtered in this file
localeWhiteList = setOf("en", "in", "fr") // keep en,en-xx,in,in-xx,fr,fr-xx and remove others locale.
}Groovy DSL configuration:
resChiper {
enableObfuscation = true // by default res obfuscate is enabled
obfuscationMode = "default" // ["dir", "file", "default"]
obfuscatedBundleName = "reschiper-app.aab" // Obfuscated file name, must end with '.aab'
// mappingFile = file("path/to/your/mapping.txt").toPath() // Mapping file used for incremental obfuscation
whiteList = [
// Whitelist rule (directory name to exclude)
"res/raw", // raw dir will not be obfuscated
"res/xml", // xml dir will not be obfuscated
// Whitelist rule (file name to exclude)
"res/raw/*", // all files inside raw directory will not be obfuscated
"res/raw/success_tick.json", // success_tick.json file will not be obfuscated
"res/xml/*", // all files inside xml directory will not be obfuscated
// Whitelist rules (resource name to exclude)
"*.R.raw.*",
"*.R.xml.*",
// for google-services
"*.R.string.google_api_key",
"*.R.string.google_app_id",
"*.R.string.default_web_client_id",
"*.R.string.gcm_defaultSenderId",
"*.R.string.ga_trackingId",
"*.R.string.firebase_database_url",
"*.R.string.google_crash_reporting_api_key",
"*.R.string.google_storage_bucket",
"*.R.integer.google_play_services_version",
// firebase
"*.R.string.project_id",
// firebase crashlytics
"*.R.string.com.google.firebase.crashlytics.mapping_file_id",
"*.R.bool.com.crashlytics.useFirebaseAppId",
"*.R.string.com.crashlytics.useFirebaseAppId",
"*.R.string.google_app_id",
"*.R.bool.com.crashlytics.CollectDeviceIdentifiers",
"*.R.string.com.crashlytics.CollectDeviceIdentifiers",
"*.R.bool.com.crashlytics.CollectUserIdentifiers",
"*.R.string.com.crashlytics.CollectUserIdentifiers",
"*.R.string.com.crashlytics.ApiEndpoint",
"*.R.string.com.crashlytics.android.build_id",
"*.R.bool.com.crashlytics.RequireBuildId",
"*.R.string.com.crashlytics.RequireBuildId",
"*.R.bool.com.crashlytics.CollectCustomLogs",
"*.R.string.com.crashlytics.CollectCustomLogs",
"*.R.bool.com.crashlytics.Trace",
"*.R.string.com.crashlytics.Trace",
"*.R.string.com.crashlytics.CollectCustomKeys"
]
mergeDuplicateResources = true // allow the merge of duplicate resources
enableFileFiltering = true
enableFilterStrings = true
fileFilterList = [ // file filter rules
"META-INF/*",
// "*/armeabi-v7a/*",
// "*/arm64-v8a/*",
// "*/x86/*",
// "*/x86_64/*"
]
unusedStringFile = "path/to/your/unused_strings.txt" // strings will be filtered in this file
localeWhiteList = [
"en",
"in",
"fr"
] // keep en,en-xx,in,in-xx,fr,fr-xx and remove others locale.
}Run the regular bundle task for a variant:
./gradlew :app:bundleRelease --stacktraceResChiper creates variant-specific tasks named resChiper<Variant>, so you can also run:
./gradlew :app:resChiperRelease --stacktraceFor each application variant, the plugin creates a resChiper<Variant> task and wires it into bundle<Variant>.
Typical flow:
- Android packaging/signing tasks produce the source
.aab. - ResChiper reads that bundle.
- Optional filters and duplicate merging run.
- Resource obfuscation runs.
- A new obfuscated
.aabis written beside the original bundle output.
| Property | Type | Default | Notes |
|---|---|---|---|
enableObfuscation |
boolean |
true |
Enables resource obfuscation. |
obfuscationMode |
String |
"default" |
Supported values: default, dir, file. |
obfuscatedBundleName |
String |
none | Required output file name for the rewritten bundle. |
mappingFile |
Path |
null |
Reuses an existing resources-mapping.txt for stable naming. |
whiteList |
Set<String> |
empty | Excludes matching resources or paths from obfuscation. |
mergeDuplicateResources |
boolean |
false |
Merges duplicate bundled resources and emits a duplicate log. |
enableFileFiltering |
boolean |
false |
Enables bundle file filtering. |
fileFilterList |
Set<String> |
empty | Supports filtering within META-INF/ and lib/. |
enableFilterStrings |
boolean |
false |
Removes unused strings and optionally filters locales. |
unusedStringFile |
String |
"" |
Path to a newline-delimited unused_strings.txt. |
localeWhiteList |
Set<String> |
empty | Keeps only listed locales, such as en, fr, in. |
whiteList accepts glob-style rules. Common patterns:
whiteList = setOf(
"res/raw",
"res/raw/*",
"res/xml/*",
"*.R.raw.*",
"*.R.xml.*",
"*.R.string.google_app_id",
"*.R.string.app_name"
)Use whitelist rules for:
- resource directories or files that must keep stable names
- generated resources from Google services or Firebase
- assets referenced by external systems or dynamic loaders
ResChiper writes files into the same bundle output directory as the original .aab.
- obfuscated bundle: the file named by
obfuscatedBundleName resources-mapping.txt: resource name mapping for incremental reuse<module>-duplicate.txt: duplicate resource report when duplicate merging is enabled
This repository includes two runnable sample apps:
- sample-app for Kotlin DSL
- sample-app-groovy for Groovy DSL
They are configured for local plugin development with a composite build, so you can test the plugin without publishing it first.
Example commands:
./gradlew -p sample-app bundleDebug --stacktrace
./gradlew -p sample-app-groovy bundleDebug --stacktrace- ResChiper requires
com.android.application; library modules are rejected. - The plugin is designed around
.aabprocessing. - If you enable file filtering, only
META-INF/andlib/entries are supported by the implementation. - If you enable string filtering, provide
unusedStringFileunless your build already produces anunused_strings.txtreport that ResChiper can reuse.
ResChiper is licensed under the Apache License 2.0. See LICENSE.
