Unearthing the Quirk: Dealing with File Access Issues that arise from Resource Optimization in Android Applications
4/4/2024
Unearthing the Quirk: Dealing with File Access Issues that arise from Resource Optimization in Android Applications

While technological advancements often make processes more efficient, they can sometimes introduce unexpected challenges. This is exemplified by the Android Gradle Plugin from version 4.2 onwards, which introduced resource optimization. This process is designed to reduce APK size and increase efficiency, but it also includes changes to file names in the resources directory.

For instance, if an Android application is designed to read localization strings from files stored in the raw subdirectory of the resources folder using a file path, this optimization process could create an obstacle. After the optimization process concludes, the file path could be obfuscated, changing, for example, from res/raw/strings.xml to something like res/Gh4.xml. This unexpected change can cause issues with accessing the required files.

For further information about these optimizations, you can refer to Jake Wharton’s detailed article titled Smaller APKs with Resource Optimization. This write-up provides a deeper understanding of the resource optimization process in Android development, which is central to the issue discussed in our article.

When Optimization Gets Tricky

During its operation, Gradle executes the ‘optimizeReleaseResources’ task which entails invoking the Android Asset Packaging Tool (AAPT) binary to operate on the assembled APK file.

For a more detailed look into the specifics of how this task operates, you can refer to the source code available on Google’s Android Project repository.

The optimizeReleaseRes task executes the following lines of code:

val optimizeFlags = mutableSetOf<String>()
    if (params.enableResourceObfuscation) {
        optimizeFlags += AAPT2OptimizeFlags.COLLAPSE_RESOURCE_NAMES.flag
    }

    if (params.enableResourcePathShortening) {
        optimizeFlags += AAPT2OptimizeFlags.SHORTEN_RESOURCE_PATHS.flag
    }

    if (params.enableSparseResourceEncoding) {
        optimizeFlags += AAPT2OptimizeFlags.ENABLE_SPARSE_ENCODING.flag
    }

invokeAapt(
    params.aapt2Executable, "optimize", params.inputApkFile.path,
    *optimizeFlags.toTypedArray(), "-o", params.outputApkFile.path
)

and maps enum values to the following AAPT parameters:

enum class AAPT2OptimizeFlags(val flag: String) {
    COLLAPSE_RESOURCE_NAMES("--collapse-resource-names"),
    SHORTEN_RESOURCE_PATHS("--shorten-resource-paths"),
    ENABLE_SPARSE_ENCODING("--enable-sparse-encoding")
}

The AAPT documentation describes these parameters as follows:

--force-sparse-encoding
    Enables encoding sparse entries using a binary search tree.
    This decreases APK size at the cost of resource retrieval performance.
    Applies sparse encoding to all resources regardless of minSdk.
 
 --collapse-resource-names
    Collapses resource names to a single value in the key string pool.
    Resources can be exempted using the "no_collapse" directive
    in a file specified by --resources-config-path.

 --shorten-resource-paths 
    Shortens the paths of resources inside the APK.
    Resources can be exempted using the "no_path_shorten" directive
    in a file specified by --resources-config-path.

It becomes evident that these parameters are at the heart of the issue that hinders the direct access of files by their names from the code.

However, as it stands at the time of writing this article, with the Android Gradle Plugin in version 8.3, Gradle does not provide an explicit way to control these parameters via its configuration settings. This means developers are required to employ other approaches to tackle this side effect of resource optimization.

Don’t worry, there are multiple potential solutions available to circumvent this issue.

Option One: Turning Off Resource Optimizations Using the Gradle Flag

The first method for resolving this issue involves the use of the android.enableResourceOptimizations=false flag. Adding this flag to the gradle.properties file effectively disables the resource optimization process and thus resolves the issue.

Pros: This solution is straightforward and can be implemented with a single line addition to your Gradle configuration.

Cons: This flag is set to be deprecated in the upcoming Android Gradle Plugin 9.0. With that in mind, this solution serves as a temporary fix and may not be applicable in the future.

Option Two: Anticipating the Forthcoming Solution from the Android team

An active issue on Google’s issue tracker reflects ongoing development to provide a way to disable specific optimizations by adding buildTypes options based on the AAPT2 Optimize suboperations. This suggests that they plan to include a provision for explicit configuration before the version 9.0 release.

Pros: This approach would only necessitate minor changes in the Gradle configuration.

Cons: As this development is still in progress, the solution is not available at the moment.

Option Three: Moving Files Outside the Resource Directory

Another straightforward solution involves moving the resource files from the /res directory to a different location, such as the /assets directory. This is a viable option because the optimization process only affects files within the /res directory.

Pros: This method offers a quick fix to the issue.

Cons: It disrupts the more standard practice of maintaining all resources within the /res directory. This inconsistency could potentially lead to confusion in large-scale applications or teams.

Option Four: Resource Path Mapping

Another approach involves deriving a mapping that converts the original paths back to their changed names and locations. This provides a way to navigate resources as if they’ve not been altered.

This might be implemented as a Gradle task that calls an AAPT to generate the mapping.

Running aapt2 dump resources <path-to-apk> command produces information about the resource files as follows:

resource 0x7f010000 anim/abc_fade_in
      () (file) res/y4.xml type=XML

resource 0x7f010001 anim/abc_fade_out
      () (file) res/Bd.xml type=XML

resource 0x7f010002 anim/abc_grow_fade_in_from_botton
      () (file) res/aM.xml type=XML

This mapping is expected to remain consistent across multiple iterations of the build, but it should be checked each time the list of resources is altered.

The Gradle task can be performed manually or incorporated into CI processes. It involves running a script each time resources change. Since the mapping is required at runtime, it must be included in the APK. Therefore, the APK assembly process should be done twice: first to build the APK and then to include the mapping. As a result the original file path can be used in the code to retrieve the obfuscated file path. Collapsed resource names remain the same, so running the assemble task twice will only be required if the resources have been modified.

Pros: The files can remain at their current locations at /res directory.

Cons: It requires running an APK file with the AAPT executable’s dump parameter, creating the need for additional Gradle tasks. If more resource files are added or renamed in future builds, this solution may not hold up.

Option Five: Providing a Custom OptimizeReleaseResources Gradle Task

The last solution is a more ambitious one. Implementation would require modifications of the task we need to optimize, as well as all dependent tasks. This strategy allows us to possibly disable undesired sub-operations of the optimization process.

Moreover, there is a more specific method to exclude certain files from the optimization process. Particular resources can be exempted using the no_collapse directive in a file defined by --resources-config-path. The specified path leads to a resources.cfg file that lists resources along with directives for each one. The expected format for providing directives is: type/resource_name#[directive][,directive]. In our scenario, it would be xml/strings#no_collapse.

Pros: This approach addresses the issue in a manner similar to a planned solution on AGP side described as Option two…

Cons: This complex solution requires an in-depth comprehension of related Gradle tasks and involves the creation of a custom chain of Gradle tasks. It’s worth noting that altering or extending internal tasks of the Android Gradle plugin is generally not advised due to potential stability, compatibility, and support issues.

Wrapping Up The Tale

While stumbling across such issues can feel like landing in a labyrinth, knowing that there are possible workarounds can equip you with the necessary solutions to navigate your way successfully.

The puzzle of accessing obfuscated resource files might seem challenging. But remember, every problem in Android development is just another opportunity for mastering the craft.

Let’s keep pushing the boundaries, one line of code at a time.

Ivan Roshchynskyi


Tags

#android; #gradle; #agp; #resources; #optimization

Author

Ivan Roshchynskyi

Versions

Android Gradle Plugin 8.3.0
Kotlin 1.9.20