系统服务之下载管理(DownloadManager)

前言:
最近一直在写一个新闻客户端练手,每天也会关注一下鸿洋的微信公众号推荐的文章,看到一篇关于系统下载服务的文章比较感兴趣,就把它实现到我的新闻客户端用来为用户提供图片新闻的下载功能,顺带着分析一下源码(Android 7.1.1),总结一下在功能实现过程中遇到的坑。

概述:
DownloadManager是Android提供的用来处理长时间HTTP下载任务的一个系统服务。客户端可以用getSystemService(DOWNLOAD_SERVICE)方法来获得这个类的实例,并通过这个服务把URI对应的文件下载到特定的文件目录下。DownloadManager会在后台运行,负责与HTTP进行交互,在下载失败、连接改变和系统重启后重新尝试下载。DownloadManager一共有三个内部类:CursorTranslatorQueryRequest。CursorTranslator是Cursor类的一个封装类,用来封装DownloadProvider类的返回值。关于Query这个类Google文档上给的解释也很模糊,从他公开的两个方法setFilterById(long… ids)setFilterByStatus(int flags)来看,是通过下载Id或是下载状态来获得对应下载的Query对象,通过这个对象就可以在DownloadManager里面查找对应的下载文件,并获取具体的下载信息。Request是用来配置下载请求的一个封装类,这个类包含了一个下载请求的所有必要的信息。简单的理解就是,Request类是一个下载请求,Query类是一个查询索引。
关于DownloadManager的用法可以参看Android系统下载管DownloadManager系统自带DownloadManager详解这两篇文章,里面有比较详细的介绍。本文从源码层面上分析一下DownloadManager都为上层应用提供了什么服务。

源码解析:
首先,来看一下DownloadManager都定义了哪些静态常量。静态常量又分为以下几块:
1、下载文件的相关信息
通过这些关键字可以查询到对应下载文件的信息。其中,COLUMN_LOCAL_FILENAME关键字已经被弃用了,在Android N之后不能够直接获取,需要通过其他方法来获得被下载的文件名称,后面会详细的说说如何解决这个问题。

    /**
     * An identifier for a particular download, unique across the system.  Clients use this ID to
     * make subsequent calls related to the download.
     */
    public final static String COLUMN_ID = Downloads.Impl._ID;

    /**
     * The client-supplied title for this download.  This will be displayed in system notifications.
     * Defaults to the empty string.
     */
    public final static String COLUMN_TITLE = Downloads.Impl.COLUMN_TITLE;

    /**
     * The client-supplied description of this download.  This will be displayed in system
     * notifications.  Defaults to the empty string.
     */
    public final static String COLUMN_DESCRIPTION = Downloads.Impl.COLUMN_DESCRIPTION;

    /**
     * URI to be downloaded.
     */
    public final static String COLUMN_URI = Downloads.Impl.COLUMN_URI;

    /**
     * Internet Media Type of the downloaded file.  If no value is provided upon creation, this will
     * initially be null and will be filled in based on the server's response once the download has
     * started.
     *
     * @see <a href="http://www.ietf.org/rfc/rfc1590.txt">RFC 1590, defining Media Types</a>
     */
    public final static String COLUMN_MEDIA_TYPE = "media_type";

    /**
     * Total size of the download in bytes.  This will initially be -1 and will be filled in once
     * the download starts.
     */
    public final static String COLUMN_TOTAL_SIZE_BYTES = "total_size";

    /**
     * Uri where downloaded file will be stored.  If a destination is supplied by client, that URI
     * will be used here.  Otherwise, the value will initially be null and will be filled in with a
     * generated URI once the download has started.
     */
    public final static String COLUMN_LOCAL_URI = "local_uri";

    /**
     * Path to the downloaded file on disk.
     * <p>
     * Note that apps may not have filesystem permissions to directly access
     * this path. Instead of trying to open this path directly, apps should use
     * {@link ContentResolver#openFileDescriptor(Uri, String)} to gain access.
     *
     * @deprecated apps should transition to using
     *             {@link ContentResolver#openFileDescriptor(Uri, String)}
     *             instead.
     */
    @Deprecated
    public final static String COLUMN_LOCAL_FILENAME = "local_filename";

    /**
     * Current status of the download, as one of the STATUS_* constants.
     */
    public final static String COLUMN_STATUS = Downloads.Impl.COLUMN_STATUS;

    /**
     * Provides more detail on the status of the download.  Its meaning depends on the value of
     * {@link #COLUMN_STATUS}.
     *
     * When {@link #COLUMN_STATUS} is {@link #STATUS_FAILED}, this indicates the type of error that
     * occurred.  If an HTTP error occurred, this will hold the HTTP status code as defined in RFC
     * 2616.  Otherwise, it will hold one of the ERROR_* constants.
     *
     * When {@link #COLUMN_STATUS} is {@link #STATUS_PAUSED}, this indicates why the download is
     * paused.  It will hold one of the PAUSED_* constants.
     *
     * If {@link #COLUMN_STATUS} is neither {@link #STATUS_FAILED} nor {@link #STATUS_PAUSED}, this
     * column's value is undefined.
     *
     * @see <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1">RFC 2616
     * status codes</a>
     */
    public final static String COLUMN_REASON = "reason";

    /**
     * Number of bytes download so far.
     */
    public final static String COLUMN_BYTES_DOWNLOADED_SO_FAR = "bytes_so_far";

    /**
     * Timestamp when the download was last modified, in {@link System#currentTimeMillis
     * System.currentTimeMillis()} (wall clock time in UTC).
     */
    public final static String COLUMN_LAST_MODIFIED_TIMESTAMP = "last_modified_timestamp";

    /**
     * The URI to the corresponding entry in MediaProvider for this downloaded entry. It is
     * used to delete the entries from MediaProvider database when it is deleted from the
     * downloaded list.
     */
    public static final String COLUMN_MEDIAPROVIDER_URI = Downloads.Impl.COLUMN_MEDIAPROVIDER_URI;

    /**
     * @hide
     */
    public final static String COLUMN_ALLOW_WRITE = Downloads.Impl.COLUMN_ALLOW_WRITE;

