最近在安卓4.4上遇到一个断开wifi后重新连接wifi, downloadProvider继续下载文件失败的问题。于是开始了解下载管理模块的断点续载功能:
1、首先,分析android log, 当将网络断开之后,下载会中止,出现如下信息:
W/DownloadManager(29473): Aborting request for download 5: Failed reading response: java.net.SocketException: recvfrom failed: ETIMEDOUT (Connection timed out)
I/DownloadManager(29473): Download 5 finished with status WAITING_FOR_NETWORK
在代码中搜索Failed reading response, 发现是在下载数据中不断读取网络数据流时抛出的异常:
/**
* Transfer as much data as possible from the HTTP response to the
* destination file.
*/
private void transferData(State state, InputStream in, OutputStream out)
throws StopRequestException {
final byte data[] = new byte[Constants.BUFFER_SIZE];
for (;;) {
int bytesRead = readFromResponse(state, data, in);
if (bytesRead == -1) { // success, end of stream already reached
handleEndOfStream(state);
return;
}
state.mGotData = true;
writeDataToDestination(state, data, bytesRead, out);
state.mCurrentBytes += bytesRead;
reportProgress(state);
checkPausedOrCanceled(state);
}
在循环中不停读取网络那边的响应,当网络断开后,InputStream的读接口应该就会抛出异常,代码中进行捕捉,并且判断之后是否能够断点续载,然后抛出相应信息:
/**
* Read some data from the HTTP response stream, handling I/O errors.
* @param data buffer to use to read data
* @param entityStream stream for reading the HTTP response entity
* @return the number of bytes actually read or -1 if the end of the stream has been reached
*/
private int readFromResponse(State state, byte[] data, InputStream entityStream)
throws StopRequestException {
try {
return entityStream.read(data);
} catch (IOException ex) {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
if (cannotResume(state)) {
throw new StopRequestException(STATUS_CANNOT_RESUME,
"Failed reading response: " + ex + "; unable to resume", ex);
} else {
throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
"Failed reading response: " + ex, ex);
}
}
}
这里的判断是否能够续载,有很多条件, 主要是两个方面,下载字节数是否大于0 或者 是否DRM 下载需要转换:
D/DownloadManager( 9658): state.mCurrentBytes=5257536 state.mHeaderETag=69b8155f8ae29636cec71afb21637c92 mInfo.mNoIntegrity=false state.mMimeType=application/vnd.android.package-archive
导出数据库,查看此时下载管理该文件状态:
这个状态 status = 195 是怎么来的呢?
我们可以继续跟踪代码,前面说了,当网络断开后,代码开始抛出异常StopRequestException, 并且带有错误码,仔细阅读代码,这个异常是各个方法,
一层一层网上抛出,最后达到下载管理线程 DownloadThread 类中的 run中, 它在catch这个异常后,也会打印出log信息,并且增加了处理:
catch (StopRequestException error) {
// remove the cause before printing, in case it contains PII
errorMsg = error.getMessage();
String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
Log.w(Constants.TAG, msg);
if (Constants.LOGV) {
Log.w(Constants.TAG, msg, error);
}
finalStatus = error.getFinalStatus();
从代码中可以看出其增加了下载文件在数据库中存放的Id信息,然后在加上出错新消息,也就我们最终看到的log:
W/DownloadManager(29473): Aborting request for download 5: Failed reading response: java.net.SocketException: recvfrom failed: ETIMEDOUT (Connection timed out)
在输出完信息之后,其会对错误码判断进行处理,想断网这种问题,会有个继续尝试,然后确定最终的错误码。最初抛出异常的错误码是STATUS_HTTP_DATA_ERROR , 即495.
W/DownloadManager(11584): Aborting request for download 5: Failed reading response: java.net.SocketException: recvfrom failed: ETIMEDOUT (Connection timed out)
D/DownloadManager(11584): -----finalStatus=495
最后经过代码转换:
// Some errors should be retryable, unless we fail too many times.
if (isStatusRetryable(finalStatus)) {
if (state.mGotData) {
numFailed = 1;
} else {
numFailed += 1;
}
if (numFailed < Constants.MAX_RETRIES) {
final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
if (info != null && info.getType() == state.mNetworkType
&& info.isConnected()) {
// Underlying network is still intact, use normal backoff
finalStatus = STATUS_WAITING_TO_RETRY;
} else {
// Network changed, retry on any next available
finalStatus = STATUS_WAITING_FOR_NETWORK;
}
}
}
会变成 STATUS_WAITING_FOR_NETWORK 195,然后在finally中处理,通过通知方法notifyDownloadCompleted将状态值存储到
数据库中, 即我们最终看到了status = 195
之所以需要转换,我觉得是最下层抛出来的错误码是 http网络那边定义的, 而我们储存到数据库中的状态值是给下载管理模块用的, 两者的
定义和使用详细程度是有区别的,因为管理方式不同。
2、网络重连后的log信息分析:
I/DownloadManager(11584):
Download 5 startingstate.mRequestUri=
http://w.gdown.baidu.com/data/wisegame/8ae29636cec71afb/17173shouyou_3300.apk?f=m1101I/DownloadManager(11584): have run thread before for id: 5, and state.mFilename: /storage/emulated/0/Download/17173shouyou_3300.apk
I/DownloadManager(11584): resuming download for id: 5, and state.mFilename: /storage/emulated/0/Download/17173shouyou_3300.apk
I/DownloadManager(11584): resuming download for id: 5, and starting with file of length: 5367618
I/DownloadManager(11584): resuming download for id: 5, state.mCurrentBytes: 5367618, and setting mContinuingDownload to true:
D/DownloadManager(11584): userAgent: AndroidDownloadManager/4.4.2 (Linux; U; Android 4.4.2; A11w Build/KOT49H)
D/DownloadManager(11584): mMimeType =application/vnd.android.package-archive, mIsPublicApi=true
I/DownloadManager(11584): Download 5 finished with status SUCCESS
D/DownloadManager(11584): drm:requestScanFile:info.mFileName= /storage/emulated/0/Download/17173shouyou_3300.apk mimeType= application/vnd.android.package-archive
DownloadReceiver中会监听网络的变化,当网络重新连接后,其会重新启动下载管理服务:
else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
final ConnectivityManager connManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo info = connManager.getActiveNetworkInfo();
if (info != null && info.isConnected()) {
startService(context);
}
这个时候在执行下载executeDownload时,检测是否已经下载过该文件就起到作用了,也就是resuming download那一段的log信息,会地区文件路径,已经下载大小等等信息。
不过此时需要注意从网络端获取的返回码的情况,正常情况下不是 HTTP_OK 200了:
final int responseCode = conn.getResponseCode();
Log.i(Constants.TAG, "-----[executeDownload] responseCode="+responseCode);
I/DownloadManager(11584): -----[executeDownload] responseCode=206
通过log信息我们可以看到此时返回的是 HTTP_PARTIAL 206 , 对比两个case:
case HTTP_OK:
if (state.mContinuingDownload) {
throw new StopRequestException(
STATUS_CANNOT_RESUME, "Expected partial, but received OK");
}
processResponseHeaders(state, conn);
transferData(state, conn);
return;
case HTTP_PARTIAL:
if (!state.mContinuingDownload) {
throw new StopRequestException(
STATUS_CANNOT_RESUME, "Expected OK, but received partial");
}
transferData(state, conn);
return;
可以看出后者不再需要重新处理头部信息,只需要直接传输数据就可以了。
以上的log信息是断开网络后,连接网络成功下载文件的情况。
3、重新打开wifi后下载失败的情况:
I/DownloadManager(11584): Download 6 starting
state.mRequestUri=
http://w.gdown.baidu.com/data/wisegame/32ef8e3c0291add2/baidunuomi_153.apk?f=m1101I/DownloadManager(11584): have run thread before for id: 6, and state.mFilename: /storage/emulated/0/Download/baidunuomi_153.apk
I/DownloadManager(11584): resuming download for id: 6, and state.mFilename: /storage/emulated/0/Download/baidunuomi_153.apk
I/DownloadManager(11584): resuming download for id: 6, and starting with file of length: 3128774
I/DownloadManager(11584): resuming download for id: 6, state.mCurrentBytes: 3128774, and setting mContinuingDownload to true:
D/DownloadManager(11584): userAgent: AndroidDownloadManager/4.4.2 (Linux; U; Android 4.4.2; A11w Build/KOT49H)
I/DownloadManager(11584): -----[executeDownload] responseCode=200
W/DownloadManager(11584): Aborting request for download 6: Expected partial, but received OK
D/DownloadManager(11584): mMimeType =application/vnd.android.package-archive, mIsPublicApi=true
I/DownloadManager(11584): Download 6 finished with status CANNOT_RESUME
从关键信息Aborting request for download 6: Expected partial, but received OK
可以看出, 在重新启动下载后,从网络那边的返回码跟正常下载已经不同了,正常情况下回返回 206, 而这里的信息返回码是200,然后代码抛出异常,
即从信息也可以看出, 代码期望得到返回值未partial, 但是实际得到的却是 OK。
在网上查询了一下HTTP的返回码信息:
HTTP协议状态码表示的意思主要分为五类 ,大体是 :
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1×× 保留
2×× 表示请求成功地接收
3×× 为完成请求客户需进一步细化请求
4×× 客户错误
5×× 服务器错误
100 Continue
指示客户端应该继续请求。回送用于通知客户端此次请求已经收到,并且没有被服务器拒绝。
客户端应该继续发送剩下的请求数据或者请求已经完成,或者忽略回送数据。服务器必须发送
最后的回送在请求之后。
101 Switching Protocols
服务器依照客服端请求,通过Upgrade头信息,改变当前连接的应用协议。服务器将根据Upgrade头立刻改变协议
在101回送以空行结束的时候。
Successful
=================================
200 OK
指示客服端的请求已经成功收到,解析,接受。
201 Created
请求已经完成并一个新的返回资源被创建。被创建的资源可能是一个URI资源,通常URI资源在Location头指定。回送应该包含一个实体数据
并且包含资源特性以及location通过用户或者用户代理来选择合适的方法。实体数据格式通过煤体类型来指定即content-type头。最开始服务 器
必须创建指定的资源在返回201状态码之前。如果行为没有被立刻执行,服务器应该返回202。
202 Accepted
请求已经被接受用来处理。但是处理并没有完成。请求可能或者根本没有遵照执行,因为处理实际执行过程中可能被拒绝。
203 Non-Authoritative Information
204 No Content
服务器已经接受请求并且没必要返回实体数据,可能需要返回更新信息。回送可能包含新的或更新信息由entity-headers呈现。
205 Reset Content
服务器已经接受请求并且用户代理应该重新设置文档视图。
206 Partial Content
服务器已经接受请求GET请求资源的部分。请求必须包含一个Range头信息以指示获取范围可能必须包含If-Range头信息以成立请求条件。
Redirection
==================================
300 Multiple Choices
请求资源符合任何一个呈现方式。
301 Moved Permanently
请求的资源已经被赋予一个新的URI。
302 Found
通过不同的URI请求资源的临时文件。
303 See Other
304 Not Modified
如果客服端已经完成一个有条件的请求并且请求是允许的,但是这个文档并没有改变,服务器应该返回304状态码。304
状态码一定不能包含信息主体,从而通常通过一个头字段后的第一个空行结束。
305 Use Proxy
请求的资源必须通过代理(由Location字段指定)来访问。Location资源给出了代理的URI。
306 Unused
307 Temporary Redirect
Client Error
=====================
400 Bad Request
因为错误的语法导致服务器无法理解请求信息。
401 Unauthorized
如果请求需要用户验证。回送应该包含一个WWW-Authenticate头字段用来指明请求资源的权限。
402 Payment Required
保留状态码
403 Forbidden
服务器接受请求,但是被拒绝处理。
404 Not Found
服务器已经找到任何匹配Request-URI的资源。
405 Menthod Not Allowed
Request-Line 请求的方法不被允许通过指定的URI。
406 Not Acceptable
407 Proxy Authentication Required
408 Reqeust Timeout
客服端没有提交任何请求在服务器等待处理时间内。
409 Conflict
410 Gone
411 Length Required
服务器拒绝接受请求在没有定义Content-Length字段的情况下。
412 Precondition Failed
413 Request Entity Too Large
服务器拒绝处理请求因为请求数据超过服务器能够处理的范围。服务器可能关闭当前连接来阻止客服端继续请求。
414 Request-URI Too Long
服务器拒绝服务当前请求因为URI的长度超过了服务器的解析范围。
415 Unsupported Media Type
服务器拒绝服务当前请求因为请求数据格式并不被请求的资源支持。
416 Request Range Not Satisfialbe
417 Expectation Failed
Server Error
===================================
500 Internal Server Error
服务器遭遇异常阻止了当前请求的执行
501 Not Implemented
服务器没有相应的执行动作来完成当前请求。
502 Bad Gateway
503 Service Unavailable
因为临时文件超载导致服务器不能处理当前请求。
504 Gateway Timeout
505 Http Version Not Supported
从如上信息来看猜想 206 是之前已经请求过了,接下来请求余下部分的内容,下载管理发送出去的请求信息应该和正常下载时是一致的。
仔细测试发现,从设置直接打开wifi后,并没有真正连接上,还是需要登录账号和输入密码,这个可能和路由器的设置有关系。
代码中对此类异常的处理同样如上所述,上层捕获,然后判断处理,最终将状态值存储到数据库:
throw new StopRequestException(
STATUS_CANNOT_RESUME, "Expected partial, but received OK");
此问题应该不算是downloadProvider的问题,因为是没有连接上网络,所以获取的返回值出问题了,导致最终下载失败,因为下载管理中已经定义了这种情况
下是不能够续载的。
4、另外再分析一下就是下载中途将网络关掉后, 通知栏中的下载进度显示也会被一起清扫掉,之前项目经理认为此处有问题,应该保留成下载暂停状态。
我之前对下载管理的特性也不了解,只好继续看代码。
通知栏的更新主要是通过mNotifier来进行的,即类DownloadNotifier中的处理, 在下载服务的updateLocked中,通过获取数据库中目前的下载字节信息
来更新通知栏的进度:
// Update notifications visible to user
mNotifier.updateWith(mDownloads.values());
private static final int TYPE_ACTIVE = 1;
private static final int TYPE_WAITING = 2;
private static final int TYPE_COMPLETE = 3;
通知栏信息分为如上三类, 正在下载, 等待下载,下载完成。
每次更新通知栏,都会将数据库中的每个下载文件的信息来构建一个tag:
/**
* Build tag used for collapsing several {@link DownloadInfo} into a single
* {@link Notification}.
*/
private static String buildNotificationTag(DownloadInfo info) {
if (info.mStatus == Downloads.Impl.STATUS_QUEUED_FOR_WIFI) {
return TYPE_WAITING + ":" + info.mPackage;
} else if (isActiveAndVisible(info)) {
return TYPE_ACTIVE + ":" + info.mPackage;
} else if (isCompleteAndVisible(info)) {
// Complete downloads always have unique notifs
return TYPE_COMPLETE + ":" + info.mId;
} else {
return null;
}
}
再构建的过程数据库有一个字段的信息也会被用到,就是Visibility属性:
在我进行的调试中只出现了type为 TYPE_ACTIVE 和 TYPE_COMPLETE 两种情况。
在更新通知栏的最后处理中,有一段代码用来清理掉一些通知信息,其中就包括这种下载中断的类型的:
// Remove stale tags that weren't renewed
final Iterator<String> it = mActiveNotifs.keySet().iterator();
while (it.hasNext()) {
final String tag = it.next();
if (!clustered.containsKey(tag)) { //没有包含在tag列表中的,需要清除
mNotifManager.cancel(tag, 0);
it.remove();
}
}
log信息, 构建好的tag形式就是type: id, 当然这是已经下载完成的:
D/DownloadManager(32155): =====tag=3:15
D/DownloadManager(32155): =====tag=3:14
D/DownloadManager(32155): =====tag=3:13
D/DownloadManager(32155): =====tag=3:12
D/DownloadManager(32155): =====tag=3:6
D/DownloadManager(32155): =====tag=3:19
D/DownloadManager(32155): =====tag=3:18
D/DownloadManager(32155): =====tag=3:17
D/DownloadManager(32155): =====tag=3:16
D/DownloadManager(32155): =====tag=3:20
D/DownloadManager(32155): =====tag=3:11
D/DownloadManager(32155): =====tag=3:10
D/DownloadManager(32155): =====tag=3:21
D/DownloadManager(32155): =====tag=1:com.android.browser
D/DownloadManager(32155): =====remove tag=1:com.android.browser
还有就是那种执行过一键清理后,那种更新信息也不会再显示在通知栏中了,因为其tag为null, 也已经不包含在tag列表中了。