The following is the first few sections of a chapter from The Busy Coder's Guide to Android Development, plus headings for the remaining major sections, to give you an idea about the content of the chapter.


Miscellaneous Network Topics

This chapter is a catch-all for various Android capabilities related to network I/O and the Internet, beyond what is covered elsewhere in the book.

(yes, this chapter could have a more exciting rationale for existing, but the author is subject to “Truth in Advertising” laws…)

Prerequisites

Readers of this chapter should have read the core chapters of the book.

Downloading Files

Android 2.3 introduced a DownloadManager, designed to handle a lot of the complexities of downloading larger files, such as:

  1. Determining whether the user is on WiFi or mobile data, and if so, whether the download should occur
  2. Handling when the user, previously on WiFi, moves out of range of the access point and “fails over” to mobile data
  3. Ensuring the device stays awake while the download proceeds

DownloadManager itself is less complicated than the alternative of writing all of that stuff yourself. However, it does present a few challenges. In this section, we will examine the Internet/Download sample project, one that uses DownloadManager.

The Permissions

To use DownloadManager, you will need to hold the INTERNET permission. You will also need the WRITE_EXTERNAL_STORAGE permission, as DownloadManager can only download to external storage. Note that you need to hold WRITE_EXTERNAL_STORAGE even if you are trying to have DownloadManager write to some location where that permission might not be needed (e.g., getExternalFilesDir() on an Android 4.4+ device). DownloadManager is requiring you to hold that permission, more so than the Android framework, and DownloadManager requires that permission for all API levels at the present time.

For example, here is the manifest for the Internet/Download application, where we request these two permissions:

<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.commonsware.android.downmgr"
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:versionCode="1"
  android:versionName="1.0">

  <supports-screens
    android:anyDensity="true"
    android:largeScreens="true"
    android:normalScreens="true"
    android:smallScreens="true" />

  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">
    <activity
      android:name=".DownloadDemo"
      android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>

</manifest>

WRITE_EXTERNAL_STORAGE is a dangerous permission. With a targetSdkVersion of 23 or higher, we need to handle that in our app. This app uses the same AbstractPermissionActivity seen in the chapter on permissions, so we can request WRITE_EXTERNAL_STORAGE from the user on the first run of our app from the DownloadDemo activity:

package com.commonsware.android.downmgr;

import android.Manifest;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Intent;
import android.os.Bundle;
import android.os.StrictMode;
import android.widget.Toast;

public class DownloadDemo extends AbstractPermissionActivity {

  @Override
  protected String[] getDesiredPermissions() {
    return(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE});
  }

  @Override
  protected void onPermissionDenied() {
    Toast
      .makeText(this, R.string.msg_sorry, Toast.LENGTH_LONG)
      .show();
    finish();
  }

  @Override
  public void onReady(Bundle savedInstanceState) {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                                .detectNetwork()
                                .penaltyDeath()
                                .build());
    
    if (getFragmentManager().findFragmentById(android.R.id.content)==null) {
      getFragmentManager().beginTransaction()
                                 .add(android.R.id.content,
                                      new DownloadFragment()).commit();
    }
  }

  public void viewLog() {
    startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS));
  }
}

That activity then goes on to display a DownloadFragment, where most of our code resides.

The Layout

Our sample application has a simple layout, consisting of three buttons:

  1. One to kick off a download
  2. One to query the status of a download
  3. One to display a system-supplied activity containing the roster of downloaded files

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  >
  <Button
    android:id="@+id/start"
    android:text="@string/start_download"
    android:layout_width="match_parent"
    android:layout_height="0dip"
    android:layout_weight="1"
  />
  <Button
    android:id="@+id/query"
    android:text="@string/query_status"
    android:layout_width="match_parent"
    android:layout_height="0dip"
    android:layout_weight="1"
    android:enabled="false"
  />
  <Button android:id="@+id/view"
    android:text="@string/view_log"
    android:layout_width="match_parent"
    android:layout_height="0dip"
    android:layout_weight="1"
  />
