您的位置:首页 > 移动开发 > Android开发

Android DownloadManager 的使用及断点续传

2015-08-29 11:16 579 查看
官方API:http://developer.android.com/reference/android/app/DownloadManager.html


Class Overview

The download manager is a system service that handles long-running HTTP downloads. Clients may request that a URI be downloaded to a particular destination file. The download manager will conduct the download in the background, taking care of
HTTP interactions and retrying downloads after failures or across connectivity changes and system reboots. Instances of this class should be obtained through
getSystemService(String)
by
passing
DOWNLOAD_SERVICE
. Apps
that request downloads through this API should register a broadcast receiver for
ACTION_NOTIFICATION_CLICKED
to
appropriately handle when the user clicks on a running download in a notification or from the downloads UI. Note that the application must have the
INTERNET
permission
to use this class.

从Android 2.3(API level 9)开始Android用系统服务(Service)的方式提供了Download Manager来优化处理长时间的下载操作。Download Manager处理HTTP连接并监控连接中的状态变化以及系统重启来确保每一个下载任务顺利完成。

在大多数涉及到下载的情况中使用Download Manager都是不错的选择,特别是当用户切换不同的应用以后下载需要在后台继续进行,以及当下载任务顺利完成非常重要的情况(DownloadManager对于断点续传功能支持很好)。

要想使用Download Manager,使用getSystemService方法请求系统的DOWNLOAD_SERVICE服务,代码片段如下:

[java] view
plaincopy

String serviceString = Context.DOWNLOAD_SERVICE;

DownloadManager downloadManager;

downloadManager = (DownloadManager) getSystemService(serviceString);

[java] view
plaincopy

String serviceString = Context.DOWNLOAD_SERVICE; 
4000
;

DownloadManager downloadManager;

downloadManager = (DownloadManager) getSystemService(serviceString);


下载文件

要请求一个下载操作,需要创建一个DownloadManager.Request对象,将要请求下载的文件的Uri传递给Download Manager的enqueue方法,代码片段如下所示:

[java] view
plaincopy

String serviceString = Context.DOWNLOAD_SERVICE;

DownloadManager downloadManager;

downloadManager = (DownloadManager)getSystemService(serviceString);

Uri uri = Uri.parse("http://developer.android.com/shareables/icon_templates-v4.0.zip");

DownloadManager.Request request = new Request(uri);

long reference
= downloadManager.enqueue(request);

[java] view
plaincopy

String serviceString = Context.DOWNLOAD_SERVICE;

DownloadManager downloadManager;

downloadManager = (DownloadManager)getSystemService(serviceString);

Uri uri = Uri.parse("http://developer.android.com/shareables/icon_templates-v4.0.zip");

DownloadManager.Request request = new Request(uri);

long reference = downloadManager.enqueue(request);

在这里返回的reference变量是系统为当前的下载请求分配的一个唯一的ID,我们可以通过这个ID重新获得这个下载任务,进行一些自己想要进行的操作或者查询

下载的状态以及取消下载等等。

我们可以通过addRequestHeader方法为DownloadManager.Request对象request添加HTTP头,也可以通过setMimeType方法重写从服务器返回的mime type。

我们还可以指定在什么连接状态下执行下载操作。setAllowedNetworkTypes方法可以用来限定在WiFi还是手机网络下进行下载,setAllowedOverRoaming方法

可以用来阻止手机在漫游状态下下载。

下面的代码片段用于指定一个较大的文件只能在WiFi下进行下载:

[java] view
plaincopy

request.setAllowedNetworkTypes(Request.NETWORK_WIFI);

[java] view
plaincopy

request.setAllowedNetworkTypes(Request.NETWORK_WIFI);

Android API level 11 介绍了getRecommendedMaxBytesOverMobile类方法(静态方法),返回一个当前手机网络连接下的最大建议字节数,可以来判断下载

是否应该限定在WiFi条件下。

调用enqueue方法之后,只要数据连接可用并且Download Manager可用,下载就会开始。

要在下载完成的时候获得一个系统通知(notification),注册一个广播接受者来接收ACTION_DOWNLOAD_COMPLETE广播,这个广播会包含一个

EXTRA_DOWNLOAD_ID信息在intent中包含了已经完成的这个下载的ID,代码片段如下所示:

[java] view
plaincopy

IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);

