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 map = new HashMap(); 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(); }}@Overrideprotected void onResume() { super.onResume(); getContentResolver().registerContentObserver(CONTENT_URI, true, observer);}@Overrideprotected 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方法去插入数据。
.... ....
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()方法。部分关键代码如下:
@Overridepublic 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
更多相关文章
- 111111111
- android加固签名工具(源码下载)
- SONY 系列手机 Android(安卓)5.1 系统 Root 方法
- Android(安卓)FrameWork——Binder机制详解
- Android(安卓)Camera2 Hal3(一)初始化
- Mac系统下利用ADB命令连接android手机并进行文件操作
- android文件的写入与读取---简单的文本读写
- Android(安卓)Studio多渠道打包和代码混淆教程
- 一道面试题引发的对android中context的研究(一)-SharedPreference