</LinearLayout>

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 the onCreateView() method of the DownloadFragment, in which we get the DownloadManager:

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup parent,
                           Bundle savedInstanceState) {
    mgr=
        (DownloadManager)getActivity().getSystemService(Context.DOWNLOAD_SERVICE);

    View result=inflater.inflate(R.layout.main, parent, false);

    query=result.findViewById(R.id.query);
    query.setOnClickListener(this);
    start=result.findViewById(R.id.start);
    start.setOnClickListener(this);

    result.findViewById(R.id.view).setOnClickListener(this);

    return(result);
  }

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 DownloadFragment, shown below:

  private void startDownload(View v) {
    Uri uri=Uri.parse("https://commonsware.com/misc/test.mp4");

    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
               .mkdirs();

    DownloadManager.Request req=new DownloadManager.Request(uri);

    req.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI
                                   | DownloadManager.Request.NETWORK_MOBILE)
       .setAllowedOverRoaming(false)
       .setTitle("Demo")
       .setDescription("Something useful. No, really.")
       .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,
                                          "test.mp4");

    lastDownload=mgr.enqueue(req);

    v.setEnabled(false);
    query.setEnabled(true);
  }

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 getExternalStoragePublicDirectory() 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:

  1. We are downloading the specific URL we want, courtesy of the Uri supplied to the Request constructor
  2. 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 (setAllowedOverRoaming())
  3. 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 (setDescription()), 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:

  private void queryStatus(View v) {
    Cursor c=
        mgr.query(new DownloadManager.Query().setFilterById(lastDownload));

    if (c == null) {
      Toast.makeText(getActivity(), R.string.download_not_found,
                     Toast.LENGTH_LONG).show();
    }
    else {
      c.moveToFirst();

      Log.d(getClass().getName(),
            "COLUMN_ID: "
                + c.getLong(c.getColumnIndex(DownloadManager.COLUMN_ID)));
      Log.d(getClass().getName(),
            "COLUMN_BYTES_DOWNLOADED_SO_FAR: "
                + c.getLong(c.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)));
      Log.d(getClass().getName(),
            "COLUMN_LAST_MODIFIED_TIMESTAMP: "
                + c.getLong(c.getColumnIndex(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP)));
      Log.d(getClass().getName(),
            "COLUMN_LOCAL_URI: "
                + c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)));
      Log.d(getClass().getName(),
            "COLUMN_STATUS: "
                + c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS)));
      Log.d(getClass().getName(),
            "COLUMN_REASON: "
                + c.getInt(c.getColumnIndex(DownloadManager.COLUMN_REASON)));

      Toast.makeText(getActivity(), statusMessage(c), Toast.LENGTH_LONG)
           .show();

      c.close();
    }
  }

The query() method returns a Cursor, containing a series of columns representing the details about our download. There is a series of constants on the DownloadManager class outlining what is possible. In our case, we retrieve (and dump to Logcat):

  1. The ID of the download (COLUMN_ID)
  2. The amount of data that has been downloaded to date (COLUMN_BYTES_DOWNLOADED_SO_FAR)
  3. What the last-modified timestamp is on the download (COLUMN_LAST_MODIFIED_TIMESTAMP)
  4. Where the file is being saved to locally (COLUMN_LOCAL_URI)
  5. What the actual status is (COLUMN_STATUS)
  6. What the reason is for that status (COLUMN_REASON)

Note that COLUMN_LOCAL_URI may be unavailable, if the user has deleted the downloaded file between when the download completed and the time you try to access the column.

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.

Note that you really should close this Cursor when you are done with it. StrictMode, for example, will complain if you do not.

Download Broadcasts