BroadcastReceiver receiver = new BroadcastReceiver()
{

@Override

public void onReceive(Context
context, Intent intent) {

long reference
= intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);

if (myDownloadReference
== reference) {

}

}

};

registerReceiver(receiver, filter);

[java] view
plaincopy

IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);

BroadcastReceiver receiver = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

long reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);

if (myDownloadReference == reference) {

}

}

};

registerReceiver(receiver, filter);

使用Download Manager的openDownloadedFile方法可以打开一个已经下载完成的文件,返回一个ParcelFileDescriptor对象。我们可以通过Download Manager来

查询下载文件的保存地址,如果在下载时制定了路径和文件名,我们也可以直接操作文件。

我们可以为ACTION_NOTIFICATION_CLICKED action注册一个广播接受者,当用户从通知栏点击了一个下载项目或者从Downloads app点击可一个下载的项目的

时候,系统就会发出一个点击下载项的广播。

代码片段如下:

[java] view
plaincopy

IntentFilter filter = new IntentFilter(DownloadManager.ACTION_NOTIFICATION_CLICKED);

BroadcastReceiver receiver = new BroadcastReceiver()
{

@Override

public void onReceive(Context
context, Intent intent) {

String extraID = DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS;

long[]
references = intent.getLongArrayExtra(extraID);

for (long reference
: references)

if (reference
== myDownloadReference) {

// Do something with downloading file.

}

}

};

registerReceiver(receiver, filter);

[java] view
plaincopy

IntentFilter filter = new IntentFilter(DownloadManager.ACTION_NOTIFICATION_CLICKED);

BroadcastReceiver receiver = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

String extraID = DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS;

long[] references = intent.getLongArrayExtra(extraID);

for (long reference : references)

if (reference == myDownloadReference) {

// Do something with downloading file.

}

}

};

registerReceiver(receiver, filter);


定制Download Manager Notifications的样式

默认情况下,通知栏中会显示被Download Manager管理的每一个download每一个Notification会显示当前的下载进度和文件的名字,如下图所示:

通过Download Manager可以为每一个download request定制Notification的样式,包括完全隐藏Notification。下面的代码片段显示了通过setTitle和setDescription

方法来定制显示在文件下载Notification中显示的文字。

[java] view
plaincopy

request.setTitle(“Earthquakes”);

request.setDescription(“Earthquake XML”);

[java] view
plaincopy

request.setTitle(“Earthquakes”);

request.setDescription(“Earthquake XML”);

setNotificationVisibility方法可以用来控制什么时候显示Notification,甚至是隐藏该request的Notification。有以下几个参数:

Request.VISIBILITY_VISIBLE:在下载进行的过程中,通知栏中会一直显示该下载的Notification,当下载完成时,该Notification会被移除,这是默认的参数值。

Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED:在下载过程中通知栏会一直显示该下载的Notification,在下载完成后该Notification会继续显示,直到用户点击该

Notification或者消除该Notification。

Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION:只有在下载完成后该Notification才会被显示。

Request.VISIBILITY_HIDDEN:不显示该下载请求的Notification。如果要使用这个参数,需要在应用的清单文件中加上DOWNLOAD_WITHOUT_NOTIFICATION权限。


指定下载保存地址

默认情况下,所有通过Download Manager下载的文件都保存在一个共享下载缓存中,使用系统生成的文件名每一个Request对象都可以制定一个下载

保存的地址,通常情况下,所有的下载文件都应该保存在外部存储中,所以我们需要在应用清单文件中加上WRITE_EXTERNAL_STORAGE权限:

[html] view
plaincopy

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

[html] view
plaincopy

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

下面的代码片段是在外部存储中指定一个任意的保存位置的方法:

[java] view
plaincopy

request.setDestinationUri(Uri.fromFile(f));

[java] view
plaincopy

request.setDestinationUri(Uri.fromFile(f));

f是一个File对象。

如果下载的这个文件是你的应用所专用的,你可能会希望把这个文件放在你的应用在外部存储中的一个专有文件夹中。注意这个文件夹不提供访问控制,

所以其他的应用也可以访问这个文件夹。在这种情况下,如果你的应用卸载了,那么在这个文件夹也会被删除。

下面的代码片段是指定存储文件的路径是应用在外部存储中的专用文件夹的方法:

[java] view
plaincopy

request.setDestinationInExternalFilesDir(this,

Environment.DIRECTORY_DOWNLOADS, “Bugdroid.png”);