2、定义下载状态

    /**
     * Value of {@link #COLUMN_STATUS} when the download is waiting to start.
     */
    public final static int STATUS_PENDING = 1 << 0;

    /**
     * Value of {@link #COLUMN_STATUS} when the download is currently running.
     */
    public final static int STATUS_RUNNING = 1 << 1;

    /**
     * Value of {@link #COLUMN_STATUS} when the download is waiting to retry or resume.
     */
    public final static int STATUS_PAUSED = 1 << 2;

    /**
     * Value of {@link #COLUMN_STATUS} when the download has successfully completed.
     */
    public final static int STATUS_SUCCESSFUL = 1 << 3;

    /**
     * Value of {@link #COLUMN_STATUS} when the download has failed (and will not be retried).
     */
    public final static int STATUS_FAILED = 1 << 4;

3、定义下载错误返回码

    /**
     * Value of COLUMN_ERROR_CODE when the download has completed with an error that doesn't fit
     * under any other error code.
     */
    public final static int ERROR_UNKNOWN = 1000;

    /**
     * Value of {@link #COLUMN_REASON} when a storage issue arises which doesn't fit under any
     * other error code. Use the more specific {@link #ERROR_INSUFFICIENT_SPACE} and
     * {@link #ERROR_DEVICE_NOT_FOUND} when appropriate.
     */
    public final static int ERROR_FILE_ERROR = 1001;

    /**
     * Value of {@link #COLUMN_REASON} when an HTTP code was received that download manager
     * can't handle.
     */
    public final static int ERROR_UNHANDLED_HTTP_CODE = 1002;

    /**
     * Value of {@link #COLUMN_REASON} when an error receiving or processing data occurred at
     * the HTTP level.
     */
    public final static int ERROR_HTTP_DATA_ERROR = 1004;

    /**
     * Value of {@link #COLUMN_REASON} when there were too many redirects.
     */
    public final static int ERROR_TOO_MANY_REDIRECTS = 1005;

    /**
     * Value of {@link #COLUMN_REASON} when there was insufficient storage space. Typically,
     * this is because the SD card is full.
     */
    public final static int ERROR_INSUFFICIENT_SPACE = 1006;

    /**
     * Value of {@link #COLUMN_REASON} when no external storage device was found. Typically,
     * this is because the SD card is not mounted.
     */
    public final static int ERROR_DEVICE_NOT_FOUND = 1007;

    /**
     * Value of {@link #COLUMN_REASON} when some possibly transient error occurred but we can't
     * resume the download.
     */
    public final static int ERROR_CANNOT_RESUME = 1008;

    /**
     * Value of {@link #COLUMN_REASON} when the requested destination file already exists (the
     * download manager will not overwrite an existing file).
     */
    public final static int ERROR_FILE_ALREADY_EXISTS = 1009;

    /**
     * Value of {@link #COLUMN_REASON} when the download has failed because of
     * {@link NetworkPolicyManager} controls on the requesting application.
     *
     * @hide
     */
    public final static int ERROR_BLOCKED = 1010;

