前言: 最近一直在写一个新闻客户端练手,每天也会关注一下鸿洋的微信公众号推荐的文章,看到一篇关于系统下载服务的文章比较感兴趣,就把它实现到我的新闻客户端用来为用户提供图片新闻的下载功能,顺带着分析一下源码(Android 7.1.1),总结一下在功能实现过程中遇到的坑。
概述: DownloadManager是Android提供的用来处理长时间HTTP下载任务的一个系统服务。客户端可以用getSystemService(DOWNLOAD_SERVICE)方法来获得这个类的实例,并通过这个服务把URI对应的文件下载到特定的文件目录下。DownloadManager会在后台运行,负责与HTTP进行交互,在下载失败、连接改变和系统重启后重新尝试下载。DownloadManager一共有三个内部类:CursorTranslator、Query和Request。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
