一、DownloadManager简介
DownloadManager是android为我们提供的一个服务,这个服务主要用于优化长时间的下载任务。
其优点是:在下载过程中如果发生一些突发情况,比如sd卡被拔出,网络变化等,其会等到状态恢复正常后继续下载任务,并且下载也支持断点下载。
但是其也有缺点,那就是它对外只提供了添加,移除,查询任务功能。对于下载过程中的可控性不佳,比如无法手动暂停一个正在下载的任务。
DownLoadManager有两个内部类,Request用于封装一个下载请求,Query则用于封装一个查询请求。
二、DownloadManager的使用
1、权限配置
需要访问网络权限和写外部存储权限(如果需要的话)
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2、配置并开启一个任务
这里以下载android 版qq来开启一个任务。
final DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
// 这里直接写死下载一个qq android application
String url = "https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk";
Uri uri = Uri.parse(url);
DownloadManager.Request request = new DownloadManager.Request(uri);
// 设置成下载过程中和下载完成都能够看到通知
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setTitle("下载");
request.setDescription("android-qq.apk 正在下载");
request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, "qq-test.apk");
// 实际的保存地址为 内部存储/android/data/com.hh.testandroid/files/Download/qq-test.apk
request.allowScanningByMediaScanner();
currentTaskId = downloadManager.enqueue(request);
上面通过对DownloadManager.Request进行一些配置然后通过downloadManager将这个request加入队列,然后其会返回一个long型的变量,
这个变量就可以代表这个任务,也就是该任务的id,后续的查询或者删除动作都需要使用此id。
3、设置任务监听
// 注册任务完成监听广播
if (receiver_download_complete == null) {
receiver_download_complete = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(DownloadActivity.this, "下载完成", Toast.LENGTH_LONG).show();
}
};
IntentFilter filter_download_complete = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
registerReceiver(receiver_download_complete, filter_download_complete);
}
// 注册下载过程中的点击事件,点击进入控制界面
if (receiver_download_clicked == null) {
receiver_download_clicked = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Intent intent_startActivity = new Intent(DownloadActivity.this, DownloadActivity.class);
startActivity(intent_startActivity);
}
};
IntentFilter filter_download_clicked = new IntentFilter(DownloadManager.ACTION_NOTIFICATION_CLICKED);
registerReceiver(receiver_download_clicked, filter_download_clicked);
}
上面代码注册了两个广播,其中第一个广播主要用于监听任务完成,第二个广播监听当任务在进行的过程中用户点击了通知这一事件(PS:注意是下载过程中点击才有回调,下载完后就是根据mineType来调用对应的程序打开)。
4、获取任务信息
DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(currentTaskId);
Cursor cursor = downloadManager.query(query);
while (cursor.moveToNext()) {
String downId= cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_ID));
String title = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE));
String address = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
String status = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
String size= cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
String sizeTotal = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
StringBuilder sb = new StringBuilder();
sb.append("downId = " + downId + "\n")
.append("title = " + title + "\n")
.append("address = " + address + "\n")
.append("status = " + status + "\n")
.append("size = " + size + "\n")
.append("sizeTotal = " + sizeTotal + "\n");
mTv.setText(sb.toString());
}
cursor.close();
上面通过DownloadManager.Query这个对象来查询下载情况,返回的是一个游标,然后我们可以通过这个来进行信息读取。
5、移除任务
private void removeTask () {
DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
downloadManager.remove(currentTaskId);
}
移除一个任务只需要传递其id值即可,如果任务正在进行则暂停,并且下载的文件也会被删除掉,后续也无法使用Downloadmanager来查询到该任务的信息。
三、了解DownloadManager
为什么DownloadManager能够实现自动断点续传,网络故障发生的时候停止下载,网络恢复的时候继续下载呢? 这是因为DownloadManager其本身就只是一个代理而已,我们enqueue一个task或者remove一个task,其都是通过内部的ContentResolver来联系到系统的DownloadProvider的,然后对于执行一些操作,比如enqueue则对应于insert一条task记录,移除一个task的时候就删除一条记录。而这部分的变化会触发DownloadService,然后执行对应的操作,并且写回数据。
上面的图大致演示了这整个工作过程, a、其中DownloadManager通过配置Request来将数据插入到DownloadProvider中,插入的过程会启动DownloadService,然后会读取DownloadProvider中的信息,再使用线程池执行,执行后的过程会回写到DownloadProvider,方便DownloadManager来查询数据。 b、另外当DownloadProvider中数据发送变化的时候也会被ContentProviderObserver监听到,同样也能够启动DownloadService中的逻辑,也当我们调用DownloadManager的remove方法的时候正是通过这条路径来的; c、而当由于储存问题,网络问题等导致下载中断后其可以通过监听特定的情况来启动DownloadReciver,进而启动DownloadService,再进行下载。
1、添加任务的过程
添加一个任务,当我们配置好DownloadManager.Request之后,我们会调用DownloadManager的enqueue方法,在该方法中将Request中的配置转化为ContentValues形式,然后插入到DownloadProvider中。 路径 : \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;
}
路径:\packages\providers\DownloadProvider\src\com\android\providers\downloads\DownloadProvider.java
public Uri insert(final Uri uri, final ContentValues values) {
// ... 省略了对参数的判断与处理
long rowID = db.insert(DB_TABLE, null, filteredValues);
if (rowID == -1) {
Log.d(Constants.TAG, "couldn't insert into downloads database");
return null;
}
insertRequestHeaders(db, rowID, values);
notifyContentChanged(uri, match);
// 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);
}
从上面的方法中可以知道,其干了三件重要的事: a、将处理好的数据插入到数据库 b、调用notifyContentChanged通知监听在该uri上的所有观察者 c、启动DownloadService.
路径:\packages\providers\DownloadProvider\src\com\android\providers\downloads\DownloadService.java
public void onCreate() {
super.onCreate();
....
mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
mUpdateThread.start();
mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
mScanner = new DownloadScanner(this);
mNotifier = new DownloadNotifier(this);
mNotifier.cancelAll();
mObserver = new DownloadManagerContentObserver();
getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
true, mObserver);
}
这DownloadService的onCreate方法中开启了一个HandlerThread,主要用来处理更新信息。并且添加了该uri上的observer。
public int onStartCommand(Intent intent, int flags, int startId) {
...
enqueueUpdate();
return returnValue;
}
public void onChange(final boolean selfChange) {
enqueueUpdate();
}
而其中开启一个DownloadService和数据库发送变化的时候都会调用enqueueUpdate方法。而enqueueUpdate则会发送消息给HandlerThread执行相应的操作。在handlerThread中会先从ContentProvider中读取所有的下载记录,然后将符合启动条件的任务使用一个默认长度为10的线程池来执行下载任务,具体下载任务的执行者为DownloadThread。
DownloadThread首先会先检查目前设备能否执行下载任务, 如果ok则会使用HttpUrlConnection来下载,如果下载过程中发生了意外情况,比如网络中断的情况,其就会抛出异常,并且将信息更新到DownloadProvider中,如果不发生异常也将信息记录到DownloadProvider中并且发送任务完成广播。
2、异常恢复
异常恢复是因为DownloadProvider这个项目有一个监听网络变化,存储器变化,机器重启的DownloadReciver存在,一旦发生这些情况的时候就会启动DownloadService,然后又开始从数据库中读取下载情况,如果发现因为故障而暂停的任务的话就会再次启动。 路径:\packages\providers\DownloadProvider\src\com\android\providers\downloads\DownloadReciver.java
public void onReceive(final Context context, final Intent intent) {
if (mSystemFacade == null) {
mSystemFacade = new RealSystemFacade(context);
}
String action = intent.getAction();
if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Received broadcast intent for " +
Intent.ACTION_BOOT_COMPLETED);
}
startService(context);
} else if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Received broadcast intent for " +
Intent.ACTION_MEDIA_MOUNTED);
}
startService(context);
} else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
final ConnectivityManager connManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkInfo info = connManager.getActiveNetworkInfo();
if (info != null && info.isConnected()) {
startService(context);
}
} else if (action.equals(Constants.ACTION_RETRY)) {
startService(context);
} else {
....
}
}
3、断点续传
任务在每次下载的过程中每隔一定的数据便更新一下DownloadProvider,这样子就能够在重新开始的时候继续上次的任务了。
路径:\packages\providers\DownloadProvider\src\com\android\providers\downloads\DownloadThread.java
private void reportProgress(State state) {
......
if (state.mCurrentBytes - state.mBytesNotified > Constants.MIN_PROGRESS_STEP &&
now - state.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
state.mBytesNotified = state.mCurrentBytes;
state.mTimeLastNotification = now;
}
}
从上面的代码中可以看出,当时间超过Constants.MIN_PROGRESS_TIME,或者下载数据超过Constants.MIN_PROGESS_STEP的话就会进行一次记录。
/** The minimum amount of progress that has to be done before the progress bar gets updated */
public static final int MIN_PROGRESS_STEP = 4096;
/** The minimum amount of time that has to elapse before the progress bar gets updated, in ms */
public static final long MIN_PROGRESS_TIME = 1500;
而在DownloadThread开始下载的时候会判断记录的已下载任务是否和整个包一致,如果是的话也会跳过。如果不一致的话则将其添加到Http请求的请求头中。
conn.addRequestProperty("Range", "bytes=" + state.mCurrentBytes + "-");
4、删除任务
删除任务的时候因为修改了DownloadProvider中的数据,将其中的数据项标志位“删除”。这时候会触发该uri上的ContentOnserver,然后进入enqueueUpdate方法,在HandlerThread中如果发现数据项被标志位删除的话则会先删除下载的文件后再请求DownloadProvider删除该项数据。
5、查询任务
因为DownloadService在下载的过程中将所有的信息都记录到了DownloadProvider上,所以可以直接通过DownloadManager中的ContentProvider来直接查询得到。
转载请注明原文地址: https://ju.6miu.com/read-500263.html