4、下载暂停原因返回码

    /**
     * Value of {@link #COLUMN_REASON} when the download is paused because some network error
     * occurred and the download manager is waiting before retrying the request.
     */
    public final static int PAUSED_WAITING_TO_RETRY = 1;

    /**
     * Value of {@link #COLUMN_REASON} when the download is waiting for network connectivity to
     * proceed.
     */
    public final static int PAUSED_WAITING_FOR_NETWORK = 2;

    /**
     * Value of {@link #COLUMN_REASON} when the download exceeds a size limit for downloads over
     * the mobile network and the download manager is waiting for a Wi-Fi connection to proceed.
     */
    public final static int PAUSED_QUEUED_FOR_WIFI = 3;

    /**
     * Value of {@link #COLUMN_REASON} when the download is paused for some other reason.
     */
    public final static int PAUSED_UNKNOWN = 4;

5、下载相关的广播及所携带的附加信息字段
每个下载任务完成后都会发送ACTION_DOWNLOAD_COMPLETE广播,在这个action的EXTRA_DOWNLOAD_ID字段中携带了刚刚下载完成任务的Id。

    /**
     * Broadcast intent action sent by the download manager when a download completes.
     */
    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
    public final static String ACTION_DOWNLOAD_COMPLETE = "android.intent.action.DOWNLOAD_COMPLETE";

    /**
     * Intent extra included with {@link #ACTION_DOWNLOAD_COMPLETE} intents, indicating the ID (as a
     * long) of the download that just completed.
     */
    public static final String EXTRA_DOWNLOAD_ID = "extra_download_id";

点击下载通知DownloadManager会发送ACTION_NOTIFICATION_CLICKED 广播,在这个action的EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS 字段中携带了正在下载任务的Id。

    /**
     * Broadcast intent action sent by the download manager when the user clicks on a running
     * download, either from a system notification or from the downloads UI.
     */
    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
    public final static String ACTION_NOTIFICATION_CLICKED =
            "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED";

    /**
     * When clicks on multiple notifications are received, the following
     * provides an array of download ids corresponding to the download notification that was
     * clicked. It can be retrieved by the receiver of this
     * Intent using {@link android.content.Intent#getLongArrayExtra(String)}.
     */
    public static final String EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS = "extra_click_download_ids";

可以通过发送ACTION_VIEW_DOWNLOADS 广播来启动注册了该广播的Activity显示所有已下载的文件,还可以在INTENT_EXTRAS_SORT_BY_SIZE 字段中携带是否按照文件大小排序模式。关于INTENT_EXTRAS_SORT_BY_SIZE 字段我搜遍了整个原生代码也没有找到有使用或是处理的逻辑,我自己尝试了一下也没有发现有任何不同,并没有按照大小排序,应该跟应用自己实现的策略有关。

    /**
     * Intent action to launch an activity to display all downloads.
     */
    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
    public final static String ACTION_VIEW_DOWNLOADS = "android.intent.action.VIEW_DOWNLOADS";

    /**
     * Intent extra included with {@link #ACTION_VIEW_DOWNLOADS} to start DownloadApp in
     * sort-by-size mode.
     */
    public final static String INTENT_EXTRAS_SORT_BY_SIZE =
            "android.app.DownloadManager.extra_sortBySize";

其次,来分析一下DownloadManager的三个内部类:CursorTranslator 、Query 和Request。
1、CursorTranslator
这个类里面重载了getInt(int columnIndex) 、getLong(int columnIndex) 和getString(int columnIndex)三个方法,客户端可以通过这三个方法来获取下载文件的相关信息。笔者在实现相关功能的时候在这里遇到一个坑,我在通过COLUMN_LOCAL_FILENAME字段获取下载文件名字的时候抛出如下安全异常:

