summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml6
-rw-r--r--res/layout/status_bar_ongoing_event_progress_bar.xml8
-rw-r--r--res/values-es-rUS-xlarge/strings.xml10
-rw-r--r--src/com/android/providers/downloads/Constants.java11
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java72
-rw-r--r--src/com/android/providers/downloads/DownloadNotification.java6
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java95
-rw-r--r--src/com/android/providers/downloads/DownloadReceiver.java13
-rw-r--r--src/com/android/providers/downloads/DownloadService.java133
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java228
-rw-r--r--src/com/android/providers/downloads/Helpers.java290
-rw-r--r--src/com/android/providers/downloads/RealSystemFacade.java15
-rw-r--r--src/com/android/providers/downloads/StopRequestException.java37
-rw-r--r--src/com/android/providers/downloads/StorageManager.java466
-rw-r--r--tests/AndroidManifest.xml2
-rw-r--r--tests/public_api_access/src/com/android/providers/downloads/public_api_access_tests/PublicApiAccessTest.java2
-rw-r--r--tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java29
-rw-r--r--tests/src/com/android/providers/downloads/AbstractPublicApiTest.java75
-rw-r--r--tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java32
-rw-r--r--tests/src/com/android/providers/downloads/FakeSystemFacade.java19
-rw-r--r--tests/src/com/android/providers/downloads/HelpersTest.java53
-rw-r--r--tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java56
-rw-r--r--tests/src/tests/http/MockResponse.java11
-rw-r--r--tests/src/tests/http/MockWebServer.java53
-rw-r--r--ui/AndroidManifest.xml2
-rw-r--r--ui/res/mipmap-hdpi/ic_launcher_download.png (renamed from ui/res/drawable-hdpi/ic_launcher_download.png)bin2561 -> 2561 bytes
-rw-r--r--ui/res/mipmap-mdpi/ic_launcher_download.png (renamed from ui/res/drawable-mdpi/ic_launcher_download.png)bin1818 -> 1818 bytes
-rw-r--r--ui/res/values-es-rUS-xlarge/strings.xml6
-rw-r--r--ui/res/values-ko/strings.xml4
-rw-r--r--ui/src/com/android/providers/downloads/ui/DownloadList.java39
30 files changed, 1145 insertions, 628 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0db696b7..2e6a5234 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -56,12 +56,14 @@
<application android:process="android.process.media"
android:label="@string/app_label">
<provider android:name=".DownloadProvider"
- android:authorities="downloads"
- android:permission="android.permission.ACCESS_ALL_DOWNLOADS">
+ android:authorities="downloads">
<!-- Anyone can access /my_downloads, the provider internally restricts access by UID for
these URIs -->
<path-permission android:pathPrefix="/my_downloads"
android:permission="android.permission.INTERNET"/>
+ <!-- to access /all_downloads, ACCESS_ALL_DOWNLOADS permission is required -->
+ <path-permission android:pathPrefix="/all_downloads"
+ android:permission="android.permission.ACCESS_ALL_DOWNLOADS"/>
<!-- Temporary, for backwards compatibility -->
<path-permission android:pathPrefix="/download"
android:permission="android.permission.INTERNET"/>
diff --git a/res/layout/status_bar_ongoing_event_progress_bar.xml b/res/layout/status_bar_ongoing_event_progress_bar.xml
index 2a4d7e6c..5b19a928 100644
--- a/res/layout/status_bar_ongoing_event_progress_bar.xml
+++ b/res/layout/status_bar_ongoing_event_progress_bar.xml
@@ -36,7 +36,6 @@
android:orientation="vertical"
android:paddingTop="8dp"
android:focusable="true"
- android:clickable="true"
>
<ImageView
android:id="@+id/appIcon"
@@ -44,6 +43,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@android:drawable/sym_def_app_icon"
+ android:paddingLeft="15dp"
/>
<TextView android:id="@+id/progress_text"
android:layout_width="wrap_content"
@@ -59,14 +59,12 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:focusable="true"
- android:clickable="true"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:focusable="true"
- android:clickable="true"
android:layout_alignParentTop="true"
android:paddingTop="10dp"
>
@@ -75,14 +73,14 @@
android:layout_height="wrap_content"
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
android:singleLine="true"
- android:paddingLeft="2dp"
+ android:paddingLeft="20dp"
/>
<TextView android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@android:style/TextAppearance.StatusBar.EventContent"
android:singleLine="true"
- android:paddingLeft="5dp"
+ android:paddingLeft="20dp"
/>
</LinearLayout>
<!-- Only one of progress_bar and paused_text will be visible. -->
diff --git a/res/values-es-rUS-xlarge/strings.xml b/res/values-es-rUS-xlarge/strings.xml
new file mode 100644
index 00000000..b64a2d2c
--- /dev/null
+++ b/res/values-es-rUS-xlarge/strings.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- XL -->
+ <string name="app_label" msgid="8975892289137558042">"Administrador de descarga"</string>
+ <!-- XL -->
+ <string name="wifi_required_title" msgid="1552884667970728815">"La descarga es demasiado grande para una red móvil"</string>
+ <!-- XL -->
+ <string name="wifi_required_body" msgid="1306428181581336527">"Debes usar Wi-Fi para completar esta <xliff:g id="SIZE">%s </xliff:g>descarga. "\n\n"Haz clic en <xliff:g id="QUEUE_TEXT">%s </xliff:g> para iniciar esta descarga la próxima vez que te conectes a una red de Wi-Fi."</string>
+</resources>
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index 5cf13531..6419a5e6 100644
--- a/src/com/android/providers/downloads/Constants.java
+++ b/src/com/android/providers/downloads/Constants.java
@@ -16,7 +16,7 @@
package com.android.providers.downloads;
-import android.util.Config;
+import android.os.Environment;
import android.util.Log;
/**
@@ -80,7 +80,7 @@ public class Constants {
public static final String FILENAME_SEQUENCE_SEPARATOR = "-";
/** Where we store downloaded files on the external storage */
- public static final String DEFAULT_DL_SUBDIR = "/download";
+ public static final String DEFAULT_DL_SUBDIR = "/" + Environment.DIRECTORY_DOWNLOADS;
/** A magic filename that is allowed to exist within the system cache */
public static final String KNOWN_SPURIOUS_FILENAME = "lost+found";
@@ -144,11 +144,10 @@ public class Constants {
static final boolean LOGX = false;
/** Enable verbose logging - use with "setprop log.tag.DownloadManager VERBOSE" */
- private static final boolean LOCAL_LOGV = false;
- public static final boolean LOGV = Config.LOGV
- || (Config.LOGD && LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE));
+ private static final boolean LOCAL_LOGV = true; // STOPSHIP change this to false before shipping
+ public static final boolean LOGV = LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE);
/** Enable super-verbose logging */
- private static final boolean LOCAL_LOGVV = false;
+ private static final boolean LOCAL_LOGVV = true; // STOPSHIP change this to false before shipping
public static final boolean LOGVV = LOCAL_LOGVV && LOGV;
}
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index 7b291683..28a38b3b 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -22,7 +22,6 @@ import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
-import android.database.CharArrayBuffer;
import android.database.Cursor;
import android.drm.mobile1.DrmRawContent;
import android.net.ConnectivityManager;
@@ -45,8 +44,6 @@ public class DownloadInfo {
public static class Reader {
private ContentResolver mResolver;
private Cursor mCursor;
- private CharArrayBuffer mOldChars;
- private CharArrayBuffer mNewChars;
public Reader(ContentResolver resolver, Cursor cursor) {
mResolver = resolver;
@@ -62,11 +59,11 @@ public class DownloadInfo {
public void updateFromDatabase(DownloadInfo info) {
info.mId = getLong(Downloads.Impl._ID);
- info.mUri = getString(info.mUri, Downloads.Impl.COLUMN_URI);
+ info.mUri = getString(Downloads.Impl.COLUMN_URI);
info.mNoIntegrity = getInt(Downloads.Impl.COLUMN_NO_INTEGRITY) == 1;
- info.mHint = getString(info.mHint, Downloads.Impl.COLUMN_FILE_NAME_HINT);
- info.mFileName = getString(info.mFileName, Downloads.Impl._DATA);
- info.mMimeType = getString(info.mMimeType, Downloads.Impl.COLUMN_MIME_TYPE);
+ info.mHint = getString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
+ info.mFileName = getString(Downloads.Impl._DATA);
+ info.mMimeType = getString(Downloads.Impl.COLUMN_MIME_TYPE);
info.mDestination = getInt(Downloads.Impl.COLUMN_DESTINATION);
info.mVisibility = getInt(Downloads.Impl.COLUMN_VISIBILITY);
info.mStatus = getInt(Downloads.Impl.COLUMN_STATUS);
@@ -74,24 +71,23 @@ public class DownloadInfo {
int retryRedirect = getInt(Constants.RETRY_AFTER_X_REDIRECT_COUNT);
info.mRetryAfter = retryRedirect & 0xfffffff;
info.mLastMod = getLong(Downloads.Impl.COLUMN_LAST_MODIFICATION);
- info.mPackage = getString(info.mPackage, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
- info.mClass = getString(info.mClass, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
- info.mExtras = getString(info.mExtras, Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS);
- info.mCookies = getString(info.mCookies, Downloads.Impl.COLUMN_COOKIE_DATA);
- info.mUserAgent = getString(info.mUserAgent, Downloads.Impl.COLUMN_USER_AGENT);
- info.mReferer = getString(info.mReferer, Downloads.Impl.COLUMN_REFERER);
+ info.mPackage = getString(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
+ info.mClass = getString(Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
+ info.mExtras = getString(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS);
+ info.mCookies = getString(Downloads.Impl.COLUMN_COOKIE_DATA);
+ info.mUserAgent = getString(Downloads.Impl.COLUMN_USER_AGENT);
+ info.mReferer = getString(Downloads.Impl.COLUMN_REFERER);
info.mTotalBytes = getLong(Downloads.Impl.COLUMN_TOTAL_BYTES);
info.mCurrentBytes = getLong(Downloads.Impl.COLUMN_CURRENT_BYTES);
- info.mETag = getString(info.mETag, Constants.ETAG);
+ info.mETag = getString(Constants.ETAG);
info.mMediaScanned = getInt(Constants.MEDIA_SCANNED) == 1;
info.mDeleted = getInt(Downloads.Impl.COLUMN_DELETED) == 1;
- info.mMediaProviderUri = getString(info.mMediaProviderUri,
- Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
+ info.mMediaProviderUri = getString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
info.mIsPublicApi = getInt(Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0;
info.mAllowedNetworkTypes = getInt(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0;
- info.mTitle = getString(info.mTitle, Downloads.Impl.COLUMN_TITLE);
- info.mDescription = getString(info.mDescription, Downloads.Impl.COLUMN_DESCRIPTION);
+ info.mTitle = getString(Downloads.Impl.COLUMN_TITLE);
+ info.mDescription = getString(Downloads.Impl.COLUMN_DESCRIPTION);
info.mBypassRecommendedSizeLimit =
getInt(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
@@ -129,35 +125,10 @@ public class DownloadInfo {
info.mRequestHeaders.add(Pair.create(header, value));
}
- /**
- * Returns a String that holds the current value of the column, optimizing for the case
- * where the value hasn't changed.
- */
- private String getString(String old, String column) {
+ private String getString(String column) {
int index = mCursor.getColumnIndexOrThrow(column);
- if (old == null) {
- return mCursor.getString(index);
- }
- if (mNewChars == null) {
- mNewChars = new CharArrayBuffer(128);
- }
- mCursor.copyStringToBuffer(index, mNewChars);
- int length = mNewChars.sizeCopied;
- if (length != old.length()) {
- return new String(mNewChars.data, 0, length);
- }
- if (mOldChars == null || mOldChars.sizeCopied < length) {
- mOldChars = new CharArrayBuffer(length);
- }
- char[] oldArray = mOldChars.data;
- char[] newArray = mNewChars.data;
- old.getChars(0, length, oldArray, 0);
- for (int i = length - 1; i >= 0; --i) {
- if (oldArray[i] != newArray[i]) {
- return new String(newArray, 0, length);
- }
- }
- return old;
+ String s = mCursor.getString(index);
+ return (TextUtils.isEmpty(s)) ? null : s;
}
private Integer getInt(String column) {
@@ -453,7 +424,7 @@ public class DownloadInfo {
return NETWORK_OK;
}
- void startIfReady(long now) {
+ void startIfReady(long now, StorageManager storageManager) {
if (!isReadyToStart(now)) {
return;
}
@@ -470,13 +441,15 @@ public class DownloadInfo {
values.put(Impl.COLUMN_STATUS, mStatus);
mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
}
- DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this);
+ DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this,
+ storageManager);
mHasActiveThread = true;
mSystemFacade.startThread(downloader);
}
public boolean isOnCache() {
return (mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION
+ || mDestination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION
|| mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
|| mDestination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
}
@@ -544,7 +517,8 @@ public class DownloadInfo {
*/
boolean shouldScanFile() {
return !mMediaScanned
- && mDestination == Downloads.Impl.DESTINATION_EXTERNAL
+ && (mDestination == Downloads.Impl.DESTINATION_EXTERNAL ||
+ mDestination == Downloads.Impl.DESTINATION_FILE_URI)
&& Downloads.Impl.isStatusSuccess(mStatus)
&& !DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mMimeType);
}
diff --git a/src/com/android/providers/downloads/DownloadNotification.java b/src/com/android/providers/downloads/DownloadNotification.java
index 4d615df7..51f0ba95 100644
--- a/src/com/android/providers/downloads/DownloadNotification.java
+++ b/src/com/android/providers/downloads/DownloadNotification.java
@@ -233,7 +233,7 @@ class DownloadNotification {
} else {
caption = mContext.getResources()
.getString(R.string.notification_download_complete);
- if (download.mDestination == Downloads.Impl.DESTINATION_EXTERNAL) {
+ if (download.mDestination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
intent = new Intent(Constants.ACTION_OPEN);
} else {
intent = new Intent(Constants.ACTION_LIST);
@@ -259,12 +259,12 @@ class DownloadNotification {
private boolean isActiveAndVisible(DownloadInfo download) {
return 100 <= download.mStatus && download.mStatus < 200
- && download.mVisibility != Downloads.VISIBILITY_HIDDEN;
+ && download.mVisibility != Downloads.Impl.VISIBILITY_HIDDEN;
}
private boolean isCompleteAndVisible(DownloadInfo download) {
return download.mStatus >= 200
- && download.mVisibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
+ && download.mVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
}
/*
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index d97d6189..a9952533 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -16,6 +16,7 @@
package com.android.providers.downloads;
+import android.app.DownloadManager;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -25,10 +26,8 @@ import android.content.UriMatcher;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
-import android.database.CrossProcessCursor;
import android.database.Cursor;
-import android.database.CursorWindow;
-import android.database.CursorWrapper;
+import android.database.DatabaseUtils;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
@@ -45,11 +44,11 @@ import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import java.util.Set;
/**
@@ -59,7 +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 = 106;
+ private static final int DB_VERSION = 107;
/** Name of table in the database */
private static final String DB_TABLE = "downloads";
@@ -80,6 +79,11 @@ public final class DownloadProvider extends ContentProvider {
private static final int ALL_DOWNLOADS_ID = 4;
/** URI matcher constant for the URI of a download's request headers */
private static final int REQUEST_HEADERS_URI = 5;
+ /** URI matcher constant for the public URI returned by
+ * {@link DownloadManager#getUriForDownloadedFile(long)} if the given downloaded file
+ * is publicly accessible.
+ */
+ private static final int PUBLIC_DOWNLOAD_ID = 6;
static {
sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS);
sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID);
@@ -97,6 +101,9 @@ public final class DownloadProvider extends ContentProvider {
sURIMatcher.addURI("downloads",
"download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
REQUEST_HEADERS_URI);
+ sURIMatcher.addURI("downloads",
+ Downloads.Impl.PUBLICLY_ACCESSIBLE_DOWNLOADS_URI_SEGMENT + "/#",
+ PUBLIC_DOWNLOAD_ID);
}
/** Different base URIs that could be used to access an individual download */
@@ -135,6 +142,8 @@ public final class DownloadProvider extends ContentProvider {
sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
}
}
+ private static final List<String> downloadManagerColumnsList =
+ Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
/** The database that lies underneath this content provider */
private SQLiteOpenHelper mOpenHelper = null;
@@ -142,6 +151,7 @@ public final class DownloadProvider extends ContentProvider {
/** List of uids that can access the downloads */
private int mSystemUid = -1;
private int mDefContainerUid = -1;
+ private File mDownloadsDataDir;
@VisibleForTesting
SystemFacade mSystemFacade;
@@ -279,6 +289,10 @@ public final class DownloadProvider extends ContentProvider {
"BOOLEAN NOT NULL DEFAULT 0");
break;
+ case 107:
+ addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_ERROR_MSG, "TEXT");
+ break;
+
default:
throw new IllegalStateException("Don't know how to upgrade to " + version);
}
@@ -408,6 +422,11 @@ public final class DownloadProvider extends ContentProvider {
if (appInfo != null) {
mDefContainerUid = appInfo.uid;
}
+ // start the DownloadService class. don't wait for the 1st download to be issued.
+ // saves us by getting some initialization code in DownloadService out of the way.
+ Context context = getContext();
+ context.startService(new Intent(context, DownloadService.class));
+ mDownloadsDataDir = StorageManager.getInstance(getContext()).getDownloadDataDirectory();
return true;
}
@@ -425,6 +444,15 @@ public final class DownloadProvider extends ContentProvider {
case MY_DOWNLOADS_ID: {
return DOWNLOAD_TYPE;
}
+ case PUBLIC_DOWNLOAD_ID: {
+ // return the mimetype of this id from the database
+ final String id = getDownloadIdFromUri(uri);
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ return DatabaseUtils.stringForQuery(db,
+ "SELECT " + Downloads.Impl.COLUMN_MIME_TYPE + " FROM " + DB_TABLE +
+ " WHERE " + Downloads.Impl._ID + " = ?",
+ new String[]{id});
+ }
default: {
if (Constants.LOGV) {
Log.v(Constants.TAG, "calling getType on an unknown URI: " + uri);
@@ -468,7 +496,8 @@ public final class DownloadProvider extends ContentProvider {
&& dest != Downloads.Impl.DESTINATION_EXTERNAL
&& dest != Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE
&& dest != Downloads.Impl.DESTINATION_FILE_URI) {
- throw new SecurityException("unauthorized destination code");
+ throw new SecurityException("setting destination to : " + dest +
+ " not allowed, unless PERMISSION_ACCESS_ADVANCED is granted");
}
// for public API behavior, if an app has CACHE_NON_PURGEABLE permission, automatically
// switch to non-purgeable download
@@ -486,6 +515,11 @@ public final class DownloadProvider extends ContentProvider {
Binder.getCallingPid(), Binder.getCallingUid(),
"need WRITE_EXTERNAL_STORAGE permission to use DESTINATION_FILE_URI");
checkFileUriDestination(values);
+ } else if (dest == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) {
+ getContext().enforcePermission(
+ android.Manifest.permission.ACCESS_CACHE_FILESYSTEM,
+ Binder.getCallingPid(), Binder.getCallingUid(),
+ "need ACCESS_CACHE_FILESYSTEM permission to use system cache");
}
filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
}
@@ -645,6 +679,7 @@ public final class DownloadProvider extends ContentProvider {
values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING);
values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
+ values.remove(Downloads.Impl.COLUMN_MEDIA_SCANNED);
Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator();
while (iterator.hasNext()) {
String key = iterator.next().getKey();
@@ -720,8 +755,10 @@ public final class DownloadProvider extends ContentProvider {
if (projection == null) {
projection = sAppReadableColumnsArray;
} else {
+ // check the validity of the columns in projection
for (int i = 0; i < projection.length; ++i) {
- if (!sAppReadableColumnsSet.contains(projection[i])) {
+ if (!sAppReadableColumnsSet.contains(projection[i]) &&
+ !downloadManagerColumnsList.contains(projection[i])) {
throw new IllegalArgumentException(
"column " + projection[i] + " is not allowed in queries");
}
@@ -737,10 +774,6 @@ public final class DownloadProvider extends ContentProvider {
fullSelection.getParameters(), null, null, sort);
if (ret != null) {
- ret = new ReadOnlyCursorWrapper(ret);
- }
-
- if (ret != null) {
ret.setNotificationUri(getContext().getContentResolver(), uri);
if (Constants.LOGVV) {
Log.v(Constants.TAG,
@@ -831,9 +864,8 @@ public final class DownloadProvider extends ContentProvider {
+ 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);
+ return db.query(Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE, projection, where,
+ null, null, null, null);
}
/**
@@ -972,7 +1004,8 @@ public final class DownloadProvider extends ContentProvider {
int uriMatch) {
SqlSelection selection = new SqlSelection();
selection.appendClause(where, whereArgs);
- if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID) {
+ if (uriMatch == MY_DOWNLOADS_ID || uriMatch == ALL_DOWNLOADS_ID ||
+ uriMatch == PUBLIC_DOWNLOAD_ID) {
selection.appendClause(Downloads.Impl._ID + " = ?", getDownloadIdFromUri(uri));
}
if ((uriMatch == MY_DOWNLOADS || uriMatch == MY_DOWNLOADS_ID)
@@ -1047,7 +1080,7 @@ public final class DownloadProvider extends ContentProvider {
if (path == null) {
throw new FileNotFoundException("No filename found.");
}
- if (!Helpers.isFilenameValid(path)) {
+ if (!Helpers.isFilenameValid(path, mDownloadsDataDir)) {
throw new FileNotFoundException("Invalid filename.");
}
if (!"r".equals(mode)) {
@@ -1128,34 +1161,4 @@ public final class DownloadProvider extends ContentProvider {
to.put(key, defaultValue);
}
}
-
- private class ReadOnlyCursorWrapper extends CursorWrapper implements CrossProcessCursor {
- public ReadOnlyCursorWrapper(Cursor cursor) {
- super(cursor);
- mCursor = (CrossProcessCursor) cursor;
- }
-
- public boolean deleteRow() {
- throw new SecurityException("Download manager cursors are read-only");
- }
-
- public boolean commitUpdates() {
- throw new SecurityException("Download manager cursors are read-only");
- }
-
- public void fillWindow(int pos, CursorWindow window) {
- mCursor.fillWindow(pos, window);
- }
-
- public CursorWindow getWindow() {
- return mCursor.getWindow();
- }
-
- public boolean onMove(int oldPosition, int newPosition) {
- return mCursor.onMove(oldPosition, newPosition);
- }
-
- private CrossProcessCursor mCursor;
- }
-
}
diff --git a/src/com/android/providers/downloads/DownloadReceiver.java b/src/com/android/providers/downloads/DownloadReceiver.java
index c41895b9..33066393 100644
--- a/src/com/android/providers/downloads/DownloadReceiver.java
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -41,6 +41,7 @@ public class DownloadReceiver extends BroadcastReceiver {
@VisibleForTesting
SystemFacade mSystemFacade = null;
+ @Override
public void onReceive(Context context, Intent intent) {
if (mSystemFacade == null) {
mSystemFacade = new RealSystemFacade(context);
@@ -169,11 +170,21 @@ public class DownloadReceiver extends BroadcastReceiver {
if (isPublicApi) {
appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
appIntent.setPackage(pckg);
+ // send id of the items clicked on.
+ if (intent.getBooleanExtra("multiple", false)) {
+ // broadcast received saying click occurred on a notification with multiple titles.
+ // don't include any ids at all - let the caller query all downloads belonging to it
+ // TODO modify the broadcast to include ids of those multiple notifications.
+ } else {
+ appIntent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
+ new long[] {
+ cursor.getLong(cursor.getColumnIndexOrThrow(Downloads.Impl._ID))});
+ }
} else { // legacy behavior
if (clazz == null) {
return;
}
- appIntent = new Intent(Downloads.Impl.ACTION_NOTIFICATION_CLICKED);
+ appIntent = new Intent(DownloadManager.ACTION_NOTIFICATION_CLICKED);
appIntent.setClassName(pckg, clazz);
if (intent.getBooleanExtra("multiple", true)) {
appIntent.setData(Downloads.Impl.CONTENT_URI);
diff --git a/src/com/android/providers/downloads/DownloadService.java b/src/com/android/providers/downloads/DownloadService.java
index 95d07d6f..bc4083c7 100644
--- a/src/com/android/providers/downloads/DownloadService.java
+++ b/src/com/android/providers/downloads/DownloadService.java
@@ -16,12 +16,14 @@
package com.android.providers.downloads;
+import com.google.android.collect.Maps;
+import com.google.common.annotations.VisibleForTesting;
+
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.ContentResolver;
-import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@@ -31,7 +33,6 @@ import android.database.Cursor;
import android.media.IMediaScannerListener;
import android.media.IMediaScannerService;
import android.net.Uri;
-import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Process;
@@ -40,13 +41,8 @@ import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
-import com.google.android.collect.Maps;
-import com.google.common.annotations.VisibleForTesting;
-
import java.io.File;
-import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.Map;
import java.util.Set;
@@ -101,6 +97,8 @@ public class DownloadService extends Service {
@VisibleForTesting
SystemFacade mSystemFacade;
+ private StorageManager mStorageManager;
+
/**
* Receives notifications when the data in the content provider changes
*/
@@ -114,6 +112,7 @@ public class DownloadService extends Service {
* Receives notification when the data in the observed content
* provider changes.
*/
+ @Override
public void onChange(final boolean selfChange) {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Service ContentObserver received notification");
@@ -188,6 +187,7 @@ public class DownloadService extends Service {
*
* @throws UnsupportedOperationException
*/
+ @Override
public IBinder onBind(Intent i) {
throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
}
@@ -195,6 +195,7 @@ public class DownloadService extends Service {
/**
* Initializes the service when it is first created
*/
+ @Override
public void onCreate() {
super.onCreate();
if (Constants.LOGVV) {
@@ -215,7 +216,7 @@ public class DownloadService extends Service {
mNotifier = new DownloadNotification(this, mSystemFacade);
mSystemFacade.cancelAllNotifications();
-
+ mStorageManager = StorageManager.getInstance(getApplicationContext());
updateFromProvider();
}
@@ -232,6 +233,7 @@ public class DownloadService extends Service {
/**
* Cleans up when the service is destroyed
*/
+ @Override
public void onDestroy() {
getContentResolver().unregisterContentObserver(mObserver);
if (Constants.LOGVV) {
@@ -258,12 +260,9 @@ public class DownloadService extends Service {
super("Download Service");
}
+ @Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
-
- trimDatabase();
- removeSpuriousFiles();
-
boolean keepService = false;
// for each update from the database, remember which download is
// supposed to get restarted soonest in the future
@@ -366,21 +365,20 @@ public class DownloadService extends Service {
if (!scanFile(info, false, true)) {
throw new IllegalStateException("scanFile failed!");
}
- } else {
- // this file should NOT be scanned. delete the file.
- Helpers.deleteFile(getContentResolver(), info.mId, info.mFileName,
- info.mMimeType);
+ continue;
}
} else {
// yes it has mediaProviderUri column already filled in.
- // delete it from MediaProvider database and then from downloads table
- // in DownProvider database (the order of deletion is important).
+ // delete it from MediaProvider database.
getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
null);
- // the following deletes the file and then deletes it from downloads db
- Helpers.deleteFile(getContentResolver(), info.mId, info.mFileName,
- info.mMimeType);
}
+ // delete the file
+ deleteFileIfExists(info.mFileName);
+ // delete from the downloads db
+ getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ Downloads.Impl._ID + " = ? ",
+ new String[]{String.valueOf(info.mId)});
}
}
}
@@ -419,76 +417,6 @@ public class DownloadService extends Service {
}
/**
- * Removes files that may have been left behind in the cache directory
- */
- private void removeSpuriousFiles() {
- File[] files = Environment.getDownloadCacheDirectory().listFiles();
- if (files == null) {
- // The cache folder doesn't appear to exist (this is likely the case
- // when running the simulator).
- return;
- }
- HashSet<String> fileSet = new HashSet<String>();
- for (int i = 0; i < files.length; i++) {
- if (files[i].getName().equals(Constants.KNOWN_SPURIOUS_FILENAME)) {
- continue;
- }
- if (files[i].getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
- continue;
- }
- fileSet.add(files[i].getPath());
- }
-
- Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- new String[] { Downloads.Impl._DATA }, null, null, null);
- if (cursor != null) {
- if (cursor.moveToFirst()) {
- do {
- fileSet.remove(cursor.getString(0));
- } while (cursor.moveToNext());
- }
- cursor.close();
- }
- Iterator<String> iterator = fileSet.iterator();
- while (iterator.hasNext()) {
- String filename = iterator.next();
- if (Constants.LOGV) {
- Log.v(Constants.TAG, "deleting spurious file " + filename);
- }
- new File(filename).delete();
- }
- }
-
- /**
- * Drops old rows from the database to prevent it from growing too large
- */
- private void trimDatabase() {
- Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- new String[] { Downloads.Impl._ID },
- Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
- Downloads.Impl.COLUMN_LAST_MODIFICATION);
- if (cursor == null) {
- // This isn't good - if we can't do basic queries in our database, nothing's gonna work
- Log.e(Constants.TAG, "null cursor in trimDatabase");
- return;
- }
- if (cursor.moveToFirst()) {
- int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
- int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
- while (numDelete > 0) {
- Uri downloadUri = ContentUris.withAppendedId(
- Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId));
- getContentResolver().delete(downloadUri, null, null);
- if (!cursor.moveToNext()) {
- break;
- }
- numDelete--;
- }
- }
- cursor.close();
- }
-
- /**
* Keeps a local copy of the info about a download, and initiates the
* download if appropriate.
*/
@@ -500,7 +428,7 @@ public class DownloadService extends Service {
info.logVerboseInfo();
}
- info.startIfReady(now);
+ info.startIfReady(now, mStorageManager);
return info;
}
@@ -524,7 +452,7 @@ public class DownloadService extends Service {
mSystemFacade.cancelNotification(info.mId);
}
- info.startIfReady(now);
+ info.startIfReady(now, mStorageManager);
}
/**
@@ -583,7 +511,7 @@ public class DownloadService extends Service {
if (updateDatabase) {
// Mark this as 'scanned' in the database
// so that it is NOT subject to re-scanning by MediaScanner
- // next time this database row is encountered
+ // next time this database row row is encountered
ContentValues values = new ContentValues();
values.put(Constants.MEDIA_SCANNED, 1);
if (uri != null) {
@@ -597,7 +525,11 @@ public class DownloadService extends Service {
getContentResolver().delete(uri, null, null);
}
// delete the file and delete its row from the downloads db
- Helpers.deleteFile(resolver, id, path, mimeType);
+ deleteFileIfExists(path);
+ getContentResolver().delete(
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ Downloads.Impl._ID + " = ? ",
+ new String[]{String.valueOf(id)});
}
}
});
@@ -608,4 +540,15 @@ public class DownloadService extends Service {
}
}
}
+
+ private void deleteFileIfExists(String path) {
+ try {
+ if (!TextUtils.isEmpty(path)) {
+ File file = new File(path);
+ file.delete();
+ }
+ } catch (Exception e) {
+ Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
+ }
+ }
}
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index 461d8cea..81e67a1a 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -16,11 +16,14 @@
package com.android.providers.downloads;
+import org.apache.http.conn.params.ConnRouteParams;
+
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.drm.mobile1.DrmRawContent;
import android.net.http.AndroidHttpClient;
+import android.net.Proxy;
import android.os.FileUtils;
import android.os.PowerManager;
import android.os.Process;
@@ -49,14 +52,17 @@ import java.util.Locale;
*/
public class DownloadThread extends Thread {
- private Context mContext;
- private DownloadInfo mInfo;
- private SystemFacade mSystemFacade;
+ private final Context mContext;
+ private final DownloadInfo mInfo;
+ private final SystemFacade mSystemFacade;
+ private final StorageManager mStorageManager;
- public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info) {
+ public DownloadThread(Context context, SystemFacade systemFacade, DownloadInfo info,
+ StorageManager storageManager) {
mContext = context;
mSystemFacade = systemFacade;
mInfo = info;
+ mStorageManager = storageManager;
}
/**
@@ -75,7 +81,7 @@ public class DownloadThread extends Thread {
/**
* State for the entire run() method.
*/
- private static class State {
+ static class State {
public String mFilename;
public FileOutputStream mStream;
public String mMimeType;
@@ -108,28 +114,6 @@ public class DownloadThread extends Thread {
}
/**
- * Raised from methods called by run() to indicate that the current request should be stopped
- * immediately.
- *
- * Note the message passed to this exception will be logged and therefore must be guaranteed
- * not to contain any PII, meaning it generally can't include any information about the request
- * URI, headers, or destination filename.
- */
- private class StopRequest extends Throwable {
- public int mFinalStatus;
-
- public StopRequest(int finalStatus, String message) {
- super(message);
- mFinalStatus = finalStatus;
- }
-
- public StopRequest(int finalStatus, String message, Throwable throwable) {
- super(message, throwable);
- mFinalStatus = finalStatus;
- }
- }
-
- /**
* Raised from methods called by executeDownload() to indicate that the download should be
* retried immediately.
*/
@@ -138,6 +122,7 @@ public class DownloadThread extends Thread {
/**
* Executes the download in a separate thread
*/
+ @Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
@@ -145,6 +130,7 @@ public class DownloadThread extends Thread {
AndroidHttpClient client = null;
PowerManager.WakeLock wakeLock = null;
int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
+ String errorMsg = null;
try {
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
@@ -161,6 +147,10 @@ public class DownloadThread extends Thread {
boolean finished = false;
while(!finished) {
Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
+ // 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, state.mRequestUri));
HttpGet request = new HttpGet(state.mRequestUri);
try {
executeDownload(state, client, request);
@@ -178,14 +168,18 @@ public class DownloadThread extends Thread {
}
finalizeDestinationFile(state);
finalStatus = Downloads.Impl.STATUS_SUCCESS;
- } catch (StopRequest error) {
+ } catch (StopRequestException error) {
// remove the cause before printing, in case it contains PII
- Log.w(Constants.TAG,
- "Aborting request for download " + mInfo.mId + ": " + error.getMessage());
+ errorMsg = "Aborting request for download " + mInfo.mId + ": " + error.getMessage();
+ Log.w(Constants.TAG, errorMsg);
+ if (Constants.LOGV) {
+ Log.w(Constants.TAG, errorMsg, error);
+ }
finalStatus = error.mFinalStatus;
// fall through to finally block
} catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
- Log.w(Constants.TAG, "Exception for id " + mInfo.mId + ": " + ex);
+ errorMsg = "Exception for id " + mInfo.mId + ": " + ex.getMessage();
+ Log.w(Constants.TAG, errorMsg, ex);
finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
// falls through to the code that reports an error
} finally {
@@ -200,9 +194,10 @@ public class DownloadThread extends Thread {
cleanupDestination(state, finalStatus);
notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
state.mGotData, state.mFilename,
- state.mNewUri, state.mMimeType);
+ state.mNewUri, state.mMimeType, errorMsg);
mInfo.mHasActiveThread = false;
}
+ mStorageManager.incrementNumDownloadsSoFar();
}
/**
@@ -210,7 +205,7 @@ public class DownloadThread extends Thread {
* and transfer the data to the destination file.
*/
private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
- throws StopRequest, RetryDownload {
+ throws StopRequestException, RetryDownload {
InnerState innerState = new InnerState();
byte data[] = new byte[Constants.BUFFER_SIZE];
@@ -235,7 +230,7 @@ public class DownloadThread extends Thread {
/**
* Check if current connectivity is valid for this request.
*/
- private void checkConnectivity(State state) throws StopRequest {
+ private void checkConnectivity(State state) throws StopRequestException {
int networkUsable = mInfo.checkCanUseNetwork();
if (networkUsable != DownloadInfo.NETWORK_OK) {
int status = Downloads.Impl.STATUS_WAITING_FOR_NETWORK;
@@ -246,7 +241,8 @@ public class DownloadThread extends Thread {
status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
mInfo.notifyPauseDueToSize(false);
}
- throw new StopRequest(status, mInfo.getLogMessageForNetworkError(networkUsable));
+ throw new StopRequestException(status,
+ mInfo.getLogMessageForNetworkError(networkUsable));
}
}
@@ -256,7 +252,7 @@ public class DownloadThread extends Thread {
* @param entityStream stream for reading the HTTP response entity
*/
private void transferData(State state, InnerState innerState, byte[] data,
- InputStream entityStream) throws StopRequest {
+ InputStream entityStream) throws StopRequestException {
for (;;) {
int bytesRead = readFromResponse(state, innerState, data, entityStream);
if (bytesRead == -1) { // success, end of stream already reached
@@ -281,7 +277,7 @@ public class DownloadThread extends Thread {
/**
* Called after a successful completion to take any necessary action on the downloaded file.
*/
- private void finalizeDestinationFile(State state) throws StopRequest {
+ private void finalizeDestinationFile(State state) throws StopRequestException {
if (isDrmFile(state)) {
transferToDrm(state);
} else {
@@ -342,13 +338,13 @@ public class DownloadThread extends Thread {
/**
* Transfer the downloaded destination file to the DRM store.
*/
- private void transferToDrm(State state) throws StopRequest {
+ private void transferToDrm(State state) throws StopRequestException {
File file = new File(state.mFilename);
Intent item = DrmStore.addDrmFile(mContext.getContentResolver(), file, null);
file.delete();
if (item == null) {
- throw new StopRequest(Downloads.Impl.STATUS_UNKNOWN_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_UNKNOWN_ERROR,
"unable to add file to DrmProvider");
} else {
state.mFilename = item.getDataString();
@@ -378,15 +374,15 @@ public class DownloadThread extends Thread {
* Check if the download has been paused or canceled, stopping the request appropriately if it
* has been.
*/
- private void checkPausedOrCanceled(State state) throws StopRequest {
+ private void checkPausedOrCanceled(State state) throws StopRequestException {
synchronized (mInfo) {
if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
- throw new StopRequest(Downloads.Impl.STATUS_PAUSED_BY_APP,
+ throw new StopRequestException(Downloads.Impl.STATUS_PAUSED_BY_APP,
"download paused by owner");
}
}
if (mInfo.mStatus == Downloads.Impl.STATUS_CANCELED) {
- throw new StopRequest(Downloads.Impl.STATUS_CANCELED, "download canceled");
+ throw new StopRequestException(Downloads.Impl.STATUS_CANCELED, "download canceled");
}
}
@@ -413,12 +409,14 @@ public class DownloadThread extends Thread {
* @param bytesRead how many bytes to write from the buffer
*/
private void writeDataToDestination(State state, byte[] data, int bytesRead)
- throws StopRequest {
+ throws StopRequestException {
for (;;) {
try {
if (state.mStream == null) {
state.mStream = new FileOutputStream(state.mFilename, true);
}
+ mStorageManager.verifySpaceBeforeWritingToFile(mInfo.mDestination, state.mFilename,
+ bytesRead);
state.mStream.write(data, 0, bytesRead);
if (mInfo.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
&& !isDrmFile(state)) {
@@ -426,23 +424,12 @@ public class DownloadThread extends Thread {
}
return;
} catch (IOException ex) {
- if (mInfo.isOnCache()) {
- if (Helpers.discardPurgeableFiles(mContext, Constants.BUFFER_SIZE)) {
- continue;
- }
- } else if (!Helpers.isExternalMediaMounted()) {
- throw new StopRequest(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
- "external media not mounted while writing destination file");
- }
-
- long availableBytes =
- Helpers.getAvailableBytes(Helpers.getFilesystemRoot(state.mFilename));
- if (availableBytes < bytesRead) {
- throw new StopRequest(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
- "insufficient space while writing destination file", ex);
+ // couldn't write to file. are we out of space? check.
+ // TODO this check should only be done once. why is this being done
+ // in a while(true) loop (see the enclosing statement: for(;;)
+ if (state.mStream != null) {
+ mStorageManager.verifySpace(mInfo.mDestination, state.mFilename, bytesRead);
}
- throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
- "while writing destination file: " + ex.toString(), ex);
}
}
}
@@ -451,7 +438,7 @@ public class DownloadThread extends Thread {
* 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 {
+ private void handleEndOfStream(State state, InnerState innerState) throws StopRequestException {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
if (innerState.mHeaderContentLength == null) {
@@ -463,10 +450,10 @@ public class DownloadThread extends Thread {
&& (innerState.mBytesSoFar != Integer.parseInt(innerState.mHeaderContentLength));
if (lengthMismatched) {
if (cannotResume(innerState)) {
- throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME,
+ throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
"mismatched content length");
} else {
- throw new StopRequest(getFinalStatusForHttpError(state),
+ throw new StopRequestException(getFinalStatusForHttpError(state),
"closed socket before end of file");
}
}
@@ -483,7 +470,7 @@ public class DownloadThread extends Thread {
* @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 {
+ InputStream entityStream) throws StopRequestException {
try {
return entityStream.read(data);
} catch (IOException ex) {
@@ -494,10 +481,10 @@ public class DownloadThread extends Thread {
if (cannotResume(innerState)) {
String message = "while reading response: " + ex.toString()
+ ", can't resume interrupted download with no ETag";
- throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME,
+ throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
message, ex);
} else {
- throw new StopRequest(getFinalStatusForHttpError(state),
+ throw new StopRequestException(getFinalStatusForHttpError(state),
"while reading response: " + ex.toString(), ex);
}
}
@@ -508,12 +495,12 @@ public class DownloadThread extends Thread {
* @return an InputStream to read the response entity
*/
private InputStream openResponseEntity(State state, HttpResponse response)
- throws StopRequest {
+ throws StopRequestException {
try {
return response.getEntity().getContent();
} catch (IOException ex) {
logNetworkState();
- throw new StopRequest(getFinalStatusForHttpError(state),
+ throw new StopRequestException(getFinalStatusForHttpError(state),
"while getting entity: " + ex.toString(), ex);
}
}
@@ -530,7 +517,7 @@ public class DownloadThread extends Thread {
* file and updating the database.
*/
private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
- throws StopRequest {
+ throws StopRequestException {
if (innerState.mContinuingDownload) {
// ignore response headers on resume requests
return;
@@ -538,25 +525,21 @@ public class DownloadThread extends Thread {
readResponseHeaders(state, innerState, response);
- try {
- state.mFilename = 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);
- } catch (Helpers.GenerateSaveFileError exc) {
- throw new StopRequest(exc.mStatus, exc.mMessage);
- }
+ state.mFilename = 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, mStorageManager);
try {
state.mStream = new FileOutputStream(state.mFilename);
} catch (FileNotFoundException exc) {
- throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
"while opening destination file: " + exc.toString(), exc);
}
if (Constants.LOGV) {
@@ -589,7 +572,7 @@ public class DownloadThread extends Thread {
* Read headers from the HTTP response and store them into local state.
*/
private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
- throws StopRequest {
+ throws StopRequestException {
Header header = response.getFirstHeader("Content-Disposition");
if (header != null) {
innerState.mHeaderContentDisposition = header.getValue();
@@ -640,7 +623,7 @@ public class DownloadThread extends Thread {
&& (headerTransferEncoding == null
|| !headerTransferEncoding.equalsIgnoreCase("chunked"));
if (!mInfo.mNoIntegrity && noSizeInfo) {
- throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
"can't know size of download, giving up");
}
}
@@ -649,7 +632,7 @@ public class DownloadThread extends Thread {
* 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 {
+ throws StopRequestException, RetryDownload {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
handleServiceUnavailable(state, response);
@@ -658,6 +641,10 @@ public class DownloadThread extends Thread {
handleRedirect(state, response, statusCode);
}
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "recevd_status = " + statusCode +
+ ", mContinuingDownload = " + innerState.mContinuingDownload);
+ }
int expectedStatus = innerState.mContinuingDownload ? 206 : Downloads.Impl.STATUS_SUCCESS;
if (statusCode != expectedStatus) {
handleOtherStatus(state, innerState, statusCode);
@@ -668,7 +655,7 @@ public class DownloadThread extends Thread {
* Handle a status that we don't know how to deal with properly.
*/
private void handleOtherStatus(State state, InnerState innerState, int statusCode)
- throws StopRequest {
+ throws StopRequestException {
int finalStatus;
if (Downloads.Impl.isStatusError(statusCode)) {
finalStatus = statusCode;
@@ -679,19 +666,21 @@ public class DownloadThread extends Thread {
} else {
finalStatus = Downloads.Impl.STATUS_UNHANDLED_HTTP_CODE;
}
- throw new StopRequest(finalStatus, "http error " + statusCode);
+ throw new StopRequestException(finalStatus, "http error " +
+ statusCode + ", mContinuingDownload: " + innerState.mContinuingDownload);
}
/**
* Handle a 3xx redirect status.
*/
private void handleRedirect(State state, HttpResponse response, int statusCode)
- throws StopRequest, RetryDownload {
+ throws StopRequestException, RetryDownload {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "got HTTP redirect " + statusCode);
}
if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
- throw new StopRequest(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS, "too many redirects");
+ throw new StopRequestException(Downloads.Impl.STATUS_TOO_MANY_REDIRECTS,
+ "too many redirects");
}
Header header = response.getFirstHeader("Location");
if (header == null) {
@@ -709,7 +698,7 @@ public class DownloadThread extends Thread {
Log.d(Constants.TAG, "Couldn't resolve redirect URI " + header.getValue()
+ " for " + mInfo.mUri);
}
- throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
"Couldn't resolve redirect URI");
}
++state.mRedirectCount;
@@ -724,7 +713,8 @@ public class DownloadThread extends Thread {
/**
* Handle a 503 Service Unavailable status by processing the Retry-After header.
*/
- private void handleServiceUnavailable(State state, HttpResponse response) throws StopRequest {
+ private void handleServiceUnavailable(State state, HttpResponse response)
+ throws StopRequestException {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "got HTTP response code 503");
}
@@ -751,7 +741,7 @@ public class DownloadThread extends Thread {
// ignored - retryAfter stays 0 in this case.
}
}
- throw new StopRequest(Downloads.Impl.STATUS_WAITING_TO_RETRY,
+ throw new StopRequestException(Downloads.Impl.STATUS_WAITING_TO_RETRY,
"got 503 Service Unavailable, will retry later");
}
@@ -759,15 +749,15 @@ public class DownloadThread extends Thread {
* Send the request to the server, handling any I/O exceptions.
*/
private HttpResponse sendRequest(State state, AndroidHttpClient client, HttpGet request)
- throws StopRequest {
+ throws StopRequestException {
try {
return client.execute(request);
} catch (IllegalArgumentException ex) {
- throw new StopRequest(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
"while trying to execute request: " + ex.toString(), ex);
} catch (IOException ex) {
logNetworkState();
- throw new StopRequest(getFinalStatusForHttpError(state),
+ throw new StopRequestException(getFinalStatusForHttpError(state),
"while trying to execute request: " + ex.toString(), ex);
}
}
@@ -789,32 +779,49 @@ public class DownloadThread extends Thread {
* appropriately for resumption.
*/
private void setupDestinationFile(State state, InnerState innerState)
- throws StopRequest {
+ throws StopRequestException {
if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already run a thread for this download
- if (!Helpers.isFilenameValid(state.mFilename)) {
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "have run thread before for id: " + mInfo.mId +
+ ", and state.mFilename: " + state.mFilename);
+ }
+ if (!Helpers.isFilenameValid(state.mFilename,
+ mStorageManager.getDownloadDataDirectory())) {
// this should never happen
- throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
"found invalid internal destination filename");
}
// We're resuming a download that got interrupted
File f = new File(state.mFilename);
if (f.exists()) {
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
+ ", and state.mFilename: " + state.mFilename);
+ }
long fileLength = f.length();
if (fileLength == 0) {
// The download hadn't actually started, we can restart from scratch
f.delete();
state.mFilename = null;
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
+ ", BUT starting from scratch again: ");
+ }
} else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
// This should've been caught upon failure
f.delete();
- throw new StopRequest(Downloads.Impl.STATUS_CANNOT_RESUME,
+ throw new StopRequestException(Downloads.Impl.STATUS_CANNOT_RESUME,
"Trying to resume a download that can't be resumed");
} else {
// All right, we'll be able to resume this download
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
+ ", and starting with file of length: " + fileLength);
+ }
try {
state.mStream = new FileOutputStream(state.mFilename, true);
} catch (FileNotFoundException exc) {
- throw new StopRequest(Downloads.Impl.STATUS_FILE_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
"while opening destination for resuming: " + exc.toString(), exc);
}
innerState.mBytesSoFar = (int) fileLength;
@@ -823,6 +830,10 @@ public class DownloadThread extends Thread {
}
innerState.mHeaderETag = mInfo.mETag;
innerState.mContinuingDownload = true;
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "resuming download for id: " + mInfo.mId +
+ ", and setting mContinuingDownload to true: ");
+ }
}
}
}
@@ -854,9 +865,10 @@ public class DownloadThread extends Thread {
*/
private void notifyDownloadCompleted(
int status, boolean countRetry, int retryAfter, boolean gotData,
- String filename, String uri, String mimeType) {
+ String filename, String uri, String mimeType, String errorMsg) {
notifyThroughDatabase(
- status, countRetry, retryAfter, gotData, filename, uri, mimeType);
+ status, countRetry, retryAfter, gotData, filename, uri, mimeType,
+ errorMsg);
if (Downloads.Impl.isStatusCompleted(status)) {
mInfo.sendIntentIfRequested();
}
@@ -864,7 +876,7 @@ public class DownloadThread extends Thread {
private void notifyThroughDatabase(
int status, boolean countRetry, int retryAfter, boolean gotData,
- String filename, String uri, String mimeType) {
+ String filename, String uri, String mimeType, String errorMsg) {
ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_STATUS, status);
values.put(Downloads.Impl._DATA, filename);
@@ -881,7 +893,11 @@ public class DownloadThread extends Thread {
} else {
values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
}
-
+ // STOPSHIP begin delete the following lines
+ if (!TextUtils.isEmpty(errorMsg)) {
+ values.put(Downloads.Impl.COLUMN_ERROR_MSG, errorMsg);
+ }
+ // STOPSHIP end
mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
}
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 855cba28..359738aa 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -16,17 +16,13 @@
package com.android.providers.downloads;
-import android.content.ContentResolver;
-import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.database.Cursor;
import android.drm.mobile1.DrmRawContent;
import android.net.Uri;
import android.os.Environment;
-import android.os.StatFs;
import android.os.SystemClock;
import android.provider.Downloads;
import android.util.Config;
@@ -43,7 +39,6 @@ import java.util.regex.Pattern;
* Some helper functions for the download manager
*/
public class Helpers {
-
public static Random sRandom = new Random(SystemClock.uptimeMillis());
/** Regex used to parse content-disposition headers */
@@ -72,22 +67,9 @@ public class Helpers {
}
/**
- * Exception thrown from methods called by generateSaveFile() for any fatal error.
- */
- public static class GenerateSaveFileError extends Exception {
- int mStatus;
- String mMessage;
-
- public GenerateSaveFileError(int status, String message) {
- mStatus = status;
- mMessage = message;
- }
- }
-
- /**
* Creates a filename (where the file should be saved) from info about a download.
*/
- public static String generateSaveFile(
+ static String generateSaveFile(
Context context,
String url,
String hint,
@@ -96,64 +78,30 @@ public class Helpers {
String mimeType,
int destination,
long contentLength,
- boolean isPublicApi) throws GenerateSaveFileError {
+ boolean isPublicApi, StorageManager storageManager) throws StopRequestException {
checkCanHandleDownload(context, mimeType, destination, isPublicApi);
+ String path;
+ File base = null;
if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
- return getPathForFileUri(hint, contentLength);
+ path = Uri.parse(hint).getPath();
} else {
- return chooseFullPath(context, url, hint, contentDisposition, contentLocation, mimeType,
- destination, contentLength);
- }
- }
-
- private static String getPathForFileUri(String hint, long contentLength)
- throws GenerateSaveFileError {
- if (!isExternalMediaMounted()) {
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
- "external media not mounted");
- }
- String path = Uri.parse(hint).getPath();
- if (new File(path).exists()) {
- Log.d(Constants.TAG, "File already exists: " + path);
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ALREADY_EXISTS_ERROR,
- "requested destination file already exists");
- }
- if (getAvailableBytes(getFilesystemRoot(path)) < contentLength) {
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
- "insufficient space on external storage");
- }
-
- return path;
- }
-
- /**
- * @return the root of the filesystem containing the given path
- */
- public static File getFilesystemRoot(String path) {
- File cache = Environment.getDownloadCacheDirectory();
- if (path.startsWith(cache.getPath())) {
- return cache;
- }
- File external = Environment.getExternalStorageDirectory();
- if (path.startsWith(external.getPath())) {
- return external;
+ base = storageManager.locateDestinationDirectory(mimeType, destination,
+ contentLength);
+ path = chooseFilename(url, hint, contentDisposition, contentLocation,
+ destination);
}
- throw new IllegalArgumentException("Cannot determine filesystem root for " + path);
+ storageManager.verifySpace(destination, path, contentLength);
+ return getFullPath(path, mimeType, destination, base);
}
- 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);
-
+ static String getFullPath(String filename, String mimeType, int destination,
+ File base) throws StopRequestException {
// 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) {
+ int dotIndex = filename.lastIndexOf('.');
+ boolean missingExtension = dotIndex < 0 || dotIndex < filename.lastIndexOf('/');
+ if (missingExtension) {
extension = chooseExtensionFromMimeType(mimeType, true);
} else {
extension = chooseExtensionFromFilename(mimeType, destination, filename, dotIndex);
@@ -162,17 +110,18 @@ public class Helpers {
boolean recoveryDir = Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(filename + extension);
- filename = base.getPath() + File.separator + filename;
+ if (base != null) {
+ 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 void checkCanHandleDownload(Context context, String mimeType, int destination,
- boolean isPublicApi) throws GenerateSaveFileError {
+ boolean isPublicApi) throws StopRequestException {
if (isPublicApi) {
return;
}
@@ -180,7 +129,7 @@ public class Helpers {
if (destination == Downloads.Impl.DESTINATION_EXTERNAL
|| destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE) {
if (mimeType == null) {
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
+ throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
"external download with no mime type not allowed");
}
if (!DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
@@ -205,89 +154,13 @@ public class Helpers {
if (Constants.LOGV) {
Log.v(Constants.TAG, "no handler found for type " + mimeType);
}
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
+ throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE,
"no handler found for this download type");
}
}
}
}
- private static File locateDestinationDirectory(Context context, String mimeType,
- int destination, long contentLength)
- throws GenerateSaveFileError {
- // 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
- || destination == Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING
- || DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
- return getCacheDestination(context, contentLength);
- }
-
- return getExternalDestination(contentLength);
- }
-
- private static File getExternalDestination(long contentLength) throws GenerateSaveFileError {
- if (!isExternalMediaMounted()) {
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
- "external media not mounted");
- }
-
- File root = Environment.getExternalStorageDirectory();
- if (getAvailableBytes(root) < contentLength) {
- // Insufficient space.
- Log.d(Constants.TAG, "download aborted - not enough free space");
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
- "insufficient space on external media");
- }
-
- File base = new File(root.getPath() + Constants.DEFAULT_DL_SUBDIR);
- if (!base.isDirectory() && !base.mkdir()) {
- // Can't create download directory, e.g. because a file called "download"
- // already exists at the root level, or the SD card filesystem is read-only.
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR,
- "unable to create external downloads directory " + base.getPath());
- }
- return base;
- }
-
- public static boolean isExternalMediaMounted() {
- if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- // No SD card found.
- Log.d(Constants.TAG, "no external storage");
- return false;
- }
- return true;
- }
-
- private static File getCacheDestination(Context context, long contentLength)
- throws GenerateSaveFileError {
- File base;
- base = Environment.getDownloadCacheDirectory();
- long bytesAvailable = getAvailableBytes(base);
- while (bytesAvailable < contentLength) {
- // Insufficient space; try discarding purgeable files.
- if (!discardPurgeableFiles(context, contentLength - bytesAvailable)) {
- // No files to purge, give up.
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
- "not enough free space in internal download storage, unable to free any "
- + "more");
- }
- bytesAvailable = getAvailableBytes(base);
- }
- return base;
- }
-
- /**
- * @return the number of bytes available on the filesystem rooted at the given File
- */
- public static long getAvailableBytes(File root) {
- StatFs stat = new StatFs(root.getPath());
- // put a bit of margin (in case creating the file grows the system by a few blocks)
- long availableBlocks = (long) stat.getAvailableBlocks() - 4;
- return stat.getBlockSize() * availableBlocks;
- }
-
private static String chooseFilename(String url, String hint, String contentDisposition,
String contentLocation, int destination) {
String filename = null;
@@ -360,8 +233,9 @@ public class Helpers {
filename = Constants.DEFAULT_DL_FILENAME;
}
- filename = filename.replaceAll("[^a-zA-Z0-9\\.\\-_]+", "_");
-
+ // The VFAT file system is assumed as target for downloads.
+ // Replace invalid characters according to the specifications of VFAT.
+ filename = replaceInvalidVfatCharacters(filename);
return filename;
}
@@ -405,12 +279,11 @@ public class Helpers {
}
private static String chooseExtensionFromFilename(String mimeType, int destination,
- String filename, int dotIndex) {
+ String filename, int lastDotIndex) {
String extension = null;
if (mimeType != null) {
// Compare the last segment of the extension against the mime type.
// If there's a mismatch, discard the entire extension.
- int lastDotIndex = filename.lastIndexOf('.');
String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
filename.substring(lastDotIndex + 1));
if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) {
@@ -430,17 +303,18 @@ public class Helpers {
if (Constants.LOGVV) {
Log.v(Constants.TAG, "keeping extension");
}
- extension = filename.substring(dotIndex);
+ extension = filename.substring(lastDotIndex);
}
return extension;
}
private static String chooseUniqueFilename(int destination, String filename,
- String extension, boolean recoveryDir) throws GenerateSaveFileError {
+ String extension, boolean recoveryDir) throws StopRequestException {
String fullFilename = filename + extension;
if (!new File(fullFilename).exists()
&& (!recoveryDir ||
(destination != Downloads.Impl.DESTINATION_CACHE_PARTITION &&
+ destination != Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION &&
destination != Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE &&
destination != Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING))) {
return fullFilename;
@@ -473,58 +347,11 @@ public class Helpers {
sequence += sRandom.nextInt(magnitude) + 1;
}
}
- throw new GenerateSaveFileError(Downloads.Impl.STATUS_FILE_ERROR,
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
"failed to generate an unused filename on internal download storage");
}
/**
- * Deletes purgeable files from the cache partition. This also deletes
- * the matching database entries. Files are deleted in LRU order until
- * the total byte size is greater than targetBytes.
- */
- public static final boolean discardPurgeableFiles(Context context, long targetBytes) {
- Cursor cursor = context.getContentResolver().query(
- Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- null,
- "( " +
- Downloads.Impl.COLUMN_STATUS + " = '" + Downloads.Impl.STATUS_SUCCESS + "' AND " +
- Downloads.Impl.COLUMN_DESTINATION +
- " = '" + Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE + "' )",
- null,
- Downloads.Impl.COLUMN_LAST_MODIFICATION);
- if (cursor == null) {
- return false;
- }
- long totalFreed = 0;
- try {
- cursor.moveToFirst();
- while (!cursor.isAfterLast() && totalFreed < targetBytes) {
- File file = new File(cursor.getString(cursor.getColumnIndex(Downloads.Impl._DATA)));
- if (Constants.LOGVV) {
- Log.v(Constants.TAG, "purging " + file.getAbsolutePath() + " for " +
- file.length() + " bytes");
- }
- totalFreed += file.length();
- file.delete();
- long id = cursor.getLong(cursor.getColumnIndex(Downloads.Impl._ID));
- context.getContentResolver().delete(
- ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id),
- null, null);
- cursor.moveToNext();
- }
- } finally {
- cursor.close();
- }
- if (Constants.LOGV) {
- if (totalFreed > 0) {
- Log.v(Constants.TAG, "Purged files, freed " + totalFreed + " for " +
- targetBytes + " requested");
- }
- }
- return totalFreed > 0;
- }
-
- /**
* Returns whether the network is available
*/
public static boolean isNetworkAvailable(SystemFacade system) {
@@ -534,9 +361,10 @@ public class Helpers {
/**
* Checks whether the filename looks legitimate
*/
- public static boolean isFilenameValid(String filename) {
+ static boolean isFilenameValid(String filename, File downloadsDataDir) {
filename = filename.replaceFirst("/+", "/"); // normalize leading slashes
return filename.startsWith(Environment.getDownloadCacheDirectory().toString())
+ || filename.startsWith(downloadsDataDir.toString())
|| filename.startsWith(Environment.getExternalStorageDirectory().toString());
}
@@ -802,17 +630,51 @@ public class Helpers {
}
/**
- * Delete the given file from device
- * and delete its row from the downloads database.
+ * Replace invalid filename characters according to
+ * specifications of the VFAT.
+ * @note Package-private due to testing.
*/
- /* package */ static void deleteFile(ContentResolver resolver, long id, String path, String mimeType) {
- try {
- File file = new File(path);
- file.delete();
- } catch (Exception e) {
- Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
+ private static String replaceInvalidVfatCharacters(String filename) {
+ final char START_CTRLCODE = 0x00;
+ final char END_CTRLCODE = 0x1f;
+ final char QUOTEDBL = 0x22;
+ final char ASTERISK = 0x2A;
+ final char SLASH = 0x2F;
+ final char COLON = 0x3A;
+ final char LESS = 0x3C;
+ final char GREATER = 0x3E;
+ final char QUESTION = 0x3F;
+ final char BACKSLASH = 0x5C;
+ final char BAR = 0x7C;
+ final char DEL = 0x7F;
+ final char UNDERSCORE = 0x5F;
+
+ StringBuffer sb = new StringBuffer();
+ char ch;
+ boolean isRepetition = false;
+ for (int i = 0; i < filename.length(); i++) {
+ ch = filename.charAt(i);
+ if ((START_CTRLCODE <= ch &&
+ ch <= END_CTRLCODE) ||
+ ch == QUOTEDBL ||
+ ch == ASTERISK ||
+ ch == SLASH ||
+ ch == COLON ||
+ ch == LESS ||
+ ch == GREATER ||
+ ch == QUESTION ||
+ ch == BACKSLASH ||
+ ch == BAR ||
+ ch == DEL){
+ if (!isRepetition) {
+ sb.append(UNDERSCORE);
+ isRepetition = true;
+ }
+ } else {
+ sb.append(ch);
+ isRepetition = false;
+ }
}
- resolver.delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, Downloads.Impl._ID + " = ? ",
- new String[]{String.valueOf(id)});
+ return sb.toString();
}
}
diff --git a/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java
index ce86f739..71dac5fe 100644
--- a/src/com/android/providers/downloads/RealSystemFacade.java
+++ b/src/com/android/providers/downloads/RealSystemFacade.java
@@ -1,5 +1,6 @@
package com.android.providers.downloads;
+import android.app.DownloadManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
@@ -62,22 +63,12 @@ class RealSystemFacade implements SystemFacade {
}
public Long getMaxBytesOverMobile() {
- try {
- return Settings.Secure.getLong(mContext.getContentResolver(),
- Settings.Secure.DOWNLOAD_MAX_BYTES_OVER_MOBILE);
- } catch (SettingNotFoundException exc) {
- return null;
- }
+ return DownloadManager.getMaxBytesOverMobile(mContext);
}
@Override
public Long getRecommendedMaxBytesOverMobile() {
- try {
- return Settings.Secure.getLong(mContext.getContentResolver(),
- Settings.Secure.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE);
- } catch (SettingNotFoundException exc) {
- return null;
- }
+ return DownloadManager.getRecommendedMaxBytesOverMobile(mContext);
}
@Override
diff --git a/src/com/android/providers/downloads/StopRequestException.java b/src/com/android/providers/downloads/StopRequestException.java
new file mode 100644
index 00000000..0ccf53cb
--- /dev/null
+++ b/src/com/android/providers/downloads/StopRequestException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.downloads;
+
+/**
+ * Raised to indicate that the current request should be stopped immediately.
+ *
+ * Note the message passed to this exception will be logged and therefore must be guaranteed
+ * not to contain any PII, meaning it generally can't include any information about the request
+ * URI, headers, or destination filename.
+ */
+class StopRequestException extends Exception {
+ public int mFinalStatus;
+
+ public StopRequestException(int finalStatus, String message) {
+ super(message);
+ mFinalStatus = finalStatus;
+ }
+
+ public StopRequestException(int finalStatus, String message, Throwable throwable) {
+ super(message, throwable);
+ mFinalStatus = finalStatus;
+ }
+}
diff --git a/src/com/android/providers/downloads/StorageManager.java b/src/com/android/providers/downloads/StorageManager.java
new file mode 100644
index 00000000..72658344
--- /dev/null
+++ b/src/com/android/providers/downloads/StorageManager.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.drm.mobile1.DrmRawContent;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.StatFs;
+import android.provider.Downloads;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.R;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Manages the storage space consumed by Downloads Data dir. When space falls below
+ * a threshold limit (set in resource xml files), starts cleanup of the Downloads data dir
+ * to free up space.
+ */
+class StorageManager {
+ /** the max amount of space allowed to be taken up by the downloads data dir */
+ private static final long sMaxdownloadDataDirSize =
+ Resources.getSystem().getInteger(R.integer.config_downloadDataDirSize) * 1024 * 1024;
+
+ /** threshold (in bytes) beyond which the low space warning kicks in and attempt is made to
+ * purge some downloaded files to make space
+ */
+ private static final long sDownloadDataDirLowSpaceThreshold =
+ Resources.getSystem().getInteger(
+ R.integer.config_downloadDataDirLowSpaceThreshold)
+ * sMaxdownloadDataDirSize / 100;
+
+ /** see {@link Environment#getExternalStorageDirectory()} */
+ private final File mExternalStorageDir;
+
+ /** see {@link Environment#getDownloadCacheDirectory()} */
+ private final File mSystemCacheDir;
+
+ /** The downloaded files are saved to this dir. it is the value returned by
+ * {@link Context#getCacheDir()}.
+ */
+ private final File mDownloadDataDir;
+
+ /** the Singleton instance of this class.
+ * TODO: once DownloadService is refactored into a long-living object, there is no need
+ * for this Singleton'ing.
+ */
+ private static StorageManager sSingleton = null;
+
+ /** how often do we need to perform checks on space to make sure space is available */
+ private static final int FREQUENCY_OF_CHECKS_ON_SPACE_AVAILABILITY = 1024 * 1024; // 1MB
+ private int mBytesDownloadedSinceLastCheckOnSpace = 0;
+
+ /** misc members */
+ private final Context mContext;
+
+ /**
+ * maintains Singleton instance of this class
+ */
+ synchronized static StorageManager getInstance(Context context) {
+ if (sSingleton == null) {
+ sSingleton = new StorageManager(context);
+ }
+ return sSingleton;
+ }
+
+ private StorageManager(Context context) { // constructor is private
+ mContext = context;
+ mDownloadDataDir = context.getCacheDir();
+ mExternalStorageDir = Environment.getExternalStorageDirectory();
+ mSystemCacheDir = Environment.getDownloadCacheDirectory();
+ startThreadToCleanupDatabaseAndPurgeFileSystem();
+ }
+
+ /** How often should database and filesystem be cleaned up to remove spurious files
+ * from the file system and
+ * The value is specified in terms of num of downloads since last time the cleanup was done.
+ */
+ private static final int FREQUENCY_OF_DATABASE_N_FILESYSTEM_CLEANUP = 250;
+ private int mNumDownloadsSoFar = 0;
+
+ synchronized void incrementNumDownloadsSoFar() {
+ if (++mNumDownloadsSoFar % FREQUENCY_OF_DATABASE_N_FILESYSTEM_CLEANUP == 0) {
+ startThreadToCleanupDatabaseAndPurgeFileSystem();
+ }
+ }
+ /* start a thread to cleanup the following
+ * remove spurious files from the file system
+ * remove excess entries from the database
+ */
+ private Thread mCleanupThread = null;
+ private synchronized void startThreadToCleanupDatabaseAndPurgeFileSystem() {
+ if (mCleanupThread != null && mCleanupThread.isAlive()) {
+ return;
+ }
+ mCleanupThread = new Thread() {
+ @Override public void run() {
+ removeSpuriousFiles();
+ trimDatabase();
+ }
+ };
+ mCleanupThread.start();
+ }
+
+ void verifySpaceBeforeWritingToFile(int destination, String path, long length)
+ throws StopRequestException {
+ // do this check only once for every 1MB of downloaded data
+ if (incrementBytesDownloadedSinceLastCheckOnSpace(length) <
+ FREQUENCY_OF_CHECKS_ON_SPACE_AVAILABILITY) {
+ return;
+ }
+ verifySpace(destination, path, length);
+ }
+
+ void verifySpace(int destination, String path, long length) throws StopRequestException {
+ resetBytesDownloadedSinceLastCheckOnSpace();
+ File dir = null;
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "in verifySpace, destination: " + destination +
+ ", path: " + path + ", length: " + length);
+ }
+ if (path == null) {
+ throw new IllegalArgumentException("path can't be null");
+ }
+ switch (destination) {
+ case Downloads.Impl.DESTINATION_CACHE_PARTITION:
+ case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING:
+ case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE:
+ dir = mDownloadDataDir;
+ break;
+ case Downloads.Impl.DESTINATION_EXTERNAL:
+ dir = mExternalStorageDir;
+ break;
+ case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION:
+ dir = mSystemCacheDir;
+ break;
+ case Downloads.Impl.DESTINATION_FILE_URI:
+ if (path.startsWith(mExternalStorageDir.getPath())) {
+ dir = mExternalStorageDir;
+ } else if (path.startsWith(mDownloadDataDir.getPath())) {
+ dir = mDownloadDataDir;
+ } else if (path.startsWith(mSystemCacheDir.getPath())) {
+ dir = mSystemCacheDir;
+ }
+ break;
+ }
+ if (dir == null) {
+ throw new IllegalStateException("invalid combination of destination: " + destination +
+ ", path: " + path);
+ }
+ findSpace(dir, length, destination);
+ }
+
+ /**
+ * finds space in the given filesystem (input param: root) to accommodate # of bytes
+ * specified by the input param(targetBytes).
+ * returns true if found. false otherwise.
+ */
+ private synchronized void findSpace(File root, long targetBytes, int destination)
+ throws StopRequestException {
+ if (targetBytes == 0) {
+ return;
+ }
+ if (destination == Downloads.Impl.DESTINATION_FILE_URI ||
+ destination == Downloads.Impl.DESTINATION_EXTERNAL) {
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ throw new StopRequestException(Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR,
+ "external media not mounted");
+ }
+ }
+ // is there enough space in the file system of the given param 'root'.
+ long bytesAvailable = getAvailableBytesInFileSystemAtGivenRoot(root);
+ if (bytesAvailable < sDownloadDataDirLowSpaceThreshold) {
+ /* filesystem's available space is below threshold for low space warning.
+ * threshold typically is 10% of download data dir space quota.
+ * try to cleanup and see if the low space situation goes away.
+ */
+ discardPurgeableFiles(destination, sDownloadDataDirLowSpaceThreshold);
+ removeSpuriousFiles();
+ bytesAvailable = getAvailableBytesInFileSystemAtGivenRoot(root);
+ if (bytesAvailable < sDownloadDataDirLowSpaceThreshold) {
+ /*
+ * available space is still below the threshold limit.
+ *
+ * If this is system cache dir, print a warning.
+ * otherwise, don't allow downloading until more space
+ * is available because downloadmanager shouldn't end up taking those last
+ * few MB of space left on the filesystem.
+ */
+ if (root.equals(mSystemCacheDir)) {
+ Log.w(Constants.TAG, "System cache dir ('/cache') is running low on space." +
+ "space available (in bytes): " + bytesAvailable);
+ } else {
+ throw new StopRequestException(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
+ "space in the filesystem rooted at: " + root +
+ " is below 10% availability. stopping this download.");
+ }
+ }
+ }
+ if (root.equals(mDownloadDataDir)) {
+ // this download is going into downloads data dir. check space in that specific dir.
+ bytesAvailable = getAvailableBytesInDownloadsDataDir(mSystemCacheDir);
+ if (bytesAvailable < sDownloadDataDirLowSpaceThreshold) {
+ // print a warning
+ Log.w(Constants.TAG, "Downloads data dir: " + root +
+ " is running low on space. space available (in b): " + bytesAvailable);
+ } else if (bytesAvailable < targetBytes) {
+ // Insufficient space; make space.
+ discardPurgeableFiles(destination, sDownloadDataDirLowSpaceThreshold);
+ removeSpuriousFiles();
+ bytesAvailable = getAvailableBytesInDownloadsDataDir(mSystemCacheDir);
+ }
+ }
+ if (bytesAvailable < targetBytes) {
+ throw new StopRequestException(Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR,
+ "not enough free space in the filesystem rooted at: " + root +
+ " and unable to free any more");
+ }
+ }
+
+ /**
+ * returns the number of bytes available in the downloads data dir
+ * TODO this implementation is too slow. optimize it.
+ */
+ private long getAvailableBytesInDownloadsDataDir(File root) {
+ File[] files = root.listFiles();
+ long space = sMaxdownloadDataDirSize;
+ if (files == null) {
+ return space;
+ }
+ int size = files.length;
+ for (int i = 0; i < size; i++) {
+ space -= files[i].length();
+ }
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "available space (in bytes) in downloads data dir: " + space);
+ }
+ return space;
+ }
+
+ private long getAvailableBytesInFileSystemAtGivenRoot(File root) {
+ StatFs stat = new StatFs(root.getPath());
+ // put a bit of margin (in case creating the file grows the system by a few blocks)
+ long availableBlocks = (long) stat.getAvailableBlocks() - 4;
+ long size = stat.getBlockSize() * availableBlocks;
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "available space (in bytes) in filesystem rooted at: " +
+ root.getPath() + " is: " + size);
+ }
+ return size;
+ }
+
+ File locateDestinationDirectory(String mimeType, int destination, long contentLength)
+ throws StopRequestException {
+ switch (destination) {
+ case Downloads.Impl.DESTINATION_CACHE_PARTITION:
+ case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE:
+ case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING:
+ return mDownloadDataDir;
+ case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION:
+ return mSystemCacheDir;
+ case Downloads.Impl.DESTINATION_EXTERNAL:
+ File base = new File(mExternalStorageDir.getPath() + Constants.DEFAULT_DL_SUBDIR);
+ if (!base.isDirectory() && !base.mkdir()) {
+ // Can't create download directory, e.g. because a file called "download"
+ // already exists at the root level, or the SD card filesystem is read-only.
+ throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
+ "unable to create external downloads directory " + base.getPath());
+ }
+ return base;
+ default:
+ // DRM messages should be temporarily stored internally and then passed to
+ // the DRM content provider
+ if (DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
+ return mDownloadDataDir;
+ }
+ throw new IllegalStateException("unexpected value for destination: " + destination);
+ }
+ }
+
+ File getDownloadDataDirectory() {
+ return mDownloadDataDir;
+ }
+
+ /**
+ * Deletes purgeable files from the cache partition. This also deletes
+ * the matching database entries. Files are deleted in LRU order until
+ * the total byte size is greater than targetBytes
+ */
+ private long discardPurgeableFiles(int destination, long targetBytes) {
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "discardPurgeableFiles: destination = " + destination +
+ ", targetBytes = " + targetBytes);
+ }
+ String destStr = (destination == Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION) ?
+ String.valueOf(destination) :
+ String.valueOf(Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE);
+ String[] bindArgs = new String[]{destStr};
+ Cursor cursor = mContext.getContentResolver().query(
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ null,
+ "( " +
+ Downloads.Impl.COLUMN_STATUS + " = '" + Downloads.Impl.STATUS_SUCCESS + "' AND " +
+ Downloads.Impl.COLUMN_DESTINATION + " = ? )",
+ bindArgs,
+ Downloads.Impl.COLUMN_LAST_MODIFICATION);
+ if (cursor == null) {
+ return 0;
+ }
+ long totalFreed = 0;
+ try {
+ while (cursor.moveToNext() && totalFreed < targetBytes) {
+ File file = new File(cursor.getString(cursor.getColumnIndex(Downloads.Impl._DATA)));
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "purging " + file.getAbsolutePath() + " for " +
+ file.length() + " bytes");
+ }
+ totalFreed += file.length();
+ file.delete();
+ long id = cursor.getLong(cursor.getColumnIndex(Downloads.Impl._ID));
+ mContext.getContentResolver().delete(
+ ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id),
+ null, null);
+ }
+ } finally {
+ cursor.close();
+ }
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "Purged files, freed " + totalFreed + " for " +
+ targetBytes + " requested");
+ }
+ return totalFreed;
+ }
+
+ /**
+ * Removes files in the systemcache and downloads data dir without corresponding entries in
+ * the downloads database.
+ * This can occur if a delete is done on the database but the file is not removed from the
+ * filesystem (due to sudden death of the process, for example).
+ * This is not a very common occurrence. So, do this only once in a while.
+ */
+ private void removeSpuriousFiles() {
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "in removeSpuriousFiles");
+ }
+ // get a list of all files in system cache dir and downloads data dir
+ List<File> files = new ArrayList<File>();
+ files.addAll(Arrays.asList(mSystemCacheDir.listFiles()));
+ files.addAll(Arrays.asList(mDownloadDataDir.listFiles()));
+ if (files.size() == 0) {
+ return;
+ }
+ Cursor cursor = mContext.getContentResolver().query(
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ new String[] { Downloads.Impl._DATA }, null, null, null);
+ try {
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ String filename = cursor.getString(0);
+ if (!TextUtils.isEmpty(filename)) {
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "in removeSpuriousFiles, preserving file " +
+ filename);
+ }
+ files.remove(new File(filename));
+ }
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ // delete the files not found in the database
+ for (File file : files) {
+ if (file.getName().equals(Constants.KNOWN_SPURIOUS_FILENAME) ||
+ file.getName().equalsIgnoreCase(Constants.RECOVERY_DIRECTORY)) {
+ continue;
+ }
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "deleting spurious file " + file.getAbsolutePath());
+ }
+ file.delete();
+ }
+ }
+
+ /**
+ * Drops old rows from the database to prevent it from growing too large
+ * TODO logic in this method needs to be optimized. maintain the number of downloads
+ * in memory - so that this method can limit the amount of data read.
+ */
+ private void trimDatabase() {
+ if (Constants.LOGV) {
+ Log.i(Constants.TAG, "in trimDatabase");
+ }
+ Cursor cursor = null;
+ try {
+ cursor = mContext.getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ new String[] { Downloads.Impl._ID },
+ Downloads.Impl.COLUMN_STATUS + " >= '200'", null,
+ Downloads.Impl.COLUMN_LAST_MODIFICATION);
+ if (cursor == null) {
+ // This isn't good - if we can't do basic queries in our database,
+ // nothing's gonna work
+ Log.e(Constants.TAG, "null cursor in trimDatabase");
+ return;
+ }
+ if (cursor.moveToFirst()) {
+ int numDelete = cursor.getCount() - Constants.MAX_DOWNLOADS;
+ int columnId = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
+ while (numDelete > 0) {
+ Uri downloadUri = ContentUris.withAppendedId(
+ Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, cursor.getLong(columnId));
+ mContext.getContentResolver().delete(downloadUri, null, null);
+ if (!cursor.moveToNext()) {
+ break;
+ }
+ numDelete--;
+ }
+ }
+ } catch (SQLiteException e) {
+ // trimming the database raised an exception. alright, ignore the exception
+ // and return silently. trimming database is not exactly a critical operation
+ // and there is no need to propagate the exception.
+ Log.w(Constants.TAG, "trimDatabase failed with exception: " + e.getMessage());
+ return;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private synchronized int incrementBytesDownloadedSinceLastCheckOnSpace(long val) {
+ mBytesDownloadedSinceLastCheckOnSpace += val;
+ return mBytesDownloadedSinceLastCheckOnSpace;
+ }
+
+ private synchronized void resetBytesDownloadedSinceLastCheckOnSpace() {
+ mBytesDownloadedSinceLastCheckOnSpace = 0;
+ }
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 4d971db1..d520123f 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -25,7 +25,7 @@
<!--
The test declared in this instrumentation can be run via this command
- "adb shell am instrument -w com.android.providers.downloads/android.test.InstrumentationTestRunner"
+ "adb shell am instrument -w com.android.providers.downloads.tests/android.test.InstrumentationTestRunner"
-->
<instrumentation android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.android.providers.downloads"
diff --git a/tests/public_api_access/src/com/android/providers/downloads/public_api_access_tests/PublicApiAccessTest.java b/tests/public_api_access/src/com/android/providers/downloads/public_api_access_tests/PublicApiAccessTest.java
index 2674e907..76339415 100644
--- a/tests/public_api_access/src/com/android/providers/downloads/public_api_access_tests/PublicApiAccessTest.java
+++ b/tests/public_api_access/src/com/android/providers/downloads/public_api_access_tests/PublicApiAccessTest.java
@@ -57,7 +57,7 @@ public class PublicApiAccessTest extends AndroidTestCase {
@Override
protected void tearDown() throws Exception {
if (mContentResolver != null) {
- mContentResolver.delete(Downloads.CONTENT_URI, null, null);
+ mContentResolver.delete(Downloads.Impl.CONTENT_URI, null, null);
}
super.tearDown();
}
diff --git a/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java b/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
index d04fd2de..5283d425 100644
--- a/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractDownloadManagerFunctionalTest.java
@@ -59,6 +59,14 @@ public abstract class AbstractDownloadManagerFunctionalTest extends
protected MockContentResolverWithNotify mResolver;
protected TestContext mTestContext;
protected FakeSystemFacade mSystemFacade;
+ protected static String STRING_1K;
+ static {
+ StringBuilder buff = new StringBuilder();
+ for (int i = 0; i < 1024; i++) {
+ buff.append("a" + i % 26);
+ }
+ STRING_1K = buff.toString();
+ }
static class MockContentResolverWithNotify extends MockContentResolver {
public boolean mNotifyWasCalled = false;
@@ -161,6 +169,7 @@ public abstract class AbstractDownloadManagerFunctionalTest extends
@Override
protected void tearDown() throws Exception {
cleanUpDownloads();
+ mServer.shutdown();
super.tearDown();
}
@@ -189,8 +198,8 @@ public abstract class AbstractDownloadManagerFunctionalTest extends
if (mResolver == null) {
return;
}
- String[] columns = new String[] {Downloads._DATA};
- Cursor cursor = mResolver.query(Downloads.CONTENT_URI, columns, null, null, null);
+ String[] columns = new String[] {Downloads.Impl._DATA};
+ Cursor cursor = mResolver.query(Downloads.Impl.CONTENT_URI, columns, null, null, null);
try {
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
String filePath = cursor.getString(0);
@@ -201,11 +210,11 @@ public abstract class AbstractDownloadManagerFunctionalTest extends
} finally {
cursor.close();
}
- mResolver.delete(Downloads.CONTENT_URI, null, null);
+ mResolver.delete(Downloads.Impl.CONTENT_URI, null, null);
}
/**
- * Enqueue a response from the MockWebServer.
+ * Enqueue a String response from the MockWebServer.
*/
MockResponse enqueueResponse(int status, String body) {
MockResponse response = new MockResponse()
@@ -216,6 +225,18 @@ public abstract class AbstractDownloadManagerFunctionalTest extends
mServer.enqueue(response);
return response;
}
+ /**
+ * Enqueue a byte[] response from the MockWebServer.
+ */
+ MockResponse enqueueResponse(int status, byte[] body) {
+ MockResponse response = new MockResponse()
+ .setResponseCode(status)
+ .setBody(body)
+ .addHeader("Content-type", "text/plain")
+ .setCloseConnectionAfter(true);
+ mServer.enqueue(response);
+ return response;
+ }
MockResponse enqueueEmptyResponse(int status) {
return enqueueResponse(status, "");
diff --git a/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java b/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
index ed443b01..c38c2f1d 100644
--- a/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractPublicApiTest.java
@@ -20,6 +20,8 @@ import android.app.DownloadManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
+import android.provider.Downloads;
+import android.util.Log;
import java.io.FileInputStream;
import java.io.InputStream;
@@ -41,6 +43,22 @@ public abstract class AbstractPublicApiTest extends AbstractDownloadManagerFunct
return (int) getLongField(DownloadManager.COLUMN_STATUS);
}
+ public int getStatusIfExists() {
+ Cursor cursor = mManager.query(new DownloadManager.Query().setFilterById(mId));
+ try {
+ if (cursor.getCount() > 0) {
+ cursor.moveToFirst();
+ return (int) cursor.getLong(cursor.getColumnIndexOrThrow(
+ DownloadManager.COLUMN_STATUS));
+ } else {
+ // the row doesn't exist
+ return -1;
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
String getStringField(String field) {
Cursor cursor = mManager.query(new DownloadManager.Query().setFilterById(mId));
try {
@@ -79,6 +97,63 @@ public abstract class AbstractPublicApiTest extends AbstractDownloadManagerFunct
runService();
assertEquals(status, getStatus());
}
+
+ // max time to wait before giving up on the current download operation.
+ private static final int MAX_TIME_TO_WAIT_FOR_OPERATION = 5;
+ // while waiting for the above time period, sleep this long to yield to the
+ // download thread
+ private static final int TIME_TO_SLEEP = 1000;
+
+ int runUntilDone() throws InterruptedException {
+ int sleepCounter = MAX_TIME_TO_WAIT_FOR_OPERATION * 1000 / TIME_TO_SLEEP;
+ for (int i = 0; i < sleepCounter; i++) {
+ int status = getStatusIfExists();
+ if (status == -1 || Downloads.Impl.isStatusCompleted(getStatus())) {
+ // row doesn't exist or the download is done
+ return status;
+ }
+ // download not done yet. sleep a while and try again
+ Thread.sleep(TIME_TO_SLEEP);
+ }
+ return 0; // failed
+ }
+
+ // waits until progress_so_far is >= (progress)%
+ boolean runUntilProgress(int progress) throws InterruptedException {
+ int sleepCounter = MAX_TIME_TO_WAIT_FOR_OPERATION * 1000 / TIME_TO_SLEEP;
+ int numBytesReceivedSoFar = 0;
+ int totalBytes = 0;
+ for (int i = 0; i < sleepCounter; i++) {
+ Cursor cursor = mManager.query(new DownloadManager.Query().setFilterById(mId));
+ try {
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ numBytesReceivedSoFar = cursor.getInt(
+ cursor.getColumnIndexOrThrow(
+ DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+ totalBytes = cursor.getInt(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
+ } finally {
+ cursor.close();
+ }
+ Log.i(LOG_TAG, "in runUntilProgress, numBytesReceivedSoFar: " +
+ numBytesReceivedSoFar + ", totalBytes: " + totalBytes);
+ if (totalBytes == 0) {
+ fail("total_bytes should not be zero");
+ return false;
+ } else {
+ if (numBytesReceivedSoFar * 100 / totalBytes >= progress) {
+ // progress_so_far is >= progress%. we are done
+ return true;
+ }
+ }
+ // download not done yet. sleep a while and try again
+ Thread.sleep(TIME_TO_SLEEP);
+ }
+ Log.i(LOG_TAG, "FAILED in runUntilProgress, numBytesReceivedSoFar: " +
+ numBytesReceivedSoFar + ", totalBytes: " + totalBytes);
+ return false; // failed
+ }
}
protected static final String PACKAGE_NAME = "my.package.name";
diff --git a/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java b/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
index 0cb63e0f..c3ac8904 100644
--- a/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/DownloadManagerFunctionalTest.java
@@ -41,15 +41,15 @@ public class DownloadManagerFunctionalTest extends AbstractDownloadManagerFuncti
super(new FakeSystemFacade());
}
- public void testBasicRequest() throws Exception {
+ public void testDownloadTextFile() throws Exception {
enqueueResponse(HTTP_OK, FILE_CONTENT);
String path = "/download_manager_test_path";
Uri downloadUri = requestDownload(path);
- assertEquals(Downloads.STATUS_PENDING, getDownloadStatus(downloadUri));
+ assertEquals(Downloads.Impl.STATUS_PENDING, getDownloadStatus(downloadUri));
assertTrue(mTestContext.mHasServiceBeenStarted);
- runUntilStatus(downloadUri, Downloads.STATUS_SUCCESS);
+ runUntilStatus(downloadUri, Downloads.Impl.STATUS_SUCCESS);
RecordedRequest request = takeRequest();
assertEquals("GET", request.getMethod());
assertEquals(path, request.getPath());
@@ -61,11 +61,11 @@ public class DownloadManagerFunctionalTest extends AbstractDownloadManagerFuncti
public void testDownloadToCache() throws Exception {
enqueueResponse(HTTP_OK, FILE_CONTENT);
Uri downloadUri = requestDownload("/path");
- updateDownload(downloadUri, Downloads.COLUMN_DESTINATION,
- Integer.toString(Downloads.DESTINATION_CACHE_PARTITION));
- runUntilStatus(downloadUri, Downloads.STATUS_SUCCESS);
+ updateDownload(downloadUri, Downloads.Impl.COLUMN_DESTINATION,
+ Integer.toString(Downloads.Impl.DESTINATION_CACHE_PARTITION));
+ runUntilStatus(downloadUri, Downloads.Impl.STATUS_SUCCESS);
assertEquals(FILE_CONTENT, getDownloadContents(downloadUri));
- assertStartsWith(Environment.getDownloadCacheDirectory().getPath(),
+ assertStartsWith(getContext().getCacheDir().getAbsolutePath(),
getDownloadFilename(downloadUri));
}
@@ -76,18 +76,18 @@ public class DownloadManagerFunctionalTest extends AbstractDownloadManagerFuncti
// for a normal download, roaming is fine
enqueueResponse(HTTP_OK, FILE_CONTENT);
Uri downloadUri = requestDownload("/path");
- runUntilStatus(downloadUri, Downloads.STATUS_SUCCESS);
+ runUntilStatus(downloadUri, Downloads.Impl.STATUS_SUCCESS);
// when roaming is disallowed, the download should pause...
downloadUri = requestDownload("/path");
- updateDownload(downloadUri, Downloads.COLUMN_DESTINATION,
- Integer.toString(Downloads.DESTINATION_CACHE_PARTITION_NOROAMING));
+ updateDownload(downloadUri, Downloads.Impl.COLUMN_DESTINATION,
+ Integer.toString(Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING));
runUntilStatus(downloadUri, Downloads.Impl.STATUS_WAITING_FOR_NETWORK);
// ...and pick up when we're off roaming
enqueueResponse(HTTP_OK, FILE_CONTENT);
mSystemFacade.mIsRoaming = false;
- runUntilStatus(downloadUri, Downloads.STATUS_SUCCESS);
+ runUntilStatus(downloadUri, Downloads.Impl.STATUS_SUCCESS);
}
/**
@@ -108,11 +108,11 @@ public class DownloadManagerFunctionalTest extends AbstractDownloadManagerFuncti
}
protected int getDownloadStatus(Uri downloadUri) {
- return Integer.valueOf(getDownloadField(downloadUri, Downloads.COLUMN_STATUS));
+ return Integer.valueOf(getDownloadField(downloadUri, Downloads.Impl.COLUMN_STATUS));
}
private String getDownloadFilename(Uri downloadUri) {
- return getDownloadField(downloadUri, Downloads._DATA);
+ return getDownloadField(downloadUri, Downloads.Impl._DATA);
}
private String getDownloadField(Uri downloadUri, String column) {
@@ -132,9 +132,9 @@ public class DownloadManagerFunctionalTest extends AbstractDownloadManagerFuncti
*/
private Uri requestDownload(String path) throws MalformedURLException {
ContentValues values = new ContentValues();
- values.put(Downloads.COLUMN_URI, getServerUri(path));
- values.put(Downloads.COLUMN_DESTINATION, Downloads.DESTINATION_EXTERNAL);
- return mResolver.insert(Downloads.CONTENT_URI, values);
+ values.put(Downloads.Impl.COLUMN_URI, getServerUri(path));
+ values.put(Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.DESTINATION_EXTERNAL);
+ return mResolver.insert(Downloads.Impl.CONTENT_URI, values);
}
/**
diff --git a/tests/src/com/android/providers/downloads/FakeSystemFacade.java b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
index 5263015c..9620ffc3 100644
--- a/tests/src/com/android/providers/downloads/FakeSystemFacade.java
+++ b/tests/src/com/android/providers/downloads/FakeSystemFacade.java
@@ -23,12 +23,16 @@ public class FakeSystemFacade implements SystemFacade {
Map<Long,Notification> mActiveNotifications = new HashMap<Long,Notification>();
List<Notification> mCanceledNotifications = new ArrayList<Notification>();
Queue<Thread> mStartedThreads = new LinkedList<Thread>();
+ private boolean returnActualTime = false;
void incrementTimeMillis(long delta) {
mTimeMillis += delta;
}
public long currentTimeMillis() {
+ if (returnActualTime) {
+ return System.currentTimeMillis();
+ }
return mTimeMillis;
}
@@ -81,9 +85,18 @@ public class FakeSystemFacade implements SystemFacade {
}
}
+ public boolean startThreadsWithoutWaiting = false;
+ public void setStartThreadsWithoutWaiting(boolean flag) {
+ this.startThreadsWithoutWaiting = flag;
+ }
+
@Override
public void startThread(Thread thread) {
- mStartedThreads.add(thread);
+ if (startThreadsWithoutWaiting) {
+ thread.start();
+ } else {
+ mStartedThreads.add(thread);
+ }
}
public void runAllThreads() {
@@ -91,4 +104,8 @@ public class FakeSystemFacade implements SystemFacade {
mStartedThreads.poll().run();
}
}
+
+ public void setReturnActualTime(boolean flag) {
+ returnActualTime = flag;
+ }
}
diff --git a/tests/src/com/android/providers/downloads/HelpersTest.java b/tests/src/com/android/providers/downloads/HelpersTest.java
new file mode 100644
index 00000000..fdd0334c
--- /dev/null
+++ b/tests/src/com/android/providers/downloads/HelpersTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.downloads;
+
+import android.provider.Downloads;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * This test exercises methods in the {@Helpers} utility class.
+ */
+@LargeTest
+public class HelpersTest extends AndroidTestCase {
+
+ public HelpersTest() {
+ }
+
+ public void testGetFullPath() throws Exception {
+ String hint = "file:///com.android.providers.downloads/test";
+
+ // Test that an extension derived from the specified mime type is appended to a filename that
+ // does not itself have an extension.
+ String fileName = Helpers.getFullPath(
+ hint,
+ "video/mp4", // MIME type corresponding to file extension .mp4
+ Downloads.Impl.DESTINATION_FILE_URI,
+ null);
+ assertEquals(hint + ".mp4", fileName);
+
+ // Test that the filename extension is replaced by one derived from the specified mime type.
+ fileName = Helpers.getFullPath(
+ hint + ".shouldbereplaced",
+ "video/mp4", // MIME type corresponding to file extension .mp4
+ Downloads.Impl.DESTINATION_FILE_URI,
+ null);
+ assertEquals(hint + ".mp4", fileName);
+ }
+
+}
diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
index cad01df6..64c19530 100644
--- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
@@ -16,6 +16,7 @@
package com.android.providers.downloads;
+
import android.app.DownloadManager;
import android.content.Intent;
import android.database.Cursor;
@@ -24,6 +25,7 @@ import android.net.Uri;
import android.os.Environment;
import android.provider.Downloads;
import android.test.suitebuilder.annotation.LargeTest;
+
import tests.http.MockResponse;
import tests.http.RecordedRequest;
@@ -53,17 +55,18 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
mTestDirectory = new File(Environment.getExternalStorageDirectory() + File.separator
+ "download_manager_functional_test");
if (mTestDirectory.exists()) {
- mTestDirectory.delete();
- }
- if (!mTestDirectory.mkdir()) {
- throw new RuntimeException("Couldn't create test directory: "
- + mTestDirectory.getPath());
+ for (File file : mTestDirectory.listFiles()) {
+ file.delete();
+ }
+ } else {
+ mTestDirectory.mkdir();
}
+ mSystemFacade.setStartThreadsWithoutWaiting(false);
}
@Override
protected void tearDown() throws Exception {
- if (mTestDirectory != null) {
+ if (mTestDirectory != null && mTestDirectory.exists()) {
for (File file : mTestDirectory.listFiles()) {
file.delete();
}
@@ -184,6 +187,18 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
return response;
}
+ // enqueue a huge response to keep the receiveing thread in DownloadThread.java busy for a while
+ // give enough time to do something (cancel/remove etc) on that downloadrequest
+ // while it is in progress
+ private void enqueueContinuingResponse() {
+ int numPackets = 100;
+ int contentLength = STRING_1K.length() * numPackets;
+ enqueueResponse(HTTP_OK, STRING_1K)
+ .addHeader("Content-length", contentLength)
+ .addHeader("Etag", ETAG)
+ .setNumPackets(numPackets);
+ }
+
public void testFiltering() throws Exception {
enqueueEmptyResponse(HTTP_OK);
Download download1 = enqueueRequest(getRequest());
@@ -301,7 +316,7 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
}
private Uri getExternalUri() {
- return Uri.fromFile(mTestDirectory).buildUpon().appendPath("testfile").build();
+ return Uri.fromFile(mTestDirectory).buildUpon().appendPath("testfile.txt").build();
}
public void testRequestHeaders() throws Exception {
@@ -379,14 +394,22 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
}
public void testCancel() throws Exception {
- enqueuePartialResponse(0, 5);
+ mSystemFacade.setStartThreadsWithoutWaiting(true);
+ // return 'real time' from FakeSystemFacade so that DownloadThread will report progress
+ mSystemFacade.setReturnActualTime(true);
+ enqueueContinuingResponse();
Download download = enqueueRequest(getRequest());
- download.runUntilStatus(DownloadManager.STATUS_PAUSED);
-
+ startService(null);
+ // give the download time to get started and progress to 1% completion
+ // before cancelling it.
+ boolean rslt = download.runUntilProgress(1);
+ assertTrue(rslt);
mManager.remove(download.mId);
- mSystemFacade.incrementTimeMillis(RETRY_DELAY_MILLIS);
- runService();
- // if the cancel didn't work, we should get an unexpected request to the HTTP server
+ startService(null);
+ int status = download.runUntilDone();
+ // make sure the row is gone from the database
+ assertEquals(-1, status);
+ mSystemFacade.setReturnActualTime(false);
}
public void testDownloadCompleteBroadcast() throws Exception {
@@ -524,14 +547,15 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest {
}
public void testExistingFile() throws Exception {
+ // download a file which already exists.
+ // downloadservice should simply create filename with "-" and a number attached
+ // at the end; i.e., download shouldnot fail.
Uri destination = getExternalUri();
new File(destination.getPath()).createNewFile();
enqueueEmptyResponse(HTTP_OK);
Download download = enqueueRequest(getRequest().setDestinationUri(destination));
- download.runUntilStatus(DownloadManager.STATUS_FAILED);
- assertEquals(DownloadManager.ERROR_FILE_ALREADY_EXISTS,
- download.getLongField(DownloadManager.COLUMN_REASON));
+ download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL);
}
public void testEmptyFields() throws Exception {
diff --git a/tests/src/tests/http/MockResponse.java b/tests/src/tests/http/MockResponse.java
index 4cda92d2..aec5490c 100644
--- a/tests/src/tests/http/MockResponse.java
+++ b/tests/src/tests/http/MockResponse.java
@@ -36,6 +36,7 @@ public class MockResponse {
private Map<String, String> headers = new HashMap<String, String>();
private byte[] body = EMPTY_BODY;
private boolean closeConnectionAfter = false;
+ private int numPackets = 0;
public MockResponse() {
addHeader("Content-Length", 0);
@@ -133,4 +134,14 @@ public class MockResponse {
this.closeConnectionAfter = closeConnectionAfter;
return this;
}
+
+ public int getNumPackets() {
+ return numPackets;
+ }
+
+ public MockResponse setNumPackets(int numPackets) {
+ this.numPackets = numPackets;
+ return this;
+ }
+
}
diff --git a/tests/src/tests/http/MockWebServer.java b/tests/src/tests/http/MockWebServer.java
index 11c8063e..6096783d 100644
--- a/tests/src/tests/http/MockWebServer.java
+++ b/tests/src/tests/http/MockWebServer.java
@@ -16,6 +16,9 @@
package tests.http;
+import android.text.TextUtils;
+import android.util.Log;
+
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
@@ -59,6 +62,7 @@ public final class MockWebServer {
private final Queue<Future<?>> futures = new LinkedList<Future<?>>();
private int port = -1;
+ private ServerSocket serverSocket;
public int getPort() {
if (port == -1) {
@@ -111,26 +115,35 @@ public final class MockWebServer {
* down.
*/
public void play() throws IOException {
- final ServerSocket ss = new ServerSocket(0);
- ss.setReuseAddress(true);
- port = ss.getLocalPort();
+ serverSocket = new ServerSocket(0);
+ serverSocket.setReuseAddress(true);
+ port = serverSocket.getLocalPort();
submitCallable(new Callable<Void>() {
public Void call() throws Exception {
int count = 0;
while (true) {
if (count > 0 && responseQueue.isEmpty()) {
- ss.close();
+ serverSocket.close();
executor.shutdown();
return null;
}
- serveConnection(ss.accept());
+ serveConnection(serverSocket.accept());
count++;
}
}
});
}
+ /**
+ * shutdown the webserver
+ */
+ public void shutdown() throws IOException {
+ responseQueue.clear();
+ serverSocket.close();
+ executor.shutdown();
+ }
+
private void serveConnection(final Socket s) {
submitCallable(new Callable<Void>() {
public Void call() throws Exception {
@@ -148,8 +161,7 @@ public final class MockWebServer {
}
}
requestQueue.add(request);
- MockResponse response = computeResponse(request);
- writeResponse(out, response);
+ MockResponse response = sendResponse(out, request);
if (response.shouldCloseConnectionAfter()) {
break;
}
@@ -241,7 +253,6 @@ public final class MockWebServer {
} else {
throw new UnsupportedOperationException("Unexpected method: " + request);
}
-
return new RecordedRequest(request, headers, chunkSizes,
requestBody.numBytesReceived, requestBody.toByteArray(), sequenceNumber);
}
@@ -249,14 +260,32 @@ public final class MockWebServer {
/**
* Returns a response to satisfy {@code request}.
*/
- private MockResponse computeResponse(RecordedRequest request) throws InterruptedException {
+ private MockResponse sendResponse(OutputStream out, RecordedRequest request)
+ throws InterruptedException, IOException {
if (responseQueue.isEmpty()) {
throw new IllegalStateException("Unexpected request: " + request);
}
- return responseQueue.take();
- }
+ MockResponse response = responseQueue.take();
+ writeResponse(out, response, false);
+ if (response.getNumPackets() > 0) {
+ // there are continuing packets to send as part of this response.
+ for (int i = 0; i < response.getNumPackets(); i++) {
+ writeResponse(out, response, true);
+ // delay sending next continuing response just a little bit
+ Thread.sleep(100);
+ }
+ }
+ return response;
+ }
- private void writeResponse(OutputStream out, MockResponse response) throws IOException {
+ private void writeResponse(OutputStream out, MockResponse response,
+ boolean continuingPacket) throws IOException {
+ if (continuingPacket) {
+ // this is a continuing response - just send the body - no headers, status
+ out.write(response.getBody());
+ out.flush();
+ return;
+ }
out.write((response.getStatus() + "\r\n").getBytes(ASCII));
for (String header : response.getHeaders()) {
out.write((header + "\r\n").getBytes(ASCII));
diff --git a/ui/AndroidManifest.xml b/ui/AndroidManifest.xml
index c2c93241..80510ed4 100644
--- a/ui/AndroidManifest.xml
+++ b/ui/AndroidManifest.xml
@@ -8,7 +8,7 @@
<application android:process="android.process.media"
android:label="@string/app_label"
- android:icon="@drawable/ic_launcher_download">
+ android:icon="@mipmap/ic_launcher_download">
<activity android:name=".DownloadList"
android:launchMode="singleTop">
<intent-filter>
diff --git a/ui/res/drawable-hdpi/ic_launcher_download.png b/ui/res/mipmap-hdpi/ic_launcher_download.png
index 308835cd..308835cd 100644
--- a/ui/res/drawable-hdpi/ic_launcher_download.png
+++ b/ui/res/mipmap-hdpi/ic_launcher_download.png
Binary files differ
diff --git a/ui/res/drawable-mdpi/ic_launcher_download.png b/ui/res/mipmap-mdpi/ic_launcher_download.png
index 6dd4ba35..6dd4ba35 100644
--- a/ui/res/drawable-mdpi/ic_launcher_download.png
+++ b/ui/res/mipmap-mdpi/ic_launcher_download.png
Binary files differ
diff --git a/ui/res/values-es-rUS-xlarge/strings.xml b/ui/res/values-es-rUS-xlarge/strings.xml
new file mode 100644
index 00000000..22e8d283
--- /dev/null
+++ b/ui/res/values-es-rUS-xlarge/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- XL -->
+ <string name="download_queued" msgid="1751435505084931039">"En cola"</string>
+</resources>
diff --git a/ui/res/values-ko/strings.xml b/ui/res/values-ko/strings.xml
index 7a8c3575..5f632dc7 100644
--- a/ui/res/values-ko/strings.xml
+++ b/ui/res/values-ko/strings.xml
@@ -20,8 +20,8 @@
<string name="download_title" msgid="2470985874255839247">"다운로드"</string>
<string name="no_downloads" msgid="1029667411186146836">"다운로드 항목이 없습니다."</string>
<string name="missing_title" msgid="830115697868833773">"&lt;알 수 없음&gt;"</string>
- <string name="download_menu_sort_by_size" msgid="2276438658769422878">"크기순 정렬"</string>
- <string name="download_menu_sort_by_date" msgid="4300882048968609945">"시간순 정렬"</string>
+ <string name="download_menu_sort_by_size" msgid="2276438658769422878">"크기별 정렬"</string>
+ <string name="download_menu_sort_by_date" msgid="4300882048968609945">"시간별 정렬"</string>
<string name="download_queued" msgid="104973307780629904">"대기 중"</string>
<string name="download_running" msgid="4656462962155580641">"진행 중"</string>
<string name="download_success" msgid="7006048006543495236">"완료"</string>
diff --git a/ui/src/com/android/providers/downloads/ui/DownloadList.java b/ui/src/com/android/providers/downloads/ui/DownloadList.java
index dfd5ffc9..133b0bfe 100644
--- a/ui/src/com/android/providers/downloads/ui/DownloadList.java
+++ b/ui/src/com/android/providers/downloads/ui/DownloadList.java
@@ -33,7 +33,6 @@ import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.provider.Downloads;
-import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
@@ -52,7 +51,6 @@ import android.widget.Toast;
import com.android.providers.downloads.ui.DownloadItem.DownloadSelectListener;
-import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashSet;
@@ -573,39 +571,10 @@ public class DownloadList extends Activity
* Delete a download from the Download Manager.
*/
private void deleteDownload(long downloadId) {
- if (moveToDownload(downloadId)) {
- int status = mDateSortedCursor.getInt(mStatusColumnId);
- boolean isComplete = status == DownloadManager.STATUS_SUCCESSFUL
- || status == DownloadManager.STATUS_FAILED;
- String localUri = mDateSortedCursor.getString(mLocalUriColumnId);
- if (isComplete && localUri != null) {
- String path = Uri.parse(localUri).getPath();
- if (path.startsWith(Environment.getExternalStorageDirectory().getPath())) {
- String mediaProviderUri = mDateSortedCursor.getString(mMediaProviderUriId);
- if (TextUtils.isEmpty(mediaProviderUri)) {
- // downloads database doesn't have the mediaprovider_uri. It means
- // this download occurred before mediaprovider_uri column existed
- // in downloads table. Since MediaProvider needs the mediaprovider_uri to
- // delete this download, just set the 'deleted' flag to 1 on this row
- // in the database. DownloadService, upon seeing this flag set to 1, will
- // re-scan the file and get the MediaProviderUri and then delete the file
- mDownloadManager.markRowDeleted(downloadId);
- return;
- } else {
- getContentResolver().delete(Uri.parse(mediaProviderUri), null, null);
- // sometimes mediaprovider doesn't delete file from sdcard after deleting it
- // from its db. delete it now
- try {
- File file = new File(path);
- file.delete();
- } catch (Exception e) {
- Log.w(LOG_TAG, "file: '" + path + "' couldn't be deleted", e);
- }
- }
- }
- }
- }
- mDownloadManager.remove(downloadId);
+ // let DownloadService do the job of cleaning up the downloads db, mediaprovider db,
+ // and removal of file from sdcard
+ // TODO do the following in asynctask - not on main thread.
+ mDownloadManager.markRowDeleted(downloadId);
}
@Override