其实之前做过给浏览器设置中添加一项选择下载路径选项并实现功能(跟小米浏览器中的设置一样),但是并没有花时间总结,也没有存档,今天有同事问起,回头去找资料,都没有了,重新整理一下,就当做处女作发布了,水平有限,如有错误,欢迎指正。闲言少许
浏览器其他代码略过不谈,从点击下载开始看
Tab.java //有一个下载监听,调用mWebViewController.onDownloadStart
mDownloadListener = new BrowserDownloadListener() {
public void onDownloadStart(String url, String userAgent,
String contentDisposition, String mimetype, String referer,
long contentLength) {
Log.d(TAG,"mark "+new Throwable().getStackTrace()[0].getClassName()+"==="+new Throwable().getStackTrace()[0].getMethodName());
mWebViewController.onDownloadStart(Tab.this, url, userAgent, contentDisposition,
mimetype, referer, contentLength);
}
};
中间经过一系列传递,最终调用到DownloadHandler.java中的onDownloadStart方法
onDownloadStart ---> onDownloadStartNoStream
static void onDownloadStartNoStream(Activity activity,
String url, String userAgent, String contentDisposition,
String mimetype, String referer, boolean privateBrowsing) {
Log.d(TAG,"mark "+new Throwable().getStackTrace()[0].getClassName()+"==="+new Throwable().getStackTrace()[0].getMethodName());
String filename = URLUtil.guessFileName(url,
contentDisposition, mimetype);
// Check to see if we have an SDCard
String status = Environment.getExternalStorageState();
if (!status.equals(Environment.MEDIA_MOUNTED)) {
int title;
String msg;
// Check to see if the SDCard is busy, same as the music app
if (status.equals(Environment.MEDIA_SHARED)) {
msg = activity.getString(R.string.download_sdcard_busy_dlg_msg);
title = R.string.download_sdcard_busy_dlg_title;
} else {
msg = activity.getString(R.string.download_no_sdcard_dlg_msg, filename);
title = R.string.download_no_sdcard_dlg_title;
}
new AlertDialog.Builder(activity)
.setTitle(title)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setMessage(msg)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
// java.net.URI is a lot stricter than KURL so we have to encode some
// extra characters. Fix for b 2538060 and b 1634719
WebAddress webAddress;
try {
webAddress = new WebAddress(url);
webAddress.setPath(encodePath(webAddress.getPath()));
} catch (Exception e) {
// This only happens for very bad urls, we want to chatch the
// exception here
Log.e(LOGTAG, "Exception trying to parse url:" + url);
return;
}
String addressString = webAddress.toString();
Uri uri = Uri.parse(addressString);
final DownloadManager.Request request;
try {
request = new DownloadManager.Request(uri);
} catch (IllegalArgumentException e) {
Toast.makeText(activity, R.string.cannot_download, Toast.LENGTH_SHORT).show();
return;
}
request.setMimeType(mimetype);
// set downloaded file destination to /sdcard/Download.
// or, should it be set to one of several Environment.DIRECTORY* dirs depending on mimetype?
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename); //设置下载路径
// let this downloaded file be scanned by MediaScanner - so that it can
// show up in Gallery app, for example.
request.allowScanningByMediaScanner();
request.setDescription(webAddress.getHost());
// XXX: Have to use the old url since the cookies were stored using the
// old percent-encoded url.
String cookies = CookieManager.getInstance().getCookie(url, privateBrowsing);
request.addRequestHeader("cookie", cookies);
request.addRequestHeader("User-Agent", userAgent);
request.addRequestHeader("Referer", referer);
request.setNotificationVisibility(
DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if (mimetype == null) {
if (TextUtils.isEmpty(addressString)) {
return;
}
// We must have long pressed on a link or image to download it. We
// are not sure of the mimetype in this case, so do a head request
new FetchUrlMimeType(activity, request, addressString, cookies,
userAgent).start();
} else {
final DownloadManager manager
= (DownloadManager) activity.getSystemService(Context.DOWNLOAD_SERVICE);
new Thread("Browser download") {
public void run() {
manager.enqueue(request);
Log.d(TAG,"mark "+new Throwable().getStackTrace()[0].getClassName()+"==="+new Throwable().getStackTrace()[0].getMethodName());
}
}.start();
}
Toast.makeText(activity, R.string.download_pending, Toast.LENGTH_SHORT)
.show();
}
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename); 跟踪代码是DownloadManager.java调用Environment.java里面的方法设置默认下载路径,设置完之后保存在request里面,然后获取系统的DownloadManager,创建线程,调用DownloadManager里面的manager.enqueue(request),并且将设置好的request传递过去。
下面来看enqueue,DownloadManager.java
/**
* Enqueue a new download. The download will start automatically once the download manager is
* ready to execute it and connectivity is available.
*
* @param request the parameters specifying this download
* @return an ID for the download, unique across the system. This ID is used to make future
* calls related to this download.
*/
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;
}
做了2个事情,1:根据request得到ContentValues;2:将改ContentValues插入到mResolver;
第一个事情没什么好说的,看第二个
mResolver的类型是ContentResolver,在DownloadManager构造函数中初始化;DownloadManager这个服务的创建在ContextImpl.java
registerService(DOWNLOAD_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
return new DownloadManager(ctx.getContentResolver(), ctx.getPackageName());
}});
这个插入动作insert 经过一系列跟踪(过程不表),最终调用的是DownloadProvider.java中的insert,代码很长,大致意思为把数据存入sqlite里面,通知ContentChanged(这个会被DownloadService.java中的DownloadManagerContentObserver检测到),启动DownloadService。
public Uri insert(final Uri uri, final ContentValues values) {
Log.d(TAG,"mark "+new Throwable().getStackTrace()[0].getClassName()+"==="+new Throwable().getStackTrace()[0].getMethodName());
Context mcontext = getContext();
Intent intent=new Intent(mcontext,com.android.providers.downloads.MyFileManager.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mcontext.startActivity(intent);
String str="";
SharedPreferences sharedPreferences = getContext().getSharedPreferences("mark_sp", Context.MODE_WORLD_READABLE);
str=sharedPreferences.getString("mark_download_file_route","");
Log.d(TAG,"file_route="+str);
checkInsertPermissions(values);
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
// note we disallow inserting into ALL_DOWNLOADS
int match = sURIMatcher.match(uri);
if (match != MY_DOWNLOADS) {
Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
}
// copy some of the input values as it
ContentValues filteredValues = new ContentValues();
copyString(Downloads.Impl.COLUMN_URI, values, filteredValues);
copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues);
copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues);
copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues);
copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues);
boolean isPublicApi =
values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE;
// validate the destination column
Integer dest = values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION);
if (dest != null) {
if (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
!= PackageManager.PERMISSION_GRANTED
&& (dest == Downloads.Impl.DESTINATION_CACHE_PARTITION
|| dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
|| dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION)) {
throw new SecurityException("setting destination to : " + dest +
" not allowed, unless PERMISSION_ACCESS_ADVANCED is granted");
}
// for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
// switch to non-purgeable download
boolean hasNonPurgeablePermission =
getContext().checkCallingPermission(
Downloads.Impl.PERMISSION_CACHE_NON_PURGEABLE)
== PackageManager.PERMISSION_GRANTED;
if (isPublicApi && dest == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
&& hasNonPurgeablePermission) {
dest = Downloads.Impl.DESTINATION_CACHE_PARTITION;
}
if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
getContext().enforcePermission(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
Binder.getCallingPid(), Binder.getCallingUid(),
"need WRITE_EXTERNAL_STORAGE permission to use DESTINATION_FILE_URI");
checkFileUriDestination(values);
} else if (dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
getContext().enforcePermission(
android.Manifest.permission.ACCESS_CACHE_FILESYSTEM,
Binder.getCallingPid(), Binder.getCallingUid(),
"need ACCESS_CACHE_FILESYSTEM permission to use system cache");
}
filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
}
// validate the visibility column
Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY);
if (vis == null) {
if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
} else {
filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY,
Downloads.Impl.VISIBILITY_HIDDEN);
}
} else {
filteredValues.put(Downloads.Impl.COLUMN_VISIBILITY, vis);
}
// copy the control column as is
copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
/*
* requests coming from
* DownloadManager.addCompletedDownload(String, String, String,
* boolean, String, String, long) need special treatment
*/
if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
// these requests always are marked as 'completed'
filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_SUCCESS);
filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES,
values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
copyInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED, values, filteredValues);
copyString(Downloads.Impl._DATA, values, filteredValues);
copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues);
} else {
filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
}
// set lastupdate to current time
long lastMod = mSystemFacade.currentTimeMillis();
filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, lastMod);
// use packagename of the caller to set the notification columns
String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
if (pckg != null && (clazz != null || isPublicApi)) {
int uid = Binder.getCallingUid();
try {
if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg);
if (clazz != null) {
filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz);
}
}
} catch (PackageManager.NameNotFoundException ex) {
/* ignored for now */
}
}
// copy some more columns as is
copyString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
copyString(Downloads.Impl.COLUMN_COOKIE_DATA, values, filteredValues);
copyString(Downloads.Impl.COLUMN_USER_AGENT, values, filteredValues);
copyString(Downloads.Impl.COLUMN_REFERER, values, filteredValues);
// UID, PID columns
if (getContext().checkCallingPermission(Downloads.Impl.PERMISSION_ACCESS_ADVANCED)
== PackageManager.PERMISSION_GRANTED) {
copyInteger(Downloads.Impl.COLUMN_OTHER_UID, values, filteredValues);
}
filteredValues.put(Constants.UID, Binder.getCallingUid());
if (Binder.getCallingUid() == 0) {
copyInteger(Constants.UID, values, filteredValues);
}
// copy some more columns as is
copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, "");
copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, "");
// is_visible_in_downloads_ui column
if (values.containsKey(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
copyBoolean(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues);
} else {
// by default, make external downloads visible in the UI
boolean isExternal = (dest == null || dest == Downloads.Impl.DESTINATION_EXTERNAL);
filteredValues.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isExternal);
}
// public api requests and networktypes/roaming columns
if (isPublicApi) {
copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
copyBoolean(Downloads.Impl.COLUMN_ALLOW_METERED, values, filteredValues);
}
if (Constants.LOGVV) {
Log.v(Constants.TAG, "initiating download with UID "
+ filteredValues.getAsInteger(Constants.UID));
if (filteredValues.containsKey(Downloads.Impl.COLUMN_OTHER_UID)) {
Log.v(Constants.TAG, "other UID " +
filteredValues.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID));
}
}
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);
notifyContentChanged(uri, match);
// Always start service to handle notifications and/or scanning
final Context context = getContext();
Log.d(TAG,"mark before startService, "+new Throwable().getStackTrace()[0].getClassName()+"==="+new Throwable().getStackTrace()[0].getMethodName());
context.startService(new Intent(context, DownloadService.class));
return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}
下面来看DownloadService.java
public void onCreate() {
super.onCreate();
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Service onCreate");
}
if (mSystemFacade == null) {
mSystemFacade = new RealSystemFacade(this);
}
mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
mStorageManager = new StorageManager(this);
mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
mUpdateThread.start();
mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
mScanner = new DownloadScanner(this);
mNotifier = new DownloadNotifier(this);
mNotifier.cancelAll();
mObserver = new DownloadManagerContentObserver();
getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
true, mObserver);
}
内容改变最终会调用下面的这个消息处理(过程自己追一下不提了),其中调用了一个方法updateLocked();
private Handler.Callback mUpdateCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
final int startId = msg.arg1;
if (DEBUG_LIFECYCLE) Log.v(TAG, "Updating for startId " + startId);
// Since database is current source of truth, our "active" status
// depends on database state. We always get one final update pass
// once the real actions have finished and persisted their state.
// TODO: switch to asking real tasks to derive active state
// TODO: handle media scanner timeouts
final boolean isActive;
synchronized (mDownloads) {
isActive = updateLocked();
}
if (msg.what == MSG_FINAL_UPDATE) {
// Dump thread stacks belonging to pool
for (Map.Entry<Thread, StackTraceElement[]> entry :
Thread.getAllStackTraces().entrySet()) {
if (entry.getKey().getName().startsWith("pool")) {
Log.d(TAG, entry.getKey() + ": " + Arrays.toString(entry.getValue()));
}
}
// Dump speed and update details
mNotifier.dumpSpeeds();
Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive
+ "; someone didn't update correctly.");
}
if (isActive) {
// Still doing useful work, keep service alive. These active
// tasks will trigger another update pass when they're finished.
// Enqueue delayed update pass to catch finished operations that
// didn't trigger an update pass; these are bugs.
enqueueFinalUpdate();
} else {
// No active tasks, and any pending update messages can be
// ignored, since any updates important enough to initiate tasks
// will always be delivered with a new startId.
if (stopSelfResult(startId)) {
if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped");
getContentResolver().unregisterContentObserver(mObserver);
mScanner.shutdown();
mUpdateThread.quit();
}
}
return true;
}
};
updateLocked();这个方法 从ContentResolver中构建DownloadInfo(insertDownloadLocked ---> reader.newDownloadInfo ---> new DownloadInfo),并且调用info.startDownloadIfReady
private boolean updateLocked() {
Log.d(TAG,"mark "+new Throwable().getStackTrace()[0].getClassName()+"==="+new Throwable().getStackTrace()[0].getMethodName());
final long now = mSystemFacade.currentTimeMillis();
boolean isActive = false;
long nextActionMillis = Long.MAX_VALUE;
final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet());
final ContentResolver resolver = getContentResolver();
final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
null, null, null, null);
try {
final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
while (cursor.moveToNext()) {
final long id = cursor.getLong(idColumn);
staleIds.remove(id);
DownloadInfo info = mDownloads.get(id);
if (info != null) {
updateDownload(reader, info, now);
} else {
info = insertDownloadLocked(reader, now);
}
if (info.mDeleted) {
// Delete download if requested, but only after cleaning up
if (!TextUtils.isEmpty(info.mMediaProviderUri)) {
resolver.delete(Uri.parse(info.mMediaProviderUri), null, null);
}
deleteFileIfExists(info.mFileName);
resolver.delete(info.getAllDownloadsUri(), null, null);
} else {
// Kick off download task if ready
Log.d(TAG,"in updateLocked before info.startDownloadIfReady");
final boolean activeDownload = info.startDownloadIfReady(mExecutor);
// Kick off media scan if completed
final boolean activeScan = info.startScanIfReady(mScanner);
if (DEBUG_LIFECYCLE && (activeDownload || activeScan)) {
Log.v(TAG, "Download " + info.mId + ": activeDownload=" + activeDownload
+ ", activeScan=" + activeScan);
}
isActive |= activeDownload;
isActive |= activeScan;
}
// Keep track of nearest next action
nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis);
}
} finally {
cursor.close();
}
// Clean up stale downloads that disappeared
for (Long id : staleIds) {
deleteDownloadLocked(id);
}
// Update notifications visible to user
mNotifier.updateWith(mDownloads.values());
// Set alarm when next action is in future. It's okay if the service
// continues to run in meantime, since it will kick off an update pass.
if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) {
if (Constants.LOGV) {
Log.v(TAG, "scheduling start in " + nextActionMillis + "ms");
}
final Intent intent = new Intent(Constants.ACTION_RETRY);
intent.setClass(this, DownloadReceiver.class);
mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis,
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT));
}
return isActive;
}
在DownloadInfo.java中的方法startDownloadIfReady,会检测是否准备好了下载,准备好了就创建DownloadThread,并且启动该线程
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);
}
Log.d(TAG,"mark "+new Throwable().getStackTrace()[0].getClassName()+"==="+new Throwable().getStackTrace()[0].getMethodName());
mTask = new DownloadThread(
mContext, mSystemFacade, this, mStorageManager, mNotifier);
mSubmittedTask = executor.submit(mTask);
}
return isReady;
}
}
DownloadThread.java中的run方法,run --> runInternal ---> executeDownload
public void run() {
Log.d(TAG,"mark "+new Throwable().getStackTrace()[0].getClassName()+"==="+new Throwable().getStackTrace()[0].getMethodName());
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
runInternal();
} finally {
mNotifier.notifyDownloadSpeed(mInfo.mId, 0);
}
}
private void runInternal() {
// Skip when download already marked as finished; this download was
// probably started again while racing with UpdateThread.
if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId)
== Downloads.Impl.STATUS_SUCCESS) {
Log.d(TAG, "Download " + mInfo.mId + " already finished; skipping");
return;
}
State state = new State(mInfo);
PowerManager.WakeLock wakeLock = null;
int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
int numFailed = mInfo.mNumFailed;
String errorMsg = null;
final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
try {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
wakeLock.setWorkSource(new WorkSource(mInfo.mUid));
wakeLock.acquire();
// while performing download, register for rules updates
netPolicy.registerListener(mPolicyListener);
Log.i(Constants.TAG, "Download " + mInfo.mId + " starting");
// Remember which network this download started on; used to
// determine if errors were due to network changes.
final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
if (info != null) {
state.mNetworkType = info.getType();
}
// Network traffic on this thread should be counted against the
// requesting UID, and is tagged with well-known value.
TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
TrafficStats.setThreadStatsUid(mInfo.mUid);
try {
// TODO: migrate URL sanity checking into client side of API
state.mUrl = new URL(state.mRequestUri);
} catch (MalformedURLException e) {
throw new StopRequestException(STATUS_BAD_REQUEST, e);
}
executeDownload(state);
finalizeDestinationFile(state);
finalStatus = Downloads.Impl.STATUS_SUCCESS;
} catch (StopRequestException error) {
// remove the cause before printing, in case it contains PII
errorMsg = error.getMessage();
String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
Log.w(Constants.TAG, msg);
if (Constants.LOGV) {
Log.w(Constants.TAG, msg, error);
}
finalStatus = error.getFinalStatus();
// Nobody below our level should request retries, since we handle
// failure counts at this level.
if (finalStatus == 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(finalStatus)) {
if (state.mGotData) {
numFailed = 1;
} else {
numFailed += 1;
}
if (numFailed < Constants.MAX_RETRIES) {
final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
if (info != null && info.getType() == state.mNetworkType
&& info.isConnected()) {
// Underlying network is still intact, use normal backoff
finalStatus = STATUS_WAITING_TO_RETRY;
} else {
// Network changed, retry on any next available
finalStatus = STATUS_WAITING_FOR_NETWORK;
}
}
}
// fall through to finally block
} catch (Throwable ex) {
errorMsg = ex.getMessage();
String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
Log.w(Constants.TAG, msg, ex);
finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
// falls through to the code that reports an error
} finally {
if (finalStatus == STATUS_SUCCESS) {
TrafficStats.incrementOperationCount(1);
}
TrafficStats.clearThreadStatsTag();
TrafficStats.clearThreadStatsUid();
cleanupDestination(state, finalStatus);
notifyDownloadCompleted(state, finalStatus, errorMsg, numFailed);
Log.i(Constants.TAG, "Download " + mInfo.mId + " finished with status "
+ Downloads.Impl.statusToString(finalStatus));
netPolicy.unregisterListener(mPolicyListener);
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
}
mStorageManager.incrementNumDownloadsSoFar();
执行下载
private void executeDownload(State state) throws StopRequestException {
state.resetBeforeExecute();
setupDestinationFile(state);
// skip when already finished; remove after fixing race in 5217390
if (state.mCurrentBytes == state.mTotalBytes) {
Log.i(Constants.TAG, "Skipping initiating request for download " +
mInfo.mId + "; already completed");
return;
}
while (state.mRedirectionCount++ < Constants.MAX_REDIRECTS) {
// Open connection and follow any redirects until we have a useful
// response with body.
HttpURLConnection conn = null;
try {
checkConnectivity();
conn = (HttpURLConnection) state.mUrl.openConnection();
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(DEFAULT_TIMEOUT);
conn.setReadTimeout(DEFAULT_TIMEOUT);
addRequestHeaders(state, conn);
final int responseCode = conn.getResponseCode();
switch (responseCode) {
case HTTP_OK:
if (state.mContinuingDownload) {
throw new StopRequestException(
STATUS_CANNOT_RESUME, "Expected partial, but received OK");
}
processResponseHeaders(state, conn);
transferData(state, conn);
return;
case HTTP_PARTIAL:
if (!state.mContinuingDownload) {
throw new StopRequestException(
STATUS_CANNOT_RESUME, "Expected OK, but received partial");
}
transferData(state, conn);
return;
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
case HTTP_TEMP_REDIRECT:
final String location = conn.getHeaderField("Location");
state.mUrl = new URL(state.mUrl, location);
if (responseCode == HTTP_MOVED_PERM) {
// Push updated URL back to database
state.mRequestUri = state.mUrl.toString();
}
continue;
case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
throw new StopRequestException(
STATUS_CANNOT_RESUME, "Requested range not satisfiable");
case HTTP_UNAVAILABLE:
parseRetryAfterHeaders(state, conn);
throw new StopRequestException(
HTTP_UNAVAILABLE, conn.getResponseMessage());
case HTTP_INTERNAL_ERROR:
throw new StopRequestException(
HTTP_INTERNAL_ERROR, conn.getResponseMessage());
default:
StopRequestException.throwUnhandledHttpError(
responseCode, conn.getResponseMessage());
}
} catch (IOException e) {
// Trouble with low-level sockets
throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
} finally {
if (conn != null) conn.disconnect();
}
}
throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
}
上面就是整个过程,关于设置下载路径的,我做了2种方式
1. 在Broswer的设置里面添加路径设置选项,设置好路径后保存在SharePreference中,在下载的时候onDownloadStartNoStream ---> setDestinationInExternalPublicDir ,把设置的路径作为参数传递给DownloadManager,在DownloadManager的setDestinationInExternalPublicDir 方法中根据路径,直接new File,然后保存到相应的ContentResolver中,最后该ContentResolver会被写入sqlite,并且转换成为DownloadInfo。
该方法遇到的问题是一点击下载,浏览器就重启,查看设置的路径是被创建好了的,跟踪代码,添加打印log,找到原因为:
1. DownloadProvider.java中的insert方法,调用checkFileUriDestination(values);这个方法有个检测路径是什么开始的,如果不是在它规定的范围,就抛出异常,canonicalPath.startsWith(externalPath)
private void checkFileUriDestination(ContentValues values) {
String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
if (fileUri == null) {
throw new IllegalArgumentException(
"DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
}
Uri uri = Uri.parse(fileUri);
String scheme = uri.getScheme();
if (scheme == null || !scheme.equals("file")) {
throw new IllegalArgumentException("Not a file URI: " + uri);
}
final String path = uri.getPath();
if (path == null) {
throw new IllegalArgumentException("Invalid file URI: " + uri);
}
try {
final String canonicalPath = new File(path).getCanonicalPath();
final String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();
if (!canonicalPath.startsWith(externalPath)) {
throw new SecurityException("Destination must be on external storage: " + uri);
}
} catch (IOException e) {
throw new SecurityException("Problem resolving path: " + uri);
}
}
2. 在执行下载的时候DownloadThread.java executeDownload ---> processResponseHeaders(state, conn) ---> generateSaveFile ---> Helpers.java
---> storageManager.verifySpace 判断,如果不是Manifest.xml中设置的路径,就会抛出异常。
static String generateSaveFile(
Context context,
String url,
String hint,
String contentDisposition,
String contentLocation,
String mimeType,
int destination,
long contentLength,
StorageManager storageManager) throws StopRequestException {
if (contentLength < 0) {
contentLength = 0;
}
String path;
File base = null;
if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
path = Uri.parse(hint).getPath();
File destdir = new File(path).getParentFile();
destdir.mkdirs();
} else {
base = storageManager.locateDestinationDirectory(mimeType, destination,
contentLength);
path = chooseFilename(url, hint, contentDisposition, contentLocation,
destination);
}
storageManager.verifySpace(destination, path, contentLength);
if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) {
path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
}
path = getFullPath(path, mimeType, destination, base);
return path;
}
void verifySpace(int destination, String path, long length) throws StopRequestException {
resetBytesDownloadedSinceLastCheckOnSpace();
File dir = null;
if (Constants.LOGV) {
Log.i(Constants.TAG, "in verifySpace, destination: " + destination +
", path: " + path + ", length: " + length);
}
if (path == null) {
throw new IllegalArgumentException("path can't be null");
}
switch (destination) {
case Downloads.Impl.DESTINATION_CACHE_PARTITION:
case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING:
case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE:
dir = mDownloadDataDir;
break;
case Downloads.Impl.DESTINATION_EXTERNAL:
dir = mExternalStorageDir;
break;
case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION:
dir = mSystemCacheDir;
break;
case Downloads.Impl.DESTINATION_FILE_URI:
if (path.startsWith(mExternalStorageDir.getPath())) {
dir = mExternalStorageDir;
} else if (path.startsWith(mDownloadDataDir.getPath())) {
dir = mDownloadDataDir;
} else if (path.startsWith(mSystemCacheDir.getPath())) {
dir = mSystemCacheDir;
}
break;
}
if (dir == null) {
throw new IllegalStateException("invalid combination of destination: " + destination +
", path: " + path);
}
findSpace(dir, length, destination);
}
解决办法:注释掉相关的异常抛出,或者在Manifest.xml中添加相关路径前缀
<provider android:name=".DownloadProvider"
android:authorities="downloads" android:exported="true">
<!-- Anyone can access /my_downloads, the provider internally restricts access by UID for
these URIs -->
<path-permission android:pathPrefix="/my_downloads"
android:permission="android.permission.INTERNET"/>
<!-- to access /all_downloads, ACCESS_ALL_DOWNLOADS permission is required -->
<path-permission android:pathPrefix="/all_downloads"
android:permission="android.permission.ACCESS_ALL_DOWNLOADS"/>
<!-- Temporary, for backwards compatibility -->
<path-permission android:pathPrefix="/download"
android:permission="android.permission.INTERNET"/>
<!-- Apps with access to /all_downloads/... can grant permissions, allowing them to share
downloaded files with other viewers -->
<grant-uri-permission android:pathPrefix="/all_downloads/"/>
<!-- Apps with access to /my_downloads/... can grant permissions, allowing them to share
downloaded files with other viewers -->
<grant-uri-permission android:pathPrefix="/my_downloads/"/>
</provider>
有试过在DownloadProvider中添加如下功能:在点击下载的时候,弹出对话框,选择下载路径;
因为未存档,所以相关代码没贴出来,也没有时间重新写了,如果你对整个过程都了解了,相信实现起来应该也会很快的。
这是第一次发博文,很紧张,但是总得有第一次的吧,就跟解决处男处女问题一样,哈哈!
我的邮箱:741790551@qq.com