[java] view
plaincopy

request.setDestinationInExternalFilesDir(this,

Environment.DIRECTORY_DOWNLOADS, “Bugdroid.png”);

如果下载的文件希望被其他的应用共享,特别是那些你下载下来希望被Media Scanner扫描到的文件(比如音乐文件),那么你可以指定你的下载路径在

外部存储的公共文件夹之下,下面的代码片段是将文件存放到外部存储中的公共音乐文件夹的方法:

[java] view
plaincopy

request.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC,

"Android_Rock.mp3");

[java] view
plaincopy

request.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC,

"Android_Rock.mp3");

在默认的情况下,通过Download Manager下载的文件是不能被Media Scanner扫描到的,进而这些下载的文件(音乐、视频等)就不会在Gallery和

Music Player这样的应用中看到。

为了让下载的音乐文件可以被其他应用扫描到,我们需要调用Request对象的allowScaningByMediaScanner方法。

如果我们希望下载的文件可以被系统的Downloads应用扫描到并管理,我们需要调用Request对象的setVisibleInDownloadsUi方法,传递参数true。


取消和删除下载

Download Manager的remove方法可以用来取消一个准备进行的下载,中止一个正在进行的下载,或者删除一个已经完成的下载。

remove方法接受若干个download 的ID作为参数,你可以设置一个或者几个你想要取消的下载的ID,如下代码段所示:

[java] view
plaincopy

downloadManager.remove(REFERENCE_1, REFERENCE_2, REFERENCE_3);

[java] view
plaincopy

downloadManager.remove(REFERENCE_1, REFERENCE_2, REFERENCE_3);

该方法返回成功取消的下载的个数,如果一个下载被取消了,所有相关联的文件,部分下载的文件和完全下载的文件都会被删除。


查询Download Manager

你可以通过查询Download Manager来获得下载任务的状态,进度,以及各种细节,通过query方法返回一个包含了下载任务细节的Cursor。

query方法传递一个DownloadManager.Query对象作为参数,通过DownloadManager.Query对象的setFilterById方法可以筛选我们希望查询的下

载任务的ID。也可以使用setFilterByStatus方法筛选我们希望查询的某一种状态的下载任务,传递的参数是DownloadManager.STATUS_*常量,可以指定

正在进行、暂停、失败、完成四种状态。

Download Manager包含了一系列COLUMN_*静态String常量,可以用来查询Cursor中的结果列索引。我们可以查询到下载任务的各种细节,包括状态,

文件大小,已经下载的字节数,标题,描述,URI,本地文件名和URI,媒体类型以及Media Provider download URI。

下面的代码段是通过注册监听下载完成事件的广播接受者来查询下载完成文件的本地文件名和URI的实现方法:

[java] view
plaincopy

@Override

public void onReceive(Context
context, Intent intent) {

long reference
= intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);

if (myDownloadReference
== reference) {

Query myDownloadQuery = new Query();

myDownloadQuery.setFilterById(reference);

Cursor myDownload = downloadManager.query(myDownloadQuery);

if (myDownload.moveToFirst())
{

int fileNameIdx
=

myDownload.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);

int fileUriIdx
=

myDownload.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);

String fileName = myDownload.getString(fileNameIdx);

String fileUri = myDownload.getString(fileUriIdx);

// TODO Do something with the file.

Log.d(TAG, fileName + " : " +
fileUri);

}

myDownload.close();

}

}

[java] view
plaincopy

@Override

public void onReceive(Context context, Intent intent) {

long reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);

if (myDownloadReference == reference) {

Query myDownloadQuery = new Query();

myDownloadQuery.setFilterById(reference);

Cursor myDownload = downloadManager.query(myDownloadQuery);

if (myDownload.moveToFirst()) {

int fileNameIdx =

myDownload.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);

int fileUriIdx =

myDownload.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);

String fileName = myDownload.getString(fileNameIdx);

String fileUri = myDownload.getString(fileUriIdx);

// TODO Do something with the file.

Log.d(TAG, fileName + " : " + fileUri);

}

myDownload.close();

}

}

对于暂停和失败的下载,我们可以通过查询COLUMN_REASON列查询出原因的整数码。

对于STATUS_PAUSED状态的下载,可以通过DownloadManager.PAUSED_*静态常量来翻译出原因的整数码,进而判断出下载是由于等待网络连接

