Android中的DownloadManager使用

前言

在Android开发中经常会用到将一个文件下载到本地存储卡,这个过程看起来很容易,就是一个网络请求过程而已,但是有的时候我们需要把下载的内容存储到一个可以公共访问的位置,供其他应用共享,这个时候我们可以使用Android官方提供的一个DownloadManage来很方便的实现。事实上DownloadManage自API 9就已经存在了,中间官方只做了一些小的调整。

下载文件

下面通过一个下载图片的小Demo来一步步理解DownloadManager的使用和涉及到的一些内在知识,类的基本关系如下。

Demo结构

接下来开始一个简单的UI实现,放一个按钮并添加点击事件。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/activity_main"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <Button
    android:id="@+id/download"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/download"
    android:layout_centerInParent="true"/>
</RelativeLayout>
//下载图片文件路径
private static final String URI_STRING = "http://dp2px.com/images/head.png";

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();
        }
    });
    //获取download对象(Downloader实现了DownloadManager的逻辑)
    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);
}

上面代码的逻辑很简单,点击按钮来根据当前下载状态切换是否下载或者暂停,此刻你也许已经看了出来,问题的核心在于Downloader类的实现。Downloader类可能会让你大吃一惊,它是如此的简单。

class Downloader{

    private final Listener listener;
    private final DownloadManager downloadManager;

    private long downloadId = -1;

    private Downloader(DownloadManager downloadManager, Listener listener) {
        this.downloadManager = downloadManager;
        this.listener = listener;
    }

    //创建DowloadManager对象并初始化实例。
    static Downloader newInstance(Listener listener) {
        Context context = listener.getContext();
        //通过Context.DOWNLOAD_SERVICE获取DownloadManager实例
        DownloadManager downloadManager = (DownloadManager)
                 context.getSystemService(Context.DOWNLOAD_SERVICE);
        return new Downloader(downloadManager, listener);
    }

    //开始下载
    void download(Uri uri) {
        if (!isDownloading()) {
            DownloadManager.Request request = new DownloadManager.Request(uri);
            downloadId = downloadManager.enqueue(request);
        }
    }

    //判断是否在下载中
    boolean isDownloading() {
        return downloadId >= 0;
    }

    //取消下载
    void cancel() {
        if (isDownloading()) {
            downloadManager.remove(downloadId);
            downloadId = -1;
        }
    }

    //下载结果监听回调接口
    interface Listener {
        void fileDownloaded(Uri uri, String mimeType);
        Context getContext();
    }
}

点击我们的下载按钮,可以下载图片了,不过这个时候我们感知不到,因为我们上面代码没有监听下载过程和结果,DownloadManager的下载结果是通过广播来接收的。

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);
    }
}

这个广播我们在Dowloader中注册,从DownloadManager中取的下载结果并处理下载结果。

class Downloader implements DownloadReceiver.Listener {

    //...省略

    @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 register() {
        if (receiver == null) {
            receiver = new DownloadReceiver(this);
            receiver.register(listener.getContext());
        }
    }

    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);
        }
    }

    interface Listener {
        void fileDownloaded(Uri uri, String mimeType);
        Context getContext();
    }
}

在分析DownloadManager中如何拿到下载到的文件数据之前,我们切记要在MainActivity中的声明周期中调用Downloader的unregister,以便在退出MainActivity的时候释放掉资源。当我们退出MainActivity的时候不需要关心下载结果,而下载过程还在后台进行。

@Override
protected void onDestroy() {
    downloader.unregister();
    super.onDestroy();
}

我们注意到上面在getFileInfo中最终获得的是一个content://...的Uri对象,这个Uri对于其他应用访问是很友好的,允许它们通过DownloadManager提供的ContentProvider访问内容,关于ContentProvider请参考我的另一篇博文《FileProvider》

Download

DownloadManager在后台有一个下载服务,当我们中途下载失败或者系统重启等特殊情况下会尝试重新下载的。

下载设置

通知栏

在我们下载文件的过程中通知栏会多一条消息,这个消息的显示内容我们可以通过setNotificationVisibility()自定义样式。也可以使用DownloadManager.Request对象来控制让通知栏不显示,但是需要获得DOWNLOAD_WITHOUT_NOTIFICATION权限。

使用DownloadManager.Request对象的setTitle()setDescription()方法更改代码如下:

void download(Uri uri) {
    if (!isDownloading()) {
        DownloadManager.Request request = new DownloadManager.Request(uri);
        request.setTitle("下载图片");
        request.setDescription("正在下载图片,请稍等...");
        downloadId = downloadManager.enqueue(request);
        register();
    }
}

Header头

有时候我们需要在提交服务器的时候添加一些Header消息头,同样可以使用DownloadManager.Request对象的addRequestHeader()方法添加。

下载限制

有的时候我们需要下载的文件非常大,这个时候可能需要去限制下载的流量,可以使用DownloadManager.Request对象的setAllowedOverRoaming()方法来设置下载流量大小,可以使用setAllowedNetworkTypes()方法来设置和过滤特定的网络类型(在API 16之后可以使用setAllowedOverMetered()方法代替)

系统可见

还可以使用DownloadManager.Request对象的allowScanningByMediaScanner()方法来设置是否下载完成后对系统可见。对系统可见的意思是,是否系统可以扫描,例如我们在下载图片的时候指定它,系统就会扫描该文件,然后在图像库中可以看到它。

我们可以控制内容可见的另一种方法是DownloadManager.Request对象的setVisibleInDownloadsUi()方法,这个方法来控制在下载过程中是否在系统下载应用程序中可见。

保存位置

默认情况下,我们的文件是被下载到系统的一个默认区域,这个区域是应用程序的私有区域,这个区域来存放文件的文件只可供应用程序内部访问,如果卸载应用程序会删除它。但是有两种情况默认的存储区域并不合适。

第一种:我们存储的文件非常大,这个时候不适合存放在应用程序私有区域,应该存储在外部存储区域上。这个时候可以使用setDestinationInExternalFilesDir()来存储到应用程序的外部存储私有区域。

第二种:我们存储的文件非常大并且需要被其他应用程序共享,上面的setDestinationInExternalFilesDir()不能方便的被其他文件共享,这个时候需要使用setDestinationInExternalPublicDir()来存储,这个会被系统扫描到。

private static final String DIRECTORY = "Download/DownloadManager";
//...
request.setDestinationInExternalPublicDir(DIRECTORY, uri.getLastPathSegment());

注意:外部存储区域需要WRITE_EXTERNAL_STORAGE权限,而且应用卸载后文件仍然存在。