Android DownloadProvider分析

2019-05-31  本文已影响0人  AI科技智库

1 如何使用DownloadManager下载文件

Android开发中经常会用到文件下载的功能,系统为我们提供了DownloadManager类,这是android提供的系统服务,可以通过这个服务完成文件下载。整个下载过程全部交给系统负责。通过API文档,可以看出DownLoadManager包含两个重要的内部类:

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设置一些属性:

1.2 DownloadManager$Query

DownManager会对所有的任务进行保存管理,那么如何获取这些信息呢?这个时候就要用到DownManager.Query对象,通过此对象,可以查询所有下载任务信息。

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

上一篇下一篇

猜你喜欢

热点阅读