Android Jetpack之DownloadManager的使用和解析
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)); } }
【附錄】

資料圖