java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead
at android.app.DownloadManager$CursorTranslator.getString(DownloadManager.java:1590)

代码抛出异常的地方:

        @Override
        public String getString(int columnIndex) {
            final String columnName = getColumnName(columnIndex);
            switch (columnName) {
                case COLUMN_LOCAL_URI:
                    return getLocalUri();
                case COLUMN_LOCAL_FILENAME:
                    if (!mAccessFilename) {
                        throw new SecurityException(
                                "COLUMN_LOCAL_FILENAME is deprecated;"
                                        + " use ContentResolver.openFileDescriptor() instead");
                    }
                default:
                    return super.getString(columnIndex);
            }
        }

从源码可以发现,当客户端通过COLUMN_LOCAL_FILENAME字段来查询下载文件名字的时候,会去判断mAccessFilename这个条件,如果这个条件为false就会抛出一个安全异常。那么问题来了,mAccessFilename的值由什么决定呢?

由以下两个地方来设置mAccessFilename的值,一个是会判断应用SDK版本,如果是Android N或是之后的版本就会为false。同时,DownloadManager还提供了setAccessFilename方法来修改这个值,但是只能供系统应用来调用。

public DownloadManager(Context context) {
        mResolver = context.getContentResolver();
        mPackageName = context.getPackageName();

        // Callers can access filename columns when targeting old platform
        // versions; otherwise we throw telling them it's deprecated.
        mAccessFilename = context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N;
    }

/** {@hide} */
public void setAccessFilename(boolean accessFilename) {
    mAccessFilename = accessFilename;
}

分析到这里,解决这个问题的方法就呼之欲出了。如果你是系统应用你可以通过它提供的setAccessFilename方法直接来修改mAccessFilename的值从而获得下载文件的名字。如果你是三方应用没有办法拿到系统权限,那就只能使用它错误log中的ContentResolver.openFileDescriptor()方法来解决。既然系统定义了这个方法,你也可以通过反射来获得这个方法去设置mAccessFilename的值。当然还有其他的方法,我在实现这个下载功能的时候直接把文件名字写在COLUMN_TITLE 或是COLUMN_DESCRIPTION 字段来绕过这个问题。

2、Query
这个静态类相对比较简单,只提供了两个公开的方法,setFilterById(long… ids)和setFilterByStatus(int flags)。

通过文件下载id来获取相应的Query对象,这里可以传递多个id。
这里写图片描述
通过文件下载状态获取相应的Query对象
这里写图片描述
3、Request
静态常量
定义什么网络类型时候下载,默认是所有网络类型都可以。

        /**
         * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
         * {@link ConnectivityManager#TYPE_MOBILE}.
         */
        public static final int NETWORK_MOBILE = 1 << 0;

        /**
         * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
         * {@link ConnectivityManager#TYPE_WIFI}.
         */
        public static final int NETWORK_WIFI = 1 << 1;

        /**
         * Bit flag for {@link #setAllowedNetworkTypes} corresponding to
         * {@link ConnectivityManager#TYPE_BLUETOOTH}.
         * @hide
         */
        @Deprecated
        public static final int NETWORK_BLUETOOTH = 1 << 2;

定义下载文件是否可以被MediaScanner扫描的静态常量

        /** if a file is designated as a MediaScanner scannable file, the following value is
         * stored in the database column {@link Downloads.Impl#COLUMN_MEDIA_SCANNED}.
         */
        private static final int SCANNABLE_VALUE_YES = 0;
        // value of 1 is stored in the above column by DownloadProvider after it is scanned by
        // MediaScanner
        /** if a file is designated as a file that should not be scanned by MediaScanner,
         * the following value is stored in the database column
         * {@link Downloads.Impl#COLUMN_MEDIA_SCANNED}.
         */
        private static final int SCANNABLE_VALUE_NO = 2;

定义下载文件过程中各个阶段是否显示通知

     /**
         * This download is visible but only shows in the notifications
         * while it's in progress.
         */
        public static final int VISIBILITY_VISIBLE = 0;

        /**
         * This download is visible and shows in the notifications while
         * in progress and after completion.
         */
        public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;

        /**
         * This download doesn't show in the UI or in the notifications.
         */
        public static final int VISIBILITY_HIDDEN = 2;

        /**
         * This download shows in the notifications after completion ONLY.
         * It is usuable only with
         * {@link DownloadManager#addCompletedDownload(String, String,
         * boolean, String, String, long, boolean)}.
         */
        public static final int VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION = 3;    

