美文网首页
Android Download Provider下载文件Mim

Android Download Provider下载文件Mim

作者: 幽客 | 来源:发表于2018-05-06 11:21 被阅读0次

    背景

    测试报了一个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版本代码会有差异, 仅作参考.

    相关文章

      网友评论

          本文标题:Android Download Provider下载文件Mim

          本文链接:https://www.haomeiwen.com/subject/fdiyrftx.html