If you, like me, are attempting to setup a download end point for Android APK files that doesn’t include a .apk in the URL, you have probably butted your head against a myriad of issues. It’s worth noting that downloading APKs in Dolphin, Firefox Mobile, or Opera all work fine with respect to using Content-Disposition headers alone. It’s just the native browser that gives us grief. For the native browser, my first pass at the endpoint’s download code was the naive approach:
header('Content-Disposition: attachment; filename='.$apk_name);
echo file_get_contents($web_inaccessible_dir.'/'.$apk_name);
Testing this code in any desktop browser, I get the expected download notification and the file downloads like a champ. When I test drive this in the Android browser the result is different. Rather than getting a file download, I get the correct filename downloading but instead of using the necessary APK extension, the file is magically transformed into an HTM file, which is completely useless. This seems like a painfully bad decision for the content handling mechanisms of the browser, perhaps even a bug. Looking for some sign in the Android logs, I see this:
D/MediaScannerService( 1386): IMediaScannerService.scanFile: /mnt/sdcard/download/that_apk_file.htm mimeType: text/html
So while this is completely bad behavior, at least there’s a trail of hope in it — MIME type. Text/HTML is not the MIME type an APK file should have and apparently the browser is making us play by the rules. I can understand that. After all, if the MIME type is the source of truth then an APK file extension is only going to make the OS confused about what to do with the file. So, the question becomes what file type should an Android APK file have?
Doing some Googling about it, I find Wikipedia has the answer. The APK file format entry explains that Android APK files should have a MIME type of application/vnd.android.package-archive. Armed with this knowledge, I tried modifying my endpoint to send the MIME type I want:
header('Content-Disposition: attachment; filename='.$apk_name);
header('Content-Type: application/vnd.android.package-archive');
echo file_get_contents($web_inaccessible_dir.'/'.$apk_name);
Hitting the endpoint now, I find that the browser gets the APK file with the APK extension. Booyah. Just for rigor, I check logcat, and see what I was hoping to see:
D/MediaScannerService( 1386): IMediaScannerService.scanFile: /mnt/sdcard/download/that_apk_file.apk mimeType: application/vnd.android.package-archive
This is good, but I notice there are still flaws in the pearl. Namely, the file downloads without an indication of filesize or progress. The downloads page shows only that the download is “in progess” all the way up to completion. On a slow connection, this will seem like the file is stuck. Not ideal.
So, knowing a little about the HTTP spec, it seems like what the browser might need is a Content-Length header to get a handle on the progress towards completion. This is a straight-forward change, requiring just a single line.
header('Content-Disposition: attachment; filename='.$apk_name);
header('Content-Type: application/vnd.android.package-archive');
header('Content-Length: '.filesize($web_inaccessible_dir.'/'.$apk_name));
echo file_get_contents($web_inaccessible_dir.'/'.$apk_name);
Giving this a whirl in the browser, I find that I now have both the correct file extension and a progress bar during the download. Huzza.
I believe I’ve got what I want, but with Android it’s not guaranteed to be that simple. Due to fragmentation, I can only assume this works on the devices I’ve tested. So far, that includes the T-Mobile G2X (2.3.3 + T-mo’s bloatware), the x86 Android Emulator running 2.2 (by the way, if you haven’t tried out the x86 Emulator for Android and are still using the ARM emulator, you need to check out http://www.android-x86.org/ — it will change your whole perspective on shit), and the HTC Sensation 4G running 2.3.4 + Sense UI. I feel like that’s a good enough start to write this post. Please comment if you have an OS version/phone that doesn’t work with this setup.