Downloading a file from the web is a common task for an Android app. For example, if you’re developing a mobile client for a social application that stores profile pictures on a remote server, you might want to download the image from your server and cache it on the SD card. In this article, I show an example of how to download a generic file from the web, but it can easily be adapted for other applications. Our example app will display a form that allows the user to enter a URL. When the user presses the “Download” button, we’ll connect to the URL, download the file, and store it on the root of the SD card. In addition, the app will also let the user know what’s happening through the use of progress bars and Toast messages.
Our application will consist of two classes: AndroidFileDownloader (the main activity) and DownloaderThread (the background thread that actually does the downloading).
AndroidFileDownloader — The main activity
The AndroidFileDownloader class will be our entry point in the app. It will display the form to the user, handle user input, launch the downloader thread, and update the user interface.
More specifically, it will:
- Inherit the Activity class
- Implement the OnClickListener interface
- Implement a custom Handler class, activityHandler, and receive Messages from the downloader thread
- Make use of ProgressDialog and Toast to let the user know what’s happening
DownloaderThread — background thread
The DownloaderThread class implements the actual logic to download the file and save it to the SD card. The AndroidFileDownloader class will instantiate a DownloaderThread object by specifying the URL to download from, and run the thread in the background. The downloader thread will then send Messages to the AndroidFileDownloader activity to update the user interface. Most of the work will be done in the run() method. But before we dive into the code for the DownloaderThread class or look at the form layout, let’s make sure to give our app the necessary permissions.
AndroidManifest.xml — Giving the app necessary permissions
Since we’re going to download a file from the Internet and store it on the SD card, we’ll need to give our app two specific permissions. Make sure the following lines are in your AndroidManifest.xml file:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission> <uses-permission android:name="android.permission.INTERNET"></uses-permission>
main.xml — The form layout
Let’s start with our form, which simply consists of a label (id: status_text_label), a text field (id: url_input), and a download button (id: download_button):
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:id="@+id/status_text_label" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/status_text_default" /> <EditText android:id="@+id/url_input" android:text="http://" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <Button android:text="Download" android:id="@+id/download_button" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
strings.xml — User messages
And just for reference, here’s what the strings.xml file looks like. It contains our user messages:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">AndroidFileDownloader</string> <string name="status_text_default">Enter the URL of the file to download.</string> <string name="progress_dialog_title_downloading">Downloading...</string> <string name="progress_dialog_title_connecting">Connecting...</string> <string name="progress_dialog_message_prefix_downloading">Downloading file</string> <string name="progress_dialog_message_prefix_connecting">Connecting to</string> <string name="user_message_download_complete">Download complete!</string> <string name="user_message_download_canceled">Download canceled!</string> <string name="error_message_bad_url">Error: Invalid URL!</string> <string name="error_message_file_not_found">Error: File not found!</string> <string name="error_message_general">Error downloading file!</string> </resources>
Details of the DownloaderThread class
Okay, time to dive into the code for the DownloaderThread class. This class basically has two instance variables:
private AndroidFileDownloader parentActivity; private String downloadUrl;
Once instantiated, a DownloaderThread object will be able to access the AndroidFileDownloader’s activityHandler (via parentActivity) to send Messages, and downloadUrl will contain the URL of the file to be downloaded. And then most of the work is done in the run() method:
/** * Connects to the URL of the file, begins the download, and notifies the * AndroidFileDownloader activity of changes in state. Writes the file to * the root of the SD card. */ @Override public void run() { URL url; URLConnection conn; int fileSize, lastSlash; String fileName; BufferedInputStream inStream; BufferedOutputStream outStream; File outFile; FileOutputStream fileStream; Message msg; // we're going to connect now msg = Message.obtain(parentActivity.activityHandler, AndroidFileDownloader.MESSAGE_CONNECTING_STARTED, 0, 0, downloadUrl); parentActivity.activityHandler.sendMessage(msg); try { url = new URL(downloadUrl); conn = url.openConnection(); conn.setUseCaches(false); fileSize = conn.getContentLength(); // get the filename lastSlash = url.toString().lastIndexOf('/'); fileName = "file.bin"; if(lastSlash >=0) { fileName = url.toString().substring(lastSlash + 1); } if(fileName.equals("")) { fileName = "file.bin"; } // notify download start int fileSizeInKB = fileSize / 1024; msg = Message.obtain(parentActivity.activityHandler, AndroidFileDownloader.MESSAGE_DOWNLOAD_STARTED, fileSizeInKB, 0, fileName); parentActivity.activityHandler.sendMessage(msg); // start download inStream = new BufferedInputStream(conn.getInputStream()); outFile = new File(Environment.getExternalStorageDirectory() + "/" + fileName); fileStream = new FileOutputStream(outFile); outStream = new BufferedOutputStream(fileStream, DOWNLOAD_BUFFER_SIZE); byte[] data = new byte[DOWNLOAD_BUFFER_SIZE]; int bytesRead = 0, totalRead = 0; while(!isInterrupted() && (bytesRead = inStream.read(data, 0, data.length)) >= 0) { outStream.write(data, 0, bytesRead); // update progress bar totalRead += bytesRead; int totalReadInKB = totalRead / 1024; msg = Message.obtain(parentActivity.activityHandler, AndroidFileDownloader.MESSAGE_UPDATE_PROGRESS_BAR, totalReadInKB, 0); parentActivity.activityHandler.sendMessage(msg); } outStream.close(); fileStream.close(); inStream.close(); if(isInterrupted()) { // the download was canceled, so let's delete the partially downloaded file outFile.delete(); } else { // notify completion msg = Message.obtain(parentActivity.activityHandler, AndroidFileDownloader.MESSAGE_DOWNLOAD_COMPLETE); parentActivity.activityHandler.sendMessage(msg); } } catch(MalformedURLException e) { String errMsg = parentActivity.getString(R.string.error_message_bad_url); msg = Message.obtain(parentActivity.activityHandler, AndroidFileDownloader.MESSAGE_ENCOUNTERED_ERROR, 0, 0, errMsg); parentActivity.activityHandler.sendMessage(msg); } catch(FileNotFoundException e) { String errMsg = parentActivity.getString(R.string.error_message_file_not_found); msg = Message.obtain(parentActivity.activityHandler, AndroidFileDownloader.MESSAGE_ENCOUNTERED_ERROR, 0, 0, errMsg); parentActivity.activityHandler.sendMessage(msg); } catch(Exception e) { String errMsg = parentActivity.getString(R.string.error_message_general); msg = Message.obtain(parentActivity.activityHandler, AndroidFileDownloader.MESSAGE_ENCOUNTERED_ERROR, 0, 0, errMsg); parentActivity.activityHandler.sendMessage(msg); } }
Details of the AndroidFileDownloader class
Now, let’s take a look at the AndroidFileDownloader class. This class uses the following instance variables:
private AndroidFileDownloader thisActivity; private Thread downloaderThread; private ProgressDialog progressDialog;
One of the first things we’re going to do in this class is to implement the onClick(View view) method. This method will be called when the user clicks on the “Download” button. We’ll basically take the URL that was given in the text field and use it to instantiate a new DownloaderThread object. We’ll then run the thread in the background:
/** Called when the user clicks on something. */ @Override public void onClick(View view) { EditText urlInputField = (EditText) this.findViewById(R.id.url_input); String urlInput = urlInputField.getText().toString(); downloaderThread = new DownloaderThread(thisActivity, urlInput); downloaderThread.start(); }
We then bind the OnClickListener to the “Download” button on the form, in our onCreate method:
/** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); thisActivity = this; downloaderThread = null; progressDialog = null; setContentView(R.layout.main); Button button = (Button) this.findViewById(R.id.download_button); button.setOnClickListener(this); }
All we need to do now is implement our Handler, to handle the messages sent by the downloader thread. Looking at the DownloaderThread class, you may have noticed the use of several messages. These message codes are declared in our AndroidFileDownloader class, with arbitrary values:
// Used to communicate state changes in the DownloaderThread public static final int MESSAGE_DOWNLOAD_STARTED = 1000; public static final int MESSAGE_DOWNLOAD_COMPLETE = 1001; public static final int MESSAGE_UPDATE_PROGRESS_BAR = 1002; public static final int MESSAGE_DOWNLOAD_CANCELED = 1003; public static final int MESSAGE_CONNECTING_STARTED = 1004; public static final int MESSAGE_ENCOUNTERED_ERROR = 1005;
We implement our Handler to handle each of these Messages in the handleMessage(Message msg) method:
/** * This is the Handler for this activity. It will receive messages from the * DownloaderThread and make the necessary updates to the UI. */ public Handler activityHandler = new Handler() { public void handleMessage(Message msg) { switch(msg.what) { /* * Handling MESSAGE_UPDATE_PROGRESS_BAR: * 1. Get the current progress, as indicated in the arg1 field * of the Message. * 2. Update the progress bar. */ case MESSAGE_UPDATE_PROGRESS_BAR: if(progressDialog != null) { int currentProgress = msg.arg1; progressDialog.setProgress(currentProgress); } break; /* * Handling MESSAGE_CONNECTING_STARTED: * 1. Get the URL of the file being downloaded. This is stored * in the obj field of the Message. * 2. Create an indeterminate progress bar. * 3. Set the message that should be sent if user cancels. * 4. Show the progress bar. */ case MESSAGE_CONNECTING_STARTED: if(msg.obj != null && msg.obj instanceof String) { String url = (String) msg.obj; // truncate the url if(url.length() > 16) { String tUrl = url.substring(0, 15); tUrl += "..."; url = tUrl; } String pdTitle = thisActivity.getString(R.string.progress_dialog_title_connecting); String pdMsg = thisActivity.getString(R.string.progress_dialog_message_prefix_connecting); pdMsg += " " + url; dismissCurrentProgressDialog(); progressDialog = new ProgressDialog(thisActivity); progressDialog.setTitle(pdTitle); progressDialog.setMessage(pdMsg); progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); progressDialog.setIndeterminate(true); // set the message to be sent when this dialog is canceled Message newMsg = Message.obtain(this, MESSAGE_DOWNLOAD_CANCELED); progressDialog.setCancelMessage(newMsg); progressDialog.show(); } break; /* * Handling MESSAGE_DOWNLOAD_STARTED: * 1. Create a progress bar with specified max value and current * value 0; assign it to progressDialog. The arg1 field will * contain the max value. * 2. Set the title and text for the progress bar. The obj * field of the Message will contain a String that * represents the name of the file being downloaded. * 3. Set the message that should be sent if dialog is canceled. * 4. Make the progress bar visible. */ case MESSAGE_DOWNLOAD_STARTED: // obj will contain a String representing the file name if(msg.obj != null && msg.obj instanceof String) { int maxValue = msg.arg1; String fileName = (String) msg.obj; String pdTitle = thisActivity.getString(R.string.progress_dialog_title_downloading); String pdMsg = thisActivity.getString(R.string.progress_dialog_message_prefix_downloading); pdMsg += " " + fileName; dismissCurrentProgressDialog(); progressDialog = new ProgressDialog(thisActivity); progressDialog.setTitle(pdTitle); progressDialog.setMessage(pdMsg); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); progressDialog.setProgress(0); progressDialog.setMax(maxValue); // set the message to be sent when this dialog is canceled Message newMsg = Message.obtain(this, MESSAGE_DOWNLOAD_CANCELED); progressDialog.setCancelMessage(newMsg); progressDialog.setCancelable(true); progressDialog.show(); } break; /* * Handling MESSAGE_DOWNLOAD_COMPLETE: * 1. Remove the progress bar from the screen. * 2. Display Toast that says download is complete. */ case MESSAGE_DOWNLOAD_COMPLETE: dismissCurrentProgressDialog(); displayMessage(getString(R.string.user_message_download_complete)); break; /* * Handling MESSAGE_DOWNLOAD_CANCELLED: * 1. Interrupt the downloader thread. * 2. Remove the progress bar from the screen. * 3. Display Toast that says download is complete. */ case MESSAGE_DOWNLOAD_CANCELED: if(downloaderThread != null) { downloaderThread.interrupt(); } dismissCurrentProgressDialog(); displayMessage(getString(R.string.user_message_download_canceled)); break; /* * Handling MESSAGE_ENCOUNTERED_ERROR: * 1. Check the obj field of the message for the actual error * message that will be displayed to the user. * 2. Remove any progress bars from the screen. * 3. Display a Toast with the error message. */ case MESSAGE_ENCOUNTERED_ERROR: // obj will contain a string representing the error message if(msg.obj != null && msg.obj instanceof String) { String errorMessage = (String) msg.obj; dismissCurrentProgressDialog(); displayMessage(errorMessage); } break; default: // nothing to do here break; } } };
And, that’s pretty much it. The dismissCurrentProgressDialog() and displayMessage(String message) methods are just helper methods for some commonly used code:
/** * If there is a progress dialog, dismiss it and set progressDialog to * null. */ public void dismissCurrentProgressDialog() { if(progressDialog != null) { progressDialog.hide(); progressDialog.dismiss(); progressDialog = null; } } /** * Displays a message to the user, in the form of a Toast. * @param message Message to be displayed. */ public void displayMessage(String message) { if(message != null) { Toast.makeText(thisActivity, message, Toast.LENGTH_SHORT).show(); } }
As always, the full source code for this example is available on Google Code. And here’s a link to AndroidFileDownloader.java and DownloaderThread.java.
To import this project into Eclipse, take a look at this post Importing Hassanpur.com example projects into Eclipse and Aptana.