美文网首页Android进阶Android 问题杂记
DownloadManager的使用和解析

DownloadManager的使用和解析

作者: Aisen | 来源:发表于2018-10-04 18:14 被阅读574次

    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

    Android系统下载管理DownloadManager功能介绍及使用示例

    Android系统下载管理DownloadManager

    相关文章

      网友评论

        本文标题:DownloadManager的使用和解析

        本文链接:https://www.haomeiwen.com/subject/yknxaftx.html