downloadprovider 断开继续下载失败问题

来源:互联网 发布:inphic i9软件格式 编辑:程序博客网 时间:2024/06/05 10:55

DownloadManager: [3] Stop requested /storage/emulated/0/ted/download/com.paem.apk with status HTTP_DATA_ERROR: Failed reading response: java.net.SocketException: recvfrom failed: ETIMEDOUT (Connection timed out)
DownloadManager: [3] Finished with status CANNOT_RESUME

最近遇到一个断开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 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列表中了。