DownloadManager的使用和解析
DownloadManager的介绍
DownloadManger是android 2.3(API 9)开始提供的系统服务,用于处理长时间的下载操作。应用场景是客户端请求一个URL地址去下载一个目标文件。DownloadManger可以构建一个后台下载服务,在发生故障或连接更改、重新启动系统等情况后,处理HTTP连接并重试下载。
如果APP通过DownloadManager请求下载,那么应用注册ACTION_NOTIFICATION_CLICKED
的广播,以便在用户单击下载通知栏或者下载UI时,进行适当处理。
需要注意使用DownloadManager时,必须申请Manifest.permission.INTERNET
权限。
获取这个类的实例的方式有:Context.getSystemService(Class)
,参数为DownloadManager.class
,或者,Context.getSystemService(String)
,其参数为Context.DOWNLOAD_SERVICE
主要的接口和类:
1、内部类DownloadManager.Query,这个类可以用于过滤DownloadManager的请求。
2、内部类DownloadManager.Request,这个类包含请求一个新下载连接的必要信息。
3、公共方法enqueue,在队列中插入一个新的下载。当连接正常
,并且DownloadManager准备执行这个请求时,开始自动下载。返回结果是系统提供的唯一下载ID,这个ID可以用于与这个下载相关的回调。
4、公共方法query,用于查询下载信息。
5、公共方法remove,用于删除下载,如果下载中则取消下载。同时会删除下载文件和记录。
DownloadManager的使用
1、在AndroidManifest中添加权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
一个是网络访问权限,一个是SDCARD写权限。
2、初始化DownloadManager.Request,调用enqueue方法开始下载
DownloadManager mDownloadManager = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
String apkUrl = “https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk”;
Uri resource = Uri.parse(apkUrl);
Request request = new Request(resource);
//下载的本地路径,表示设置下载地址为SD卡的Download文件夹,文件名为mobileqq_android.apk。
request.setDestinationInExternalPublicDir(“Download”, “mobileqq_android.apk”);
//start 一些非必要的设置
request.setAllowedNetworkTypes(Request.NETWORK_MOBILE | Request.NETWORK_WIFI);
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setVisibleInDownloadsUi(true);
request.setTitle(displayName);
//end 一些非必要的设置
mDownloadManager.enqueue(request);
DownloadManager.Request除了构造函数的Uri必须外,其他设置都为可选设置。例如:
request.setMimeType(“application/cn.trinea.download.file”);
设置下载文件的mineType。因为在下载管理UI中,点击某个已下载完成文件,以及,在下载完成后,点击通知栏提示,都会根据mimeType去打开文件,所以我们可以利用这个属性。比如设置了mimeType为application/cn.trinea.download.file,我们可以同时设置某个Activity的intent-filter为application/cn.trinea.download.file,用于响应点击的打开文件。
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/cn.trinea.download.file" />
</intent-filter>
3、下载进度状态的监听及查询
DownloadManager没有提供相应的回调接口,用于返回实时的下载进度状态,但通过四大组件之一ContentProvider,可以监听到当前下载项的进度状态变化。
DownloadManager.getUriForDownloadedFile(id);
该方法会返回一个下载项的Uri,如content://downloads/my_downloads/125
,因此,我们通过ContentObserver监听Uri.parse(“content://downloads/my_downloads”)
(即Downloads.Impl.CONTENT_URI),观察这个Uri指向的数据库项的变化,然后进行下一步操作,如发送handler进行更新UI。例子如下:
private Handler handler = new Handler(Looper.getMainLooper());
private static final Uri CONTENT_URI = Uri.parse("content://downloads/my_downloads");
private DownloadContentObserver observer = new DownloadStatusObserver();
class DownloadContentObserver extends ContentObserver {
public DownloadContentObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
updateView();
}
}
@Override
protected void onResume() {
super.onResume();
getContentResolver().registerContentObserver(CONTENT_URI, true, observer);
}
@Override
protected void onDestroy() {
super.onDestroy();
getContentResolver().unregisterContentObserver(observer);
}
public void updateView() {
int[] bytesAndStatus = getBytesAndStatus(downloadId);
int currentSize = bytesAndStatus[0];//当前大小
int totalSize = bytesAndStatus[1];//总大小
int status = bytesAndStatus[2];//下载状态
Message.obtain(handler, 0, currentSize, totalSize, status).sendToTarget();
}
public int[] getBytesAndStatus(long downloadId) {
int[] bytesAndStatus = new int[] { -1, -1, 0 };
Query query = new Query().setFilterById(downloadId);
Cursor c = null;
try {
c = mDownloadManager.query(query);
if (c != null && c.moveToFirst()) {
bytesAndStatus[0] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
bytesAndStatus[1] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
bytesAndStatus[2] = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
}
} finally {
if (c != null) {
c.close();
}
}
return bytesAndStatus;
}
上面的代码主要调用queue()
进行查询,参数属性封装在DownloadManager.Query()
类中。这个类主要包括以下接口:
- setFilterById(long… ids),根据下载id进行过滤
- setFilterByStatus(int flags),根据下载状态进行过滤
- setOnlyIncludeVisibleInDownloadsUi(boolean value),根据是否在Download UI中可见进行过滤。
- orderBy(String column, int direction),根据列进行排序,不过目前仅支持
DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP
和
DownloadManager.COLUMN_TOTAL_SIZE_BYTES
排序。
补充
如果界面上过多元素需要更新,且网速较快不断的执行onChange会对页面性能有一定影响,或者出现一些异常情况,那么推荐ScheduledExecutorService定期查询,如下:
//三秒定时刷新一次
public static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
Runnable command = new Runnable() {
@Override
public void run() {
updateView();
}
};
scheduledExecutorService.scheduleAtFixedRate(command, 0, 3, TimeUnit.SECONDS);
4、下载成功监听
下载完成后,下载管理服务会发出DownloadManager.ACTION_DOWNLOAD_COMPLETE
这个广播,并传递downloadId作为参数。通过接受广播我们可以打开对下载完成的内容进行操作。
private CompleteReceiver completeReceiver;
class CompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// get complete download id
long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
// to do here
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
completeReceiver = new CompleteReceiver();
//register download success broadcast
registerReceiver(completeReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(completeReceiver);
}
5、响应通知栏的点击
(1)下载中点击
点击下载中通知栏提示,系统会对下载的应用单独发送Action为DownloadManager.ACTION_NOTIFICATION_CLICKED
广播。intent.getData为content://downloads/all_downloads/29669,最后一位为downloadId。
如果同时下载多个应用,intent会包含DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS
这个key,表示下载的downloadId数组。
(2)下载完成后点击
下载完成后系统会调用下面代码进行处理,从中我们可以发现系统会调用View Action根据mimeType去查询。所以可以利用上文第2条介绍的DownloadManager.Request的setMimeType函数。
private void openDownload(Context context, Cursor cursor) {
String filename = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl._DATA));
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
Uri path = Uri.parse(filename);
// If there is no scheme, then it must be a file
if (path.getScheme() == null) {
path = Uri.fromFile(new File(filename));
}
Intent activityIntent = new Intent(Intent.ACTION_VIEW);
mimetype = DownloadDrmHelper.getOriginalMimeType(context, filename, mimetype);
activityIntent.setDataAndType(path, mimetype);
activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(activityIntent);
} catch (ActivityNotFoundException ex) {
Log.d(Constants.TAG, "no activity for " + mimetype, ex);
}
}
DownloadManager的解析
DownloadManager开始下载的入口enqueue方法,这个方法的源码如下:
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;
}
使用的ContentProvider方式,将Request信息转换为ContentValues类,然后调用ContentResolver进行插入,底层会调用对应的ContentProvider的insert方法。URI是Downloads.Impl.CONTENT_URI,即"content://downloads/my_downloads",找到对应的Provider即系统提供的DownloadProvider
。
DownloadProvider
类在系统源码的src/com/android/providers/downloads的路径下,找都其insert方法的实现,可以发现最后部分的代码:
public Uri insert(final Uri uri, final ContentValues values) {
...
// 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);
}
即插入信息后,会启动DownloadService
开始进行下载。(从Android N (API 24) 开始实现方式不同)
DownloadService
的入口是onStartCommand方法,其中用mUpdateHandler发送消息MSG_UPDATE,mUpdateHandler处理消息的方式如下:
mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
private Handler.Callback mUpdateCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
...
final boolean isActive;
synchronized (mDownloads) {
isActive = updateLocked();
}
...
}
};
private boolean updateLocked() {
...
// Kick off download task if ready
final boolean activeDownload = info.startDownloadIfReady(mExecutor);
...
}
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);
}
//启动DownloadThread开始下载任务
mTask = new DownloadThread(mContext, mSystemFacade, mNotifier, this);
mSubmittedTask = executor.submit(mTask);
}
return isReady;
}
}
从上面源码可以看,DownloadService
的onStartCommand方法,最终启动DownloadThread
,开始下载的任务(网络请求接口使用的是HttpURLConnection)。DownloadThread
在下载过程中,会更新DownloadProvider。
综上所述,DownloadManager的enqueue方法的流程是:
DownloadProvider插入信息 >> 启动DownloadService >> 开始DownloadThread进行下载
扩展
1、DownloadManager出现崩溃
Fatal Exception: java.lang.IllegalArgumentException: Unknown URL content://downloads/my_downloads
at android.content.ContentResolver.insert(ContentResolver.java:882)
at android.app.DownloadManager.enqueue(DownloadManager.java:904)
原因:这一般是因为手动禁用了下载器
(现象可以从打开Google Play Store看到,会出现提示被禁用的弹窗)
(手动禁用的方式可以是点击在下载过程的通知栏信息,进入设置页面点击“禁用”按钮)
解决方法:
https://stackoverflow.com/questions/21551538/how-to-enable-android-download-manager
https://github.com/HanteIsHante/file/issues/25
可以在代码中判断下载管理器是否可用
static boolean downLoadMangerIsEnable(Context context) {
int state = context.getApplicationContext().getPackageManager()
.getApplicationEnabledSetting("com.android.providers.downloads");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
return !(state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED ||
state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
|| state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED);
} else {
return !(state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED ||
state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER);
}
}
如果不可用,则打开 系统下载管理器 设置页面 或者 打开系统设置,让用户设置
try {
//Open the specific App Info page:
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + "com.android.providers.downloads"));
startActivity(intent);
} catch ( ActivityNotFoundException e ) {
e.printStackTrace();
//Open the generic Apps page:
Intent intent = new Intent(android.provider.Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);
startActivity(intent);
}
2、DownloaderManager的断点续传是怎么触发的?
比如断开网络,然后恢复网络,DownloaderManger可以继续下载,这是怎么触发的?
从源码可以看到,实现方式是监听网络变化广播,实现类是DownloadReceiver.java。
//DownloadReceiver.java
public class DownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
if (mSystemFacade == null) {
mSystemFacade = new RealSystemFacade(context);
}
final String action = intent.getAction();
//...
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(action)) {
final ConnectivityManager connManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo info = connManager.getActiveNetworkInfo();
if (info != null && info.isConnected()) {
startService(context);
}
}
}
private void startService(Context context) {
context.startService(new Intent(context, DownloadService.class));
}
}
参考
https://developer.android.com/reference/android/app/DownloadManager