A fairly common operation in Android apps is the need to download content to the device. While it is easy enough to construct a network call to download the required content, there is also a built in way which can be particularly useful if the content being downloaded should also be shared with other apps, or stored in a publicly accessible location on the device. In this short series of articles we’ll take a look at DownloadManager which can solve many of the common problems in a really easy to use API.
Before we dive in, it is work mentioning that DownloadManager has been around since API 9. Although there have been a couple of small API tweaks since then, the API has largely been stable since then. Anything that we cover which isn’t available back to API 9 will be mentioned when we cover it.
Let’s begin by creating a simple Activity which will will encapsulate our UI. It consists of a single button which, when clicked, will download a PDF file to the device.
The important bits to note here are the Downloader object which is where all of the DownloadManager logic is housed and there is also a callback method named fileDownloaded()
which will get called when a download successfully completes. One thing also worth noting is that we call the unregister() methods on the Downloader in onDestroy()
more on this in due course.
public class MainActivity extends AppCompatActivity implements Downloader.Listener { private static final String URI_STRING = "http://www.cbu.edu.zm/downloads/pdf-sample.pdf"; private Button download; private Downloader downloader; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); download = (Button) findViewById(R.id.download); download.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { downloadOrCancel(); } }); downloader = Downloader.newInstance(this); } void downloadOrCancel() { if (downloader.isDownloading()) { cancel(); } else { download(); } updateUi(); } private void cancel() { downloader.cancel(); } private void download() { Uri uri = Uri.parse(URI_STRING); downloader.download(uri); } @Override public void fileDownloaded(Uri uri, String mimeType) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, mimeType); startActivity(intent); updateUi(); } @Override public Context getContext() { return getApplicationContext(); } private void updateUi() { if (downloader.isDownloading()) { download.setText(R.string.cancel); } else { download.setText(R.string.download); } } @Override protected void onDestroy() { downloader.unregister(); super.onDestroy(); } }
Hopefully that is all pretty straightforward, but we’ll return to this later on once we’ve explored how DownloadManager does its magic and understand better what we’re doing in fileDownloaded()
.
So let’s dive in to the Downloader class to see how we actually use DownloadManager:
class Downloader implements DownloadReceiver.Listener { private final Listener listener; private final DownloadManager downloadManager; private DownloadReceiver receiver = null; private long downloadId = -1; static Downloader newInstance(Listener listener) { Context context = listener.getContext(); DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); return new Downloader(downloadManager, listener); } Downloader(DownloadManager downloadManager, Listener listener) { this.downloadManager = downloadManager; this.listener = listener; } void download(Uri uri) { if (!isDownloading()) { register(); DownloadManager.Request request = new DownloadManager.Request(uri); downloadId = downloadManager.enqueue(request); } } boolean isDownloading() { return downloadId >= 0; } void register() { if (receiver == null) { receiver = new DownloadReceiver(this); receiver.register(listener.getContext()); } } @Override public void downloadComplete(long completedDownloadId) { if (downloadId == completedDownloadId) { DownloadManager.Query query = new DownloadManager.Query(); query.setFilterById(downloadId); downloadId = -1; unregister(); Cursor cursor = downloadManager.query(query); while (cursor.moveToNext()) { getFileInfo(cursor); } cursor.close(); } } void unregister() { if (receiver != null) { receiver.unregister(listener.getContext()); } receiver = null; } private void getFileInfo(Cursor cursor) { int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)); if (status == DownloadManager.STATUS_SUCCESSFUL) { Long id = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID)); Uri uri = downloadManager.getUriForDownloadedFile(id); String mimeType = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE)); listener.fileDownloaded(uri, mimeType); } } void cancel() { if (isDownloading()) { downloadManager.remove(downloadId); downloadId = -1; unregister(); } } interface Listener { void fileDownloaded(Uri uri, String mimeType); Context getContext(); } }
We get an instance of DownloadManager by calling getSystemService(Context.DOWNLOAD_SERVICE)
on a Context.
Initiating a basic download is simplicity itself and is performed in the download()
method. We first register a BroadcastReceiver (more on this later) which will handle callbacks from the DownloadManager once the download completes. Then we construct a DownloadManager.Request from the supplied Uri. Finally we enqueue the request to DownloadManager which returns a unique ID which identifies our download request and we’re done.
We can cancel the download request my removing from the DownloadManager using the ID – this is performed in cancel()
.
The unregister()
method which we saw the Activity calling in its onDestroy()
method actually unregisters the BroadcastReceiver. The reason that this is important is that we have no guarantee that DownloadManager will actually complete the download before the user exits the app. This is one of the benefits of using DownloadManager – we don’t need to create background Services to perform the download – DownloadManager does that for us. By unregistering the receiver when the Activity is destroyed we ensure that we won’t attempt to do anything with the completed download if it finishes when the app is not active.
The downloadComplete()
method will get called by the BroadcastReceiver when the download actually completes. We first check that it matches the id of our download and, if so, we query the DownloadManager to get the results of the download. If we supported multiple concurrent downloads we could specify multiple download IDs in the query, so a Cursor is returned which allows us to iterate through them. In our case we’ll only permit a single download, but the sample code works for multiple files as well.
We first extract the status field which indicates the status of the download – in our case this will only get triggered once a download completes so we just need to check whether the download was successful, however in a full implementation it may be useful to query download manager on resuming the app to update the status of any downloads which may have been started in a previous app session.
if the download was successful then we extract the local Uri (where the download was stored) and the MIME type of the content, and pass this back to the listener (in our case the Activity).
Let’s now take a look at the BroadcastReceiver:
class DownloadReceiver extends BroadcastReceiver { private final Listener listener; DownloadReceiver(Listener listener) { this.listener = listener; } @Override public void onReceive(Context context, Intent intent) { long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); listener.downloadComplete(downloadId); } public void register(Context context) { IntentFilter downloadFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE); context.registerReceiver(this, downloadFilter); } public void unregister(Context context) { context.unregisterReceiver(this); } interface Listener { void downloadComplete(long downloadId); } }
There’s nothing much to see here. The main things worth noting are that the action that we need to register for is DownloadManager.ACTION_DOWNLOAD_COMPLETE
and then we’ll get woken for download completion events. We can register for other events such as the user clicking on the download notification so that we could bring the user in to our app to control see or cancel the download, but we’re only interested in completions.
The other thing worthy of note is that when we receive the completion Intent, we extract the downloadId from the Intent extras.
Finally let’s turn our attention back to the Activity and look at what we do when we receive the callback once the download completes.
@Override public void fileDownloaded(Uri uri, String mimeType) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, mimeType); startActivity(intent); updateUi(); }
The content in question is actually a PDF file. Rather thasn build in a PDF view to the app, I’ve elected to let other apps on the hard work, and use an implicit Intent to allow the system to determine an appropriate app to view the content.
The important thing which facilitates that is the Uri that DownloadManager returns is actually a content://...
Uri which is very friendly to other apps andf allows them to easily access the content through as ContentProvider provided by DownloadManager.
So DownloadManager enables us to download content and not have to worry about implementing the background handling ourselves, and also permits really easy mechanisms to share that content with other apps. All for some relatively simple and straightforward code.
If we run this we can see a notification appear while the download is in progress, and then we get prompted by the OS to select a PDF view within which to view the content. When we chose one, the content is displayed:
In the next article we’ll explore DownloadManager in greater depth and see some more of powerful features.
The source code for this article is available here.
© 2016 – 2017, Mark Allison. All rights reserved.
Copyright © 2016 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.
Hi. Thanks for the great article.
In the Downloader class, the line register(); in the download() method should be moved to the last line inside if block.
Why? I want to ensure that everything is correctly registered *before* I enqueue a download. If I do it as you suggest there is a possibility (albeit a very small one) that the download could complete before the receiver has been registered. My way avoids that possibility.
Yes, that’s a valid point. But in the register() method you are checking isDownloading() before registering. And isDowloading() will be false before we enqueue a download.
Hi, Mark.
I tried running the downloadmanager app on my moto g3 android phone but the pdf file, while looking like it’s downloaded, didn’t open up, even with a pdf viewer application. Am I missing permissions or something else ?
Thanks,
Owen