最近在安卓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列表中了。

网络断开后重连downloadProvider继续下载问题调试分析的更多相关文章

  1. SSH自动断开后重连的解决方案

    注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 本文源链接:https://www.cnblogs.com/chloneda/p/ssh-conn ...

  2. Delphi使用ADO连接网络数据库,断网后重连问题

    原始文章: https://blog.csdn.net/blog_jihq/article/details/11737699# 使用TADOConnection对象连接网络数据库(以MySQL为例), ...

  3. [转载]win7休眠后网络断开怎么办?如何设置?

    http://jingyan.baidu.com/article/8065f87fc87d0423312498af.html 有时会遇到在Windows7系统休眠模式下会自动断开网络连接,唤醒系统也是 ...

  4. Python爬虫实战三之实现山东大学无线网络掉线自动重连

    综述 最近山大软件园校区QLSC_STU无线网掉线掉的厉害,连上之后平均十分钟左右掉线一次,很是让人心烦,还能不能愉快地上自习了?能忍吗?反正我是不能忍了,嗯,自己动手,丰衣足食!写个程序解决掉它! ...

  5. ssh连接断开后 shell进程退出

    问题描述:当SSH远程连接到服务器上,然后运行一个服务 ./catalina.sh start,然后把终端开闭(切断SSH连接)之后,发现该服务中断,导致网页无法访问.   解决方法:使用nohup命 ...

  6. Python网络数据采集PDF高清完整版免费下载|百度云盘

    百度云盘:Python网络数据采集PDF高清完整版免费下载 提取码:1vc5   内容简介 本书采用简洁强大的Python语言,介绍了网络数据采集,并为采集新式网络中的各种数据类型提供了全面的指导.第 ...

  7. Android 动态监听网络 断网重连

    需求: 网络连接断开 弹出popupwindow 当前网络连接断开 网络恢复时popupwindow 消失重新请求网络. 需求描述完毕 上一张帅图 思路:广播 发送及时消息 断网flag  popup ...

  8. SQL Server Mirror 断开后,删除副本上镜像数据库

    一般而言,SQL Server 在数据库级别进行数据同步的方式主要有三种 1.日志传送:2.Mirror(镜像):3. AlwaysOn.复制订阅技术理解为表级别的同步,不归结为数据库级别的同步. 在 ...

  9. linux ------ 使用 screen 后 SSH 断开后程序依旧能在后台运行

    为什么ssh断开后你运行的进程会退出呢? 因为所有进程都得有个父进程.当你ssh到一个服务器上时,打开的shell就是你所有执行命令的父进程. 当你断开ssh连接时,你的命令的父进程就没了.如果处理不 ...

随机推荐

  1. Javascript模块化编程 require.js使用详解

    一.为什么用require.js,产生的背景 最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了.后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载. & ...

  2. Eclipse+Pydev +Django搭建开发环境时容易出错的几点

    1.注意安装的软件和系统的位数是否匹配. 2.安装Django框架的时候注意是否安装了setuptools工具.在Python中,安装第三方模块,是通过setuptools这个工具完成的.Python ...

  3. iphone升级ios7之后出现蓝框框一直跳的问题

    问题描述:iphone升级ios7之后出现蓝框框一直跳            解决办法:设置-通用-辅助功能-切换控制-里边打开了切换控制及自动扫描,直接关闭切换控制就好了

  4. C# Attribute

    Attribute 是C#非常重要的一块内容,需要研究一下. Attribute  的简单使用:简而言之,就是可以自定义通用标志位,而不是在每个所需的类型中分别增加标志位. //class专用attr ...

  5. Mifare S50与Mifare S70

    转自http://blog.sina.com.cn/s/blog_9ed067ad0100zyjx.html Mifare S50和Mifare S70又常被称为Mifare Standard.Mif ...

  6. FLASK安装--兼收EZ_INSTALL及PIP

    参考URL: http://www.cnblogs.com/haython/p/3970426.html http://www.pythondoc.com/flask/installation.htm ...

  7. 自定义Qt按钮

    转自:http://blog.csdn.net/starcloud_zxt/article/details/5185556 Qt自带的PushButton样式比较单一,在开发的时候往往按钮的形状各异, ...

  8. Android 4.4 上实现透明导航栏和状态栏 Translucent system bar

    Translucent system UI styling To get the most impact out of your content, you can now use new window ...

  9. POJ2248 A Knight's Journey(DFS)

    题目链接. 题目大意: 给定一个矩阵,马的初始位置在(0,0),要求给出一个方案,使马走遍所有的点. 列为数字,行为字母,搜索按字典序. 分析: 用 vis[x][y] 标记是否已经访问.因为要搜索所 ...

  10. -_-#【Canvas】

    context.lineWidth = 0.5 incorrect display lineWidth=1 at html5 canvas canvas.save() canvas.restore() ...