public函数
在下载请求中添加HTTP包头。
这里写图片描述

设置即将下载的文件是否允许MediaScanner扫描,默认是false,需要在下载请求加入到下载任务序列之前调用。
这里写图片描述

下面这三个方法是跟网络相关:
设置何种网络类型下可以开启下载,一共三种网络类型:NETWORK_MOBILE 、NETWORK_WIFI 和NETWORK_BLUETOOTH ,默认为全部。其中,NETWORK_BLUETOOTH类型已经被废弃。
这里写图片描述
设置下载任务是否能在计量的网络连接下载,默认是允许。
这里写图片描述
设置下载任务是否能在漫游的网络连接下载,默认是允许。
这里写图片描述

设置设备是否在充电状态下下载,默认是false。
这里写图片描述

设置是否在设备空闲状态下下载,默认是false。
这里写图片描述

设置下载任务是否在系统下载界面中显示,默认是true。
这里写图片描述

下面这三个方法是跟下载通知相关的方法:
设置通知的标题。
这里写图片描述
设置通知的描述。
这里写图片描述
设置系统在下过程中或是下载完成时是否发送通知,一共四种下载类型,默认是VISIBILITY_VISIBLE 。
这里写图片描述

设置下载文件的MIME类型,这个方法会覆盖从服务器端返回的内容类型声明。
这里写图片描述

下面这三个方法是跟下载文件最终的存储位置相关:
为下载的文件设置存储位置,必须是指向外置SD卡的一个文件URI。
这里写图片描述
设置文件下载路径为应用外部文件目录,具体目录可以通过getExternalFilesDir(String)方法来获得,当应用被删除这些下载文件也会被清除。
这里写图片描述
设置文件下载路径为设备公开的外部文件目录,具体目录可以通过getExternalStoragePublicDirectory(String)方法来获得。
这里写图片描述

静态常量和内部类分析完了,最后来看一下DownloadManager都给我们提供了什么方法。

添加一个文件到系统的下载文件数据库中,并且可以在系统下载app中显示出该文件的下载条目。
这里写图片描述

查询DownloadManager中的下载请求。
这里写图片描述

把下载请求添加到请求序列中。
这里写图片描述

取消下载并从DownloadManager中移除。如果是在下载过程中会被终止,如果是已经下载完成会被删除。
这里写图片描述

返回在移动网络下下载的最大值,如果没有限制返回null。
这里写图片描述

返回在移动网络下推荐的下载最大值,如果没有限制返回null。
这里写图片描述

至此,关于DownloadManager的源码分析基本完成。下面来说一下我遇到的另外一个坑。我想在下载完成后发送一个下载完成消息,想为用户提供点击这个消息可以预览刚刚下载的这个图片的功能,这里会遇到文件外部调用异常(android.os.FileUriExposedException),这是因为Google在Android 7.0之后加入了
” StrictMode API 政策”机制,就是禁止向你的应用外公开 file:// URI。如果一项包含文件 file:// URI类型 的 Intent 离开你的应用就会出现 FileUriExposedException 异常。这时候就需要通过FileProvider类来解决问题,具体的使用方法可以参考 下载安装APK(兼容Android7.0)或是Android7.0须知–应用间共享文件(FileProvider),这篇文章对出错的原因和如何使用FileProvider进行了详细的讲解。

参考文献:
https://developer.android.google.cn/reference/android/app/DownloadManager.html
https://developer.android.google.cn/reference/android/app/DownloadManager.Query.html
https://developer.android.google.cn/reference/android/app/DownloadManager.Request.html
http://blog.csdn.net/u012209506/article/details/56012744
http://www.jianshu.com/p/7ad92b3d9069
http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/java/android/app/DownloadManager.java
http://androidxref.com/7.1.1_r6/xref/frameworks/support/core-utils/java/android/support/v4/content/FileProvider.java
http://blog.csdn.net/yulianlin/article/details/52775160
http://www.jianshu.com/p/3f9e3fc38eae

猜你喜欢

转载自blog.csdn.net/lj19851227/article/details/61196096