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”)
,觀察這個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進行下載
參考
https://developer.android.com/reference/android/app/DownloadManager