背景
測(cè)試報(bào)了一個(gè)bug,說(shuō)有些下載的文件(視頻啡氢、音頻)無(wú)法在Download中無(wú)法打開(kāi)状囱,文件管理器中可以打開(kāi),
并且下載應(yīng)用的里文件對(duì)應(yīng)圖標(biāo)顯示不正確倘是。
問(wèn)題初步定位
從描述中可以確定亭枷,下載的文件可以在文件管理器中打開(kāi),說(shuō)明文件本身沒(méi)有問(wèn)題搀崭,文件沒(méi)有問(wèn)題卻打不開(kāi)叨粘,
說(shuō)明是"Downloads"這個(gè)程序的問(wèn)題,出現(xiàn)這個(gè)問(wèn)題一般是MimeType類型出錯(cuò)了门坷,因?yàn)榇蜷_(kāi)文件時(shí)文件類型是由MimeType決定的, 因此得先看看所打開(kāi)文件的MimeType是否有問(wèn)題宣鄙。
代碼分析
確定源碼位置
????在看代碼之前,先說(shuō)點(diǎn)別的東西默蚌,如果你是第一次改Download這種類型的bug冻晤,你第一步肯定是先要找到Download程序源碼位置,這里有很多方法绸吸,比如在openGrok上搜索關(guān)鍵字符串鼻弧,或者打開(kāi)程序使用hierarchyviewer 這個(gè)工具看看包名设江,然后再去確定位置。
????如果你打開(kāi)Downloads這個(gè)程序攘轩,然后用hierarchyviewer 進(jìn)行查看叉存,你會(huì)發(fā)現(xiàn)你當(dāng)前運(yùn)行的Activity是一個(gè)名叫DocumentsActivity
的東東,好像跟Download沒(méi)啥關(guān)系,然后你就能找到它的位置了度帮,在framework/base/package/DocumentsUI
這個(gè)路徑下歼捏,然后你就開(kāi)始了漫長(zhǎng)的代碼之旅,然而笨篷,你看了很久瞳秽,并沒(méi)有發(fā)現(xiàn)任何關(guān)于下載的程序,既然是MimeType錯(cuò)誤率翅,MimeType肯定是在下載時(shí)生成的练俐,然而都沒(méi)找到下載的代碼.于是你就很郁悶,是不是不是這個(gè)程序冕臭,找錯(cuò)位置了腺晾??辜贵,事實(shí)的確是找錯(cuò)源碼位置了悯蝉,各種折騰后,你終于找到正確的位置了托慨,至于方法百度泉粉、Google最終都能找到,我當(dāng)初是用下載程序的通知欄 "Download Complete"這個(gè)字符串去openGrok上搜索找到的榴芳。。跺撼。, 當(dāng)然對(duì)Android源碼非常熟悉的人一般都知道一些程序具體的位置, 有經(jīng)驗(yàn)的人一般能很快找到.
????Download這個(gè)程序正確的源碼位置是 packages/providers/DownloadProvider/
這個(gè)路徑下窟感,這個(gè)DownloadProvider
其實(shí)是不提供任何UI界面的, 除了通知欄,這個(gè)是屬于系統(tǒng)UI的.你在launcher上看到的Downloads這個(gè)程序其實(shí)只是進(jìn)入另一個(gè)程序的入口歉井,實(shí)際顯示下載列表的是DocumentsUi這個(gè)程序柿祈,這個(gè)會(huì)在后面講.
源碼分析
在正式分析源代碼之前,你可能想確認(rèn)一下MimeType是否的確有問(wèn)題哩至,DownloadProvider中比較重要的類是DownloadService這個(gè)類,你可以在這個(gè)類中打印一下MimeType值躏嚎,來(lái)確定是否是有問(wèn)題, 我此次遇到的文件就是MimeType錯(cuò)了。我們要知道MimeType為什么會(huì)出錯(cuò)菩貌,首先要了解下載流程卢佣,MimeType是在哪個(gè)地方生成的,入手點(diǎn)肯定是從DownloadManager
這個(gè)類開(kāi)始箭阶,因?yàn)?code>DownloadProvider是系統(tǒng)提供給其他應(yīng)用使用的虚茶,而DownloadManager
則是DownloadProvider
和應(yīng)用的媒介戈鲁,我們通過(guò)調(diào)用DownloadManager
中提供的API來(lái)啟動(dòng)DownloadProvider
.
具體使用方法如下:
DownloadManager manager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(uri);
request.setMimeType(mimeType);
manager.enqueue(request);
從上面代碼可以看出,調(diào)用下載程序的應(yīng)用可以設(shè)置mimeType, 然后我們?cè)倏碊ownloadManager中對(duì)應(yīng)方法的代碼:
frameworks/base/core/java/android/app/DownloadManager.java
public long enqueue(Request request) {
ContentValues values = request.toContentValues(mPackageName);
Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
long id = Long.parseLong(downloadUri.getLastPathSegment());
return id;
}
/**
* Set the MIME content type of this download. This will override the content type declared
* in the server's response.
* @see <a >HTTP/1.1
* Media Types</a>
* @return this object
*/
public Request setMimeType(String mimeType) {
mMimeType = mimeType;
return this;
}
enqueue方法會(huì)將信息插入到Download數(shù)據(jù)庫(kù)中嘹叫,而setMimeType()
則會(huì)設(shè)置mMimeType
的值婆殿,這個(gè)值也會(huì)被插入到數(shù)據(jù)庫(kù),我們看一下setMimeType()
方法的注釋: Set the MIME content type of this download. This will override the content type declared in the server's response.
這里也提前告訴我們還有一種生成mimeType的方法,從下載路徑的服務(wù)器上獲取MimeType罩扇,我們會(huì)在后面代碼中看到這部分內(nèi)容,enqueue()
方法是DownloadManager中提供啟動(dòng)一個(gè)下載的接口婆芦,調(diào)用這個(gè)方法后就能開(kāi)始下載我們的文件了,源碼中我們可以看到喂饥,enqueue方法中主要操作是將信息插入到數(shù)據(jù)庫(kù)中消约,即imResolver.insert(Downloads.Impl.CONTENT_URI, values);
調(diào)用insert方法后,程序終于執(zhí)行到DownloadProvider中了仰泻,在DownloadProvider這個(gè)程序文件夾下荆陆,我們可以找到一個(gè)名為DownloadProvider 的類
packages/providers/DownloadProvider/src/com/android/providers/downloads/DownloadProvider.java
/**
* Allows application to interact with the download manager.
*/
public final class DownloadProvider extends ContentProvider {
/** Database filename */
private static final String DB_NAME = "downloads.db";
......
從源碼中我們可以看出,這個(gè)類繼承自ContentProvider集侯,并且注釋也表明這個(gè)類是與DownloadManager進(jìn)行交互的,DownloadManager中調(diào)用的insert()方法就是這ContentProvider的insert()
方法被啼,我們來(lái)看看里面具體有什么:
......
// Always start service to handle notifications and/or scanning
final Context context = getContext();
context.startService(new Intent(context, DownloadService.class));
return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}
DownloadProvider.java中insert()
方法代碼較多,接近200行棠枉,其中大多數(shù)操作是將信息插入數(shù)據(jù)庫(kù)浓体,我這里值截取了最后幾行代碼,可以看到,在最后調(diào)用了context.startService(new Intent(context, DownloadService.class));
來(lái)啟動(dòng)DownloadService辈讶,從而開(kāi)始下載文件, DownloadService中代碼量不多命浴,不過(guò)看起來(lái)比較亂,不容易讀懂贱除,大多是一些狀態(tài)的判斷生闲,這里我們只關(guān)注主要流程, 在updateLocked()
方法中,我們可以看到如下代碼:
......
// Kick off download task if ready
final boolean activeDownload = info.startDownloadIfReady(mExecutor);
// Kick off media scan if completed
final boolean activeScan = info.startScanIfReady(mScanner);
......
上面的info對(duì)象是DownloadInfo.java這個(gè)類的實(shí)例月幌,我們到DownloadInfo.java中繼續(xù)跟蹤startDownloadIfReady()
這個(gè)方法,
packages/providers/DownloadProvider/src/com/android/providers/downloads/DownloadInfo.java
......
public boolean startDownloadIfReady(ExecutorService executor) {
synchronized (this) {
final boolean isReady = isReadyToDownload();
final boolean isActive = mSubmittedTask != null && !mSubmittedTask.isDone();
if (isReady && !isActive) {
if (mStatus != Impl.STATUS_RUNNING) {
mStatus = Impl.STATUS_RUNNING;
ContentValues values = new ContentValues();
values.put(Impl.COLUMN_STATUS, mStatus);
mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
}
mTask = new DownloadThread(mContext, mSystemFacade, mNotifier, this);
mSubmittedTask = executor.submit(mTask);
}
return isReady;
}
}
......
可以看到碍讯,再這個(gè)方法中,啟動(dòng)了一個(gè)DownloadThread來(lái)從網(wǎng)絡(luò)上下載文件扯躺,因?yàn)镾ervice默認(rèn)是運(yùn)行在主線程的捉兴,下載這種耗時(shí)操作肯定要放到其他其他線程中執(zhí)行,繼續(xù)分析DownloadThread中代碼,DownloadThread.java
中, 有個(gè)parseOkHeaders(HttpURLConnection conn)
方法录语,部分內(nèi)容如下:
......
if (mInfoDelta.mMimeType == null) {
mInfoDelta.mMimeType = Intent.normalizeMimeType(conn.getContentType());
}
......
這里的mInfoDelta.mMimeType
值來(lái)源: DownloadManager通過(guò)調(diào)用DownloadProvider的insert方法倍啥,將其插入到數(shù)據(jù)庫(kù)中,然后在DownloadService查詢數(shù)據(jù)庫(kù)澎埠,得到值后通過(guò)啟動(dòng)DownloadThread傳遞過(guò)來(lái)的虽缕,這個(gè)過(guò)程中,只有在DownloadManager中setMimeType方法中改變過(guò)其內(nèi)容,也就是說(shuō)mInfoDelta.mMimeType
是否為null
失暂,取決于調(diào)用DownloadProvider的應(yīng)用是否設(shè)置過(guò)MimeType彼宠,如果設(shè)置過(guò)MimeType鳄虱,則不對(duì)其進(jìn)行處理,使用設(shè)置的值凭峡,如果沒(méi)有設(shè)置過(guò)拙已,則通過(guò)conn.getContentType()
來(lái)從下載的服務(wù)器上獲取MimeType值, 這樣印證了之前setMimeType方法注釋上寫的內(nèi)容摧冀,分析到這來(lái)倍踪,已經(jīng)完全確定了MimeType是如何生成和哪些地方可能更改了MimeType.
尋找解決方法
找到了設(shè)置更改MimeType的地方,可以再次打log驗(yàn)證MimeType是否有問(wèn)題索昂,當(dāng)然建车,最終確定是MimeType的問(wèn)題,測(cè)試使用的是chrome下載文件的椒惨,這里MimeType類型出現(xiàn)錯(cuò)誤,通過(guò)分析缤至,是由于下載地址對(duì)應(yīng)的服務(wù)器上關(guān)于MimeType類型是錯(cuò)誤的,如果服務(wù)器上是正確的, 下載的文件就不會(huì)有問(wèn)題康谆,由于設(shè)置MimeType是系統(tǒng)提供的API领斥,并且下載過(guò)程中,應(yīng)用還可以通過(guò)API查詢下載文件的MimeType等信息沃暗,我們是不能直接將MimeType值進(jìn)行更改,只能在打開(kāi)文件過(guò)程中月洛,如果打開(kāi)失敗,則使用文件后綴名得到新的MimeType孽锥,然后再次進(jìn)行打開(kāi)嚼黔,這是我想到一種解決方法,當(dāng)然肯定有其他方法惜辑,接下來(lái)就講如何在打開(kāi)文件失敗時(shí)唬涧,重新計(jì)算MimeType。
首先你的找到打開(kāi)文件的代碼盛撑,這個(gè)過(guò)程中還是有不少坑爵卒,這里就簡(jiǎn)單略過(guò),只是從整體角度說(shuō)明一下,前面提到過(guò)撵彻,我們?cè)趌auncher上看到的Downloads程序只是進(jìn)入DocumentsUI這個(gè)程序的入口,功能就是發(fā)送一個(gè)Intent打開(kāi)DocumentsUI,我們看到的下載列表是DocumentsUi中的一種呈現(xiàn)形式实牡,DocumentsUI這個(gè)程序比較奇特陌僵,具體有如下作用:
三個(gè)分別是 文件瀏覽,選擇文件(第三方應(yīng)用調(diào)用)创坞,顯示下載
另外要說(shuō)明的是 DownloadProvider中除了下載Service外碗短,ui目錄下包下還有個(gè)小程序,編譯出來(lái)后為DownloadProviderUi.apk(packages/providers/DownloadProvider/ui/),也就是顯示的Downloads程序题涨,這個(gè)和下載的代碼是分開(kāi)的偎谁,他就只有兩個(gè)功能总滩,打開(kāi)下載文件和顯示下載列表,我們點(diǎn)擊launcher的Downloads圖標(biāo)后,默認(rèn)是啟動(dòng)DownloadList
這Activity(Android O上代碼有改動(dòng), 已經(jīng)沒(méi)有這個(gè)類了)巡雨,然后這個(gè)Activity只做了一件事闰渔,就是發(fā)送一個(gè)稍微特殊點(diǎn)的Intent,用來(lái)打開(kāi)DocumentsUI铐望,從而告訴DocumentsUI要顯示下載列表冈涧,這樣我們就可以在DocumentsUI中看到下載列表了,也就是說(shuō)DownloadProvider本身不提供列表顯示正蛙,交由其他應(yīng)用來(lái)完成這件事督弓。
看到下載列表后,我們點(diǎn)擊列表中的某一項(xiàng)乒验,然后流程是這樣的:DocumentsUI中會(huì)發(fā)送一個(gè)Action為android.provider.action.MANAGE_DOCUMENT
的Intent愚隧,在DownloadProvider中ui包下的TrampolineActivity
會(huì)接受到這個(gè)Intent,然后根據(jù)MimeType锻全,Uri來(lái)創(chuàng)建一個(gè)Intent來(lái)打開(kāi)文件狂塘,
具體創(chuàng)建Intent的代碼在DownloadProvider下OpenHelper.java
這個(gè)類的buildViewIntent()
方法中,因此我們只需修改此處代碼.
解決問(wèn)題
最終代碼修改如下:
packages/providers/DownloadProvider/src/com/android/providers/downloads/OpenHelper.java
+++ b/LINUX/android/packages/providers/DownloadProvider/src/com/android/providers/downloads/OpenHelper.java
@@ -32,6 +32,7 @@ import android.database.Cursor;
import android.net.Uri;
import android.provider.Downloads.Impl.RequestHeaders;
import android.util.Log;
+import android.webkit.MimeTypeMap;
import java.io.File;
@@ -98,12 +99,27 @@ public class OpenHelper {
intent.setDataAndType(localUri, mimeType);
}
+ if (intent.resolveActivity(context.getPackageManager()) == null) {
+ intent.setDataAndType(localUri, getMimeTypeFromExtensionName(file.getPath()));
+ }
+
return intent;
} finally {
cursor.close();
}
}
+ private static String getMimeTypeFromExtensionName(String fileName) {
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ if ((fileName != null) && (fileName.length() > 0)) {
+ int dot = fileName.lastIndexOf('.');
+ if ((dot >-1) && (dot < (fileName.length() - 1))) {
+ return mimeTypeMap.getMimeTypeFromExtension(fileName.substring(dot + 1));
+ }
+ }
+ return null;
+ }
+
增加一個(gè)通過(guò)文件名獲取MimeType的方法虱痕,然后判斷如果沒(méi)有程序能打開(kāi)文件睹耐,則重新設(shè)置MimeType和Uri
當(dāng)然這個(gè)也不能保證文件類型是完全是正確的,但用戶很容易接受部翘,至此就差編譯驗(yàn)證了硝训,如果你發(fā)現(xiàn)你push apk 后發(fā)現(xiàn)修改沒(méi)有效果,恭喜你新思,又踩到坑了窖梁,你要push的是DownloadProviderUi.apk,而不是DownloadProvider.apk, 編譯后有兩個(gè)apk,而你的修改是DownloadProviderUi這apk中用到的夹囚。
總結(jié)
以上是我在工作中解決一個(gè)bug的思路, Android版本為5.1, 由于不同Android版本代碼會(huì)有差異, 僅作參考.