DownloadManager被用来在安卓平台进行下载。典型用法如下:
DownloadManager downloadManager = (DownloadManager)mContext.getSystemService(mContext.DOWNLOAD_SERVICE);
downloadManager.enqueue(new DownloadManager.Request(Uri.parse(uri))
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
.setDestinationInExternalFilesDir(mContext, "", fileName)
.setTitle("title"));
首先创建DownloadManager.Request
,然后调用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;
}
先将request转换成ContentValues,然后插入到content://downloads/my_downloads
中。
request.toContentValues:
ContentValues toContentValues(String packageName) {
ContentValues values = new ContentValues();
assert mUri != null;
values.put(Downloads.Impl.COLUMN_URI, mUri.toString());
values.put(Downloads.Impl.COLUMN_IS_PUBLIC_API, true);
values.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, packageName);
if (mDestinationUri != null) {
values.put(Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.DESTINATION_FILE_URI);
values.put(Downloads.Impl.COLUMN_FILE_NAME_HINT, mDestinationUri.toString());
} else {
values.put(Downloads.Impl.COLUMN_DESTINATION,
(this.mUseSystemCache) ?
Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION :
Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
}
// is the file supposed to be media-scannable?
values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, (mScannable) ? SCANNABLE_VALUE_YES :
SCANNABLE_VALUE_NO);
if (!mRequestHeaders.isEmpty()) {
encodeHttpHeaders(values);
}
putIfNonNull(values, Downloads.Impl.COLUMN_TITLE, mTitle);
putIfNonNull(values, Downloads.Impl.COLUMN_DESCRIPTION, mDescription);
putIfNonNull(values, Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
values.put(Downloads.Impl.COLUMN_VISIBILITY, mNotificationVisibility);
values.put(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, mAllowedNetworkTypes);
values.put(Downloads.Impl.COLUMN_ALLOW_ROAMING, mRoamingAllowed);
values.put(Downloads.Impl.COLUMN_ALLOW_METERED, mMeteredAllowed);
values.put(Downloads.Impl.COLUMN_FLAGS, mFlags);
values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, mIsVisibleInDownloadsUi);
return values;
}
DownloadProvider
的authority是downloads,最终插入是在DownloadProvider
<provider android:name=".DownloadProvider" 69 android:authorities="downloads" android:exported="true">
DownloadProvider.insert
@Override
public Uri insert(final Uri uri, final ContentValues values) {
//...验证values中的值
long rowID = db.insert(DB_TABLE, null, filteredValues);
if (rowID == -1) {
Log.d(Constants.TAG, "couldn't insert into downloads database");
return null;
}
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);
}
先将contentvalue插入数据库,然后调用Helpers.scheduleJob(getContext(), rowID);
去安排一次下载
Helpers.scheduleJob
:
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();
}
}
先将downloadId对应的信息保存在DownloadInfo中。然后调用scheduleJob(Context context, DownloadInfo info)
public static boolean scheduleJob(Context context, DownloadInfo info) {
if (info == null) return false;
final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
// Tear down any existing job for this download
final int jobId = (int) info.mId;
scheduler.cancel(jobId);
.......
final JobInfo.Builder builder = new JobInfo.Builder(jobId,
new ComponentName(context, DownloadJobService.class));
.....
scheduler.scheduleAsPackage(builder.build(), packageName, UserHandle.myUserId(), TAG);
return true;
}
在JobScheduler中提交一个Job。Job的执行条件由DownloadManager.Request指定。
Job的条件满足后,会调用DownloadJobService.onStartJob
@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);
final DownloadThread thread;
synchronized (mActiveThreads) {
thread = new DownloadThread(this, params, info);
mActiveThreads.put(id, thread);
}
thread.start();
return true;
}
创建一个DownloadThread,然后启动
DownloadThread.run
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// Skip when download already marked as finished; this download was
// probably started again while racing with UpdateThread.
if (mInfo.queryDownloadStatus() == Downloads.Impl.STATUS_SUCCESS) {
logDebug("Already finished; skipping");
return;
}
try {
mInfoDelta.mStatus = STATUS_RUNNING;
mInfoDelta.writeToDatabase();
executeDownload();
mInfoDelta.mStatus = STATUS_SUCCESS;
// If we just finished a chunked file, record total size
if (mInfoDelta.mTotalBytes == -1) {
mInfoDelta.mTotalBytes = mInfoDelta.mCurrentBytes;
}
} catch (StopRequestException e) {
mInfoDelta.mStatus = e.getFinalStatus();
mInfoDelta.mErrorMsg = e.getMessage();
logWarning("Stop requested with status "
+ Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": "
+ mInfoDelta.mErrorMsg);
// Nobody below our level should request retries, since we handle
// failure counts at this level.
if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) {
throw new IllegalStateException("Execution should always throw final error codes");
}
// Some errors should be retryable, unless we fail too many times.
if (isStatusRetryable(mInfoDelta.mStatus)) {
if (mMadeProgress) {
mInfoDelta.mNumFailed = 1;
} else {
mInfoDelta.mNumFailed += 1;
}
if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) {
final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid,
mIgnoreBlocked);
if (info != null && info.getType() == mNetworkType && info.isConnected()) {
// Underlying network is still intact, use normal backoff
mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY;
} else {
// Network changed, retry on any next available
mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK;
}
if ((mInfoDelta.mETag == null && mMadeProgress)
|| DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
// However, if we wrote data and have no ETag to verify
// contents against later, we can't actually resume.
mInfoDelta.mStatus = STATUS_CANNOT_RESUME;
}
}
}
// If we're waiting for a network that must be unmetered, our status
// is actually queued so we show relevant notifications
if (mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK
&& !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) {
mInfoDelta.mStatus = STATUS_QUEUED_FOR_WIFI;
}
} catch (Throwable t) {
mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR;
mInfoDelta.mErrorMsg = t.toString();
logError("Failed: " + mInfoDelta.mErrorMsg, t);
} finally {
finalizeDestination();
mInfoDelta.writeToDatabase();
}
mJobService.jobFinishedInternal(mParams, false);
}
把线程优先级置为后台,然后将status置为STATUS_RUNNING
,然后执行executeDownload
,如果下载成功,将status置为STATUS_SUCCESS
。如果有异常抛出,先判断能否retry,如果行,则retry,否则就走到finally,先调用finalizeDestination
,然后再将DownloadInfo写会数据库。
executeDownload
private void executeDownload() throws StopRequestException {
final boolean resuming = mInfoDelta.mCurrentBytes != 0;
URL url;
try {
url = new URL(mInfoDelta.mUri);
} catch (MalformedURLException e) {
throw new StopRequestException(STATUS_BAD_REQUEST, e);
}
while (redirectionCount++ < Constants.MAX_REDIRECTS) {
// Open connection and follow any redirects until we have a useful
// response with body.
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) mNetwork.openConnection(url);
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);
return;
创建一个HttpURLConnection进行连接,如果返回200, 则调用parseOkHeaders
private void parseOkHeaders(HttpURLConnection conn) throws StopRequestException {
if (mInfoDelta.mFileName == null) {
final String contentDisposition = conn.getHeaderField("Content-Disposition");
final String contentLocation = conn.getHeaderField("Content-Location");
try {
mInfoDelta.mFileName = Helpers.generateSaveFile(mContext, mInfoDelta.mUri,
mInfo.mHint, contentDisposition, contentLocation, mInfoDelta.mMimeType,
mInfo.mDestination);
} catch (IOException e) {
throw new StopRequestException(
Downloads.Impl.STATUS_FILE_ERROR, "Failed to generate filename: " + e);
}
}
if (mInfoDelta.mMimeType == null) {
mInfoDelta.mMimeType = Intent.normalizeMimeType(conn.getContentType());
}
final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
if (transferEncoding == null) {
mInfoDelta.mTotalBytes = getHeaderFieldLong(conn, "Content-Length", -1);
} else {
mInfoDelta.mTotalBytes = -1;
}
mInfoDelta.mETag = conn.getHeaderField("ETag");
mInfoDelta.writeToDatabaseOrThrow();
}
从response header中确定下载文件的名称,mimetype,etag,contentlength。
static String generateSaveFile(Context context, String url, String hint,
String contentDisposition, String contentLocation, String mimeType, int destination)
throws IOException {
final File parent;
final File[] parentTest;
String name = null;
if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
final File file = new File(Uri.parse(hint).getPath());
parent = file.getParentFile().getAbsoluteFile();
parentTest = new File[] { parent };
name = file.getName();
}
// Ensure target directories are ready
for (File test : parentTest) {
if (!(test.isDirectory() || test.mkdirs())) {
throw new IOException("Failed to create parent for " + test);
}
}
final String prefix;
final String suffix;
final int dotIndex = name.lastIndexOf('.');
final boolean missingExtension = dotIndex < 0;
if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
// Destination is explicitly set - do not change the extension
if (missingExtension) {
prefix = name;
suffix = "";
} else {
prefix = name.substring(0, dotIndex);
suffix = name.substring(dotIndex);
}
}
synchronized (sUniqueLock) {
name = generateAvailableFilenameLocked(parentTest, prefix, suffix);
// Claim this filename inside lock to prevent other threads from
// clobbering us. We're not paranoid enough to use O_EXCL.
final File file = new File(parent, name);
file.createNewFile();
return file.getAbsolutePath();
}
}
根据Request中设定的文件名做一些校验,如果文件已经存在,则在后缀名之前加入随机数字,如
test.apk已经存在,则可能生成test-1.apk,然后创建文件
transferData将文件下载到本地文件中
finally中会调用finalizeDestination
private void finalizeDestination() {
if (Downloads.Impl.isStatusError(mInfoDelta.mStatus)) {
// When error, free up any disk space
try {
final ParcelFileDescriptor target = mContext.getContentResolver()
.openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
try {
Os.ftruncate(target.getFileDescriptor(), 0);
} catch (ErrnoException ignored) {
} finally {
IoUtils.closeQuietly(target);
}
} catch (FileNotFoundException ignored) {
}
// Delete if local file
if (mInfoDelta.mFileName != null) {
new File(mInfoDelta.mFileName).delete();
mInfoDelta.mFileName = null;
}
} else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) {
// When success, open access if local file
if (mInfoDelta.mFileName != null) {
if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) {
try {
// Move into final resting place, if needed
final File before = new File(mInfoDelta.mFileName);
final File beforeDir = Helpers.getRunningDestinationDirectory(
mContext, mInfo.mDestination);
final File afterDir = Helpers.getSuccessDestinationDirectory(
mContext, mInfo.mDestination);
if (!beforeDir.equals(afterDir)
&& before.getParentFile().equals(beforeDir)) {
final File after = new File(afterDir, before.getName());
if (before.renameTo(after)) {
mInfoDelta.mFileName = after.getAbsolutePath();
}
}
} catch (IOException ignored) {
}
}
}
}
}
如果下载失败会将文件删除,如果成功而且没有设置本地文件,则做一下文件的拷贝。
下载完成的通知:
DownloadThread中如果下载成功,会将DownloadInfo的status置为SUCCESS(200),然后在update provider的时候,如果发现状态是complete,则会发送广播
DownloadProvider.update
@Override
public int update(final Uri uri, final ContentValues values,
final String where, final String[] whereArgs) {
.......
if (isCompleting) {
info.sendIntentIfRequested();
}
}
DownloadInfo.sendIntentIfRequested
public void sendIntentIfRequested() {
if (mPackage == null) {
return;
}
Intent intent;
if (mIsPublicApi) {
intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
intent.setPackage(mPackage); //指定向发起download的package中broadcast receiver发送
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, mId);
}
mSystemFacade.sendBroadcast(intent);
}
下载进度的更新
在transferData中,如果有流更新了数据,会调用updateProgress
private void transferData(InputStream in, OutputStream out, FileDescriptor outFd)
throws StopRequestException {
.......
out.write(buffer, 0, len);
mMadeProgress = true;
mInfoDelta.mCurrentBytes += len;
updateProgress(outFd);
........
}
updateProgress:
private void updateProgress(FileDescriptor outFd) throws IOException, StopRequestException {
final long now = SystemClock.elapsedRealtime();
final long currentBytes = mInfoDelta.mCurrentBytes;
final long sampleDelta = now - mSpeedSampleStart;
if (sampleDelta > 500) {
final long sampleSpeed = ((currentBytes - mSpeedSampleBytes) * 1000)
/ sampleDelta;
if (mSpeed == 0) {
mSpeed = sampleSpeed;
} else {
mSpeed = ((mSpeed * 3) + sampleSpeed) / 4;
}
// Only notify once we have a full sample window
if (mSpeedSampleStart != 0) {
mNotifier.notifyDownloadSpeed(mId, mSpeed);
}
mSpeedSampleStart = now;
mSpeedSampleBytes = currentBytes;
}
final long bytesDelta = currentBytes - mLastUpdateBytes;
final long timeDelta = now - mLastUpdateTime;
if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) {
// fsync() to ensure that current progress has been flushed to disk,
// so we can always resume based on latest database information.
outFd.sync();
mInfoDelta.writeToDatabaseOrThrow();
mLastUpdateBytes = currentBytes;
mLastUpdateTime = now;
}
}
算出速度,然后更新到通知栏,如果byteDelta大于65536 and timeDelta > 2s,则flush数据到磁盘,然后update DownloadProvider
,DownloadProvider
udpate方法的最后会通知ContentObserver
notifyContentChanged(uri, match);
private void notifyContentChanged(final Uri uri, int uriMatch) {
Long downloadId = null;
if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
downloadId = Long.parseLong(getDownloadIdFromUri(uri));
}
for (Uri uriToNotify : BASE_URIS) {
if (downloadId != null) {
uriToNotify = ContentUris.withAppendedId(uriToNotify, downloadId);
}
getContext().getContentResolver().notifyChange(uriToNotify, null);
}
}
先将downloadid提取出来,然后通知"content://downloads/my_downloads/id"和"content://downloads/all_downloads/id"这两个地址,所以要获得下载进度,只要设置ContentObserver监听"content://downloads/my_downloads“”
getContentResolver().registerContentObserver("content://downloads/my_downloads", true,
downloadObserver);
public class MyContentObserver extends extends ContentObserver {
@Override
public void onChange(boolean selfChange, Uri uri) {
int downloadId = uri.getPathSegments().get(1)
getBytesAndStatus(downloadId);
}
public int[] getBytesAndStatus(long downloadId) {
int[] bytesAndStatus = new int[] { -1, -1, 0 };
DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
Cursor c = null;
try {
c = downloadManager.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;
}
}