Android Development: Downloading a file from the web

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.



This entry was posted in Android Development and tagged , , , . Bookmark the permalink.
  • dwayne

    Excellent! I was having some trouble getting a file to download on honeycomb – my previous attempts using Asynch worked with 10 and under…  I just started on android today and was getting frustrated.  Your example is way more professional than I was anticipating finding on the web – I will not be using as is, and will take the time to review and rewrite from my perspective.  Thank you very much for putting this out.

    • http://www.hassanpur.com/ Mujtaba Hassanpur

      Thank you. I’m glad you found it helpful. :-)

  • ricky

    it really helps,. thanks.. :)

  • Umesh

    Really nice…………

  • preet

    how to change download directory?
    thnx in advance :)

  • Danny Peters

    Thanks very much. This saved me a lot of time. I have adapted it for my own needs but having such a solid base to work from was enormously helpful.

  • darshan

    I cant download more than 10% of the file, it directly shows Download complete after 10% and only 10% of the file gets downloaded.. can you help what could be the problem?

  • Robert Gichohi

    thanks… good stuff

  • Arunprabhus87

    thanks good tutorial…..

  • Javier Ramos Saralegui

    Just Beautiful Code, Straight, Clean and very well explained :)
    I think you should put an encoder for the url so it will not give problems with weird urls with spaces and things like that, this will do just the job on the Thread Class after the assignation of the url:URI uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef());
    url = uri.toURL();Thanks a lot for Sharing it ;) Javier

  • http://www.facebook.com/kailash.pawar Kailash Pawar

    @mhassanpur:disqus  I’m working on application where it downloads images from url’s. I have added a progress bar. The problem is the download starts before clicking on download button and if clicking download button progress bar starts and stops earlier itself..
    But still the images are downloaded. I don’t know how to sync these?
    Any help please struggling from 1 week.
    Please Do reply and I’m new to android and Java.
    Please Please

  • unm

    Hi, I have Error: File Not Found.Why?

  • Bruce

    Mr. Hassanpur,
    I posted a comment a few minutes ago after your “first post,” and I am writing here to let you know that I wish you to read it.  Thank you.

  • Sourabh Kasliwal
  • Prima

    Hello,
    Excellent work, but am having an error which says ERROR downloading file, what kind of a file I can download? Please reply.
    Thank you

  • https://www.linkedin.com/profile/view?id=208004494&trk=spm_pic Philly Stafford

    Thanks for taking the time to post this mate! It really helped me out. As they say in Human Traffic, “NICE ONE BRUVA” :)

  • twostarii

    I click on download and nothing happens

  • Karabo Slasheth Mithi

    error file not found?

    I tested with firefox and file is there