最近在安卓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 starting
state.mRequestUri=http://w.gdown.baidu.com/data/wisegame/8ae29636cec71afb/17173shouyou_3300.apk?f=m1101
I/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=m1101
I/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列表中了。