diff options
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 Binary files differindex 308835cd..308835cd 100644 --- a/ui/res/drawable-hdpi/ic_launcher_download.png +++ b/ui/res/mipmap-hdpi/ic_launcher_download.png diff --git a/ui/res/drawable-mdpi/ic_launcher_download.png b/ui/res/mipmap-mdpi/ic_launcher_download.png Binary files differindex 6dd4ba35..6dd4ba35 100644 --- a/ui/res/drawable-mdpi/ic_launcher_download.png +++ b/ui/res/mipmap-mdpi/ic_launcher_download.png 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">"<알 수 없음>"</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 |