Android Download Provider下载文件MimeType错误

背景

测试报了一个bug,说有些下载的文件(视频、音频)无法在Download中无法打开,文件管理器中可以打开,
并且下载应用的里文件对应图标显示不正确。

问题初步定位

从描述中可以确定,下载的文件可以在文件管理器中打开,说明文件本身没有问题,文件没有问题却打不开,
说明是"Downloads"这个程序的问题,出现这个问题一般是MimeType类型出错了,因为打开文件时文件类型是由MimeType决定的, 因此得先看看所打开文件的MimeType是否有问题。

代码分析

确定源码位置

    在看代码之前,先说点别的东西,如果你是第一次改Download这种类型的bug,你第一步肯定是先要找到Download程序源码位置,这里有很多方法,比如在openGrok上搜索关键字符串,或者打开程序使用hierarchyviewer 这个工具看看包名,然后再去确定位置。
    如果你打开Downloads这个程序,然后用hierarchyviewer 进行查看,你会发现你当前运行的Activity是一个名叫DocumentsActivity的东东,好像跟Download没啥关系,然后你就能找到它的位置了,在framework/base/package/DocumentsUI这个路径下,然后你就开始了漫长的代码之旅,然而,你看了很久,并没有发现任何关于下载的程序,既然是MimeType错误,MimeType肯定是在下载时生成的,然而都没找到下载的代码.于是你就很郁闷,是不是不是这个程序,找错位置了??,事实的确是找错源码位置了,各种折腾后,你终于找到正确的位置了,至于方法百度、Google最终都能找到,我当初是用下载程序的通知栏 "Download Complete"这个字符串去openGrok上搜索找到的。。。, 当然对Android源码非常熟悉的人一般都知道一些程序具体的位置, 有经验的人一般能很快找到.
    Download这个程序正确的源码位置是 packages/providers/DownloadProvider/这个路径下,这个DownloadProvider其实是不提供任何UI界面的, 除了通知栏,这个是属于系统UI的.你在launcher上看到的Downloads这个程序其实只是进入另一个程序的入口,实际显示下载列表的是DocumentsUi这个程序,这个会在后面讲.

源码分析

在正式分析源代码之前,你可能想确认一下MimeType是否的确有问题,DownloadProvider中比较重要的类是DownloadService这个类,你可以在这个类中打印一下MimeType值,来确定是否是有问题, 我此次遇到的文件就是MimeType错了。我们要知道MimeType为什么会出错,首先要了解下载流程,MimeType是在哪个地方生成的,入手点肯定是从DownloadManager这个类开始,因为DownloadProvider是系统提供给其他应用使用的,而DownloadManager则是DownloadProvider和应用的媒介,我们通过调用DownloadManager中提供的API来启动DownloadProvider.
具体使用方法如下:

DownloadManager manager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(uri);
request.setMimeType(mimeType);
manager.enqueue(request);

从上面代码可以看出,调用下载程序的应用可以设置mimeType, 然后我们再看DownloadManager中对应方法的代码:
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 href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7">HTTP/1.1
         *      Media Types</a>
         * @return this object
         */
        public Request setMimeType(String mimeType) {
            mMimeType = mimeType;
            return this;
        }

enqueue方法会将信息插入到Download数据库中,而setMimeType()则会设置mMimeType的值,这个值也会被插入到数据库,我们看一下setMimeType()方法的注释: Set the MIME content type of this download. This will override the content type declared in the server's response.
这里也提前告诉我们还有一种生成mimeType的方法,从下载路径的服务器上获取MimeType,我们会在后面代码中看到这部分内容,enqueue()方法是DownloadManager中提供启动一个下载的接口,调用这个方法后就能开始下载我们的文件了,源码中我们可以看到,enqueue方法中主要操作是将信息插入到数据库中,即imResolver.insert(Downloads.Impl.CONTENT_URI, values); 调用insert方法后,程序终于执行到DownloadProvider中了,在DownloadProvider这个程序文件夹下,我们可以找到一个名为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";

    ......

从源码中我们可以看出,这个类继承自ContentProvider,并且注释也表明这个类是与DownloadManager进行交互的,DownloadManager中调用的insert()方法就是这ContentProvider的insert()方法,我们来看看里面具体有什么:

        ......
        // 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行,其中大多数操作是将信息插入数据库,我这里值截取了最后几行代码,可以看到,在最后调用了context.startService(new Intent(context, DownloadService.class));来启动DownloadService,从而开始下载文件, DownloadService中代码量不多,不过看起来比较乱,不容易读懂,大多是一些状态的判断,这里我们只关注主要流程, 在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对象是DownloadInfo.java这个类的实例,我们到DownloadInfo.java中继续跟踪startDownloadIfReady()这个方法,
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;
        }
    }
