Android Download Provider下載文件MimeType錯(cuò)誤

背景

測(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è)程序比較奇特陌僵,具體有如下作用:


DocumentsUi

三個(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ì)有差異, 僅作參考.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纵刘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子荸哟,更是在濱河造成了極大的恐慌假哎,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鞍历,死亡現(xiàn)場(chǎng)離奇詭異舵抹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)劣砍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門惧蛹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事香嗓⊙盖唬” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵靠娱,是天一觀的道長(zhǎng)沧烈。 經(jīng)常有香客問(wèn)我,道長(zhǎng)饱岸,這世上最難降的妖魔是什么掺出? 我笑而不...
    開(kāi)封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮苫费,結(jié)果婚禮上汤锨,老公的妹妹穿的比我還像新娘。我一直安慰自己百框,他們只是感情好闲礼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著铐维,像睡著了一般柬泽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嫁蛇,一...
    開(kāi)封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天锨并,我揣著相機(jī)與錄音,去河邊找鬼睬棚。 笑死第煮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的抑党。 我是一名探鬼主播包警,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼底靠!你這毒婦竟也來(lái)了害晦?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤暑中,失蹤者是張志新(化名)和其女友劉穎壹瘟,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鳄逾,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡俐筋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了严衬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡笆呆,死狀恐怖请琳,靈堂內(nèi)的尸體忽然破棺而出粱挡,到底是詐尸還是另有隱情,我是刑警寧澤俄精,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布询筏,位于F島的核電站,受9級(jí)特大地震影響竖慧,放射性物質(zhì)發(fā)生泄漏嫌套。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一圾旨、第九天 我趴在偏房一處隱蔽的房頂上張望踱讨。 院中可真熱鬧,春花似錦砍的、人聲如沸痹筛。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)帚稠。三九已至,卻和暖如春床佳,著一層夾襖步出監(jiān)牢的瞬間滋早,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工砌们, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杆麸,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓怨绣,卻偏偏與公主長(zhǎng)得像角溃,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子篮撑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容