diff options
8 files changed, 258 insertions, 25 deletions
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java index 900797f8..525de70a 100644 --- a/src/com/android/providers/downloads/DownloadProvider.java +++ b/src/com/android/providers/downloads/DownloadProvider.java @@ -58,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 = 102; + private static final int DB_VERSION = 103; /** Name of table in the database */ private static final String DB_TABLE = "downloads"; @@ -99,6 +99,7 @@ public final class DownloadProvider extends ContentProvider { Downloads.Impl.COLUMN_TITLE, Downloads.Impl.COLUMN_DESCRIPTION, Downloads.Impl.COLUMN_URI, + Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, }; private static HashSet<String> sAppReadableColumnsSet; @@ -194,12 +195,29 @@ public final class DownloadProvider extends ContentProvider { "INTEGER NOT NULL DEFAULT 0"); break; + case 103: + addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, + "INTEGER NOT NULL DEFAULT 1"); + makeCacheDownloadsInvisible(db); + break; + default: throw new IllegalStateException("Don't know how to upgrade to " + version); } } /** + * Set all existing downloads to the cache partition to be invisible in the downloads UI. + */ + private void makeCacheDownloadsInvisible(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + values.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, false); + String cacheSelection = Downloads.Impl.COLUMN_DESTINATION + + " != " + Downloads.Impl.DESTINATION_EXTERNAL; + db.update(DB_TABLE, values, cacheSelection, null); + } + + /** * Add a column to a table using ALTER TABLE. * @param dbTable name of the table * @param columnName name of the column to add @@ -419,6 +437,14 @@ public final class DownloadProvider extends ContentProvider { copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, ""); filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES, -1); + if (values.containsKey(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) { + copyBoolean(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues); + } else { + // by default, make external downloads visible in the UI + boolean isExternal = (dest == null || dest == Downloads.Impl.DESTINATION_EXTERNAL); + filteredValues.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isExternal); + } + if (isPublicApi) { copyInteger(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES, values, filteredValues); copyBoolean(Downloads.Impl.COLUMN_ALLOW_ROAMING, values, filteredValues); @@ -519,6 +545,7 @@ public final class DownloadProvider extends ContentProvider { values.remove(Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE); // checked later in insert() values.remove(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES); values.remove(Downloads.Impl.COLUMN_ALLOW_ROAMING); + values.remove(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI); Iterator<Map.Entry<String, Object>> iterator = values.valueSet().iterator(); while (iterator.hasNext()) { String key = iterator.next().getKey(); @@ -765,7 +792,6 @@ public final class DownloadProvider extends ContentProvider { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; - long rowId = 0; boolean startService = false; ContentValues filteredValues; @@ -793,6 +819,12 @@ public final class DownloadProvider extends ContentProvider { } c.close(); } + + Integer status = values.getAsInteger(Downloads.Impl.COLUMN_STATUS); + boolean isRestart = status != null && status == Downloads.Impl.STATUS_PENDING; + if (isRestart) { + startService = true; + } } int match = sURIMatcher.match(uri); switch (match) { diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java index 794bb062..e2dc9e86 100644 --- a/src/com/android/providers/downloads/Helpers.java +++ b/src/com/android/providers/downloads/Helpers.java @@ -530,7 +530,7 @@ public class Helpers { */ public static void validateSelection(String selection, Set<String> allowedColumns) { try { - if (selection == null) { + if (selection == null || selection.isEmpty()) { return; } Lexer lexer = new Lexer(selection, allowedColumns); diff --git a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java index cf2b990c..e48ce22e 100644 --- a/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java +++ b/tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java @@ -205,6 +205,48 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest { cursor = mManager.query(new DownloadManager.Query() .setFilterByStatus(DownloadManager.STATUS_RUNNING)); checkAndCloseCursor(cursor); + + mSystemFacade.incrementTimeMillis(1); + Download invisibleDownload = enqueueRequest(getRequest().setVisibleInDownloadsUi(false)); + cursor = mManager.query(new DownloadManager.Query()); + checkAndCloseCursor(cursor, invisibleDownload, download3, download2, download1); + cursor = mManager.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)); + checkAndCloseCursor(cursor, download3, download2, download1); + } + + public void testOrdering() throws Exception { + enqueueResponse(HTTP_OK, "small contents"); + Download download1 = enqueueRequest(getRequest()); + download1.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL); + + mSystemFacade.incrementTimeMillis(1); + enqueueResponse(HTTP_OK, "large contents large contents"); + Download download2 = enqueueRequest(getRequest()); + download2.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL); + + mSystemFacade.incrementTimeMillis(1); + enqueueEmptyResponse(HTTP_NOT_FOUND); + Download download3 = enqueueRequest(getRequest()); + download3.runUntilStatus(DownloadManager.STATUS_FAILED); + + // default ordering -- by timestamp descending + Cursor cursor = mManager.query(new DownloadManager.Query()); + checkAndCloseCursor(cursor, download3, download2, download1); + + cursor = mManager.query(new DownloadManager.Query() + .orderBy(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP, + DownloadManager.Query.ORDER_ASCENDING)); + checkAndCloseCursor(cursor, download1, download2, download3); + + cursor = mManager.query(new DownloadManager.Query() + .orderBy(DownloadManager.COLUMN_TOTAL_SIZE_BYTES, + DownloadManager.Query.ORDER_DESCENDING)); + checkAndCloseCursor(cursor, download2, download1, download3); + + cursor = mManager.query(new DownloadManager.Query() + .orderBy(DownloadManager.COLUMN_TOTAL_SIZE_BYTES, + DownloadManager.Query.ORDER_ASCENDING)); + checkAndCloseCursor(cursor, download3, download1, download2); } private void checkAndCloseCursor(Cursor cursor, Download... downloads) { @@ -494,6 +536,18 @@ public class PublicApiFunctionalTest extends AbstractPublicApiTest { download.getLongField(DownloadManager.COLUMN_ERROR_CODE); } + public void testRestart() throws Exception { + enqueueEmptyResponse(HTTP_NOT_FOUND); + Download download = enqueueRequest(getRequest()); + download.runUntilStatus(DownloadManager.STATUS_FAILED); + + enqueueEmptyResponse(HTTP_OK); + mManager.restartDownload(download.mId); + assertEquals(DownloadManager.STATUS_PENDING, + download.getLongField(DownloadManager.COLUMN_STATUS)); + download.runUntilStatus(DownloadManager.STATUS_SUCCESSFUL); + } + private void checkCompleteDownload(Download download) throws Exception { assertEquals(FILE_CONTENT.length(), download.getLongField(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); diff --git a/ui/res/layout/download_list.xml b/ui/res/layout/download_list.xml index 241bb3d3..696030ff 100644 --- a/ui/res/layout/download_list.xml +++ b/ui/res/layout/download_list.xml @@ -54,8 +54,11 @@ <Button android:id="@+id/selection_delete" android:layout_width="wrap_content" android:layout_height="match_parent" - android:textAppearance="?android:attr/textAppearanceMedium" - android:paddingLeft="30dip" - android:paddingRight="30dip"/> + android:layout_weight="1"/> + <Button android:id="@+id/deselect_all" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_weight="1" + android:text="@string/deselect_all"/> </LinearLayout> </LinearLayout> diff --git a/ui/res/values/strings.xml b/ui/res/values/strings.xml index 5bebb3ca..8806f7e4 100644 --- a/ui/res/values/strings.xml +++ b/ui/res/values/strings.xml @@ -23,6 +23,10 @@ [CHAR LIMIT=200] --> <string name="no_downloads">No downloads.</string> + <!-- Default title for an item in the download list for which no title was provided by the app. + [CHAR LIMIT=20] --> + <string name="missing_title"><Unknown></string> + <!-- Menu items --> <!-- Menu option to sort the list of downloads by the size of the downloaded file @@ -58,6 +62,9 @@ <!-- Text for dialog when user clicks on a download that has not yet begun, but will be started in the future. [CHAR LIMIT=200] --> <string name="dialog_queued_body">This file is queued for future download.</string> + <!-- Text for dialog when user clicks on a completed download but the file is missing + [CHAR LIMIT=200] --> + <string name="dialog_file_missing_body">The downloaded file cannot be found.</string> <!-- Text for a toast appearing when a user clicks on a completed download, informing the user that there is no application on the device that can open the file that was downloaded [CHAR LIMIT=200] --> @@ -75,4 +82,10 @@ <string name="keep_queued_download">Keep</string> <!-- Text for button to cancel a download that is currently in progress [CHAR LIMIT=25] --> <string name="cancel_running_download">Cancel</string> + <!-- Text for button appearing in a dialog to restart a download, either one that failed or one + for which the downloaded file is now missing [CHAR LIMIT=25] --> + <string name="retry_download">Retry</string> + <!-- Text for button appearing in the pop-up selection menu to deselect all currently selected + downloads in the download list [CHAR LIMIT=25] --> + <string name="deselect_all">Clear selection</string> </resources> diff --git a/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java b/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java index 88ffdee3..58dd4bb3 100644 --- a/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java +++ b/ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java @@ -66,6 +66,16 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { } } + private class MyDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + buildMap(); + for (DataSetObserver o : mObservers) { + o.onChanged(); + } + } + } + public DateSortedExpandableListAdapter(Context context, Cursor cursor, int dateIndex) { mContext = context; @@ -74,6 +84,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { mCursor = cursor; mIdIndex = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID); cursor.registerContentObserver(new ChangeObserver()); + cursor.registerDataSetObserver(new MyDataSetObserver()); mDateIndex = dateIndex; buildMap(); } @@ -255,10 +266,6 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter { return; } mCursor.requery(); - buildMap(); - for (DataSetObserver o : mObservers) { - o.onChanged(); - } } public View getGroupView(int groupPosition, boolean isExpanded, diff --git a/ui/src/com/android/providers/downloads/ui/DownloadAdapter.java b/ui/src/com/android/providers/downloads/ui/DownloadAdapter.java index a79122a4..b868ffc4 100644 --- a/ui/src/com/android/providers/downloads/ui/DownloadAdapter.java +++ b/ui/src/com/android/providers/downloads/ui/DownloadAdapter.java @@ -96,8 +96,11 @@ public class DownloadAdapter extends CursorAdapter { // Retrieve the icon for this download retrieveAndSetIcon(convertView); - // TODO: default text for null title? - setTextForView(convertView, R.id.download_title, mCursor.getString(mTitleColumnId)); + String title = mCursor.getString(mTitleColumnId); + if (title.isEmpty()) { + title = mResources.getString(R.string.missing_title); + } + setTextForView(convertView, R.id.download_title, title); setTextForView(convertView, R.id.domain, mCursor.getString(mDescriptionColumnId)); setTextForView(convertView, R.id.size_text, getSizeText()); setTextForView(convertView, R.id.status_text, mResources.getString(getStatusStringId())); diff --git a/ui/src/com/android/providers/downloads/ui/DownloadList.java b/ui/src/com/android/providers/downloads/ui/DownloadList.java index 1b7c727e..dd9a6083 100644 --- a/ui/src/com/android/providers/downloads/ui/DownloadList.java +++ b/ui/src/com/android/providers/downloads/ui/DownloadList.java @@ -21,11 +21,14 @@ import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; import android.content.Intent; +import android.database.ContentObserver; import android.database.Cursor; import android.net.DownloadManager; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; import android.provider.Downloads; import android.view.Menu; import android.view.MenuInflater; @@ -44,6 +47,7 @@ import android.widget.Toast; import com.android.providers.downloads.ui.DownloadItem.DownloadSelectListener; +import java.io.File; import java.util.HashSet; import java.util.Iterator; import java.util.Set; @@ -53,7 +57,7 @@ import java.util.Set; */ public class DownloadList extends Activity implements OnChildClickListener, OnItemClickListener, DownloadSelectListener, - OnClickListener { + OnClickListener, OnCancelListener { private ExpandableListView mDateOrderedListView; private ListView mSizeOrderedListView; private View mEmptyView; @@ -65,6 +69,7 @@ public class DownloadList extends Activity private DateSortedDownloadAdapter mDateSortedAdapter; private Cursor mSizeSortedCursor; private DownloadAdapter mSizeSortedAdapter; + private MyContentObserver mContentObserver = new MyContentObserver(); private int mStatusColumnId; private int mIdColumnId; @@ -74,14 +79,34 @@ public class DownloadList extends Activity private boolean mIsSortedBySize = false; private Set<Long> mSelectedIds = new HashSet<Long>(); + /** + * We keep track of when a dialog is being displayed for a pending download, because if that + * download starts running, we want to immediately hide the dialog. + */ + private Long mPendingDownloadId = null; + private AlertDialog mPendingDialog; + + private class MyContentObserver extends ContentObserver { + public MyContentObserver() { + super(new Handler()); + } + + @Override + public void onChange(boolean selfChange) { + handleDownloadsChanged(); + } + } + @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setupViews(); mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); - mDateSortedCursor = mDownloadManager.query(new DownloadManager.Query()); - mSizeSortedCursor = mDownloadManager.query(new DownloadManager.Query() + DownloadManager.Query baseQuery = new DownloadManager.Query() + .setOnlyIncludeVisibleInDownloadsUi(true); + mDateSortedCursor = mDownloadManager.query(baseQuery); + mSizeSortedCursor = mDownloadManager.query(baseQuery .orderBy(DownloadManager.COLUMN_TOTAL_SIZE_BYTES, DownloadManager.Query.ORDER_DESCENDING)); @@ -131,15 +156,28 @@ public class DownloadList extends Activity mSelectionMenuView = (ViewGroup) findViewById(R.id.selection_menu); mSelectionDeleteButton = (Button) findViewById(R.id.selection_delete); mSelectionDeleteButton.setOnClickListener(this); + + ((Button) findViewById(R.id.deselect_all)).setOnClickListener(this); } @Override protected void onResume() { super.onResume(); + if (mDateSortedCursor != null) { + mDateSortedCursor.registerContentObserver(mContentObserver); + } refresh(); } @Override + protected void onPause() { + super.onPause(); + if (mDateSortedCursor != null) { + mDateSortedCursor.unregisterContentObserver(mContentObserver); + } + } + + @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean("isSortedBySize", mIsSortedBySize); @@ -237,11 +275,28 @@ public class DownloadList extends Activity } /** + * @return an OnClickListener to restart the given downloadId in the Download Manager + */ + private DialogInterface.OnClickListener getRestartClickHandler(final long downloadId) { + return new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mDownloadManager.restartDownload(downloadId); + } + }; + } + + /** * Send an Intent to open the download currently pointed to by the given cursor. */ private void openCurrentDownload(Cursor cursor) { - Intent intent = new Intent(Intent.ACTION_VIEW); Uri fileUri = Uri.parse(cursor.getString(mLocalUriColumnId)); + if (!new File(fileUri.getPath()).exists()) { + showFailedDialog(cursor.getLong(mIdColumnId), R.string.dialog_file_missing_body); + return; + } + + Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(fileUri, cursor.getString(mMediaTypeColumnId)); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { @@ -255,11 +310,13 @@ public class DownloadList extends Activity long id = cursor.getInt(mIdColumnId); switch (cursor.getInt(mStatusColumnId)) { case DownloadManager.STATUS_PENDING: - new AlertDialog.Builder(this) + mPendingDownloadId = id; + mPendingDialog = new AlertDialog.Builder(this) .setTitle(R.string.dialog_title_not_available) - .setMessage("This file is queued for future download.") + .setMessage(R.string.dialog_queued_body) .setPositiveButton(R.string.keep_queued_download, null) .setNegativeButton(R.string.remove_download, getDeleteClickHandler(id)) + .setOnCancelListener(this) .show(); break; @@ -273,16 +330,20 @@ public class DownloadList extends Activity break; case DownloadManager.STATUS_FAILED: - new AlertDialog.Builder(this) - .setTitle(R.string.dialog_title_not_available) - .setMessage(getResources().getString(R.string.dialog_failed_body)) - .setPositiveButton(R.string.remove_download, getDeleteClickHandler(id)) - // TODO button to retry download - .show(); + showFailedDialog(id, R.string.dialog_failed_body); break; } } + private void showFailedDialog(long downloadId, int dialogBodyResource) { + new AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_not_available) + .setMessage(getResources().getString(dialogBodyResource)) + .setPositiveButton(R.string.remove_download, getDeleteClickHandler(downloadId)) + .setNegativeButton(R.string.retry_download, getRestartClickHandler(downloadId)) + .show(); + } + /** * TODO use constants/shared code? */ @@ -377,7 +438,11 @@ public class DownloadList extends Activity deleteDownload(downloadId); } clearSelection(); - return; + break; + + case R.id.deselect_all: + clearSelection(); + break; } } @@ -406,4 +471,60 @@ public class DownloadList extends Activity public boolean isDownloadSelected(long id) { return mSelectedIds.contains(id); } + + /** + * Called when there's a change to the downloads database. + */ + void handleDownloadsChanged() { + checkSelectionForDeletedEntries(); + + if (mPendingDownloadId != null && moveToDownload(mPendingDownloadId)) { + if (mDateSortedCursor.getInt(mStatusColumnId) != DownloadManager.STATUS_PENDING) { + mPendingDialog.cancel(); + } + } + } + + /** + * Check if any of the selected downloads have been deleted from the downloads database, and + * remove such downloads from the selection. + */ + private void checkSelectionForDeletedEntries() { + // gather all existing IDs... + Set<Long> allIds = new HashSet<Long>(); + for (mDateSortedCursor.moveToFirst(); !mDateSortedCursor.isAfterLast(); + mDateSortedCursor.moveToNext()) { + allIds.add(mDateSortedCursor.getLong(mIdColumnId)); + } + + // ...and check if any selected IDs are now missing + for (Iterator<Long> iterator = mSelectedIds.iterator(); iterator.hasNext(); ) { + if (!allIds.contains(iterator.next())) { + iterator.remove(); + } + } + } + + /** + * Move {@link #mDateSortedCursor} to the download with the given ID. + * @return true if the specified download ID was found; false otherwise + */ + private boolean moveToDownload(long downloadId) { + for (mDateSortedCursor.moveToFirst(); !mDateSortedCursor.isAfterLast(); + mDateSortedCursor.moveToNext()) { + if (mDateSortedCursor.getLong(mIdColumnId) == downloadId) { + return true; + } + } + return false; + } + + /** + * Called when a dialog for a pending download is canceled. + */ + @Override + public void onCancel(DialogInterface dialog) { + mPendingDownloadId = null; + mPendingDialog = null; + } } |