还是等待WiFi连接还是准备重新下载三种原因而暂停。

对于STATUS_FAILED状态的下载,我们可以通过DownloadManager.ERROR_*来判断失败的原因,可能是错误码(失败原因)包括没有存储设备,

存储空间不足,重复的文件名,或者HTTP errors。

下面的代码是如何查询出当前所有的暂停的下载任务,提取出暂停的原因以及文件名称,下载标题以及当前进度的实现方法:

[java] view
plaincopy

// Obtain the Download Manager Service.

String serviceString = Context.DOWNLOAD_SERVICE;

DownloadManager downloadManager;

downloadManager = (DownloadManager)getSystemService(serviceString);

// Create a query for paused downloads.

Query pausedDownloadQuery = new Query();

pausedDownloadQuery.setFilterByStatus(DownloadManager.STATUS_PAUSED);

// Query the Download Manager for paused downloads.

Cursor pausedDownloads = downloadManager.query(pausedDownloadQuery);

// Find the column indexes for the data we require.

int reasonIdx
= pausedDownloads.getColumnIndex(DownloadManager.COLUMN_REASON);

int titleIdx
= pausedDownloads.getColumnIndex(DownloadManager.COLUMN_TITLE);

int fileSizeIdx
=

pausedDownloads.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);

int bytesDLIdx
=

pausedDownloads.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);

// Iterate over the result Cursor.

while (pausedDownloads.moveToNext())
{

// Extract the data we require from the Cursor.

String title = pausedDownloads.getString(titleIdx);

int fileSize
= pausedDownloads.getInt(fileSizeIdx);

int bytesDL
= pausedDownloads.getInt(bytesDLIdx);

// Translate the pause reason to friendly text.

int reason
= pausedDownloads.getInt(reasonIdx);

String reasonString = "Unknown";

switch (reason)
{

case DownloadManager.PAUSED_QUEUED_FOR_WIFI
:

reasonString = "Waiting for WiFi"; break;

case DownloadManager.PAUSED_WAITING_FOR_NETWORK
:

reasonString = "Waiting for connectivity"; break;

case DownloadManager.PAUSED_WAITING_TO_RETRY
:

reasonString = "Waiting to retry"; break;

default : break;

}

// Construct a status summary

StringBuilder sb = new StringBuilder();

sb.append(title).append("\n");

sb.append(reasonString).append("\n");

sb.append("Downloaded ").append(bytesDL).append("
/ " ).append(fileSize);

// Display the status

Log.d("DOWNLOAD",
sb.toString());

}

// Close the result Cursor.

pausedDownloads.close();

[java] view
plaincopy

// Obtain the Download Manager Service.

String serviceString = Context.DOWNLOAD_SERVICE;

DownloadManager downloadManager;

downloadManager = (DownloadManager)getSystemService(serviceString);

// Create a query for paused downloads.

Query pausedDownloadQuery = new Query();

pausedDownloadQuery.setFilterByStatus(DownloadManager.STATUS_PAUSED);

// Query the Download Manager for paused downloads.

Cursor pausedDownloads = downloadManager.query(pausedDownloadQuery);

// Find the column indexes for the data we require.

int reasonIdx = pausedDownloads.getColumnIndex(DownloadManager.COLUMN_REASON);

int titleIdx = pausedDownloads.getColumnIndex(DownloadManager.COLUMN_TITLE);

int fileSizeIdx =

pausedDownloads.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);

int bytesDLIdx =

pausedDownloads.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);

// Iterate over the result Cursor.

while (pausedDownloads.moveToNext()) {

// Extract the data we require from the Cursor.

String title = pausedDownloads.getString(titleIdx);

int fileSize = pausedDownloads.getInt(fileSizeIdx);

int bytesDL = pausedDownloads.getInt(bytesDLIdx);

// Translate the pause reason to friendly text.

int reason = pausedDownloads.getInt(reasonIdx);

String reasonString = "Unknown";

switch (reason) {

case DownloadManager.PAUSED_QUEUED_FOR_WIFI :

reasonString = "Waiting for WiFi"; break;

case DownloadManager.PAUSED_WAITING_FOR_NETWORK :

reasonString = "Waiting for connectivity"; break;

case DownloadManager.PAUSED_WAITING_TO_RETRY :

reasonString = "Waiting to retry"; break;

default : break;

}

// Construct a status summary

StringBuilder sb = new StringBuilder();

sb.append(title).append("\n");

sb.append(reasonString).append("\n");

sb.append("Downloaded ").append(bytesDL).append(" / " ).append(fileSize);

// Display the status

Log.d("DOWNLOAD", sb.toString());

}