......

可以看到,再这个方法中,启动了一个DownloadThread来从网络上下载文件,因为Service默认是运行在主线程的,下载这种耗时操作肯定要放到其他其他线程中执行,继续分析DownloadThread中代码,DownloadThread.java中, 有个parseOkHeaders(HttpURLConnection conn)方法,部分内容如下:

......
        if (mInfoDelta.mMimeType == null) {
            mInfoDelta.mMimeType = Intent.normalizeMimeType(conn.getContentType());
        }
......

这里的mInfoDelta.mMimeType值来源: DownloadManager通过调用DownloadProvider的insert方法,将其插入到数据库中,然后在DownloadService查询数据库,得到值后通过启动DownloadThread传递过来的,这个过程中,只有在DownloadManager中setMimeType方法中改变过其内容,也就是说mInfoDelta.mMimeType是否为null,取决于调用DownloadProvider的应用是否设置过MimeType,如果设置过MimeType,则不对其进行处理,使用设置的值,如果没有设置过,则通过conn.getContentType()来从下载的服务器上获取MimeType值, 这样印证了之前setMimeType方法注释上写的内容,分析到这来,已经完全确定了MimeType是如何生成和哪些地方可能更改了MimeType.

寻找解决方法

找到了设置更改MimeType的地方,可以再次打log验证MimeType是否有问题,当然,最终确定是MimeType的问题,测试使用的是chrome下载文件的,这里MimeType类型出现错误,通过分析,是由于下载地址对应的服务器上关于MimeType类型是错误的,如果服务器上是正确的, 下载的文件就不会有问题,由于设置MimeType是系统提供的API,并且下载过程中,应用还可以通过API查询下载文件的MimeType等信息,我们是不能直接将MimeType值进行更改,只能在打开文件过程中,如果打开失败,则使用文件后缀名得到新的MimeType,然后再次进行打开,这是我想到一种解决方法,当然肯定有其他方法,接下来就讲如何在打开文件失败时,重新计算MimeType。

首先你的找到打开文件的代码,这个过程中还是有不少坑,这里就简单略过,只是从整体角度说明一下,前面提到过,我们在launcher上看到的Downloads程序只是进入DocumentsUI这个程序的入口,功能就是发送一个Intent打开DocumentsUI,我们看到的下载列表是DocumentsUi中的一种呈现形式,DocumentsUI这个程序比较奇特,具体有如下作用:


DocumentsUi

三个分别是 文件浏览,选择文件(第三方应用调用),显示下载
另外要说明的是 DownloadProvider中除了下载Service外,ui目录下包下还有个小程序,编译出来后为DownloadProviderUi.apk(packages/providers/DownloadProvider/ui/),也就是显示的Downloads程序,这个和下载的代码是分开的,他就只有两个功能,打开下载文件和显示下载列表,我们点击launcher的Downloads图标后,默认是启动DownloadList这Activity(Android O上代码有改动, 已经没有这个类了),然后这个Activity只做了一件事,就是发送一个稍微特殊点的Intent,用来打开DocumentsUI,从而告诉DocumentsUI要显示下载列表,这样我们就可以在DocumentsUI中看到下载列表了,也就是说DownloadProvider本身不提供列表显示,交由其他应用来完成这件事。

看到下载列表后,我们点击列表中的某一项,然后流程是这样的:DocumentsUI中会发送一个Action为android.provider.action.MANAGE_DOCUMENT的Intent,在DownloadProvider中ui包下的TrampolineActivity会接受到这个Intent,然后根据MimeType,Uri来创建一个Intent来打开文件,
具体创建Intent的代码在DownloadProvider下OpenHelper.java这个类的buildViewIntent()方法中,因此我们只需修改此处代码.

解决问题

最终代码修改如下:
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;
+    }
+

增加一个通过文件名获取MimeType的方法,然后判断如果没有程序能打开文件,则重新设置MimeType和Uri
当然这个也不能保证文件类型是完全是正确的,但用户很容易接受,至此就差编译验证了,如果你发现你push apk 后发现修改没有效果,恭喜你,又踩到坑了,你要push的是DownloadProviderUi.apk,而不是DownloadProvider.apk, 编译后有两个apk,而你的修改是DownloadProviderUi这apk中用到的。

总结

以上是我在工作中解决一个bug的思路, Android版本为5.1, 由于不同Android版本代码会有差异, 仅作参考.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容