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