Skip to content

Android Assets, Where Did You Go?

June 12, 2012

I accidentally discovered an interesting quirk with the Android assets manager when I attempted to add a compressed file to it. It turns out it’s not necessary or desirable to include a gzipped file–and if you do, it may not work as expected.

Assets

Assets are just files that you wish to bundle with an Android application, accomplished by dropping them into the /assets/ directory at the root of its source tree.

Most kinds of resources such as bitmaps, strings, XML, etc. should be included under the /res/ hierarchy. The code then usually refers to them by their resource id, especially when used in the Android APIs that deal with UI elements, since these functions are resource aware.

Assets come in handy when the data doesn’t quite fit the usual type or use case of a resource. Another way to include a file resource is to place it into the /res/raw/ directory, and it’s not totally clear from the documentation which option to prefer. The main difference is that the raw directory is flat, but you can put a directory hierarchy under the assets path.

Prelude to the loss

My application needs to include precomputed values that are generated offline into a file. Rather than attempting to hard-code the values in the source, which would be a pain to update if the file is re-generated, the best way is to store the file and read it at application startup. I copy this “precomputed-values.txt” file into my assets directory /assets/precomputed-values.txt and use the AssetsManager to read it. Here’s a snippet of code:

// The path is relative to the root of assets directory.
InputStream is = context.getAssets().open("precomputed-values.txt");
 
// This file happens to be line-delimited and UTF-8 encoded, so the 
// easiest way to read it is wrapping the input stream with an 
// InputStreamReader (which decodes from bytes to UTF-8) and a 
// BufferedReader (which can parse by line).
BufferedReader rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
 
// Now read the file line-by-line.
String line = null;
while ((line = rd.readLine()) != null) {
  processLine(line);
}
rd.close();

In the above example, context refers to my Service object, but it could also be the application context associated with an Activity. The getAssets() function returns the AssetsManager, which needed only briefly here to retrieve a stream holding the contents of the requested file.

One thing to notice is that you never find the file by directly reading the filesystem of the Android device, so you can’t use the java.io.FileReader to get at it. You may only obtain the contents through an InputStream provided by the Android framework. This important point explains the rest of the post: the individual file is not actually placed onto the filesystem, it remains archived inside the application .apk file and retrieved by the assets manager on demand.

Lost

After finishing the above approach, I decided that maybe I should compress my assets file. After all, it compresses well, and requires reading only once, so cost of inflating is not high. This is also easy to implement, as the only necessity is to wrap one more stream reader that understands zip files. So I compressed the file to “precomputed-values.txt.gz” and added a gzip reader to the code:

InputStream is = context.getAssets().open("precomputed-values.txt.gz");
// Why not another layer of stream?
GZIPInputStream zis = new GZIPInputStream(is);
BufferedReader rd = new BufferedReader(new InputStreamReader(zis, "UTF-8"));
 
// Now read the file line-by-line

When running this code, I got an error “java.io.FileNotFoundException: precomputed-values.txt.gz” In disbelief, I assumed the executable did not get packaged correctly by the build and scratched my head as to why the new asset file was not there. I renamed the file to “foo.txt.gz” and rebuilt the package. Examining the contents of the new .apk file by unzipping it, I saw that indeed the only file in the assets directory was “foo.txt” but without the expected .gz extension.

Found

It turns out that the compressed file gets included in the assets after all, but is visible only in its uncompressed form. For kicks, I renamed the file with a .bin extension, and found that the file remained intact (compressed, and requiring the GZIPInputStream to read it), so this only affects files with an extension that indicates compression.

I asked people more knowledgeable about Android internals, and they told me that this is an optimization by aapt (the program responsible for building the final package). Since it recognizes that the file is already compressed, it just adds it to the .apk archive without further attempting to compress it, removing the extension on the way. This explains why a gzip file is not found in the assets directory of the final application.

My idea to compress isn’t really warranted, since ultimately an asset is compressed in the application file (so doing it in advance won’t save space), and it’s automatically inflated by the Android system when reading (making the coding slightly more simple).

But it is confusing (and not well-documented) to find that the file in the assets directory is not always retrieved in the same name or format it is stored.

2 Comments leave one →
  1. mostafa elgendy permalink
    January 19, 2013 3:49 PM

    i make and application that need to add a jar file to assets folder or any other folder and at run time when necessary i need to read this jar file and send it to a server
    can you help me to read this file

  2. Ashok permalink
    November 11, 2015 8:11 AM

    When I try to do this today in Android Studio my project doesn’t even compile. Instead it fails with a “Zip add failed” error. Have you seen this?

    Basically I’m trying to build my Android Studio project with an assets folder. Inside my assets folder I have a .gz file (i.e. file.gz). When trying to compile this I get

    Unable to add ‘/path/to/assets/debug/subdir/page.acd65f439406aae92bbba6b.js.gz’: Zip add failed (-2147483648)

    This is only happening for .gz files. Why is this happening? If I rename it to file.gz.mp3 the project compiles. I really don’t want to do this as I’d like to use the .gz file in my app.

    Also I’m running into the same issue if I put this in the res/raw/ folder.

Leave a Reply

Your email address will not be published. Required fields are marked *