Android DownloadProvider分析
1 如何使用DownloadManager下载文件
Android开发中经常会用到文件下载的功能,系统为我们提供了DownloadManager类,这是android提供的系统服务,可以通过这个服务完成文件下载。整个下载过程全部交给系统负责。通过API文档,可以看出DownLoadManager包含两个重要的内部类:
- DownLoadManager.Request:主要用于发起一个下载请求。
- DownLoadManager.Query:主要用于查询下载信息。
1.1 DownloadManager$Request
DownLoadManager.Request此类封装了一个下载请求所需要的所有信息,通过构造函数可以初始化一个request对象,构造对象时需要传入下载文件的地址。
DownloadManager.Request request = new DownloadManager.Request(Uri.parse("http://gdown.baidu.com/data/wisegame/55dc62995fe9ba82/jinritoutiao_448.apk"));
//设置在什么网络情况下进行下载
request.setAllowedNetworkTypes(Request.NETWORK_WIFI);
//设置通知栏标题
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE);
request.setTitle("下载");
request.setDescription("今日头条正在下载");
request.setAllowedOverRoaming(false);
//设置文件存放目录
request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, "mydown");
//调用系统服务下载文件
DownloadManager downManager = (DownloadManager)getSystemService(Context.DOWNLOAD_SERVICE);
long id= downManager.enqueue(request);
构造完对象后,可以为request设置一些属性:
- addRequestHeader(String header,String value):添加网络下载请求的http头信息
- allowScanningByMediaScanner():用于设置是否允许本MediaScanner扫描。
- setAllowedNetworkTypes(int flags):设置用于下载时的网络类型,默认任何网络都可以下载,提供的网络常量有:NETWORK_BLUETOOTH、NETWORK_MOBILE、NETWORK_WIFI。
- setAllowedOverRoaming(Boolean allowed):用于设置漫游状态下是否可以下载
- setNotificationVisibility(int visibility):用于设置下载时时候在状态栏显示通知信息
- setTitle(CharSequence):设置Notification的title信息
- setDescription(CharSequence):设置Notification的message信息
- setDestinationInExternalFilesDir、setDestinationInExternalPublicDir、setDestinationUri等方法用于设置下载文件的存放路径,注意如果将下载文件存放在默认路径,那么在空间不足的情况下系统会将文件删除,所以使用上述方法设置文件存放目录是十分必要的。
1.2 DownloadManager$Query
DownManager会对所有的任务进行保存管理,那么如何获取这些信息呢?这个时候就要用到DownManager.Query对象,通过此对象,可以查询所有下载任务信息。
- setFilterById(long... ids):根据任务编号查询下载任务信息
- setFilterByStatus(int flags):根据下载状态查询下载任务
private void queryDownTask(DownloadManager downManager,int status) {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterByStatus(status);
Cursor cursor= downManager.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 statuss = 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));
Map<String, String> map = new HashMap<String, String>();
map.put("downid", downId);
map.put("title", title);
map.put("address", address);
map.put("status", sizeTotal+":"+size);
this.data.add(map);
}
cursor.close();
}
1.3 下载进度的监听及查询
DownloadManager没有提供相应的回调接口,用于返回实时的下载进度状态,但通过ContentProvider可以监听到当前下载项的进度状态变化。
DownloadManager.getUriForDownloadedFile(id);
该方法会返回一个下载项的Uri,如content://downloads/my_downloads/125
,通过ContentObserver监听Uri.parse(“content://downloads/my_downloads”)
观察这个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;
}
1.4 下载成功监听
文件下载完成后经常需要做一下后操作,那么如何监听文件时候已经下载完成了呢?DownLoadManager在文件现在完成时会发送一个action为ACTION_DOWNLOAD_COMPLETE的广播,可以注册一个广播接收器即可进行处理:
private class DownLoadCompleteReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)){
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
Toast.makeText(MainActivity.this, "编号:"+id+"的下载任务已经完成!", Toast.LENGTH_SHORT).show();
}
}
}
2 DownloadProvider下载流程分析
Download下载的工作流程,应用层通过FrameWork层DownloadManager API调用到DownloadProvider,通过操作数据库,最后通过DownloadJobService中的线程调度完成工作。整体上都是由DownloadProvider进行过渡调用。而数据库与Service都通过DownloadProvider进行隔离。
关系图 Download下载时序图
Step1 : DownloadManager.enqueue
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方法。从pacakges/providers/DownloadProvider的清单文件中很容易知道最终是调用了DownloadProvider的insert方法去插入数据。
....
<provider android:name=".DownloadProvider"
android:authorities="downloads" android:exported="true">
....
Step2 : DownloadProvider.insert
insert方法即是往DB_TABLE(downloads)表中插入了一条数据。Android N版本中引入了JobSchedule组件来进行异步下载任务的处理。然后调用Helpers.scheduleJob(getContext(), rowID);去安排一次下载。
@Override
public Uri insert(final Uri uri, final ContentValues values) {
//...验证values中的值,检验下载路径
long rowID = db.insert(DB_TABLE, null, filteredValues);
insertRequestHeaders(db, rowID, values); //将request header插入request_headers表
grantAllDownloadsPermission(rowID, Binder.getCallingUid());
notifyContentChanged(uri, match);
final long token = Binder.clearCallingIdentity();
try {
Helpers.scheduleJob(getContext(), rowID);
} finally {
Binder.restoreCallingIdentity(token);
}
......
return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}
Step3 : Helpers.scheduleJob
调用queryDownloadInfo先将downloadId对应的信息保存在DownloadInfo中,Step2中会将ContentValues 插入到数据库中,DownloadInfo就是再从数据库中将下载uri、地址等Request信息取出来,然后调用scheduleJob(Context context, DownloadInfo info)
public static void scheduleJob(Context context, long downloadId) {
final boolean scheduled = scheduleJob(context,
DownloadInfo.queryDownloadInfo(context, downloadId));
if (!scheduled) {
// If we didn't schedule a future job, kick off a notification
// update pass immediately
getDownloadNotifier(context).update();
}
public static DownloadInfo queryDownloadInfo(Context context, long downloadId) {
final ContentResolver resolver = context.getContentResolver();
try (Cursor cursor = resolver.query(
ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, downloadId),
null, null, null, null)) {
final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
final DownloadInfo info = new DownloadInfo(context);
if (cursor.moveToFirst()) {
reader.updateFromDatabase(info);
reader.readRequestHeaders(info);
return info;
}
}
return null;
}
继续调用scheduleJob,用绑定的DownloadJobService进行下载任务。如果线程调度失败,会返回false。
public static boolean scheduleJob(Context context, DownloadInfo info) {
//关键代码
final JobScheduler scheduler = context.getSystemService(JobScheduler.class); // 使用Android JobScheduler/JobService 工作调度
final JobInfo.Builder builder = new JobInfo.Builder(jobId, new ComponentName(context, DownloadJobService.class));
scheduler.scheduleAsPackage(builder.build(), packageName, UserHandle.myUserId(), TAG);
}
Step4 : DownloadJobService.onStartJob
DownloadService中调度的线程开始下载,在onStartJob中用rowId查出来后,直接开线程DownloadThread开始下载。DownloadService服务启动时会注册一个Observer,用来监听下载进度的变化,用来更新下载通知栏。
public class DownloadJobService extends JobService {
@Override
public void onCreate() {
super.onCreate();
getContentResolver().registerContentObserver(ALL_DOWNLOADS_CONTENT_URI, true, mObserver);
}
@Override
public boolean onStartJob(JobParameters params) {
final int id = params.getJobId();
// Spin up thread to handle this download
final DownloadInfo info = DownloadInfo.queryDownloadInfo(this, id);
if (info == null) {
Log.w(TAG, "Odd, no details found for download " + id);
return false;
}
final DownloadThread thread;
synchronized (mActiveThreads) {
thread = new DownloadThread(this, params, info);
mActiveThreads.put(id, thread);
}
thread.start();
return true;
}
private ContentObserver mObserver = new ContentObserver(Helpers.getAsyncHandler()) {
@Override
public void onChange(boolean selfChange) {
Helpers.getDownloadNotifier(DownloadJobService.this).update();
}
};
}
Step5 : DownloadThread.run
其实在DownloadThread中,主要的下载方法就是就是线程中的excuteDownload()方法。部分关键代码如下:
@Override
public void run() {
//关键代码
executeDownload(); //开始下载
}
private void executeDownload() throws StopRequestException {
//关键代码
URL url;
try {
// TODO: migrate URL sanity checking into client side of API
url = new URL(mInfoDelta.mUri);
} catch (MalformedURLException e) {
throw new StopRequestException(STATUS_BAD_REQUEST, e);
}
HttpURLConnection conn = null;
checkConnectivity();
conn = (HttpURLConnection) mNetwork.openConnection(url); //使用HttpsURLConnection下载
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(DEFAULT_TIMEOUT);
conn.setReadTimeout(DEFAULT_TIMEOUT);
// If this is going over HTTPS configure the trust to be the same as the calling package.
if (conn instanceof HttpsURLConnection) {
((HttpsURLConnection)conn).setSSLSocketFactory(appContext.getSocketFactory());
}
addRequestHeaders(conn, resuming);
final int responseCode = conn.getResponseCode();
switch (responseCode) {
case HTTP_OK: //网络请求成功
if (resuming) {
throw new StopRequestException(STATUS_CANNOT_RESUME, "Expected partial, but received OK");
}
parseOkHeaders(conn);
transferData(conn); //Transfer data from the given connection to the destination file
return;
.............
}
参考
https://www.jianshu.com/p/fe4935f27dc1
https://blog.csdn.net/weixin_34281537/article/details/86872919