DownloadManager的介紹
DownloadManger是android 2.3(API 9)開(kāi)始提供的系統(tǒng)服務(wù),用于處理長(zhǎng)時(shí)間的下載操作鸟妙。應(yīng)用場(chǎng)景是客戶端請(qǐng)求一個(gè)URL地址去下載一個(gè)目標(biāo)文件。DownloadManger可以構(gòu)建一個(gè)后臺(tái)下載服務(wù),在發(fā)生故障或連接更改草讶、重新啟動(dòng)系統(tǒng)等情況后,處理HTTP連接并重試下載炉菲。
如果APP通過(guò)DownloadManager請(qǐng)求下載堕战,那么應(yīng)用注冊(cè)ACTION_NOTIFICATION_CLICKED
的廣播,以便在用戶單擊下載通知欄或者下載UI時(shí)拍霜,進(jìn)行適當(dāng)處理嘱丢。
需要注意使用DownloadManager時(shí),必須申請(qǐng)Manifest.permission.INTERNET
權(quán)限祠饺。
獲取這個(gè)類的實(shí)例的方式有:Context.getSystemService(Class)
越驻,參數(shù)為DownloadManager.class
,或者道偷,Context.getSystemService(String)
缀旁,其參數(shù)為Context.DOWNLOAD_SERVICE
主要的接口和類:
1、內(nèi)部類DownloadManager.Query勺鸦,這個(gè)類可以用于過(guò)濾DownloadManager的請(qǐng)求并巍。
2、內(nèi)部類DownloadManager.Request换途,這個(gè)類包含請(qǐng)求一個(gè)新下載連接的必要信息懊渡。
3嘶窄、公共方法enqueue,在隊(duì)列中插入一個(gè)新的下載距贷。當(dāng)連接正常
,并且DownloadManager準(zhǔn)備執(zhí)行這個(gè)請(qǐng)求時(shí)吻谋,開(kāi)始自動(dòng)下載忠蝗。返回結(jié)果是系統(tǒng)提供的唯一下載ID,這個(gè)ID可以用于與這個(gè)下載相關(guān)的回調(diào)漓拾。
4阁最、公共方法query,用于查詢下載信息骇两。
5速种、公共方法remove,用于刪除下載低千,如果下載中則取消下載配阵。同時(shí)會(huì)刪除下載文件和記錄。
DownloadManager的使用
1示血、在AndroidManifest中添加權(quán)限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
一個(gè)是網(wǎng)絡(luò)訪問(wèn)權(quán)限棋傍,一個(gè)是SDCARD寫權(quán)限。
2难审、初始化DownloadManager.Request瘫拣,調(diào)用enqueue方法開(kāi)始下載
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);
//下載的本地路徑,表示設(shè)置下載地址為SD卡的Download文件夾告喊,文件名為mobileqq_android.apk麸拄。
request.setDestinationInExternalPublicDir(“Download”, “mobileqq_android.apk”);
//start 一些非必要的設(shè)置
request.setAllowedNetworkTypes(Request.NETWORK_MOBILE | Request.NETWORK_WIFI);
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setVisibleInDownloadsUi(true);
request.setTitle(displayName);
//end 一些非必要的設(shè)置
mDownloadManager.enqueue(request);
DownloadManager.Request除了構(gòu)造函數(shù)的Uri必須外,其他設(shè)置都為可選設(shè)置黔姜。例如:
request.setMimeType(“application/cn.trinea.download.file”);
設(shè)置下載文件的mineType拢切。因?yàn)樵谙螺d管理UI中,點(diǎn)擊某個(gè)已下載完成文件地淀,以及失球,在下載完成后,點(diǎn)擊通知欄提示帮毁,都會(huì)根據(jù)mimeType去打開(kāi)文件实苞,所以我們可以利用這個(gè)屬性。比如設(shè)置了mimeType為application/cn.trinea.download.file烈疚,我們可以同時(shí)設(shè)置某個(gè)Activity的intent-filter為application/cn.trinea.download.file黔牵,用于響應(yīng)點(diǎn)擊的打開(kāi)文件。
<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爷肝、下載進(jìn)度狀態(tài)的監(jiān)聽(tīng)及查詢
DownloadManager沒(méi)有提供相應(yīng)的回調(diào)接口猾浦,用于返回實(shí)時(shí)的下載進(jìn)度狀態(tài)陆错,但通過(guò)四大組件之一ContentProvider捌木,可以監(jiān)聽(tīng)到當(dāng)前下載項(xiàng)的進(jìn)度狀態(tài)變化肢础。
DownloadManager.getUriForDownloadedFile(id);
該方法會(huì)返回一個(gè)下載項(xiàng)的Uri,如content://downloads/my_downloads/125
瓣俯,因此夹抗,我們通過(guò)ContentObserver監(jiān)聽(tīng)Uri.parse(“content://downloads/my_downloads”)
(即Downloads.Impl.CONTENT_URI)绳慎,觀察這個(gè)Uri指向的數(shù)據(jù)庫(kù)項(xiàng)的變化,然后進(jìn)行下一步操作漠烧,如發(fā)送handler進(jìn)行更新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];//當(dāng)前大小
int totalSize = bytesAndStatus[1];//總大小
int status = bytesAndStatus[2];//下載狀態(tài)
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;
}
上面的代碼主要調(diào)用queue()
進(jìn)行查詢,參數(shù)屬性封裝在DownloadManager.Query()
類中已脓。這個(gè)類主要包括以下接口:
- setFilterById(long… ids)珊楼,根據(jù)下載id進(jìn)行過(guò)濾
- setFilterByStatus(int flags),根據(jù)下載狀態(tài)進(jìn)行過(guò)濾
- setOnlyIncludeVisibleInDownloadsUi(boolean value)度液,根據(jù)是否在Download UI中可見(jiàn)進(jìn)行過(guò)濾厕宗。
- orderBy(String column, int direction),根據(jù)列進(jìn)行排序,不過(guò)目前僅支持
DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP
和
DownloadManager.COLUMN_TOTAL_SIZE_BYTES
排序恨诱。
補(bǔ)充
如果界面上過(guò)多元素需要更新媳瞪,且網(wǎng)速較快不斷的執(zhí)行onChange會(huì)對(duì)頁(yè)面性能有一定影響,或者出現(xiàn)一些異常情況照宝,那么推薦ScheduledExecutorService定期查詢蛇受,如下:
//三秒定時(shí)刷新一次
public static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
Runnable command = new Runnable() {
@Override
public void run() {
updateView();
}
};
scheduledExecutorService.scheduleAtFixedRate(command, 0, 3, TimeUnit.SECONDS);
4、下載成功監(jiān)聽(tīng)
下載完成后厕鹃,下載管理服務(wù)會(huì)發(fā)出DownloadManager.ACTION_DOWNLOAD_COMPLETE
這個(gè)廣播兢仰,并傳遞downloadId作為參數(shù)。通過(guò)接受廣播我們可以打開(kāi)對(duì)下載完成的內(nèi)容進(jìn)行操作剂碴。
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把将、響應(yīng)通知欄的點(diǎn)擊
(1)下載中點(diǎn)擊
點(diǎn)擊下載中通知欄提示,系統(tǒng)會(huì)對(duì)下載的應(yīng)用單獨(dú)發(fā)送Action為DownloadManager.ACTION_NOTIFICATION_CLICKED
廣播忆矛。intent.getData為content://downloads/all_downloads/29669察蹲,最后一位為downloadId。
如果同時(shí)下載多個(gè)應(yīng)用催训,intent會(huì)包含DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS
這個(gè)key洽议,表示下載的downloadId數(shù)組。
(2)下載完成后點(diǎn)擊
下載完成后系統(tǒng)會(huì)調(diào)用下面代碼進(jìn)行處理漫拭,從中我們可以發(fā)現(xiàn)系統(tǒng)會(huì)調(diào)用View Action根據(jù)mimeType去查詢亚兄。所以可以利用上文第2條介紹的DownloadManager.Request的setMimeType函數(shù)。
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開(kāi)始下載的入口enqueue方法采驻,這個(gè)方法的源碼如下:
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信息轉(zhuǎn)換為ContentValues類匈勋,然后調(diào)用ContentResolver進(jìn)行插入,底層會(huì)調(diào)用對(duì)應(yīng)的ContentProvider的insert方法膳叨。URI是Downloads.Impl.CONTENT_URI洽洁,即"content://downloads/my_downloads",找到對(duì)應(yīng)的Provider即系統(tǒng)提供的DownloadProvider
菲嘴。
DownloadProvider
類在系統(tǒng)源碼的src/com/android/providers/downloads的路徑下诡挂,找都其insert方法的實(shí)現(xiàn),可以發(fā)現(xiàn)最后部分的代碼:
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);
}
即插入信息后临谱,會(huì)啟動(dòng)DownloadService
開(kāi)始進(jìn)行下載。(從Android N (API 24) 開(kāi)始實(shí)現(xiàn)方式不同)
DownloadService
的入口是onStartCommand方法奴璃,其中用mUpdateHandler發(fā)送消息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);
}
//啟動(dòng)DownloadThread開(kāi)始下載任務(wù)
mTask = new DownloadThread(mContext, mSystemFacade, mNotifier, this);
mSubmittedTask = executor.submit(mTask);
}
return isReady;
}
}
從上面源碼可以看,DownloadService
的onStartCommand方法苟穆,最終啟動(dòng)DownloadThread
抄课,開(kāi)始下載的任務(wù)(網(wǎng)絡(luò)請(qǐng)求接口使用的是HttpURLConnection)。DownloadThread
在下載過(guò)程中雳旅,會(huì)更新DownloadProvider跟磨。
綜上所述,DownloadManager的enqueue方法的流程是:
DownloadProvider插入信息 >> 啟動(dòng)DownloadService >> 開(kāi)始DownloadThread進(jìn)行下載
擴(kuò)展
1攒盈、DownloadManager出現(xiàn)崩潰
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)
原因:這一般是因?yàn)槭謩?dòng)禁用了下載器
(現(xiàn)象可以從打開(kāi)Google Play Store看到抵拘,會(huì)出現(xiàn)提示被禁用的彈窗)
(手動(dòng)禁用的方式可以是點(diǎn)擊在下載過(guò)程的通知欄信息,進(jìn)入設(shè)置頁(yè)面點(diǎn)擊“禁用”按鈕)
解決方法:
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);
}
}
如果不可用型豁,則打開(kāi) 系統(tǒng)下載管理器 設(shè)置頁(yè)面 或者 打開(kāi)系統(tǒng)設(shè)置僵蛛,讓用戶設(shè)置
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的斷點(diǎn)續(xù)傳是怎么觸發(fā)的迎变?
比如斷開(kāi)網(wǎng)絡(luò)充尉,然后恢復(fù)網(wǎng)絡(luò),DownloaderManger可以繼續(xù)下載衣形,這是怎么觸發(fā)的驼侠?
從源碼可以看到,實(shí)現(xiàn)方式是監(jiān)聽(tīng)網(wǎng)絡(luò)變化廣播谆吴,實(shí)現(xiàn)類是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