To find out about the results of the download, we need to register a BroadcastReceiver, to watch for two actions used by DownloadManager:

  1. ACTION_DOWNLOAD_COMPLETE, to let us know when the download is done
  2. ACTION_NOTIFICATION_CLICKED, to let us know if the user taps on the Notification displayed on the user’s device related to our download

So, in onResume() of our fragment, we register a single BroadcastReceiver for both of those events:

  @Override
  public void onResume() {
    super.onResume();

    IntentFilter f=
        new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);

    f.addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED);

    getActivity().registerReceiver(onEvent, f);
  }

That BroadcastReceiver is unregistered in onPause():

  @Override
  public void onPause() {
    getActivity().unregisterReceiver(onEvent);

    super.onPause();
  }

The BroadcastReceiver implementation examines the action string of the incoming Intent (via a call to getAction() and either displays a Toast (for ACTION_NOTIFICATION_CLICKED) or enables the start-download Button:

    public void onReceive(Context ctxt, Intent i) {
      if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(i.getAction())) {
        Toast.makeText(ctxt, R.string.hi, Toast.LENGTH_LONG).show();
      }
      else {
        start.setEnabled(true);
      }
    }
  };
}

What the User Sees

The user, upon launching the application, sees our three pretty buttons:

The Download Demo Sample, As Initially Launched
Figure 797: The Download Demo Sample, As Initially Launched

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):

The Download Demo Sample, Downloading
Figure 798: The Download Demo Sample, Downloading

Sliding down the notification drawer shows the user the progress in the form of a ProgressBar widget:

The DownloadManager Notification
Figure 799: The DownloadManager Notification

Tapping on the entry in the notification drawer returns control to our original activity, where they see a Toast, raised by our BroadcastReceiver.

If they tap the middle button during the download, a different Toast will appear indicating that the download is in progress:

The Download Demo, Showing Download Status
Figure 800: The Download Demo, Showing Download Status

Additional details are also dumped to Logcat:

12-10 08:45:01.289: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_ID: 12
12-10 08:45:01.289: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_BYTES_DOWNLOADED_SO_FAR: 615400
12-10 08:45:01.289: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_LAST_MODIFIED_TIMESTAMP: 1291988696232
12-10 08:45:01.289: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_LOCAL_URI: file:///mnt/sdcard/Download/test.mp4
12-10 08:45:01.299: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_STATUS: 2
12-10 08:45:01.299: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_REASON: 0

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:

12-10 08:49:27.360: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_ID: 12
12-10 08:49:27.360: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_BYTES_DOWNLOADED_SO_FAR: 6219229
12-10 08:49:27.370: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_LAST_MODIFIED_TIMESTAMP: 1291988713409
12-10 08:49:27.370: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_LOCAL_URI: file:///mnt/sdcard/Download/test.mp4
12-10 08:49:27.370: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_STATUS: 8
12-10 08:49:27.370: DEBUG/com.commonsware.android.download.DownloadDemo(372): COLUMN_REASON: 0

Tapping the bottom button brings up the activity displaying all downloads, including both successes and failures:

The DownloadManager Results
Figure 801: The DownloadManager Results

And, of course, the file is downloaded.

Limitations

While DownloadManager nowadays supports HTTPS (SSL) URLs, that was not the case when it was introduced back in Android 2.3. You will want to test any HTTPS URLs you intend to use with DownloadManager if you are supporting older versions of Android.

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.

Also, starting with Android 5.0, the Downloads app that provides the core implementation of DownloadManager keeps track of when other apps get uninstalled. At that point, the Downloads app deletes the files downloaded by DownloadManager on behalf of that app. This includes files stored in common locations (e.g., DIRECTORY_DOWNLOADS) that would ordinarily survive an uninstall. For example, if you run the Internet/Download sample app on an Android 5.0+ device, then uninstall the app, the downloaded file vanishes from the Downloads app. If you elect to use DownloadManager, you should either:

Data Saver

The preview of this section apparently resembled a Pokémon.