summaryrefslogtreecommitdiffstats
path: root/src/com/android/providers/downloads
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/providers/downloads')
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java266
-rw-r--r--src/com/android/providers/downloads/DownloadNotification.java216
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java570
-rw-r--r--src/com/android/providers/downloads/DownloadReceiver.java49
-rw-r--r--src/com/android/providers/downloads/DownloadService.java156
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java1345
-rw-r--r--src/com/android/providers/downloads/Helpers.java214
-rw-r--r--src/com/android/providers/downloads/RealSystemFacade.java102
-rw-r--r--src/com/android/providers/downloads/SystemFacade.java61
9 files changed, 1792 insertions, 1187 deletions
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index 81895439..4380059b 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -16,10 +16,21 @@
package com.android.providers.downloads;
+import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.DownloadManager;
import android.net.Uri;
import android.provider.Downloads;
+import android.provider.Downloads.Impl;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
/**
* Stores information about an individual download.
@@ -45,52 +56,124 @@ public class DownloadInfo {
public String mCookies;
public String mUserAgent;
public String mReferer;
- public int mTotalBytes;
- public int mCurrentBytes;
+ public long mTotalBytes;
+ public long mCurrentBytes;
public String mETag;
public boolean mMediaScanned;
+ public boolean mIsPublicApi;
+ public int mAllowedNetworkTypes;
+ public boolean mAllowRoaming;
+ public String mTitle;
+ public String mDescription;
+ public String mPausedReason;
public int mFuzz;
public volatile boolean mHasActiveThread;
- public DownloadInfo(int id, String uri, boolean noIntegrity,
- String hint, String fileName,
- String mimeType, int destination, int visibility, int control,
- int status, int numFailed, int retryAfter, int redirectCount, long lastMod,
- String pckg, String clazz, String extras, String cookies,
- String userAgent, String referer, int totalBytes, int currentBytes, String eTag,
- boolean mediaScanned) {
- mId = id;
- mUri = uri;
- mNoIntegrity = noIntegrity;
- mHint = hint;
- mFileName = fileName;
- mMimeType = mimeType;
- mDestination = destination;
- mVisibility = visibility;
- mControl = control;
- mStatus = status;
- mNumFailed = numFailed;
- mRetryAfter = retryAfter;
- mRedirectCount = redirectCount;
- mLastMod = lastMod;
- mPackage = pckg;
- mClass = clazz;
- mExtras = extras;
- mCookies = cookies;
- mUserAgent = userAgent;
- mReferer = referer;
- mTotalBytes = totalBytes;
- mCurrentBytes = currentBytes;
- mETag = eTag;
- mMediaScanned = mediaScanned;
- mFuzz = Helpers.sRandom.nextInt(1001);
+ private Map<String, String> mRequestHeaders = new HashMap<String, String>();
+ private SystemFacade mSystemFacade;
+ private Context mContext;
+
+ public DownloadInfo(Context context, SystemFacade systemFacade, Cursor cursor) {
+ mContext = context;
+ mSystemFacade = systemFacade;
+
+ int retryRedirect =
+ cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER_X_REDIRECT_COUNT));
+ mId = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl._ID));
+ mUri = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_URI));
+ mNoIntegrity = cursor.getInt(cursor.getColumnIndexOrThrow(
+ Downloads.Impl.COLUMN_NO_INTEGRITY)) == 1;
+ mHint = cursor.getString(cursor.getColumnIndexOrThrow(
+ Downloads.Impl.COLUMN_FILE_NAME_HINT));
+ mFileName = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl._DATA));
+ mMimeType = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
+ mDestination =
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESTINATION));
+ mVisibility = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_VISIBILITY));
+ mControl = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CONTROL));
+ mStatus = cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS));
+ mNumFailed = cursor.getInt(cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS));
+ mRetryAfter = retryRedirect & 0xfffffff;
+ mRedirectCount = retryRedirect >> 28;
+ mLastMod = cursor.getLong(cursor.getColumnIndexOrThrow(
+ Downloads.Impl.COLUMN_LAST_MODIFICATION));
+ mPackage = cursor.getString(cursor.getColumnIndexOrThrow(
+ Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE));
+ mClass = cursor.getString(cursor.getColumnIndexOrThrow(
+ Downloads.Impl.COLUMN_NOTIFICATION_CLASS));
+ mExtras = cursor.getString(cursor.getColumnIndexOrThrow(
+ Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS));
+ mCookies =
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_COOKIE_DATA));
+ mUserAgent =
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_USER_AGENT));
+ mReferer = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_REFERER));
+ mTotalBytes =
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES));
+ mCurrentBytes =
+ cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CURRENT_BYTES));
+ mETag = cursor.getString(cursor.getColumnIndexOrThrow(Constants.ETAG));
+ mMediaScanned = cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;
+ mIsPublicApi = cursor.getInt(
+ cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_IS_PUBLIC_API)) != 0;
+ mAllowedNetworkTypes = cursor.getInt(
+ cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES));
+ mAllowRoaming = cursor.getInt(
+ cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_ALLOW_ROAMING)) != 0;
+ mTitle = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TITLE));
+ mDescription =
+ cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESCRIPTION));
+ mFuzz = Helpers.sRandom.nextInt(1001);
+
+ readRequestHeaders(mId);
+ }
+
+ private void readRequestHeaders(long downloadId) {
+ Uri headerUri = Downloads.Impl.CONTENT_URI.buildUpon()
+ .appendPath(Long.toString(downloadId))
+ .appendPath(Downloads.Impl.RequestHeaders.URI_SEGMENT).build();
+ Cursor cursor = mContext.getContentResolver().query(headerUri, null, null, null, null);
+ try {
+ int headerIndex =
+ cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_HEADER);
+ int valueIndex =
+ cursor.getColumnIndexOrThrow(Downloads.Impl.RequestHeaders.COLUMN_VALUE);
+ for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+ mRequestHeaders.put(cursor.getString(headerIndex), cursor.getString(valueIndex));
+ }
+ } finally {
+ cursor.close();
+ }
+
+ if (mCookies != null) {
+ mRequestHeaders.put("Cookie", mCookies);
+ }
+ if (mReferer != null) {
+ mRequestHeaders.put("Referer", mReferer);
+ }
}
- public void sendIntentIfRequested(Uri contentUri, Context context) {
- if (mPackage != null && mClass != null) {
- Intent intent = new Intent(Downloads.Impl.ACTION_DOWNLOAD_COMPLETED);
+ public Map<String, String> getHeaders() {
+ return Collections.unmodifiableMap(mRequestHeaders);
+ }
+
+ public void sendIntentIfRequested(Uri contentUri) {
+ if (mPackage == null) {
+ return;
+ }
+
+ Intent intent;
+ if (mIsPublicApi) {
+ intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
+ intent.setPackage(mPackage);
+ intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, (long) mId);
+ } else { // legacy behavior
+ if (mClass == null) {
+ return;
+ }
+ intent = new Intent(Downloads.Impl.ACTION_DOWNLOAD_COMPLETED);
intent.setClassName(mPackage, mClass);
if (mExtras != null) {
intent.putExtra(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, mExtras);
@@ -99,8 +182,8 @@ public class DownloadInfo {
// applications would have an easier time spoofing download results by
// sending spoofed intents.
intent.setData(contentUri);
- context.sendBroadcast(intent);
}
+ mSystemFacade.sendBroadcast(intent);
}
/**
@@ -175,7 +258,7 @@ public class DownloadInfo {
if (mStatus == Downloads.Impl.STATUS_RUNNING_PAUSED) {
if (mNumFailed == 0) {
// download is waiting for network connectivity to return before it can resume
- return true;
+ return canUseNetwork();
}
if (restartTime() < now) {
// download was waiting for a delayed restart, and the delay has expired
@@ -202,14 +285,107 @@ public class DownloadInfo {
/**
* Returns whether this download is allowed to use the network.
*/
- public boolean canUseNetwork(boolean available, boolean roaming) {
- if (!available) {
+ public boolean canUseNetwork() {
+ Integer networkType = mSystemFacade.getActiveNetworkType();
+ if (networkType == null) {
return false;
}
- if (mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING) {
- return !roaming;
- } else {
- return true;
+ if (!isNetworkTypeAllowed(networkType)) {
+ return false;
+ }
+ if (!isRoamingAllowed() && mSystemFacade.isNetworkRoaming()) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isRoamingAllowed() {
+ if (mIsPublicApi) {
+ return mAllowRoaming;
+ } else { // legacy behavior
+ return mDestination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING;
+ }
+ }
+
+ /**
+ * Check if this download can proceed over the given network type.
+ * @param networkType a constant from ConnectivityManager.TYPE_*.
+ */
+ private boolean isNetworkTypeAllowed(int networkType) {
+ if (mIsPublicApi) {
+ int flag = translateNetworkTypeToApiFlag(networkType);
+ if ((flag & mAllowedNetworkTypes) == 0) {
+ return false;
+ }
+ }
+ if (!isSizeAllowedForNetwork(networkType)) {
+ mPausedReason = mContext.getResources().getString(
+ R.string.notification_need_wifi_for_size);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Translate a ConnectivityManager.TYPE_* constant to the corresponding
+ * DownloadManager.Request.NETWORK_* bit flag.
+ */
+ private int translateNetworkTypeToApiFlag(int networkType) {
+ switch (networkType) {
+ case ConnectivityManager.TYPE_MOBILE:
+ return DownloadManager.Request.NETWORK_MOBILE;
+
+ case ConnectivityManager.TYPE_WIFI:
+ return DownloadManager.Request.NETWORK_WIFI;
+
+ case ConnectivityManager.TYPE_WIMAX:
+ return DownloadManager.Request.NETWORK_WIMAX;
+
+ default:
+ return 0;
}
}
+
+ /**
+ * Check if the download's size prohibits it from running over the current network.
+ */
+ private boolean isSizeAllowedForNetwork(int networkType) {
+ if (mTotalBytes <= 0) {
+ return true; // we don't know the size yet
+ }
+ if (networkType == ConnectivityManager.TYPE_WIFI) {
+ return true; // anything goes over wifi
+ }
+ Long maxBytesOverMobile = mSystemFacade.getMaxBytesOverMobile();
+ if (maxBytesOverMobile == null) {
+ return true; // no limit
+ }
+ return mTotalBytes <= maxBytesOverMobile;
+ }
+
+ void start(long now) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "Service spawning thread to handle download " + mId);
+ }
+ if (mHasActiveThread) {
+ throw new IllegalStateException("Multiple threads on same download");
+ }
+ if (mStatus != Impl.STATUS_RUNNING) {
+ mStatus = Impl.STATUS_RUNNING;
+ ContentValues values = new ContentValues();
+ values.put(Impl.COLUMN_STATUS, mStatus);
+ mContext.getContentResolver().update(
+ ContentUris.withAppendedId(Impl.CONTENT_URI, mId),
+ values, null, null);
+ }
+ DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this);
+ mHasActiveThread = true;
+ mSystemFacade.startThread(downloader);
+ }
+
+ public boolean isOnCache() {
+ return (mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION
+ || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
+ || mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
+ }
}
diff --git a/src/com/android/providers/downloads/DownloadNotification.java b/src/com/android/providers/downloads/DownloadNotification.java
index e9c0d4e6..2c30644b 100644
--- a/src/com/android/providers/downloads/DownloadNotification.java
+++ b/src/com/android/providers/downloads/DownloadNotification.java
@@ -17,32 +17,33 @@
package com.android.providers.downloads;
import android.app.Notification;
-import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
-import android.database.Cursor;
import android.net.Uri;
import android.provider.Downloads;
+import android.util.Log;
+import android.view.View;
import android.widget.RemoteViews;
import java.util.HashMap;
+import java.util.List;
/**
- * This class handles the updating of the Notification Manager for the
+ * This class handles the updating of the Notification Manager for the
* cases where there is an ongoing download. Once the download is complete
- * (be it successful or unsuccessful) it is no longer the responsibility
+ * (be it successful or unsuccessful) it is no longer the responsibility
* of this component to show the download in the notification manager.
*
*/
class DownloadNotification {
Context mContext;
- public NotificationManager mNotificationMgr;
HashMap <String, NotificationItem> mNotifications;
-
+ private SystemFacade mSystemFacade;
+
static final String LOGTAG = "DownloadNotification";
- static final String WHERE_RUNNING =
+ static final String WHERE_RUNNING =
"(" + Downloads.Impl.COLUMN_STATUS + " >= '100') AND (" +
Downloads.Impl.COLUMN_STATUS + " <= '199') AND (" +
Downloads.Impl.COLUMN_VISIBILITY + " IS NULL OR " +
@@ -53,8 +54,8 @@ class DownloadNotification {
Downloads.Impl.COLUMN_STATUS + " >= '200' AND " +
Downloads.Impl.COLUMN_VISIBILITY +
" == '" + Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED + "'";
-
-
+
+
/**
* This inner class is used to collate downloads that are owned by
* the same application. This is so that only one notification line
@@ -63,17 +64,18 @@ class DownloadNotification {
*/
static class NotificationItem {
int mId; // This first db _id for the download for the app
- int mTotalCurrent = 0;
- int mTotalTotal = 0;
+ long mTotalCurrent = 0;
+ long mTotalTotal = 0;
int mTitleCount = 0;
String mPackageName; // App package name
String mDescription;
String[] mTitles = new String[2]; // download titles.
-
+ String mPausedText = null;
+
/*
* Add a second download to this notification item.
*/
- void addItem(String title, int currentBytes, int totalBytes) {
+ void addItem(String title, long currentBytes, long totalBytes) {
mTotalCurrent += currentBytes;
if (totalBytes <= 0 || mTotalTotal == -1) {
mTotalTotal = -1;
@@ -86,92 +88,76 @@ class DownloadNotification {
mTitleCount++;
}
}
-
-
+
+
/**
* Constructor
- * @param ctx The context to use to obtain access to the
+ * @param ctx The context to use to obtain access to the
* Notification Service
*/
- DownloadNotification(Context ctx) {
+ DownloadNotification(Context ctx, SystemFacade systemFacade) {
mContext = ctx;
- mNotificationMgr = (NotificationManager) mContext
- .getSystemService(Context.NOTIFICATION_SERVICE);
+ mSystemFacade = systemFacade;
mNotifications = new HashMap<String, NotificationItem>();
}
-
+
/*
- * Update the notification ui.
+ * Update the notification ui.
*/
- public void updateNotification() {
- updateActiveNotification();
- updateCompletedNotification();
+ public void updateNotification(List<DownloadInfo> downloads) {
+ updateActiveNotification(downloads);
+ updateCompletedNotification(downloads);
}
- private void updateActiveNotification() {
- // Active downloads
- Cursor c = mContext.getContentResolver().query(
- Downloads.Impl.CONTENT_URI, new String [] {
- Downloads.Impl._ID,
- Downloads.Impl.COLUMN_TITLE,
- Downloads.Impl.COLUMN_DESCRIPTION,
- Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
- Downloads.Impl.COLUMN_NOTIFICATION_CLASS,
- Downloads.Impl.COLUMN_CURRENT_BYTES,
- Downloads.Impl.COLUMN_TOTAL_BYTES,
- Downloads.Impl.COLUMN_STATUS
- },
- WHERE_RUNNING, null, Downloads.Impl._ID);
-
- if (c == null) {
- return;
- }
-
- // Columns match projection in query above
- final int idColumn = 0;
- final int titleColumn = 1;
- final int descColumn = 2;
- final int ownerColumn = 3;
- final int classOwnerColumn = 4;
- final int currentBytesColumn = 5;
- final int totalBytesColumn = 6;
- final int statusColumn = 7;
-
+ private void updateActiveNotification(List<DownloadInfo> downloads) {
// Collate the notifications
mNotifications.clear();
- for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
- String packageName = c.getString(ownerColumn);
- int max = c.getInt(totalBytesColumn);
- int progress = c.getInt(currentBytesColumn);
- long id = c.getLong(idColumn);
- String title = c.getString(titleColumn);
+ for (DownloadInfo download : downloads) {
+ if (!isActiveAndVisible(download)) {
+ continue;
+ }
+ String packageName = download.mPackage;
+ long max = download.mTotalBytes;
+ long progress = download.mCurrentBytes;
+ long id = download.mId;
+ String title = download.mTitle;
if (title == null || title.length() == 0) {
title = mContext.getResources().getString(
R.string.download_unknown_title);
}
+
+ NotificationItem item;
if (mNotifications.containsKey(packageName)) {
- mNotifications.get(packageName).addItem(title, progress, max);
+ item = mNotifications.get(packageName);
+ item.addItem(title, progress, max);
} else {
- NotificationItem item = new NotificationItem();
+ item = new NotificationItem();
item.mId = (int) id;
item.mPackageName = packageName;
- item.mDescription = c.getString(descColumn);
- String className = c.getString(classOwnerColumn);
+ item.mDescription = download.mDescription;
+ String className = download.mClass;
item.addItem(title, progress, max);
mNotifications.put(packageName, item);
}
-
+ if (hasPausedReason(download) && item.mPausedText == null) {
+ item.mPausedText = download.mPausedReason;
+ }
}
- c.close();
-
+
// Add the notifications
for (NotificationItem item : mNotifications.values()) {
// Build the notification object
Notification n = new Notification();
- n.icon = android.R.drawable.stat_sys_download;
+
+ boolean hasPausedText = (item.mPausedText != null);
+ int iconResource = android.R.drawable.stat_sys_download;
+ if (hasPausedText) {
+ iconResource = android.R.drawable.stat_sys_warning;
+ }
+ n.icon = iconResource;
n.flags |= Notification.FLAG_ONGOING_EVENT;
-
+
// Build the RemoteView object
RemoteViews expandedView = new RemoteViews(
"com.android.providers.downloads",
@@ -186,18 +172,24 @@ class DownloadNotification {
new Object[] { Integer.valueOf(item.mTitleCount - 2) }));
}
} else {
- expandedView.setTextViewText(R.id.description,
+ expandedView.setTextViewText(R.id.description,
item.mDescription);
}
expandedView.setTextViewText(R.id.title, title);
- expandedView.setProgressBar(R.id.progress_bar,
- item.mTotalTotal,
- item.mTotalCurrent,
- item.mTotalTotal == -1);
- expandedView.setTextViewText(R.id.progress_text,
+
+ if (hasPausedText) {
+ expandedView.setViewVisibility(R.id.progress_bar, View.GONE);
+ expandedView.setTextViewText(R.id.paused_text, item.mPausedText);
+ } else {
+ expandedView.setViewVisibility(R.id.paused_text, View.GONE);
+ expandedView.setProgressBar(R.id.progress_bar,
+ (int) item.mTotalTotal,
+ (int) item.mTotalCurrent,
+ item.mTotalTotal == -1);
+ }
+ expandedView.setTextViewText(R.id.progress_text,
getDownloadingText(item.mTotalTotal, item.mTotalCurrent));
- expandedView.setImageViewResource(R.id.appIcon,
- android.R.drawable.stat_sys_download);
+ expandedView.setImageViewResource(R.id.appIcon, iconResource);
n.contentView = expandedView;
Intent intent = new Intent(Constants.ACTION_LIST);
@@ -208,51 +200,26 @@ class DownloadNotification {
n.contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
- mNotificationMgr.notify(item.mId, n);
-
+ mSystemFacade.postNotification(item.mId, n);
+
}
}
- private void updateCompletedNotification() {
- // Completed downloads
- Cursor c = mContext.getContentResolver().query(
- Downloads.Impl.CONTENT_URI, new String [] {
- Downloads.Impl._ID,
- Downloads.Impl.COLUMN_TITLE,
- Downloads.Impl.COLUMN_DESCRIPTION,
- Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
- Downloads.Impl.COLUMN_NOTIFICATION_CLASS,
- Downloads.Impl.COLUMN_CURRENT_BYTES,
- Downloads.Impl.COLUMN_TOTAL_BYTES,
- Downloads.Impl.COLUMN_STATUS,
- Downloads.Impl.COLUMN_LAST_MODIFICATION,
- Downloads.Impl.COLUMN_DESTINATION
- },
- WHERE_COMPLETED, null, Downloads.Impl._ID);
-
- if (c == null) {
- return;
- }
-
- // Columns match projection in query above
- final int idColumn = 0;
- final int titleColumn = 1;
- final int descColumn = 2;
- final int ownerColumn = 3;
- final int classOwnerColumn = 4;
- final int currentBytesColumn = 5;
- final int totalBytesColumn = 6;
- final int statusColumn = 7;
- final int lastModColumnId = 8;
- final int destinationColumnId = 9;
-
- for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
+ private boolean hasPausedReason(DownloadInfo download) {
+ return download.mStatus == Downloads.STATUS_RUNNING_PAUSED && download.mPausedReason != null;
+ }
+
+ private void updateCompletedNotification(List<DownloadInfo> downloads) {
+ for (DownloadInfo download : downloads) {
+ if (!isCompleteAndVisible(download)) {
+ return;
+ }
// Add the notifications
Notification n = new Notification();
n.icon = android.R.drawable.stat_sys_download_done;
- long id = c.getLong(idColumn);
- String title = c.getString(titleColumn);
+ long id = download.mId;
+ String title = download.mTitle;
if (title == null || title.length() == 0) {
title = mContext.getResources().getString(
R.string.download_unknown_title);
@@ -260,14 +227,14 @@ class DownloadNotification {
Uri contentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + id);
String caption;
Intent intent;
- if (Downloads.Impl.isStatusError(c.getInt(statusColumn))) {
+ if (Downloads.Impl.isStatusError(download.mStatus)) {
caption = mContext.getResources()
.getString(R.string.notification_download_failed);
intent = new Intent(Constants.ACTION_LIST);
} else {
caption = mContext.getResources()
.getString(R.string.notification_download_complete);
- if (c.getInt(destinationColumnId) == Downloads.Impl.DESTINATION_EXTERNAL) {
+ if (download.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
intent = new Intent(Constants.ACTION_OPEN);
} else {
intent = new Intent(Constants.ACTION_LIST);
@@ -277,7 +244,7 @@ class DownloadNotification {
DownloadReceiver.class.getName());
intent.setData(contentUri);
- n.when = c.getLong(lastModColumnId);
+ n.when = download.mLastMod;
n.setLatestEventInfo(mContext, title, caption,
PendingIntent.getBroadcast(mContext, 0, intent, 0));
@@ -287,9 +254,18 @@ class DownloadNotification {
intent.setData(contentUri);
n.deleteIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
- mNotificationMgr.notify(c.getInt(idColumn), n);
+ mSystemFacade.postNotification(download.mId, n);
}
- c.close();
+ }
+
+ private boolean isActiveAndVisible(DownloadInfo download) {
+ return 100 <= download.mStatus && download.mStatus < 200
+ && download.mVisibility != Downloads.VISIBILITY_HIDDEN;
+ }
+
+ private boolean isCompleteAndVisible(DownloadInfo download) {
+ return download.mStatus >= 200
+ && download.mVisibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
}
/*
@@ -305,5 +281,5 @@ class DownloadNotification {
sb.append('%');
return sb.toString();
}
-
+
}
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index d7c24f9a..f6b091b1 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -34,15 +34,20 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
+import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.provider.Downloads;
import android.util.Config;
import android.util.Log;
+import com.google.common.annotations.VisibleForTesting;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
/**
@@ -53,11 +58,7 @@ public final class DownloadProvider extends ContentProvider {
/** Database filename */
private static final String DB_NAME = "downloads.db";
/** Current database version */
- private static final int DB_VERSION = 100;
- /** Database version from which upgrading is a nop */
- private static final int DB_VERSION_NOP_UPGRADE_FROM = 31;
- /** Database version to which upgrading is a nop */
- private static final int DB_VERSION_NOP_UPGRADE_TO = 100;
+ private static final int DB_VERSION = 102;
/** Name of table in the database */
private static final String DB_TABLE = "downloads";
@@ -72,9 +73,13 @@ public final class DownloadProvider extends ContentProvider {
private static final int DOWNLOADS = 1;
/** URI matcher constant for the URI of an individual download */
private static final int DOWNLOADS_ID = 2;
+ /** URI matcher constant for the URI of a download's request headers */
+ private static final int REQUEST_HEADERS_URI = 3;
static {
sURIMatcher.addURI("downloads", "download", DOWNLOADS);
sURIMatcher.addURI("downloads", "download/#", DOWNLOADS_ID);
+ sURIMatcher.addURI("downloads", "download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
+ REQUEST_HEADERS_URI);
}
private static final String[] sAppReadableColumnsArray = new String[] {
@@ -92,7 +97,8 @@ public final class DownloadProvider extends ContentProvider {
Downloads.Impl.COLUMN_TOTAL_BYTES,
Downloads.Impl.COLUMN_CURRENT_BYTES,
Downloads.Impl.COLUMN_TITLE,
- Downloads.Impl.COLUMN_DESCRIPTION
+ Downloads.Impl.COLUMN_DESCRIPTION,
+ Downloads.Impl.COLUMN_URI,
};
private static HashSet<String> sAppReadableColumnsSet;
@@ -110,6 +116,9 @@ public final class DownloadProvider extends ContentProvider {
private int mSystemUid = -1;
private int mDefContainerUid = -1;
+ @VisibleForTesting
+ SystemFacade mSystemFacade;
+
/**
* Creates and updated database on demand when opening it.
* Helper class to create database the first time the provider is
@@ -117,7 +126,6 @@ public final class DownloadProvider extends ContentProvider {
* an updated version of the database.
*/
private final class DatabaseHelper extends SQLiteOpenHelper {
-
public DatabaseHelper(final Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@@ -130,40 +138,130 @@ public final class DownloadProvider extends ContentProvider {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "populating new database");
}
- createTable(db);
+ onUpgrade(db, 0, DB_VERSION);
}
- /* (not a javadoc comment)
- * Checks data integrity when opening the database.
- */
- /*
- * @Override
- * public void onOpen(final SQLiteDatabase db) {
- * super.onOpen(db);
- * }
- */
-
/**
* Updates the database format when a content provider is used
* with a database that was created with a different format.
+ *
+ * Note: to support downgrades, creating a table should always drop it first if it already
+ * exists.
*/
- // Note: technically, this could also be a downgrade, so if we want
- // to gracefully handle upgrades we should be careful about
- // what to do on downgrades.
@Override
public void onUpgrade(final SQLiteDatabase db, int oldV, final int newV) {
- if (oldV == DB_VERSION_NOP_UPGRADE_FROM) {
- if (newV == DB_VERSION_NOP_UPGRADE_TO) { // that's a no-op upgrade.
- return;
- }
- // NOP_FROM and NOP_TO are identical, just in different codelines. Upgrading
- // from NOP_FROM is the same as upgrading from NOP_TO.
- oldV = DB_VERSION_NOP_UPGRADE_TO;
+ if (oldV == 31) {
+ // 31 and 100 are identical, just in different codelines. Upgrading from 31 is the
+ // same as upgrading from 100.
+ oldV = 100;
+ } else if (oldV < 100) {
+ // no logic to upgrade from these older version, just recreate the DB
+ Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV
+ + " to version " + newV + ", which will destroy all old data");
+ oldV = 99;
+ } else if (oldV > newV) {
+ // user must have downgraded software; we have no way to know how to downgrade the
+ // DB, so just recreate it
+ Log.i(Constants.TAG, "Downgrading downloads database from version " + oldV
+ + " (current version is " + newV + "), destroying all old data");
+ oldV = 99;
+ }
+
+ for (int version = oldV + 1; version <= newV; version++) {
+ upgradeTo(db, version);
}
- Log.i(Constants.TAG, "Upgrading downloads database from version " + oldV + " to " + newV
- + ", which will destroy all old data");
- dropTable(db);
- createTable(db);
+ }
+
+ /**
+ * Upgrade database from (version - 1) to version.
+ */
+ private void upgradeTo(SQLiteDatabase db, int version) {
+ switch (version) {
+ case 100:
+ createDownloadsTable(db);
+ break;
+
+ case 101:
+ createHeadersTable(db);
+ break;
+
+ case 102:
+ addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_PUBLIC_API,
+ "INTEGER NOT NULL DEFAULT 0");
+ addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOW_ROAMING,
+ "INTEGER NOT NULL DEFAULT 0");
+ addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES,
+ "INTEGER NOT NULL DEFAULT 0");
+ break;
+
+ default:
+ throw new IllegalStateException("Don't know how to upgrade to " + version);
+ }
+ }
+
+ /**
+ * Add a column to a table using ALTER TABLE.
+ * @param dbTable name of the table
+ * @param columnName name of the column to add
+ * @param columnDefinition SQL for the column definition
+ */
+ private void addColumn(SQLiteDatabase db, String dbTable, String columnName,
+ String columnDefinition) {
+ db.execSQL("ALTER TABLE " + dbTable + " ADD COLUMN " + columnName + " "
+ + columnDefinition);
+ }
+
+ /**
+ * Creates the table that'll hold the download information.
+ */
+ private void createDownloadsTable(SQLiteDatabase db) {
+ try {
+ db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
+ db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
+ Downloads.Impl._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Downloads.Impl.COLUMN_URI + " TEXT, " +
+ Constants.RETRY_AFTER_X_REDIRECT_COUNT + " INTEGER, " +
+ Downloads.Impl.COLUMN_APP_DATA + " TEXT, " +
+ Downloads.Impl.COLUMN_NO_INTEGRITY + " BOOLEAN, " +
+ Downloads.Impl.COLUMN_FILE_NAME_HINT + " TEXT, " +
+ Constants.OTA_UPDATE + " BOOLEAN, " +
+ Downloads.Impl._DATA + " TEXT, " +
+ Downloads.Impl.COLUMN_MIME_TYPE + " TEXT, " +
+ Downloads.Impl.COLUMN_DESTINATION + " INTEGER, " +
+ Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
+ Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
+ Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
+ Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
+ Constants.FAILED_CONNECTIONS + " INTEGER, " +
+ Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
+ Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
+ Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
+ Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS + " TEXT, " +
+ Downloads.Impl.COLUMN_COOKIE_DATA + " TEXT, " +
+ Downloads.Impl.COLUMN_USER_AGENT + " TEXT, " +
+ Downloads.Impl.COLUMN_REFERER + " TEXT, " +
+ Downloads.Impl.COLUMN_TOTAL_BYTES + " INTEGER, " +
+ Downloads.Impl.COLUMN_CURRENT_BYTES + " INTEGER, " +
+ Constants.ETAG + " TEXT, " +
+ Constants.UID + " INTEGER, " +
+ Downloads.Impl.COLUMN_OTHER_UID + " INTEGER, " +
+ Downloads.Impl.COLUMN_TITLE + " TEXT, " +
+ Downloads.Impl.COLUMN_DESCRIPTION + " TEXT, " +
+ Constants.MEDIA_SCANNED + " BOOLEAN);");
+ } catch (SQLException ex) {
+ Log.e(Constants.TAG, "couldn't create table in downloads database");
+ throw ex;
+ }
+ }
+
+ private void createHeadersTable(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE);
+ db.execSQL("CREATE TABLE " + Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE + "(" +
+ "id INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + " INTEGER NOT NULL," +
+ Downloads.Impl.RequestHeaders.COLUMN_HEADER + " TEXT NOT NULL," +
+ Downloads.Impl.RequestHeaders.COLUMN_VALUE + " TEXT NOT NULL" +
+ ");");
}
}
@@ -172,6 +270,10 @@ public final class DownloadProvider extends ContentProvider {
*/
@Override
public boolean onCreate() {
+ if (mSystemFacade == null) {
+ mSystemFacade = new RealSystemFacade(getContext());
+ }
+
mOpenHelper = new DatabaseHelper(getContext());
// Initialize the system uid
mSystemUid = Process.SYSTEM_UID;
@@ -215,64 +317,11 @@ public final class DownloadProvider extends ContentProvider {
}
/**
- * Creates the table that'll hold the download information.
- */
- private void createTable(SQLiteDatabase db) {
- try {
- db.execSQL("CREATE TABLE " + DB_TABLE + "(" +
- Downloads.Impl._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
- Downloads.Impl.COLUMN_URI + " TEXT, " +
- Constants.RETRY_AFTER_X_REDIRECT_COUNT + " INTEGER, " +
- Downloads.Impl.COLUMN_APP_DATA + " TEXT, " +
- Downloads.Impl.COLUMN_NO_INTEGRITY + " BOOLEAN, " +
- Downloads.Impl.COLUMN_FILE_NAME_HINT + " TEXT, " +
- Constants.OTA_UPDATE + " BOOLEAN, " +
- Downloads.Impl._DATA + " TEXT, " +
- Downloads.Impl.COLUMN_MIME_TYPE + " TEXT, " +
- Downloads.Impl.COLUMN_DESTINATION + " INTEGER, " +
- Constants.NO_SYSTEM_FILES + " BOOLEAN, " +
- Downloads.Impl.COLUMN_VISIBILITY + " INTEGER, " +
- Downloads.Impl.COLUMN_CONTROL + " INTEGER, " +
- Downloads.Impl.COLUMN_STATUS + " INTEGER, " +
- Constants.FAILED_CONNECTIONS + " INTEGER, " +
- Downloads.Impl.COLUMN_LAST_MODIFICATION + " BIGINT, " +
- Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + " TEXT, " +
- Downloads.Impl.COLUMN_NOTIFICATION_CLASS + " TEXT, " +
- Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS + " TEXT, " +
- Downloads.Impl.COLUMN_COOKIE_DATA + " TEXT, " +
- Downloads.Impl.COLUMN_USER_AGENT + " TEXT, " +
- Downloads.Impl.COLUMN_REFERER + " TEXT, " +
- Downloads.Impl.COLUMN_TOTAL_BYTES + " INTEGER, " +
- Downloads.Impl.COLUMN_CURRENT_BYTES + " INTEGER, " +
- Constants.ETAG + " TEXT, " +
- Constants.UID + " INTEGER, " +
- Downloads.Impl.COLUMN_OTHER_UID + " INTEGER, " +
- Downloads.Impl.COLUMN_TITLE + " TEXT, " +
- Downloads.Impl.COLUMN_DESCRIPTION + " TEXT, " +
- Constants.MEDIA_SCANNED + " BOOLEAN);");
- } catch (SQLException ex) {
- Log.e(Constants.TAG, "couldn't create table in downloads database");
- throw ex;
- }
- }
-
- /**
- * Deletes the table that holds the download information.
- */
- private void dropTable(SQLiteDatabase db) {
- try {
- db.execSQL("DROP TABLE IF EXISTS " + DB_TABLE);
- } catch (SQLException ex) {
- Log.e(Constants.TAG, "couldn't drop table in downloads database");
- throw ex;
- }
- }
-
- /**
* Inserts a row in the database
*/
@Override
public Uri insert(final Uri uri, final ContentValues values) {
+ checkInsertPermissions(values);
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
if (sURIMatcher.match(uri) != DOWNLOADS) {
@@ -289,14 +338,37 @@ public final class DownloadProvider extends ContentProvider {
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;
+
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_EXTERNAL
- && dest != Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
+ && dest != Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
+ && dest != Downloads.Impl.DESTINATION_FILE_URI) {
throw new SecurityException("unauthorized destination code");
}
+ // 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);
+ }
filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
}
Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY);
@@ -313,16 +385,19 @@ public final class DownloadProvider extends ContentProvider {
}
copyInteger(Downloads.Impl.COLUMN_CONTROL, values, filteredValues);
filteredValues.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_PENDING);
- filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, System.currentTimeMillis());
+ filteredValues.put(Downloads.Impl.COLUMN_LAST_MODIFICATION,
+ mSystemFacade.currentTimeMillis());
+
String pckg = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
String clazz = values.getAsString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
- if (pckg != null && clazz != null) {
+ if (pckg != null && (clazz != null || isPublicApi)) {
int uid = Binder.getCallingUid();
try {
- if (uid == 0 ||
- getContext().getPackageManager().getApplicationInfo(pckg, 0).uid == uid) {
+ if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, pckg);
- filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz);
+ if (clazz != null) {
+ filteredValues.put(Downloads.Impl.COLUMN_NOTIFICATION_CLASS, clazz);
+ }
}
} catch (PackageManager.NameNotFoundException ex) {
/* ignored for now */
@@ -340,8 +415,14 @@ public final class DownloadProvider extends ContentProvider {
if (Binder.getCallingUid() == 0) {
copyInteger(Constants.UID, values, filteredValues);
}
- copyString(Downloads.Impl.COLUMN_TITLE, values, filteredValues);
- copyString(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues);
+ copyStringWithDefault(Downloads.Impl.COLUMN_TITLE, values, filteredValues, "");
+ copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, "");
+ filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1);
+
+ if (isPublicApi) {
+ copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues);
+ copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues);
+ }
if (Constants.LOGVV) {
Log.v(Constants.TAG, "initiating download with UID "
@@ -356,6 +437,7 @@ public final class DownloadProvider extends ContentProvider {
context.startService(new Intent(context, DownloadService.class));
long rowID = db.insert(DB_TABLE, null, filteredValues);
+ insertRequestHeaders(db, rowID, values);
Uri ret = null;
@@ -373,6 +455,112 @@ public final class DownloadProvider extends ContentProvider {
}
/**
+ * Check that the file URI provided for DESTINATION_FILE_URI is valid.
+ */
+ 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);
+ if (!uri.getScheme().equals("file")) {
+ throw new IllegalArgumentException("Not a file URI: " + uri);
+ }
+ File path = new File(uri.getSchemeSpecificPart());
+ String externalPath = Environment.getExternalStorageDirectory().getAbsolutePath();
+ if (!path.getPath().startsWith(externalPath)) {
+ throw new SecurityException("Destination must be on external storage: " + uri);
+ }
+ }
+
+ /**
+ * Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to
+ * constraints in the rest of the code. Apps without that may still access this provider through
+ * the public API, but additional restrictions are imposed. We check those restrictions here.
+ *
+ * @param values ContentValues provided to insert()
+ * @throws SecurityException if the caller has insufficient permissions
+ */
+ private void checkInsertPermissions(ContentValues values) {
+ if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_ACCESS)
+ == PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+
+ getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET,
+ "INTERNET permission is required to use the download manager");
+
+ // ensure the request fits within the bounds of a public API request
+ // first copy so we can remove values
+ values = new ContentValues(values);
+
+ // check columns whose values are restricted
+ enforceAllowedValues(values, Downloads.Impl.COLUMN_IS_PUBLIC_API, Boolean.TRUE);
+ enforceAllowedValues(values, Downloads.Impl.COLUMN_DESTINATION,
+ Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE,
+ Downloads.Impl.DESTINATION_FILE_URI);
+
+ if (getContext().checkCallingOrSelfPermission(Downloads.Impl.PERMISSION_NO_NOTIFICATION)
+ == PackageManager.PERMISSION_GRANTED) {
+ enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
+ Downloads.Impl.VISIBILITY_HIDDEN, Downloads.Impl.VISIBILITY_VISIBLE);
+ } else {
+ enforceAllowedValues(values, Downloads.Impl.COLUMN_VISIBILITY,
+ Downloads.Impl.VISIBILITY_VISIBLE);
+ }
+
+ // remove the rest of the columns that are allowed (with any value)
+ values.remove(Downloads.Impl.COLUMN_URI);
+ values.remove(Downloads.Impl.COLUMN_TITLE);
+ values.remove(Downloads.Impl.COLUMN_DESCRIPTION);
+ values.remove(Downloads.Impl.COLUMN_MIME_TYPE);
+ values.remove(Downloads.Impl.COLUMN_FILE_NAME_HINT); // checked later in insert()
+ values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert()
+ values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
+ values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
+ Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator();
+ while (iterator.hasNext()) {
+ String key = iterator.next().getKey();
+ if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
+ iterator.remove();
+ }
+ }
+
+ // any extra columns are extraneous and disallowed
+ if (values.size() > 0) {
+ StringBuilder error = new StringBuilder("Invalid columns in request: ");
+ boolean first = true;
+ for (Map.Entry<String, Object> entry : values.valueSet()) {
+ if (!first) {
+ error.append(", ");
+ }
+ error.append(entry.getKey());
+ }
+ throw new SecurityException(error.toString());
+ }
+ }
+
+ /**
+ * Remove column from values, and throw a SecurityException if the value isn't within the
+ * specified allowedValues.
+ */
+ private void enforceAllowedValues(ContentValues values, String column,
+ Object... allowedValues) {
+ Object value = values.get(column);
+ values.remove(column);
+ for (Object allowedValue : allowedValues) {
+ if (value == null && allowedValue == null) {
+ return;
+ }
+ if (value != null && value.equals(allowedValue)) {
+ return;
+ }
+ }
+ throw new SecurityException("Invalid value for " + column + ": " + value);
+ }
+
+ /**
* Starts a database query
*/
@Override
@@ -396,10 +584,16 @@ public final class DownloadProvider extends ContentProvider {
case DOWNLOADS_ID: {
qb.setTables(DB_TABLE);
qb.appendWhere(Downloads.Impl._ID + "=");
- qb.appendWhere(uri.getPathSegments().get(1));
+ qb.appendWhere(getDownloadIdFromUri(uri));
emptyWhere = false;
break;
}
+ case REQUEST_HEADERS_URI:
+ if (projection != null || selection != null || sort != null) {
+ throw new UnsupportedOperationException("Request header queries do not support "
+ + "projections, selections or sorting");
+ }
+ return queryRequestHeaders(db, uri);
default: {
if (Constants.LOGV) {
Log.v(Constants.TAG, "querying unknown URI: " + uri);
@@ -408,45 +602,22 @@ public final class DownloadProvider extends ContentProvider {
}
}
- int callingUid = Binder.getCallingUid();
- if (Binder.getCallingPid() != Process.myPid() &&
- callingUid != mSystemUid &&
- callingUid != mDefContainerUid &&
- Process.supportsProcesses()) {
- boolean canSeeAllExternal;
+ if (shouldRestrictVisibility()) {
if (projection == null) {
projection = sAppReadableColumnsArray;
- // sAppReadableColumnsArray includes _DATA, which is not allowed
- // to be seen except by the initiating application
- canSeeAllExternal = false;
} else {
- canSeeAllExternal = getContext().checkCallingPermission(
- Downloads.Impl.PERMISSION_SEE_ALL_EXTERNAL)
- == PackageManager.PERMISSION_GRANTED;
for (int i = 0; i < projection.length; ++i) {
if (!sAppReadableColumnsSet.contains(projection[i])) {
throw new IllegalArgumentException(
"column " + projection[i] + " is not allowed in queries");
}
- canSeeAllExternal = canSeeAllExternal
- && !projection[i].equals(Downloads.Impl._DATA);
}
}
if (!emptyWhere) {
qb.appendWhere(" AND ");
emptyWhere = false;
}
- String validUid = "( " + Constants.UID + "="
- + Binder.getCallingUid() + " OR "
- + Downloads.Impl.COLUMN_OTHER_UID + "="
- + Binder.getCallingUid() + " )";
- if (canSeeAllExternal) {
- qb.appendWhere("( " + validUid + " OR "
- + Downloads.Impl.DESTINATION_EXTERNAL + " = "
- + Downloads.Impl.COLUMN_DESTINATION + " )");
- } else {
- qb.appendWhere(validUid);
- }
+ qb.appendWhere(getRestrictedUidClause());
}
if (Constants.LOGVV) {
@@ -513,6 +684,80 @@ public final class DownloadProvider extends ContentProvider {
return ret;
}
+ private String getDownloadIdFromUri(final Uri uri) {
+ return uri.getPathSegments().get(1);
+ }
+
+ /**
+ * Insert request headers for a download into the DB.
+ */
+ private void insertRequestHeaders(SQLiteDatabase db, long downloadId, ContentValues values) {
+ ContentValues rowValues = new ContentValues();
+ rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID, downloadId);
+ for (Map.Entry<String, Object> entry : values.valueSet()) {
+ String key = entry.getKey();
+ if (key.startsWith(Downloads.Impl.RequestHeaders.INSERT_KEY_PREFIX)) {
+ String headerLine = entry.getValue().toString();
+ if (!headerLine.contains(":")) {
+ throw new IllegalArgumentException("Invalid HTTP header line: " + headerLine);
+ }
+ String[] parts = headerLine.split(":", 2);
+ rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_HEADER, parts[0].trim());
+ rowValues.put(Downloads.Impl.RequestHeaders.COLUMN_VALUE, parts[1].trim());
+ db.insert(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, null, rowValues);
+ }
+ }
+ }
+
+ /**
+ * Handle a query for the custom request headers registered for a download.
+ */
+ private Cursor queryRequestHeaders(SQLiteDatabase db, Uri uri) {
+ String where = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
+ + getDownloadIdFromUri(uri);
+ String[] projection = new String[] {Downloads.Impl.RequestHeaders.COLUMN_HEADER,
+ Downloads.Impl.RequestHeaders.COLUMN_VALUE};
+ Cursor cursor = db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where,
+ null, null, null, null);
+ return new ReadOnlyCursorWrapper(cursor);
+ }
+
+ /**
+ * Delete request headers for downloads matching the given query.
+ */
+ private void deleteRequestHeaders(SQLiteDatabase db, String where, String[] whereArgs) {
+ String[] projection = new String[] {Downloads.Impl._ID};
+ Cursor cursor = db.query(DB_TABLE, projection, where, whereArgs, null, null, null, null);
+ try {
+ for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+ long id = cursor.getLong(0);
+ String idWhere = Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "=" + id;
+ db.delete(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, idWhere, null);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * @return true if we should restrict this caller to viewing only its own downloads
+ */
+ private boolean shouldRestrictVisibility() {
+ int callingUid = Binder.getCallingUid();
+ return Binder.getCallingPid() != Process.myPid() &&
+ callingUid != mSystemUid &&
+ callingUid != mDefContainerUid &&
+ Process.supportsProcesses();
+ }
+
+ /**
+ * @return a SQL WHERE clause to restrict the query to downloads accessible to the caller's UID
+ */
+ private String getRestrictedUidClause() {
+ return "( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
+ + Downloads.Impl.COLUMN_OTHER_UID + "=" + Binder.getCallingUid() + " )";
+ }
+
/**
* Updates a row in the database
*/
@@ -558,30 +803,9 @@ public final class DownloadProvider extends ContentProvider {
switch (match) {
case DOWNLOADS:
case DOWNLOADS_ID: {
- String myWhere;
- if (where != null) {
- if (match == DOWNLOADS) {
- myWhere = "( " + where + " )";
- } else {
- myWhere = "( " + where + " ) AND ";
- }
- } else {
- myWhere = "";
- }
- if (match == DOWNLOADS_ID) {
- String segment = uri.getPathSegments().get(1);
- rowId = Long.parseLong(segment);
- myWhere += " ( " + Downloads.Impl._ID + " = " + rowId + " ) ";
- }
- int callingUid = Binder.getCallingUid();
- if (Binder.getCallingPid() != Process.myPid() &&
- callingUid != mSystemUid &&
- callingUid != mDefContainerUid) {
- myWhere += " AND ( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
- + Downloads.Impl.COLUMN_OTHER_UID + "=" + Binder.getCallingUid() + " )";
- }
+ String fullWhere = getWhereClause(uri, where);
if (filteredValues.size() > 0) {
- count = db.update(DB_TABLE, filteredValues, myWhere, whereArgs);
+ count = db.update(DB_TABLE, filteredValues, fullWhere, whereArgs);
} else {
count = 0;
}
@@ -602,6 +826,22 @@ public final class DownloadProvider extends ContentProvider {
return count;
}
+ private String getWhereClause(final Uri uri, final String where) {
+ StringBuilder myWhere = new StringBuilder();
+ if (where != null) {
+ myWhere.append("( " + where + " )");
+ }
+ if (sURIMatcher.match(uri) == DOWNLOADS_ID) {
+ String segment = getDownloadIdFromUri(uri);
+ long rowId = Long.parseLong(segment);
+ appendClause(myWhere, " ( " + Downloads.Impl._ID + " = " + rowId + " ) ");
+ }
+ if (shouldRestrictVisibility()) {
+ appendClause(myWhere, getRestrictedUidClause());
+ }
+ return myWhere.toString();
+ }
+
/**
* Deletes a row in the database
*/
@@ -617,30 +857,9 @@ public final class DownloadProvider extends ContentProvider {
switch (match) {
case DOWNLOADS:
case DOWNLOADS_ID: {
- String myWhere;
- if (where != null) {
- if (match == DOWNLOADS) {
- myWhere = "( " + where + " )";
- } else {
- myWhere = "( " + where + " ) AND ";
- }
- } else {
- myWhere = "";
- }
- if (match == DOWNLOADS_ID) {
- String segment = uri.getPathSegments().get(1);
- long rowId = Long.parseLong(segment);
- myWhere += " ( " + Downloads.Impl._ID + " = " + rowId + " ) ";
- }
- int callingUid = Binder.getCallingUid();
- if (Binder.getCallingPid() != Process.myPid() &&
- callingUid != mSystemUid &&
- callingUid != mDefContainerUid) {
- myWhere += " AND ( " + Constants.UID + "=" + Binder.getCallingUid() + " OR "
- + Downloads.Impl.COLUMN_OTHER_UID + "="
- + Binder.getCallingUid() + " )";
- }
- count = db.delete(DB_TABLE, myWhere, whereArgs);
+ String fullWhere = getWhereClause(uri, where);
+ deleteRequestHeaders(db, fullWhere, whereArgs);
+ count = db.delete(DB_TABLE, fullWhere, whereArgs);
break;
}
default: {
@@ -654,6 +873,13 @@ public final class DownloadProvider extends ContentProvider {
return count;
}
+ private void appendClause(StringBuilder whereClause, String newClause) {
+ if (whereClause.length() != 0) {
+ whereClause.append(" AND ");
+ }
+ whereClause.append(newClause);
+ }
+
/**
* Remotely opens a file
*/
@@ -733,7 +959,7 @@ public final class DownloadProvider extends ContentProvider {
throw new FileNotFoundException("couldn't open file");
} else {
ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, System.currentTimeMillis());
+ values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
update(uri, values, null, null);
}
return ret;
@@ -760,6 +986,14 @@ public final class DownloadProvider extends ContentProvider {
}
}
+ private static final void copyStringWithDefault(String key, ContentValues from,
+ ContentValues to, String defaultValue) {
+ copyString(key, from, to);
+ if (!to.containsKey(key)) {
+ to.put(key, defaultValue);
+ }
+ }
+
private class ReadOnlyCursorWrapper extends CursorWrapper implements CrossProcessCursor {
public ReadOnlyCursorWrapper(Cursor cursor) {
super(cursor);
diff --git a/src/com/android/providers/downloads/DownloadReceiver.java b/src/com/android/providers/downloads/DownloadReceiver.java
index e8f10e7d..852c3712 100644
--- a/src/com/android/providers/downloads/DownloadReceiver.java
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -16,7 +16,6 @@
package com.android.providers.downloads;
-import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
@@ -25,20 +24,29 @@ import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.ConnectivityManager;
+import android.net.DownloadManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.provider.Downloads;
import android.util.Config;
import android.util.Log;
+import com.google.common.annotations.VisibleForTesting;
+
import java.io.File;
/**
* Receives system broadcasts (boot, network connectivity)
*/
public class DownloadReceiver extends BroadcastReceiver {
+ @VisibleForTesting
+ SystemFacade mSystemFacade = null;
public void onReceive(Context context, Intent intent) {
+ if (mSystemFacade == null) {
+ mSystemFacade = new RealSystemFacade(context);
+ }
+
if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Receiver onBoot");
@@ -52,7 +60,7 @@ public class DownloadReceiver extends BroadcastReceiver {
intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
if (info != null && info.isConnected()) {
if (Constants.LOGX) {
- if (Helpers.isNetworkAvailable(context)) {
+ if (Helpers.isNetworkAvailable(mSystemFacade)) {
Log.i(Constants.TAG, "Broadcast: Network Up");
} else {
Log.i(Constants.TAG, "Broadcast: Network Up, Actually Down");
@@ -61,7 +69,7 @@ public class DownloadReceiver extends BroadcastReceiver {
context.startService(new Intent(context, DownloadService.class));
} else {
if (Constants.LOGX) {
- if (Helpers.isNetworkAvailable(context)) {
+ if (Helpers.isNetworkAvailable(mSystemFacade)) {
Log.i(Constants.TAG, "Broadcast: Network Down, Actually Up");
} else {
Log.i(Constants.TAG, "Broadcast: Network Down");
@@ -127,28 +135,35 @@ public class DownloadReceiver extends BroadcastReceiver {
Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
int classColumn = cursor.getColumnIndexOrThrow(
Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
+ int isPublicApiColumn = cursor.getColumnIndex(
+ Downloads.Impl.COLUMN_IS_PUBLIC_API);
String pckg = cursor.getString(packageColumn);
String clazz = cursor.getString(classColumn);
- if (pckg != null && clazz != null) {
- Intent appIntent =
- new Intent(Downloads.Impl.ACTION_NOTIFICATION_CLICKED);
- appIntent.setClassName(pckg, clazz);
- if (intent.getBooleanExtra("multiple", true)) {
- appIntent.setData(Downloads.Impl.CONTENT_URI);
- } else {
- appIntent.setData(intent.getData());
+ boolean isPublicApi = cursor.getInt(isPublicApiColumn) != 0;
+
+ if (pckg != null) {
+ Intent appIntent = null;
+ if (isPublicApi) {
+ appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
+ appIntent.setPackage(pckg);
+ } else if (clazz != null) { // legacy behavior
+ appIntent = new Intent(Downloads.Impl.ACTION_NOTIFICATION_CLICKED);
+ appIntent.setClassName(pckg, clazz);
+ if (intent.getBooleanExtra("multiple", true)) {
+ appIntent.setData(Downloads.Impl.CONTENT_URI);
+ } else {
+ appIntent.setData(intent.getData());
+ }
+ }
+ if (appIntent != null) {
+ mSystemFacade.sendBroadcast(appIntent);
}
- context.sendBroadcast(appIntent);
}
}
}
cursor.close();
}
- NotificationManager notMgr = (NotificationManager) context
- .getSystemService(Context.NOTIFICATION_SERVICE);
- if (notMgr != null) {
- notMgr.cancel((int) ContentUris.parseId(intent.getData()));
- }
+ mSystemFacade.cancelNotification((int) ContentUris.parseId(intent.getData()));
} else if (intent.getAction().equals(Constants.ACTION_HIDE)) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Receiver hide for " + intent.getData());
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
index 9e890ea0..6d9ee220 100644
--- a/src/com/android/providers/downloads/DownloadService.java
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -16,8 +16,6 @@
package com.android.providers.downloads;
-import com.google.android.collect.Lists;
-
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
@@ -44,11 +42,13 @@ import android.provider.Downloads;
import android.util.Config;
import android.util.Log;
+import com.google.android.collect.Lists;
+import com.google.common.annotations.VisibleForTesting;
+
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
-import java.util.List;
/**
@@ -62,7 +62,7 @@ public class DownloadService extends Service {
/** Observer to get notified when the content observer's data changes */
private DownloadManagerContentObserver mObserver;
-
+
/** Class to handle Notification Manager updates */
private DownloadNotification mNotifier;
@@ -78,7 +78,8 @@ public class DownloadService extends Service {
* The thread that updates the internal download list from the content
* provider.
*/
- private UpdateThread mUpdateThread;
+ @VisibleForTesting
+ UpdateThread mUpdateThread;
/**
* Whether the internal download list should be updated from the content
@@ -109,6 +110,9 @@ public class DownloadService extends Service {
*/
private CharArrayBuffer mNewChars;
+ @VisibleForTesting
+ SystemFacade mSystemFacade;
+
/* ------------ Inner Classes ------------ */
/**
@@ -200,6 +204,10 @@ public class DownloadService extends Service {
Log.v(Constants.TAG, "Service onCreate");
}
+ if (mSystemFacade == null) {
+ mSystemFacade = new RealSystemFacade(this);
+ }
+
mDownloads = Lists.newArrayList();
mObserver = new DownloadManagerContentObserver();
@@ -209,10 +217,10 @@ public class DownloadService extends Service {
mMediaScannerService = null;
mMediaScannerConnecting = false;
mMediaScannerConnection = new MediaScannerConnection();
-
- mNotifier = new DownloadNotification(this);
- mNotifier.mNotificationMgr.cancelAll();
- mNotifier.updateNotification();
+
+ mNotifier = new DownloadNotification(this, mSystemFacade);
+ mSystemFacade.cancelAllNotifications();
+ mNotifier.updateNotification(mDownloads);
trimDatabase();
removeSpuriousFiles();
@@ -250,7 +258,7 @@ public class DownloadService extends Service {
mPendingUpdate = true;
if (mUpdateThread == null) {
mUpdateThread = new UpdateThread();
- mUpdateThread.start();
+ mSystemFacade.startThread(mUpdateThread);
}
}
}
@@ -259,10 +267,10 @@ public class DownloadService extends Service {
public UpdateThread() {
super("Download Service");
}
-
+
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
+
boolean keepService = false;
// for each update from the database, remember which download is
// supposed to get restarted soonest in the future
@@ -292,7 +300,7 @@ public class DownloadService extends Service {
DownloadReceiver.class.getName());
alarms.set(
AlarmManager.RTC_WAKEUP,
- System.currentTimeMillis() + wakeUp,
+ mSystemFacade.currentTimeMillis() + wakeUp,
PendingIntent.getBroadcast(DownloadService.this, 0, intent,
PendingIntent.FLAG_ONE_SHOT));
}
@@ -303,9 +311,7 @@ public class DownloadService extends Service {
}
mPendingUpdate = false;
}
- boolean networkAvailable = Helpers.isNetworkAvailable(DownloadService.this);
- boolean networkRoaming = Helpers.isNetworkRoaming(DownloadService.this);
- long now = System.currentTimeMillis();
+ long now = mSystemFacade.currentTimeMillis();
Cursor cursor = getContentResolver().query(Downloads.Impl.CONTENT_URI,
null, null, null, Downloads.Impl._ID);
@@ -361,7 +367,7 @@ public class DownloadService extends Service {
int id = cursor.getInt(idColumn);
if (arrayPos == mDownloads.size()) {
- insertDownload(cursor, arrayPos, networkAvailable, networkRoaming, now);
+ insertDownload(cursor, arrayPos, now);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Array update: appending " +
id + " @ " + arrayPos);
@@ -398,9 +404,7 @@ public class DownloadService extends Service {
deleteDownload(arrayPos); // this advances in the array
} else if (arrayId == id) {
// This cursor row already exists in the stored array
- updateDownload(
- cursor, arrayPos,
- networkAvailable, networkRoaming, now);
+ updateDownload(cursor, arrayPos, now);
if (shouldScanFile(arrayPos)
&& (!mediaScannerConnected()
|| !scanFile(cursor, arrayPos))) {
@@ -425,9 +429,7 @@ public class DownloadService extends Service {
Log.v(Constants.TAG, "Array update: inserting " +
id + " @ " + arrayPos);
}
- insertDownload(
- cursor, arrayPos,
- networkAvailable, networkRoaming, now);
+ insertDownload(cursor, arrayPos, now);
if (shouldScanFile(arrayPos)
&& (!mediaScannerConnected()
|| !scanFile(cursor, arrayPos))) {
@@ -451,7 +453,7 @@ public class DownloadService extends Service {
}
}
- mNotifier.updateNotification();
+ mNotifier.updateNotification(mDownloads);
if (mustScan) {
if (!mMediaScannerConnecting) {
@@ -544,44 +546,8 @@ public class DownloadService extends Service {
* Keeps a local copy of the info about a download, and initiates the
* download if appropriate.
*/
- private void insertDownload(
- Cursor cursor, int arrayPos,
- boolean networkAvailable, boolean networkRoaming, long now) {
- int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
- int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
- int retryRedirect =
- cursor.getInt(cursor.getColumnIndexOrThrow(Constants.RETRY_AFTER_X_REDIRECT_COUNT));
- DownloadInfo info = new DownloadInfo(
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl._ID)),
- cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_URI)),
- cursor.getInt(cursor.getColumnIndexOrThrow(
- Downloads.Impl.COLUMN_NO_INTEGRITY)) == 1,
- cursor.getString(cursor.getColumnIndexOrThrow(
- Downloads.Impl.COLUMN_FILE_NAME_HINT)),
- cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl._DATA)),
- cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_DESTINATION)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_VISIBILITY)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CONTROL)),
- cursor.getInt(statusColumn),
- cursor.getInt(failedColumn),
- retryRedirect & 0xfffffff,
- retryRedirect >> 28,
- cursor.getLong(cursor.getColumnIndexOrThrow(
- Downloads.Impl.COLUMN_LAST_MODIFICATION)),
- cursor.getString(cursor.getColumnIndexOrThrow(
- Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE)),
- cursor.getString(cursor.getColumnIndexOrThrow(
- Downloads.Impl.COLUMN_NOTIFICATION_CLASS)),
- cursor.getString(cursor.getColumnIndexOrThrow(
- Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS)),
- cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_COOKIE_DATA)),
- cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_USER_AGENT)),
- cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_REFERER)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_TOTAL_BYTES)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_CURRENT_BYTES)),
- cursor.getString(cursor.getColumnIndexOrThrow(Constants.ETAG)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1);
+ private void insertDownload(Cursor cursor, int arrayPos, long now) {
+ DownloadInfo info = new DownloadInfo(this, mSystemFacade, cursor);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Service adding new entry");
@@ -644,51 +610,20 @@ public class DownloadService extends Service {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
getContentResolver().update(uri, values, null, null);
- info.sendIntentIfRequested(uri, this);
+ info.sendIntentIfRequested(uri);
return;
}
}
- if (info.canUseNetwork(networkAvailable, networkRoaming)) {
- if (info.isReadyToStart(now)) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "Service spawning thread to handle new download " +
- info.mId);
- }
- if (info.mHasActiveThread) {
- throw new IllegalStateException("Multiple threads on same download on insert");
- }
- if (info.mStatus != Downloads.Impl.STATUS_RUNNING) {
- info.mStatus = Downloads.Impl.STATUS_RUNNING;
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_STATUS, info.mStatus);
- getContentResolver().update(
- ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId),
- values, null, null);
- }
- DownloadThread downloader = new DownloadThread(this, info);
- info.mHasActiveThread = true;
- downloader.start();
- }
- } else {
- if (info.mStatus == 0
- || info.mStatus == Downloads.Impl.STATUS_PENDING
- || info.mStatus == Downloads.Impl.STATUS_RUNNING) {
- info.mStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- Uri uri = ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId);
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_STATUS, Downloads.Impl.STATUS_RUNNING_PAUSED);
- getContentResolver().update(uri, values, null, null);
- }
+ if (info.isReadyToStart(now)) {
+ info.start(now);
}
}
/**
* Updates the local copy of the info about a download.
*/
- private void updateDownload(
- Cursor cursor, int arrayPos,
- boolean networkAvailable, boolean networkRoaming, long now) {
+ private void updateDownload(Cursor cursor, int arrayPos, long now) {
DownloadInfo info = (DownloadInfo) mDownloads.get(arrayPos);
int statusColumn = cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_STATUS);
int failedColumn = cursor.getColumnIndexOrThrow(Constants.FAILED_CONNECTIONS);
@@ -706,7 +641,7 @@ public class DownloadService extends Service {
if (info.mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
&& newVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
&& Downloads.Impl.isStatusCompleted(info.mStatus)) {
- mNotifier.mNotificationMgr.cancel(info.mId);
+ mSystemFacade.cancelNotification(info.mId);
}
info.mVisibility = newVisibility;
synchronized (info) {
@@ -716,7 +651,7 @@ public class DownloadService extends Service {
int newStatus = cursor.getInt(statusColumn);
if (!Downloads.Impl.isStatusCompleted(info.mStatus) &&
Downloads.Impl.isStatusCompleted(newStatus)) {
- mNotifier.mNotificationMgr.cancel(info.mId);
+ mSystemFacade.cancelNotification(info.mId);
}
info.mStatus = newStatus;
info.mNumFailed = cursor.getInt(failedColumn);
@@ -742,25 +677,8 @@ public class DownloadService extends Service {
info.mMediaScanned =
cursor.getInt(cursor.getColumnIndexOrThrow(Constants.MEDIA_SCANNED)) == 1;
- if (info.canUseNetwork(networkAvailable, networkRoaming)) {
- if (info.isReadyToRestart(now)) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "Service spawning thread to handle updated download " +
- info.mId);
- }
- if (info.mHasActiveThread) {
- throw new IllegalStateException("Multiple threads on same download on update");
- }
- info.mStatus = Downloads.Impl.STATUS_RUNNING;
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_STATUS, info.mStatus);
- getContentResolver().update(
- ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, info.mId),
- values, null, null);
- DownloadThread downloader = new DownloadThread(this, info);
- info.mHasActiveThread = true;
- downloader.start();
- }
+ if (info.isReadyToRestart(now)) {
+ info.start(now);
}
}
@@ -806,7 +724,7 @@ public class DownloadService extends Service {
&& info.mFileName != null) {
new File(info.mFileName).delete();
}
- mNotifier.mNotificationMgr.cancel(info.mId);
+ mSystemFacade.cancelNotification(info.mId);
mDownloads.remove(arrayPos);
}
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index 9e93e6a1..0073c6af 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -16,12 +16,7 @@
package com.android.providers.downloads;
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.HttpClient;
import org.apache.http.conn.params.ConnRouteParams;
-import org.apache.http.entity.StringEntity;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -39,9 +34,11 @@ import android.provider.DrmStore;
import android.util.Config;
import android.util.Log;
+import org.apache.http.Header;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
import java.io.File;
-import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -50,6 +47,7 @@ import java.io.SyncFailedException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
+import java.util.Map;
/**
* Runs an actual download
@@ -58,9 +56,11 @@ public class DownloadThread extends Thread {
private Context mContext;
private DownloadInfo mInfo;
+ private SystemFacade mSystemFacade;
- public DownloadThread(Context context, DownloadInfo info) {
+ public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info) {
mContext = context;
+ mSystemFacade = systemFacade;
mInfo = info;
}
@@ -78,573 +78,121 @@ public class DownloadThread extends Thread {
}
/**
+ * State for the entire run() method.
+ */
+ private static class State {
+ public String mFilename;
+ public FileOutputStream mStream;
+ public String mMimeType;
+ public boolean mCountRetry = false;
+ public int mRetryAfter = 0;
+ public int mRedirectCount = 0;
+ public String mNewUri;
+ public Uri mContentUri;
+ public boolean mGotData = false;
+ public String mRequestUri;
+
+ public State(DownloadInfo info) {
+ mMimeType = sanitizeMimeType(info.mMimeType);
+ mRedirectCount = info.mRedirectCount;
+ mContentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + info.mId);
+ mRequestUri = info.mUri;
+ mFilename = info.mFileName;
+ }
+ }
+
+ /**
+ * State within executeDownload()
+ */
+ private static class InnerState {
+ public int mBytesSoFar = 0;
+ public String mHeaderETag;
+ public boolean mContinuingDownload = false;
+ public String mHeaderContentLength;
+ public String mHeaderContentDisposition;
+ public String mHeaderContentLocation;
+ public int mBytesNotified = 0;
+ public long mTimeLastNotification = 0;
+ }
+
+ /**
+ * Raised from methods called by run() to indicate that the current request should be stopped
+ * immediately.
+ */
+ private class StopRequest extends Throwable {
+ public int mFinalStatus;
+
+ public StopRequest(int finalStatus) {
+ mFinalStatus = finalStatus;
+ }
+
+ public StopRequest(int finalStatus, Throwable throwable) {
+ super(throwable);
+ mFinalStatus = finalStatus;
+ }
+ }
+
+ /**
+ * Raised from methods called by executeDownload() to indicate that the download should be
+ * retried immediately.
+ */
+ private class RetryDownload extends Throwable {}
+
+ /**
* Executes the download in a separate thread
*/
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
- boolean countRetry = false;
- int retryAfter = 0;
- int redirectCount = mInfo.mRedirectCount;
- String newUri = null;
- boolean gotData = false;
- String filename = null;
- String mimeType = sanitizeMimeType(mInfo.mMimeType);
- FileOutputStream stream = null;
+ State state = new State(mInfo);
AndroidHttpClient client = null;
PowerManager.WakeLock wakeLock = null;
- Uri contentUri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
+ int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
+ mInfo.mPausedReason = null;
try {
- boolean continuingDownload = false;
- String headerAcceptRanges = null;
- String headerContentDisposition = null;
- String headerContentLength = null;
- String headerContentLocation = null;
- String headerETag = null;
- String headerTransferEncoding = null;
-
- byte data[] = new byte[Constants.BUFFER_SIZE];
-
- int bytesSoFar = 0;
-
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
wakeLock.acquire();
- filename = mInfo.mFileName;
- if (filename != null) {
- if (!Helpers.isFilenameValid(filename)) {
- finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
- notifyDownloadCompleted(
- finalStatus, false, 0, 0, false, filename, null, mInfo.mMimeType);
- return;
- }
- // We're resuming a download that got interrupted
- File f = new File(filename);
- if (f.exists()) {
- long fileLength = f.length();
- if (fileLength == 0) {
- // The download hadn't actually started, we can restart from scratch
- f.delete();
- filename = null;
- } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
- // Tough luck, that's not a resumable download
- if (Config.LOGD) {
- Log.d(Constants.TAG,
- "can't resume interrupted non-resumable download");
- }
- f.delete();
- finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
- notifyDownloadCompleted(
- finalStatus, false, 0, 0, false, filename, null, mInfo.mMimeType);
- return;
- } else {
- // All right, we'll be able to resume this download
- stream = new FileOutputStream(filename, true);
- bytesSoFar = (int) fileLength;
- if (mInfo.mTotalBytes != -1) {
- headerContentLength = Integer.toString(mInfo.mTotalBytes);
- }
- headerETag = mInfo.mETag;
- continuingDownload = true;
- }
- }
- }
- int bytesNotified = bytesSoFar;
- // starting with MIN_VALUE means that the first write will commit
- // progress to the database
- long timeLastNotification = 0;
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
+ }
client = AndroidHttpClient.newInstance(userAgent(), mContext);
- if (stream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
- && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
- .equalsIgnoreCase(mimeType)) {
- try {
- stream.close();
- stream = null;
- } catch (IOException ex) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "exception when closing the file before download : " +
- ex);
- }
- // nothing can really be done if the file can't be closed
- }
- }
-
- /*
- * This loop is run once for every individual HTTP request that gets sent.
- * The very first HTTP request is a "virgin" request, while every subsequent
- * request is done with the original ETag and a byte-range.
- */
-http_request_loop:
- while (true) {
+ boolean finished = false;
+ while(!finished) {
// Set or unset proxy, which may have changed since last GET request.
// setDefaultProxy() supports null as proxy parameter.
ConnRouteParams.setDefaultProxy(client.getParams(),
- Proxy.getPreferredHttpHost(mContext, mInfo.mUri));
- // Prepares the request and fires it.
- HttpGet request = new HttpGet(mInfo.mUri);
-
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
- }
-
- if (mInfo.mCookies != null) {
- request.addHeader("Cookie", mInfo.mCookies);
- }
- if (mInfo.mReferer != null) {
- request.addHeader("Referer", mInfo.mReferer);
- }
- if (continuingDownload) {
- if (headerETag != null) {
- request.addHeader("If-Match", headerETag);
- }
- request.addHeader("Range", "bytes=" + bytesSoFar + "-");
- }
-
- HttpResponse response;
+ Proxy.getPreferredHttpHost(mContext, state.mRequestUri));
+ HttpGet request = new HttpGet(state.mRequestUri);
try {
- response = client.execute(request);
- } catch (IllegalArgumentException ex) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "Arg exception trying to execute request for " +
- mInfo.mUri + " : " + ex);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "Arg exception trying to execute request for " +
- mInfo.mId + " : " + ex);
- }
- finalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
- request.abort();
- break http_request_loop;
- } catch (IOException ex) {
- if (Constants.LOGX) {
- if (Helpers.isNetworkAvailable(mContext)) {
- Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Up");
- } else {
- Log.i(Constants.TAG, "Execute Failed " + mInfo.mId + ", Net Down");
- }
- }
- if (!Helpers.isNetworkAvailable(mContext)) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- countRetry = true;
- } else {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "IOException trying to execute request for " +
- mInfo.mUri + " : " + ex);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "IOException trying to execute request for " +
- mInfo.mId + " : " + ex);
- }
- finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
- }
- request.abort();
- break http_request_loop;
- }
-
- int statusCode = response.getStatusLine().getStatusCode();
- if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "got HTTP response code 503");
- }
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- countRetry = true;
- Header header = response.getFirstHeader("Retry-After");
- if (header != null) {
- try {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Retry-After :" + header.getValue());
- }
- retryAfter = Integer.parseInt(header.getValue());
- if (retryAfter < 0) {
- retryAfter = 0;
- } else {
- if (retryAfter < Constants.MIN_RETRY_AFTER) {
- retryAfter = Constants.MIN_RETRY_AFTER;
- } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
- retryAfter = Constants.MAX_RETRY_AFTER;
- }
- retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
- retryAfter *= 1000;
- }
- } catch (NumberFormatException ex) {
- // ignored - retryAfter stays 0 in this case.
- }
- }
+ executeDownload(state, client, request);
+ finished = true;
+ } catch (RetryDownload exc) {
+ // fall through
+ } finally {
request.abort();
- break http_request_loop;
- }
- if (statusCode == 301 ||
- statusCode == 302 ||
- statusCode == 303 ||
- statusCode == 307) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
- }
- if (redirectCount >= Constants.MAX_REDIRECTS) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId +
- " at " + mInfo.mUri);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId);
- }
- finalStatus = Downloads.Impl.STATUS_TOO_MANY_REDIRECTS;
- request.abort();
- break http_request_loop;
- }
- Header header = response.getFirstHeader("Location");
- if (header != null) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Location :" + header.getValue());
- }
- try {
- newUri = new URI(mInfo.mUri).
- resolve(new URI(header.getValue())).
- toString();
- } catch(URISyntaxException ex) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG,
- "Couldn't resolve redirect URI " +
- header.getValue() +
- " for " +
- mInfo.mUri);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG,
- "Couldn't resolve redirect URI for download " +
- mInfo.mId);
- }
- finalStatus = Downloads.Impl.STATUS_BAD_REQUEST;
- request.abort();
- break http_request_loop;
- }
- ++redirectCount;
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- request.abort();
- break http_request_loop;
- }
+ request = null;
}
- if ((!continuingDownload && statusCode != Downloads.Impl.STATUS_SUCCESS)
- || (continuingDownload && statusCode != 206)) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.mUri);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "http error " + statusCode + " for download " +
- mInfo.mId);
- }
- if (Downloads.Impl.isStatusError(statusCode)) {
- finalStatus = statusCode;
- } else if (statusCode >= 300 && statusCode < 400) {
- finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
- } else if (continuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
- finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
- } else {
- finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
- }
- request.abort();
- break http_request_loop;
- } else {
- // Handles the response, saves the file
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "received response for " + mInfo.mUri);
- }
-
- if (!continuingDownload) {
- Header header = response.getFirstHeader("Accept-Ranges");
- if (header != null) {
- headerAcceptRanges = header.getValue();
- }
- header = response.getFirstHeader("Content-Disposition");
- if (header != null) {
- headerContentDisposition = header.getValue();
- }
- header = response.getFirstHeader("Content-Location");
- if (header != null) {
- headerContentLocation = header.getValue();
- }
- if (mimeType == null) {
- header = response.getFirstHeader("Content-Type");
- if (header != null) {
- mimeType = sanitizeMimeType(header.getValue());
- }
- }
- header = response.getFirstHeader("ETag");
- if (header != null) {
- headerETag = header.getValue();
- }
- header = response.getFirstHeader("Transfer-Encoding");
- if (header != null) {
- headerTransferEncoding = header.getValue();
- }
- if (headerTransferEncoding == null) {
- header = response.getFirstHeader("Content-Length");
- if (header != null) {
- headerContentLength = header.getValue();
- }
- } else {
- // Ignore content-length with transfer-encoding - 2616 4.4 3
- if (Constants.LOGVV) {
- Log.v(Constants.TAG,
- "ignoring content-length because of xfer-encoding");
- }
- }
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "Accept-Ranges: " + headerAcceptRanges);
- Log.v(Constants.TAG, "Content-Disposition: " +
- headerContentDisposition);
- Log.v(Constants.TAG, "Content-Length: " + headerContentLength);
- Log.v(Constants.TAG, "Content-Location: " + headerContentLocation);
- Log.v(Constants.TAG, "Content-Type: " + mimeType);
- Log.v(Constants.TAG, "ETag: " + headerETag);
- Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
- }
-
- if (!mInfo.mNoIntegrity && headerContentLength == null &&
- (headerTransferEncoding == null
- || !headerTransferEncoding.equalsIgnoreCase("chunked"))
- ) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "can't know size of download, giving up");
- }
- finalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
- request.abort();
- break http_request_loop;
- }
-
- DownloadFileInfo fileInfo = Helpers.generateSaveFile(
- mContext,
- mInfo.mUri,
- mInfo.mHint,
- headerContentDisposition,
- headerContentLocation,
- mimeType,
- mInfo.mDestination,
- (headerContentLength != null) ?
- Integer.parseInt(headerContentLength) : 0);
- if (fileInfo.mFileName == null) {
- finalStatus = fileInfo.mStatus;
- request.abort();
- break http_request_loop;
- }
- filename = fileInfo.mFileName;
- stream = fileInfo.mStream;
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + filename);
- }
-
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl._DATA, filename);
- if (headerETag != null) {
- values.put(Constants.ETAG, headerETag);
- }
- if (mimeType != null) {
- values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
- }
- int contentLength = -1;
- if (headerContentLength != null) {
- contentLength = Integer.parseInt(headerContentLength);
- }
- values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, contentLength);
- mContext.getContentResolver().update(contentUri, values, null, null);
- }
+ }
- InputStream entityStream;
- try {
- entityStream = response.getEntity().getContent();
- } catch (IOException ex) {
- if (Constants.LOGX) {
- if (Helpers.isNetworkAvailable(mContext)) {
- Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Up");
- } else {
- Log.i(Constants.TAG, "Get Failed " + mInfo.mId + ", Net Down");
- }
- }
- if (!Helpers.isNetworkAvailable(mContext)) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- countRetry = true;
- } else {
- if (Constants.LOGV) {
- Log.d(Constants.TAG,
- "IOException getting entity for " +
- mInfo.mUri +
- " : " +
- ex);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "IOException getting entity for download " +
- mInfo.mId + " : " + ex);
- }
- finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
- }
- request.abort();
- break http_request_loop;
- }
- for (;;) {
- int bytesRead;
- try {
- bytesRead = entityStream.read(data);
- } catch (IOException ex) {
- if (Constants.LOGX) {
- if (Helpers.isNetworkAvailable(mContext)) {
- Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Up");
- } else {
- Log.i(Constants.TAG, "Read Failed " + mInfo.mId + ", Net Down");
- }
- }
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
- mContext.getContentResolver().update(contentUri, values, null, null);
- if (!mInfo.mNoIntegrity && headerETag == null) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "download IOException for " + mInfo.mUri +
- " : " + ex);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "download IOException for download " +
- mInfo.mId + " : " + ex);
- }
- if (Config.LOGD) {
- Log.d(Constants.TAG,
- "can't resume interrupted download with no ETag");
- }
- finalStatus = Downloads.Impl.STATUS_PRECONDITION_FAILED;
- } else if (!Helpers.isNetworkAvailable(mContext)) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- countRetry = true;
- } else {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "download IOException for " + mInfo.mUri +
- " : " + ex);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "download IOException for download " +
- mInfo.mId + " : " + ex);
- }
- finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
- }
- request.abort();
- break http_request_loop;
- }
- if (bytesRead == -1) { // success
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
- if (headerContentLength == null) {
- values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, bytesSoFar);
- }
- mContext.getContentResolver().update(contentUri, values, null, null);
- if ((headerContentLength != null)
- && (bytesSoFar
- != Integer.parseInt(headerContentLength))) {
- if (!mInfo.mNoIntegrity && headerETag == null) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "mismatched content length " +
- mInfo.mUri);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "mismatched content length for " +
- mInfo.mId);
- }
- finalStatus = Downloads.Impl.STATUS_LENGTH_REQUIRED;
- } else if (!Helpers.isNetworkAvailable(mContext)) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- countRetry = true;
- } else {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "closed socket for " + mInfo.mUri);
- } else if (Config.LOGD) {
- Log.d(Constants.TAG, "closed socket for download " +
- mInfo.mId);
- }
- finalStatus = Downloads.Impl.STATUS_HTTP_DATA_ERROR;
- }
- break http_request_loop;
- }
- break;
- }
- gotData = true;
- for (;;) {
- try {
- if (stream == null) {
- stream = new FileOutputStream(filename, true);
- }
- stream.write(data, 0, bytesRead);
- if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
- && !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
- .equalsIgnoreCase(mimeType)) {
- try {
- stream.close();
- stream = null;
- } catch (IOException ex) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG,
- "exception when closing the file " +
- "during download : " + ex);
- }
- // nothing can really be done if the file can't be closed
- }
- }
- break;
- } catch (IOException ex) {
- if (!Helpers.discardPurgeableFiles(
- mContext, Constants.BUFFER_SIZE)) {
- finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
- break http_request_loop;
- }
- }
- }
- bytesSoFar += bytesRead;
- long now = System.currentTimeMillis();
- if (bytesSoFar - bytesNotified > Constants.MIN_PROGRESS_STEP
- && now - timeLastNotification
- > Constants.MIN_PROGRESS_TIME) {
- ContentValues values = new ContentValues();
- values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, bytesSoFar);
- mContext.getContentResolver().update(
- contentUri, values, null, null);
- bytesNotified = bytesSoFar;
- timeLastNotification = now;
- }
-
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "downloaded " + bytesSoFar + " for " + mInfo.mUri);
- }
- synchronized (mInfo) {
- if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "paused " + mInfo.mUri);
- }
- finalStatus = Downloads.Impl.STATUS_RUNNING_PAUSED;
- request.abort();
- break http_request_loop;
- }
- }
- if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "canceled " + mInfo.mUri);
- } else if (Config.LOGD) {
- // Log.d(Constants.TAG, "canceled id " + mInfo.mId);
- }
- finalStatus = Downloads.Impl.STATUS_CANCELED;
- break http_request_loop;
- }
- }
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
- }
- finalStatus = Downloads.Impl.STATUS_SUCCESS;
- }
- break;
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
}
- } catch (FileNotFoundException ex) {
- if (Config.LOGD) {
- Log.d(Constants.TAG, "FileNotFoundException for " + filename + " : " + ex);
+ finalizeDestinationFile(state);
+ finalStatus = Downloads.Impl.STATUS_SUCCESS;
+ } catch (StopRequest error) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "Aborting request for " + mInfo.mUri, error);
}
+ finalStatus = error.mFinalStatus;
+ // fall through to finally block
+ } catch (FileNotFoundException ex) {
+ Log.d(Constants.TAG, "FileNotFoundException for " + state.mFilename + " : " + ex);
finalStatus = Downloads.Impl.STATUS_FILE_ERROR;
// falls through to the code that reports an error
} catch (RuntimeException ex) { //sometimes the socket code throws unchecked exceptions
@@ -665,69 +213,656 @@ http_request_loop:
client.close();
client = null;
}
+ cleanupDestination(state, finalStatus);
+ notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
+ state.mRedirectCount, state.mGotData, state.mFilename,
+ state.mNewUri, state.mMimeType);
+ }
+ }
+
+ /**
+ * Fully execute a single download request - setup and send the request, handle the response,
+ * and transfer the data to the destination file.
+ */
+ private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
+ throws StopRequest, RetryDownload, FileNotFoundException {
+ InnerState innerState = new InnerState();
+ byte data[] = new byte[Constants.BUFFER_SIZE];
+
+ setupDestinationFile(state, innerState);
+ addRequestHeaders(innerState, request);
+
+ // check just before sending the request to avoid using an invalid connection at all
+ checkConnectivity(state);
+
+ HttpResponse response = sendRequest(state, client, request);
+ handleExceptionalStatus(state, innerState, response);
+
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "received response for " + mInfo.mUri);
+ }
+
+ processResponseHeaders(state, innerState, response);
+ InputStream entityStream = openResponseEntity(state, response);
+ transferData(state, innerState, data, entityStream);
+ }
+
+ /**
+ * Check if current connectivity is valid for this request.
+ */
+ private void checkConnectivity(State state) throws StopRequest {
+ if (!mInfo.canUseNetwork()) {
+ throw new StopRequest(Downloads.Impl.STATUS_RUNNING_PAUSED);
+ }
+ }
+
+ /**
+ * Transfer as much data as possible from the HTTP response to the destination file.
+ * @param data buffer to use to read data
+ * @param entityStream stream for reading the HTTP response entity
+ */
+ private void transferData(State state, InnerState innerState, byte[] data,
+ InputStream entityStream) throws StopRequest {
+ for (;;) {
+ int bytesRead = readFromResponse(state, innerState, data, entityStream);
+ if (bytesRead == -1) { // success, end of stream already reached
+ handleEndOfStream(state, innerState);
+ return;
+ }
+
+ state.mGotData = true;
+ writeDataToDestination(state, data, bytesRead);
+ innerState.mBytesSoFar += bytesRead;
+ reportProgress(state, innerState);
+
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "downloaded " + innerState.mBytesSoFar + " for "
+ + mInfo.mUri);
+ }
+
+ checkPausedOrCanceled(state);
+ }
+ }
+
+ /**
+ * Called after a successful completion to take any necessary action on the downloaded file.
+ */
+ private void finalizeDestinationFile(State state) throws StopRequest {
+ if (isDrmFile(state)) {
+ transferToDrm(state);
+ } else {
+ // make sure the file is readable
+ FileUtils.setPermissions(state.mFilename, 0644, -1, -1);
+ syncDestination(state);
+ }
+ }
+
+ /**
+ * Called just before the thread finishes, regardless of status, to take any necessary action on
+ * the downloaded file.
+ */
+ private void cleanupDestination(State state, int finalStatus) {
+ closeDestination(state);
+ if (state.mFilename != null && Downloads.Impl.isStatusError(finalStatus)) {
+ new File(state.mFilename).delete();
+ state.mFilename = null;
+ }
+ }
+
+ /**
+ * Sync the destination file to storage.
+ */
+ private void syncDestination(State state) {
+ FileOutputStream downloadedFileStream = null;
+ try {
+ downloadedFileStream = new FileOutputStream(state.mFilename, true);
+ downloadedFileStream.getFD().sync();
+ } catch (FileNotFoundException ex) {
+ Log.w(Constants.TAG, "file " + state.mFilename + " not found: " + ex);
+ } catch (SyncFailedException ex) {
+ Log.w(Constants.TAG, "file " + state.mFilename + " sync failed: " + ex);
+ } catch (IOException ex) {
+ Log.w(Constants.TAG, "IOException trying to sync " + state.mFilename + ": " + ex);
+ } catch (RuntimeException ex) {
+ Log.w(Constants.TAG, "exception while syncing file: ", ex);
+ } finally {
+ if(downloadedFileStream != null) {
+ try {
+ downloadedFileStream.close();
+ } catch (IOException ex) {
+ Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
+ } catch (RuntimeException ex) {
+ Log.w(Constants.TAG, "exception while closing file: ", ex);
+ }
+ }
+ }
+ }
+
+ /**
+ * @return true if the current download is a DRM file
+ */
+ private boolean isDrmFile(State state) {
+ return DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(state.mMimeType);
+ }
+
+ /**
+ * Transfer the downloaded destination file to the DRM store.
+ */
+ private void transferToDrm(State state) throws StopRequest {
+ File file = new File(state.mFilename);
+ Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
+ file.delete();
+
+ if (item == null) {
+ Log.w(Constants.TAG, "unable to add file " + state.mFilename + " to DrmProvider");
+ throw new StopRequest(Downloads.Impl.STATUS_UNKNOWN_ERROR);
+ } else {
+ state.mFilename = item.getDataString();
+ state.mMimeType = item.getType();
+ }
+ }
+
+ /**
+ * Close the destination output stream.
+ */
+ private void closeDestination(State state) {
+ try {
+ // close the file
+ if (state.mStream != null) {
+ state.mStream.close();
+ state.mStream = null;
+ }
+ } catch (IOException ex) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
+ }
+ // nothing can really be done if the file can't be closed
+ }
+ }
+
+ /**
+ * Check if the download has been paused or canceled, stopping the request appropriately if it
+ * has been.
+ */
+ private void checkPausedOrCanceled(State state) throws StopRequest {
+ synchronized (mInfo) {
+ if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "paused " + mInfo.mUri);
+ }
+ throw new StopRequest(Downloads.Impl.STATUS_RUNNING_PAUSED);
+ }
+ }
+ if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "canceled " + mInfo.mUri);
+ }
+ throw new StopRequest(Downloads.Impl.STATUS_CANCELED);
+ }
+ }
+
+ /**
+ * Report download progress through the database if necessary.
+ */
+ private void reportProgress(State state, InnerState innerState) {
+ long now = mSystemFacade.currentTimeMillis();
+ if (innerState.mBytesSoFar - innerState.mBytesNotified
+ > Constants.MIN_PROGRESS_STEP
+ && now - innerState.mTimeLastNotification
+ > Constants.MIN_PROGRESS_TIME) {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
+ mContext.getContentResolver().update(
+ state.mContentUri, values, null, null);
+ innerState.mBytesNotified = innerState.mBytesSoFar;
+ innerState.mTimeLastNotification = now;
+ }
+ }
+
+ /**
+ * Write a data buffer to the destination file.
+ * @param data buffer containing the data to write
+ * @param bytesRead how many bytes to write from the buffer
+ */
+ private void writeDataToDestination(State state, byte[] data, int bytesRead)
+ throws StopRequest {
+ for (;;) {
try {
- // close the file
- if (stream != null) {
- stream.close();
+ if (state.mStream == null) {
+ state.mStream = new FileOutputStream(state.mFilename, true);
+ }
+ state.mStream.write(data, 0, bytesRead);
+ if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
+ && !isDrmFile(state)) {
+ closeDestination(state);
}
+ return;
} catch (IOException ex) {
+ if (mInfo.isOnCache()
+ && Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
+ continue;
+ }
+ throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR, ex);
+ }
+ }
+ }
+
+ /**
+ * Called when we've reached the end of the HTTP response stream, to update the database and
+ * check for consistency.
+ */
+ private void handleEndOfStream(State state, InnerState innerState) throws StopRequest {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
+ if (innerState.mHeaderContentLength == null) {
+ values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
+ }
+ mContext.getContentResolver().update(state.mContentUri, values, null, null);
+
+ boolean lengthMismatched = (innerState.mHeaderContentLength != null)
+ && (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
+ if (lengthMismatched) {
+ if (cannotResume(innerState)) {
if (Constants.LOGV) {
- Log.v(Constants.TAG, "exception when closing the file after download : " + ex);
+ Log.d(Constants.TAG, "mismatched content length " +
+ mInfo.mUri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "mismatched content length for " +
+ mInfo.mId);
}
- // nothing can really be done if the file can't be closed
+ throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME);
+ } else {
+ throw new StopRequest(handleHttpError(state, "closed socket"));
}
- if (filename != null) {
- // if the download wasn't successful, delete the file
- if (Downloads.Impl.isStatusError(finalStatus)) {
- new File(filename).delete();
- filename = null;
- } else if (Downloads.Impl.isStatusSuccess(finalStatus) &&
- DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING
- .equalsIgnoreCase(mimeType)) {
- // transfer the file to the DRM content provider
- File file = new File(filename);
- Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
- if (item == null) {
- Log.w(Constants.TAG, "unable to add file " + filename + " to DrmProvider");
- finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
- } else {
- filename = item.getDataString();
- mimeType = item.getType();
- }
-
- file.delete();
- } else if (Downloads.Impl.isStatusSuccess(finalStatus)) {
- // make sure the file is readable
- FileUtils.setPermissions(filename, 0644, -1, -1);
-
- // Sync to storage after completion
- FileOutputStream downloadedFileStream = null;
- try {
- downloadedFileStream = new FileOutputStream(filename, true);
- downloadedFileStream.getFD().sync();
- } catch (FileNotFoundException ex) {
- Log.w(Constants.TAG, "file " + filename + " not found: " + ex);
- } catch (SyncFailedException ex) {
- Log.w(Constants.TAG, "file " + filename + " sync failed: " + ex);
- } catch (IOException ex) {
- Log.w(Constants.TAG, "IOException trying to sync " + filename + ": " + ex);
- } catch (RuntimeException ex) {
- Log.w(Constants.TAG, "exception while syncing file: ", ex);
- } finally {
- if(downloadedFileStream != null) {
- try {
- downloadedFileStream.close();
- } catch (IOException ex) {
- Log.w(Constants.TAG, "IOException while closing synced file: ", ex);
- } catch (RuntimeException ex) {
- Log.w(Constants.TAG, "exception while closing file: ", ex);
- }
- }
+ }
+ }
+
+ private boolean cannotResume(InnerState innerState) {
+ return innerState.mBytesSoFar > 0 && !mInfo.mNoIntegrity && innerState.mHeaderETag == null;
+ }
+
+ /**
+ * Read some data from the HTTP response stream, handling I/O errors.
+ * @param data buffer to use to read data
+ * @param entityStream stream for reading the HTTP response entity
+ * @return the number of bytes actually read or -1 if the end of the stream has been reached
+ */
+ private int readFromResponse(State state, InnerState innerState, byte[] data,
+ InputStream entityStream) throws StopRequest {
+ try {
+ return entityStream.read(data);
+ } catch (IOException ex) {
+ logNetworkState();
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
+ mContext.getContentResolver().update(state.mContentUri, values, null, null);
+ if (cannotResume(innerState)) {
+ Log.d(Constants.TAG, "download IOException for download " + mInfo.mId, ex);
+ Log.d(Constants.TAG, "can't resume interrupted download with no ETag");
+ throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME, ex);
+ } else {
+ throw new StopRequest(handleHttpError(state, "download IOException"), ex);
+ }
+ }
+ }
+
+ /**
+ * Open a stream for the HTTP response entity, handling I/O errors.
+ * @return an InputStream to read the response entity
+ */
+ private InputStream openResponseEntity(State state, HttpResponse response)
+ throws StopRequest {
+ try {
+ return response.getEntity().getContent();
+ } catch (IOException ex) {
+ logNetworkState();
+ throw new StopRequest(handleHttpError(state, "IOException getting entity"), ex);
+ }
+ }
+
+ private void logNetworkState() {
+ if (Constants.LOGX) {
+ Log.i(Constants.TAG,
+ "Net " + (Helpers.isNetworkAvailable(mSystemFacade) ? "Up" : "Down"));
+ }
+ }
+
+ /**
+ * Read HTTP response headers and take appropriate action, including setting up the destination
+ * file and updating the database.
+ */
+ private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
+ throws StopRequest, FileNotFoundException {
+ if (innerState.mContinuingDownload) {
+ // ignore response headers on resume requests
+ return;
+ }
+
+ readResponseHeaders(state, innerState, response);
+
+ DownloadFileInfo fileInfo = Helpers.generateSaveFile(
+ mContext,
+ mInfo.mUri,
+ mInfo.mHint,
+ innerState.mHeaderContentDisposition,
+ innerState.mHeaderContentLocation,
+ state.mMimeType,
+ mInfo.mDestination,
+ (innerState.mHeaderContentLength != null) ?
+ Long.parseLong(innerState.mHeaderContentLength) : 0,
+ mInfo.mIsPublicApi);
+ if (fileInfo.mFileName == null) {
+ throw new StopRequest(fileInfo.mStatus);
+ }
+ state.mFilename = fileInfo.mFileName;
+ state.mStream = fileInfo.mStream;
+ if (Constants.LOGV) {
+ Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
+ }
+
+ updateDatabaseFromHeaders(state, innerState);
+ // check connectivity again now that we know the total size
+ checkConnectivity(state);
+ }
+
+ /**
+ * Update necessary database fields based on values of HTTP response headers that have been
+ * read.
+ */
+ private void updateDatabaseFromHeaders(State state, InnerState innerState) {
+ ContentValues values = new ContentValues();
+ values.put(Downloads.Impl._DATA, state.mFilename);
+ if (innerState.mHeaderETag != null) {
+ values.put(Constants.ETAG, innerState.mHeaderETag);
+ }
+ if (state.mMimeType != null) {
+ values.put(Downloads.Impl.COLUMN_MIME_TYPE, state.mMimeType);
+ }
+ values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
+ mContext.getContentResolver().update(state.mContentUri, values, null, null);
+ }
+
+ /**
+ * Read headers from the HTTP response and store them into local state.
+ */
+ private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
+ throws StopRequest {
+ Header header = response.getFirstHeader("Content-Disposition");
+ if (header != null) {
+ innerState.mHeaderContentDisposition = header.getValue();
+ }
+ header = response.getFirstHeader("Content-Location");
+ if (header != null) {
+ innerState.mHeaderContentLocation = header.getValue();
+ }
+ if (state.mMimeType == null) {
+ header = response.getFirstHeader("Content-Type");
+ if (header != null) {
+ state.mMimeType = sanitizeMimeType(header.getValue());
+ }
+ }
+ header = response.getFirstHeader("ETag");
+ if (header != null) {
+ innerState.mHeaderETag = header.getValue();
+ }
+ String headerTransferEncoding = null;
+ header = response.getFirstHeader("Transfer-Encoding");
+ if (header != null) {
+ headerTransferEncoding = header.getValue();
+ }
+ if (headerTransferEncoding == null) {
+ header = response.getFirstHeader("Content-Length");
+ if (header != null) {
+ innerState.mHeaderContentLength = header.getValue();
+ mInfo.mTotalBytes = Long.parseLong(innerState.mHeaderContentLength);
+ }
+ } else {
+ // Ignore content-length with transfer-encoding - 2616 4.4 3
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG,
+ "ignoring content-length because of xfer-encoding");
+ }
+ }
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Content-Disposition: " +
+ innerState.mHeaderContentDisposition);
+ Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
+ Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
+ Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
+ Log.v(Constants.TAG, "ETag: " + innerState.mHeaderETag);
+ Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
+ }
+
+ boolean noSizeInfo = innerState.mHeaderContentLength == null
+ && (headerTransferEncoding == null
+ || !headerTransferEncoding.equalsIgnoreCase("chunked"));
+ if (!mInfo.mNoIntegrity && noSizeInfo) {
+ Log.d(Constants.TAG, "can't know size of download, giving up");
+ throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR);
+ }
+ }
+
+ /**
+ * Check the HTTP response status and handle anything unusual (e.g. not 200/206).
+ */
+ private void handleExceptionalStatus(State state, InnerState innerState, HttpResponse response)
+ throws StopRequest, RetryDownload {
+ int statusCode = response.getStatusLine().getStatusCode();
+ if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
+ handleServiceUnavailable(state, response);
+ }
+ if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 307) {
+ handleRedirect(state, response, statusCode);
+ }
+
+ int expectedStatus = innerState.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
+ if (statusCode != expectedStatus) {
+ handleOtherStatus(state, innerState, statusCode);
+ }
+ }
+
+ /**
+ * Handle a status that we don't know how to deal with properly.
+ */
+ private void handleOtherStatus(State state, InnerState innerState, int statusCode)
+ throws StopRequest {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "http error " + statusCode + " for " + mInfo.mUri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "http error " + statusCode + " for download " +
+ mInfo.mId);
+ }
+ int finalStatus;
+ if (Downloads.Impl.isStatusError(statusCode)) {
+ finalStatus = statusCode;
+ } else if (statusCode >= 300 && statusCode < 400) {
+ finalStatus = Downloads.Impl.STATUS_UNHANDLED_REDIRECT;
+ } else if (innerState.mContinuingDownload && statusCode == Downloads.Impl.STATUS_SUCCESS) {
+ finalStatus = Downloads.Impl.STATUS_CANNOT_RESUME;
+ } else {
+ finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
+ }
+ throw new StopRequest(finalStatus);
+ }
+
+ /**
+ * Handle a 3xx redirect status.
+ */
+ private void handleRedirect(State state, HttpResponse response, int statusCode)
+ throws StopRequest, RetryDownload {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
+ }
+ if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId +
+ " at " + mInfo.mUri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "too many redirects for download " + mInfo.mId);
+ }
+ throw new StopRequest(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS);
+ }
+ Header header = response.getFirstHeader("Location");
+ if (header == null) {
+ return;
+ }
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Location :" + header.getValue());
+ }
+
+ String newUri;
+ try {
+ newUri = new URI(mInfo.mUri).resolve(new URI(header.getValue())).toString();
+ } catch(URISyntaxException ex) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
+ + " for " + mInfo.mUri);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG,
+ "Couldn't resolve redirect URI for download " +
+ mInfo.mId);
+ }
+ throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR);
+ }
+ ++state.mRedirectCount;
+ state.mRequestUri = newUri;
+ if (statusCode == 301 || statusCode == 303) {
+ // use the new URI for all future requests (should a retry/resume be necessary)
+ state.mNewUri = newUri;
+ }
+ throw new RetryDownload();
+ }
+
+ /**
+ * Handle a 503 Service Unavailable status by processing the Retry-After header.
+ */
+ private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "got HTTP response code 503");
+ }
+ state.mCountRetry = true;
+ Header header = response.getFirstHeader("Retry-After");
+ if (header != null) {
+ try {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "Retry-After :" + header.getValue());
+ }
+ state.mRetryAfter = Integer.parseInt(header.getValue());
+ if (state.mRetryAfter < 0) {
+ state.mRetryAfter = 0;
+ } else {
+ if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
+ state.mRetryAfter = Constants.MIN_RETRY_AFTER;
+ } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
+ state.mRetryAfter = Constants.MAX_RETRY_AFTER;
+ }
+ state.mRetryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
+ state.mRetryAfter *= 1000;
+ }
+ } catch (NumberFormatException ex) {
+ // ignored - retryAfter stays 0 in this case.
+ }
+ }
+ throw new StopRequest(Downloads.Impl.STATUS_RUNNING_PAUSED);
+ }
+
+ /**
+ * Send the request to the server, handling any I/O exceptions.
+ */
+ private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
+ throws StopRequest {
+ try {
+ return client.execute(request);
+ } catch (IllegalArgumentException ex) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, "Arg exception trying to execute request for " +
+ mInfo.mUri + " : " + ex);
+ } else if (Config.LOGD) {
+ Log.d(Constants.TAG, "Arg exception trying to execute request for " +
+ mInfo.mId + " : " + ex);
+ }
+ throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR, ex);
+ } catch (IOException ex) {
+ logNetworkState();
+ throw new StopRequest(handleHttpError(state, "IOException trying to execute request"),
+ ex);
+ }
+ }
+
+ /**
+ * @return the final status for this attempt
+ */
+ private int handleHttpError(State state, String message) {
+ if (Constants.LOGV) {
+ Log.d(Constants.TAG, message + " for " + mInfo.mUri);
+ }
+
+ if (!Helpers.isNetworkAvailable(mSystemFacade)) {
+ return Downloads.Impl.STATUS_RUNNING_PAUSED;
+ } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
+ state.mCountRetry = true;
+ return Downloads.Impl.STATUS_RUNNING_PAUSED;
+ } else {
+ Log.d(Constants.TAG, "reached max retries: " + message + " for " + mInfo.mId);
+ return Downloads.Impl.STATUS_HTTP_DATA_ERROR;
+ }
+ }
+
+ /**
+ * Prepare the destination file to receive data. If the file already exists, we'll set up
+ * appropriately for resumption.
+ */
+ private void setupDestinationFile(State state, InnerState innerState)
+ throws StopRequest, FileNotFoundException {
+ if (state.mFilename != null) { // only true if we've already run a thread for this download
+ if (!Helpers.isFilenameValid(state.mFilename)) {
+ throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR);
+ }
+ // We're resuming a download that got interrupted
+ File f = new File(state.mFilename);
+ if (f.exists()) {
+ long fileLength = f.length();
+ if (fileLength == 0) {
+ // The download hadn't actually started, we can restart from scratch
+ f.delete();
+ state.mFilename = null;
+ } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
+ // This should've been caught upon failure
+ Log.wtf(Constants.TAG, "Trying to resume a download that can't be resumed");
+ f.delete();
+ throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME);
+ } else {
+ // All right, we'll be able to resume this download
+ state.mStream = new FileOutputStream(state.mFilename, true);
+ innerState.mBytesSoFar = (int) fileLength;
+ if (mInfo.mTotalBytes != -1) {
+ innerState.mHeaderContentLength = Long.toString(mInfo.mTotalBytes);
}
+ innerState.mHeaderETag = mInfo.mETag;
+ innerState.mContinuingDownload = true;
}
}
- notifyDownloadCompleted(finalStatus, countRetry, retryAfter, redirectCount,
- gotData, filename, newUri, mimeType);
+ }
+
+ if (state.mStream != null && mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
+ && !isDrmFile(state)) {
+ closeDestination(state);
+ }
+ }
+
+ /**
+ * Add custom headers for this download to the HTTP request.
+ */
+ private void addRequestHeaders(InnerState innerState, HttpGet request) {
+ for (Map.Entry<String, String> header : mInfo.getHeaders().entrySet()) {
+ request.addHeader(header.getKey(), header.getValue());
+ }
+
+ if (innerState.mContinuingDownload) {
+ if (innerState.mHeaderETag != null) {
+ request.addHeader("If-Match", innerState.mHeaderETag);
+ }
+ request.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
}
}
@@ -754,7 +889,7 @@ http_request_loop:
values.put(Downloads.Impl.COLUMN_URI, uri);
}
values.put(Downloads.Impl.COLUMN_MIME_TYPE, mimeType);
- values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, System.currentTimeMillis());
+ values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, mSystemFacade.currentTimeMillis());
values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter + (redirectCount << 28));
if (!countRetry) {
values.put(Constants.FAILED_CONNECTIONS, 0);
@@ -774,7 +909,7 @@ http_request_loop:
*/
private void notifyThroughIntent() {
Uri uri = Uri.parse(Downloads.Impl.CONTENT_URI + "/" + mInfo.mId);
- mInfo.sendIntentIfRequested(uri, mContext);
+ mInfo.sendIntentIfRequested(uri);
}
/**
@@ -784,7 +919,7 @@ http_request_loop:
* @return null if mimeType was null. Otherwise a string which represents a
* single mimetype in lowercase and with surrounding whitespaces trimmed.
*/
- private String sanitizeMimeType(String mimeType) {
+ private static String sanitizeMimeType(String mimeType) {
try {
mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 0c256a75..794bb062 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -23,19 +23,16 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.drm.mobile1.DrmRawContent;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Environment;
import android.os.StatFs;
import android.os.SystemClock;
import android.provider.Downloads;
-import android.telephony.TelephonyManager;
import android.util.Config;
import android.util.Log;
import android.webkit.MimeTypeMap;
-import java.io.File;
+import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.Random;
@@ -76,6 +73,17 @@ public class Helpers {
}
/**
+ * Exception thrown from methods called by generateSaveFile() for any fatal error.
+ */
+ private static class GenerateSaveFileError extends Exception {
+ int mStatus;
+
+ public GenerateSaveFileError(int status) {
+ mStatus = status;
+ }
+ }
+
+ /**
* Creates a filename (where the file should be saved) from a uri.
*/
public static DownloadFileInfo generateSaveFile(
@@ -86,18 +94,82 @@ public class Helpers {
String contentLocation,
String mimeType,
int destination,
- int contentLength) throws FileNotFoundException {
+ long contentLength,
+ boolean isPublicApi) throws FileNotFoundException {
+
+ if (!canHandleDownload(context, mimeType, destination, isPublicApi)) {
+ return new DownloadFileInfo(null, null, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
+ }
+
+ String fullFilename;
+ try {
+ if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
+ fullFilename = getPathForFileUri(hint);
+ } else {
+ fullFilename = chooseFullPath(context, url, hint, contentDisposition,
+ contentLocation, mimeType, destination,
+ contentLength);
+ }
+ } catch (GenerateSaveFileError exc) {
+ return new DownloadFileInfo(null, null, exc.mStatus);
+ }
+
+ return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
+ }
+
+ private static String getPathForFileUri(String hint) throws GenerateSaveFileError {
+ String path = Uri.parse(hint).getSchemeSpecificPart();
+ if (new File(path).exists()) {
+ Log.d(Constants.TAG, "File already exists: " + path);
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR);
+ }
+
+ return path;
+ }
+
+ private static String chooseFullPath(Context context, String url, String hint,
+ String contentDisposition, String contentLocation,
+ String mimeType, int destination, long contentLength)
+ throws GenerateSaveFileError {
+ File base = locateDestinationDirectory(context, mimeType, destination, contentLength);
+ String filename = chooseFilename(url, hint, contentDisposition, contentLocation,
+ destination);
+
+ // Split filename between base and extension
+ // Add an extension if filename does not have one
+ String extension = null;
+ int dotIndex = filename.indexOf('.');
+ if (dotIndex < 0) {
+ extension = chooseExtensionFromMimeType(mimeType, true);
+ } else {
+ extension = chooseExtensionFromFilename(mimeType, destination, filename, dotIndex);
+ filename = filename.substring(0, dotIndex);
+ }
+
+ boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
+
+ filename = base.getPath() + File.separator + filename;
+
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "target file: " + filename + extension);
+ }
+
+ return chooseUniqueFilename(destination, filename, extension, recoveryDir);
+ }
+
+ private static boolean canHandleDownload(Context context, String mimeType, int destination,
+ boolean isPublicApi) {
+ if (isPublicApi) {
+ return true;
+ }
- /*
- * Don't download files that we won't be able to handle
- */
if (destination == Downloads.Impl.DESTINATION_EXTERNAL
|| destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
if (mimeType == null) {
if (Config.LOGD) {
Log.d(Constants.TAG, "external download with no mime type not allowed");
}
- return new DownloadFileInfo(null, null, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
+ return false;
}
if (!DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
// Check to see if we are allowed to download this file. Only files
@@ -121,32 +193,19 @@ public class Helpers {
if (Config.LOGD) {
Log.d(Constants.TAG, "no handler found for type " + mimeType);
}
- return new DownloadFileInfo(null, null, Downloads.Impl.STATUS_NOT_ACCEPTABLE);
+ return false;
}
}
}
- String filename = chooseFilename(
- url, hint, contentDisposition, contentLocation, destination);
-
- // Split filename between base and extension
- // Add an extension if filename does not have one
- String extension = null;
- int dotIndex = filename.indexOf('.');
- if (dotIndex < 0) {
- extension = chooseExtensionFromMimeType(mimeType, true);
- } else {
- extension = chooseExtensionFromFilename(
- mimeType, destination, filename, dotIndex);
- filename = filename.substring(0, dotIndex);
- }
-
- /*
- * Locate the directory where the file will be saved
- */
+ return true;
+ }
+ private static File locateDestinationDirectory(Context context, String mimeType,
+ int destination, long contentLength)
+ throws GenerateSaveFileError {
File base = null;
StatFs stat = null;
- // DRM messages should be temporarily stored internally and then passed to
+ // DRM messages should be temporarily stored internally and then passed to
// the DRM content provider
if (destination == Downloads.Impl.DESTINATION_CACHE_PARTITION
|| destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
@@ -170,8 +229,7 @@ public class Helpers {
Log.d(Constants.TAG,
"download aborted - not enough free space in internal storage");
}
- return new DownloadFileInfo(null, null,
- Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR);
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR);
} else {
// Recalculate available space and try again.
stat.restat(base.getPath());
@@ -192,8 +250,7 @@ public class Helpers {
if (Config.LOGD) {
Log.d(Constants.TAG, "download aborted - not enough free space");
}
- return new DownloadFileInfo(null, null,
- Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR);
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR);
}
base = new File(root + Constants.DEFAULT_DL_SUBDIR);
@@ -204,35 +261,17 @@ public class Helpers {
Log.d(Constants.TAG, "download aborted - can't create base directory "
+ base.getPath());
}
- return new DownloadFileInfo(null, null, Downloads.Impl.STATUS_FILE_ERROR);
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR);
}
} else {
// No SD card found.
if (Config.LOGD) {
Log.d(Constants.TAG, "download aborted - no external storage");
}
- return new DownloadFileInfo(null, null,
- Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR);
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR);
}
- boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
-
- filename = base.getPath() + File.separator + filename;
-
- /*
- * Generate a unique filename, create the file, return it.
- */
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "target file: " + filename + extension);
- }
-
- String fullFilename = chooseUniqueFilename(
- destination, filename, extension, recoveryDir);
- if (fullFilename != null) {
- return new DownloadFileInfo(fullFilename, new FileOutputStream(fullFilename), 0);
- } else {
- return new DownloadFileInfo(null, null, Downloads.Impl.STATUS_FILE_ERROR);
- }
+ return base;
}
private static String chooseFilename(String url, String hint, String contentDisposition,
@@ -384,7 +423,7 @@ public class Helpers {
}
private static String chooseUniqueFilename(int destination, String filename,
- String extension, boolean recoveryDir) {
+ String extension, boolean recoveryDir) throws GenerateSaveFileError {
String fullFilename = filename + extension;
if (!new File(fullFilename).exists()
&& (!recoveryDir ||
@@ -421,7 +460,7 @@ public class Helpers {
sequence += sRandom.nextInt(magnitude) + 1;
}
}
- return null;
+ throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR);
}
/**
@@ -473,68 +512,17 @@ public class Helpers {
/**
* Returns whether the network is available
*/
- public static boolean isNetworkAvailable(Context context) {
- ConnectivityManager connectivity =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (connectivity == null) {
- Log.w(Constants.TAG, "couldn't get connectivity manager");
- } else {
- NetworkInfo[] info = connectivity.getAllNetworkInfo();
- if (info != null) {
- for (int i = 0; i < info.length; i++) {
- if (info[i].getState() == NetworkInfo.State.CONNECTED) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "network is available");
- }
- return true;
- }
- }
- }
- }
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "network is not available");
- }
- return false;
- }
-
- /**
- * Returns whether the network is roaming
- */
- public static boolean isNetworkRoaming(Context context) {
- ConnectivityManager connectivity =
- (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
- if (connectivity == null) {
- Log.w(Constants.TAG, "couldn't get connectivity manager");
- } else {
- NetworkInfo info = connectivity.getActiveNetworkInfo();
- if (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE) {
- if (TelephonyManager.getDefault().isNetworkRoaming()) {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "network is roaming");
- }
- return true;
- } else {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "network is not roaming");
- }
- }
- } else {
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "not using mobile network");
- }
- }
- }
- return false;
+ public static boolean isNetworkAvailable(SystemFacade system) {
+ return system.getActiveNetworkType() != null;
}
/**
* Checks whether the filename looks legitimate
*/
public static boolean isFilenameValid(String filename) {
- File dir = new File(filename).getParentFile();
- return dir.equals(Environment.getDownloadCacheDirectory())
- || dir.equals(new File(Environment.getExternalStorageDirectory()
- + Constants.DEFAULT_DL_SUBDIR));
+ filename = filename.replaceFirst("/+", "/"); // normalize leading slashes
+ return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
+ || filename.startsWith(Environment.getExternalStorageDirectory().toString());
}
/**
@@ -781,7 +769,7 @@ public class Helpers {
}
// anything we don't recognize
- throw new IllegalArgumentException("illegal character");
+ throw new IllegalArgumentException("illegal character: " + chars[mOffset]);
}
private static final boolean isIdentifierStart(char c) {
diff --git a/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java
new file mode 100644
index 00000000..710da10d
--- /dev/null
+++ b/src/com/android/providers/downloads/RealSystemFacade.java
@@ -0,0 +1,102 @@
+package com.android.providers.downloads;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+
+class RealSystemFacade implements SystemFacade {
+ private Context mContext;
+ private NotificationManager mNotificationManager;
+
+ public RealSystemFacade(Context context) {
+ mContext = context;
+ mNotificationManager = (NotificationManager)
+ mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ public long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ public Integer getActiveNetworkType() {
+ ConnectivityManager connectivity =
+ (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (connectivity == null) {
+ Log.w(Constants.TAG, "couldn't get connectivity manager");
+ return null;
+ }
+
+ NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
+ if (activeInfo == null) {
+ if (Constants.LOGVV) {
+ Log.v(Constants.TAG, "network is not available");
+ }
+ return null;
+ }
+ return activeInfo.getType();
+ }
+
+ public boolean isNetworkRoaming() {
+ ConnectivityManager connectivity =
+ (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (connectivity == null) {
+ Log.w(Constants.TAG, "couldn't get connectivity manager");
+ return false;
+ }
+
+ NetworkInfo info = connectivity.getActiveNetworkInfo();
+ boolean isMobile = (info != null && info.getType() == ConnectivityManager.TYPE_MOBILE);
+ boolean isRoaming = isMobile && TelephonyManager.getDefault().isNetworkRoaming();
+ if (Constants.LOGVV && isRoaming) {
+ Log.v(Constants.TAG, "network is roaming");
+ }
+ return isRoaming;
+ }
+
+ public Long getMaxBytesOverMobile() {
+ try {
+ return Settings.Secure.getLong(mContext.getContentResolver(),
+ Settings.Secure.DOWNLOAD_MAX_BYTES_OVER_MOBILE);
+ } catch (SettingNotFoundException exc) {
+ return null;
+ }
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent) {
+ mContext.sendBroadcast(intent);
+ }
+
+ @Override
+ public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
+ return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
+ }
+
+ @Override
+ public void postNotification(int id, Notification notification) {
+ mNotificationManager.notify(id, notification);
+ }
+
+ @Override
+ public void cancelNotification(int id) {
+ mNotificationManager.cancel(id);
+ }
+
+ @Override
+ public void cancelAllNotifications() {
+ mNotificationManager.cancelAll();
+ }
+
+ @Override
+ public void startThread(Thread thread) {
+ thread.start();
+ }
+}
diff --git a/src/com/android/providers/downloads/SystemFacade.java b/src/com/android/providers/downloads/SystemFacade.java
new file mode 100644
index 00000000..c1941692
--- /dev/null
+++ b/src/com/android/providers/downloads/SystemFacade.java
@@ -0,0 +1,61 @@
+
+package com.android.providers.downloads;
+
+import android.app.Notification;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+
+
+interface SystemFacade {
+ /**
+ * @see System#currentTimeMillis()
+ */
+ public long currentTimeMillis();
+
+ /**
+ * @return Network type (as in ConnectivityManager.TYPE_*) of currently active network, or null
+ * if there's no active connection.
+ */
+ public Integer getActiveNetworkType();
+
+ /**
+ * @see android.telephony.TelephonyManager#isNetworkRoaming
+ */
+ public boolean isNetworkRoaming();
+
+ /**
+ * @return maximum size, in bytes, of downloads that may go over a mobile connection; or null if
+ * there's no limit
+ */
+ public Long getMaxBytesOverMobile();
+
+ /**
+ * Send a broadcast intent.
+ */
+ public void sendBroadcast(Intent intent);
+
+ /**
+ * Returns true if the specified UID owns the specified package name.
+ */
+ public boolean userOwnsPackage(int uid, String pckg) throws NameNotFoundException;
+
+ /**
+ * Post a system notification to the NotificationManager.
+ */
+ public void postNotification(int id, Notification notification);
+
+ /**
+ * Cancel a system notification.
+ */
+ public void cancelNotification(int id);
+
+ /**
+ * Cancel all system notifications.
+ */
+ public void cancelAllNotifications();
+
+ /**
+ * Start a thread.
+ */
+ public void startThread(Thread thread);
+}