Merge "Fix silly bug with completed notifications." into gingerbread
Steve Howard [Mon, 13 Sep 2010 02:13:16 +0000 (19:13 -0700)]
src/com/android/providers/downloads/DownloadProvider.java
src/com/android/providers/downloads/Helpers.java
tests/src/com/android/providers/downloads/PublicApiFunctionalTest.java
ui/res/layout/download_list.xml
ui/res/values/strings.xml
ui/src/com/android/providers/downloads/ui/DateSortedExpandableListAdapter.java
ui/src/com/android/providers/downloads/ui/DownloadAdapter.java
ui/src/com/android/providers/downloads/ui/DownloadList.java

index f6b091b..d957989 100644 (file)
@@ -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();
@@ -770,7 +797,6 @@ public final class DownloadProvider extends ContentProvider {
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
         int count;
-        long rowId = 0;
         boolean startService = false;
 
         ContentValues filteredValues;
@@ -798,6 +824,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) {
index 5d546ff..42a49f1 100644 (file)
@@ -529,7 +529,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);
index cf2b990..e48ce22 100644 (file)
@@ -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));
index 241bb3d..696030f 100644 (file)
           <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>
index 5bebb3c..8806f7e 100644 (file)
          [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">&lt;Unknown&gt;</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] -->
     <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>
index 88ffdee..58dd4bb 100644 (file)
@@ -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,
index a79122a..b868ffc 100644 (file)
@@ -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()));
index 1b7c727..dd9a608 100644 (file)
@@ -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;
+    }
 }