Downloading Files in Android
‘The Busy Coder’s Guide to Android Development’ author Mark Murphy looks at the challenges of downloading large files on mobile devices.
Downloading large files on a mobile device is rather complicated. While simple HTTP operations are, well, simple, there are many challenges involved in edge cases.
Let’s see what kind of challenges we have to face:
• Determining whether the user is on WiFi or mobile data, and if so, whether the download should occur.
• Handling when the user, previously on WiFi, moves out of range of the access point and “fails over” to mobile data.
• Ensuring the device stays awake while the download proceeds.
Android 2.3 has added DownloadManager to handle these large downloads. Using this class is less complicated than the alternative of writing all of it yourself. However, it presents its own set of challenges.
In this article, we will cover the use of DownloadManager, by examining the Internet/Download sample project from the GitHub repository for The Busy Coder’s Guide to Android Development .
To use DownloadManager, you will need to hold the Internet permission. Depending on where you elect to download the file, you may also need the Write_External_Storage permission. However, at the time of this writing, if you lack sufficient permissions, you may get an error complaining that you are missing Acces_All_Downloads. This appears to be a bug  in the DownloadManager implementation – it should be complaining about Internet and/or Write_External_Storage. You do not need to hold the Acces_All_Downloads permission, which is not even documented as of Android 2.3. For example, Listing 1 shows the manifest for the Internet/Download application.
Our sample application has a simple layout, consisting of three buttons (Listing 2):
• One to kick off a download
• One to query the status of a download
• One to display a system-supplied activity containing the roster of downloaded files
Requesting the Download
To kick off a download, we first need to get access to the DownloadManager. This is a so-called “system service”. You can call getSystemService() on any activity (or other Context), provide it the identifier of the system service you want, and receive the system service object back. However, since getSystemService() supports a wide range of these objects, you need to cast it to the proper type for the service you requested.
So, for example, here is a line from onCreate() of the DownloadDemo activity where we get the DownloadManager:
Most of these managers have no close() or release() or goAwayPlease() sort of methods – you can just use them and let garbage collection take care of cleaning them up. Given the manager, we can now call an enqueue() method to request a download. The name is relevant – do not assume that your download will begin immediately, though often times it will. The enqueue() method takes a DownloadManager. Request object as a parameter. The Request object uses the builder pattern, in that most methods return the Request itself, so you can chain a series of calls together with less typing. For example, the top-most button in our layout is tied to a startDownload() method in DownloadDemo, shown in Listing 3.
We are downloading a sample MP4 file, and we want to download it to the external storage area. To do the latter, we are using getExternalStoragePublicDirectory() on Environment, which gives us a directory suitable for storing a certain class of content. In this case, we are going to store the download in the Environment.DIRECTORY_DOWNLOADS, though we could just as easily have chosen Environment. DIRECTORY_MOVIES, since we are downloading a video clip. Note that the File object returned by getExternalStorage-PublicDirectory() may point to a not-yet-created directory, which is why we call mkdirs() on it, to ensure the directory exists.
We then create the DownloadManager.Request object, with the following attributes:
• We are downloading the specific URL we want, courtesy of the Uri supplied to the Request constructor
• We are willing to use either mobile data or WiFi for the download (setAllowedNetworkTypes()), but we do not want the download to incur roaming charges (setAllowed-OverRoaming())
• We want the file downloaded as test.mp4 in the downloads area on the external storage (setDestinationInExternalPublicDir())
We also provide a name (setTitle()) and description (set-Description()), which are used as part of the notification drawer entry for this download. The user will see these when they slide down the drawer while the download is progressing.
The enqueue() method returns an ID of this download, which we hold onto for use in querying the download status.
Keeping Track of Download Status
If the user presses the Query Status button, we want to find out the details of how the download is progressing. To do that, we can call query() on the DownloadManager. The query() method takes a DownloadManager. Query object, describing what download(s) you are interested in. In our case, we use the value we got from the enqueue() method when the user requested the download (Listing 4).
The query() method returns a Cursor, containing a series of columns representing the details about our download. There are a series of constants on the DownloadManager class outlining what is possible. In our case, we retrieve (and dump to LogCat):
• The ID of the download (COLUMN_ID)
• The amount of data that has been downloaded to date (COLUMN_BYTES_DOWNLOADED_SO_FAR)
• What the last-modified timestamp is on the download (COLUMN_LAST_MODIFIED_TIMESTAMP)
• Where the file is being saved to locally (COLUMN_LOCAL_URI)
• What the actual status is (COLUMN_STATUS)
• What the reason is for that status (COLUMN_REASON)
There are a number of possible status codes (e.g., STATUS_FAILED, STATUS_SUCCESSFUL, STATUS_RUNNING). Some, like STATUS_FAILED, may have an accompanying reason to provide more details.
What the User Sees
The user, upon launching the application, sees our three pretty buttons (Figure 1).
Clicking the first disables the button while the download is going on, and a download icon appears in the status bar (though it is a bit difficult to see, given the poor contrast between Android’s icon and Android’s status bar), see Figure 2.
Sliding down the notification drawer shows the user the progress in the form of a ProgressBar widget (Figure 3). Tapping on the entry in the notification drawer returns control to our original activity, where they see a Toast (Figure 4). If they tap the middle button during the download, a Toast will appear indicating that the download is in progress (Figure 5). Additional details are also dumped to LogCat, visible via DDMS or adb logcat (Listing 5).
Once the download is complete, tapping the middle button will indicate that the download is, indeed, complete, and final information about the download is emitted to LogCat (Listing 6). Tapping the bottom button brings up the activity displaying all downloads, including both successes and failures (Figure 6). And, of course, the fi le is downloaded. In Android 2.3, in the emulator, our chosen location maps to /mnt/sdcard/ Downloads/test.mp4.
DownloadManager works with HTTP URLs, but not HTTPS (SSL) URLs. This is unfortunate, as more and more sites are switching to SSL encryption across the board, to deal with various security challenges. Hopefully, in the future, DownloadManager will have more options here.
If you display the list of all downloads, and your download is among them, it is a really good idea to make sure that some activity (perhaps one of yours) is able to respond to an ACTION_VIEW Intent on that download’s MIME type.
Otherwise, when the user taps on the entry in the list, they will get a Toast indicating that there is nothing available to view the download. This may confuse users. Alternatively, use setVisibleInDownloadsUi() on your request, passing in false, to suppress it from this list.