// Close the result Cursor.

pausedDownloads.close();

断点续传

先说说断点续传的原理:这是HTTP 1.1协议的一部分,并不需要客户端特意去做多么复杂的事情。以前我曾经看过一个单位的技术标书,其中有下载的断点续传这一要求,给出的offer居然还挺高的...

简单的说,只要利用了HTTP协议http://www.ietf.org/rfc/rfc2616.txt)中的如下字段来和服务器端交互,就可以实现文件下载的断点续传:

Range : 用于客户端到服务器端的请求,可通过该字段指定下载文件的某一段大小,及其单位。典型的格式如:

Range: bytes=0-499 下载第0-499字节范围的内容

Range: bytes=500-999 下载第500-999字节范围的内容

Range: bytes=-500 下载最后500字节的内容

Range: bytes=500- 下载从第500字节开始到文件结束部分的内容(这是最常用的一种格式)

Range: bytes=0-0,-1 下载第一以及最后一个字节的内容(这个看上去有点变态...)

Accept-Ranges : 用于服务器端到客户端的应答,客户端通过该字段可以判断服务器是否支持断点续传(注意RFC中注明了这一部分并不是必须的)。格式如下:

Accept-Ranges: bytes 表示支持以bytes为单位进行传输。

Accept-Ranges: none 表示不支持

Content-Ranges : 用于服务器端到客户端的应答,与Accept-Ranges在同一个报文内,通过该字段指定了返回的文件资源的字节范围。格式如下:

Content-Ranges: bytes 0-499/1234 大小为1234的文件的第0-499字节范围的内容

Content-Ranges: bytes 734-1233/1234 大小为1234字节的文件的第734-结尾范围的内容

据此我们可以知道,断点续传这个功能是需要客户端和服务器端同时支持才能完成。

Android平台面向开发者提供了DownloadManager这个服务(service),可以用来完成下载,同时异步地得到下载进度的实时更新提示。原生的浏览器,Android Market以及GMail等客户端都使用了该接口。该接口也部分的提供了断点续传功能:如果在下载过程中遇到网络错误,如信号中断等,DownloadManager会在网络恢复时尝试断点续传继续下载该文件。但不支持由用户发起的暂停然后断点续传。

要扩展该功能也不难,只要为下载任务新增一种状态(类似paused_by_user),以及相关逻辑即可,这里暂不赘述,把话题引到一些常见问题上。

1. 关于ETag

RFC中的定义有些抽象,简单的说,ETag可以用来标识/保证文件的唯一性或完整性,你可以把它看作是服务器为某个文件生产的唯一标识值,每次文件有更新该值就会变化。通过这种机制客户端可以检查某个文件在断点续传(当然它不仅仅用于断点续传)的前后是否有所改动:如果ETag改变了就应该重新下载整个文件以保证它的完整性。

但是在现实环境中,有一些服务器并不返回ETag字段,同时它又是支持断点续传的,这种情况下原生的Android就会认为服务器端不支持断点续传。这应该不是什么bug,仅仅是这么实现而已。还有更麻烦的情况是,有些服务器给了错误的ETag,但文件是从未更改的,这时候要想从客户端修改这个“bug”,估计只能忽略ETag值了。

2. 关于HTTP 206

RFC中定义了断点续传时服务器端的应答情况:如果支持且返回的内容如请求所要求的那样,是该文件的一部分,则使用HTTP 206状态码;如果不支持,或需要返回整个文件,则使用HTTP 200状态码。但是现实网络中有些服务器不管三七二十一,都返回200。没办法,如果还是想从客户端来修改这个“bug”,那就多做一些判断处理吧:如果服务器指定了“Content-Ranges”,就忽略HTTP 200的状态码。

附图一张,简述流程。



补记:有一次被问起如何在原生的Android手机上暂停一个下载任务,回头再断点续传。我想是不是可以在下载过程中将手机信号关闭,下次再打开手机信号时,那个下载任务就可以自动接着续传了(当然前提是服务器支持)...这个用例没多大实用价值,懒得实测了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: