Unified handling of errors around opening.
[android/platform/packages/providers/DownloadProvider.git] / ui / src / com / android / providers / downloads / ui / DownloadList.java
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.providers.downloads.ui;
18
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.DownloadManager;
22 import android.content.ContentUris;
23 import android.content.Context;
24 import android.content.DialogInterface;
25 import android.content.Intent;
26 import android.database.ContentObserver;
27 import android.database.Cursor;
28 import android.database.DataSetObserver;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Environment;
32 import android.os.Handler;
33 import android.os.Parcelable;
34 import android.provider.BaseColumns;
35 import android.provider.DocumentsContract;
36 import android.provider.Downloads;
37 import android.util.Log;
38 import android.util.SparseBooleanArray;
39 import android.view.ActionMode;
40 import android.view.Menu;
41 import android.view.MenuInflater;
42 import android.view.MenuItem;
43 import android.view.View;
44 import android.view.View.OnClickListener;
45 import android.widget.AbsListView.MultiChoiceModeListener;
46 import android.widget.AdapterView;
47 import android.widget.AdapterView.OnItemClickListener;
48 import android.widget.Button;
49 import android.widget.ExpandableListView;
50 import android.widget.ExpandableListView.OnChildClickListener;
51 import android.widget.ListView;
52 import android.widget.Toast;
53
54 import com.android.providers.downloads.Constants;
55 import com.android.providers.downloads.OpenHelper;
56
57 import java.io.FileNotFoundException;
58 import java.io.IOException;
59 import java.util.ArrayList;
60 import java.util.Collection;
61 import java.util.HashMap;
62 import java.util.HashSet;
63 import java.util.Iterator;
64 import java.util.Map;
65 import java.util.Set;
66
67 /**
68  *  View showing a list of all downloads the Download Manager knows about.
69  */
70 public class DownloadList extends Activity {
71     static final String LOG_TAG = "DownloadList";
72
73     private ExpandableListView mDateOrderedListView;
74     private ListView mSizeOrderedListView;
75     private View mEmptyView;
76
77     private DownloadManager mDownloadManager;
78     private Cursor mDateSortedCursor;
79     private DateSortedDownloadAdapter mDateSortedAdapter;
80     private Cursor mSizeSortedCursor;
81     private DownloadAdapter mSizeSortedAdapter;
82     private ActionMode mActionMode;
83     private MyContentObserver mContentObserver = new MyContentObserver();
84     private MyDataSetObserver mDataSetObserver = new MyDataSetObserver();
85
86     private int mStatusColumnId;
87     private int mIdColumnId;
88     private int mLocalUriColumnId;
89     private int mMediaTypeColumnId;
90     private int mReasonColumndId;
91
92     // TODO this shouldn't be necessary
93     private final Map<Long, SelectionObjAttrs> mSelectedIds =
94             new HashMap<Long, SelectionObjAttrs>();
95     private static class SelectionObjAttrs {
96         private String mFileName;
97         private String mMimeType;
98         SelectionObjAttrs(String fileName, String mimeType) {
99             mFileName = fileName;
100             mMimeType = mimeType;
101         }
102         String getFileName() {
103             return mFileName;
104         }
105         String getMimeType() {
106             return mMimeType;
107         }
108     }
109     private ListView mCurrentView;
110     private Cursor mCurrentCursor;
111     private boolean mCurrentViewIsExpandableListView = false;
112     private boolean mIsSortedBySize = false;
113
114     /**
115      * We keep track of when a dialog is being displayed for a pending download, because if that
116      * download starts running, we want to immediately hide the dialog.
117      */
118     private Long mQueuedDownloadId = null;
119     private AlertDialog mQueuedDialog;
120     String mSelectedCountFormat;
121
122     private Button mSortOption;
123
124     private class MyContentObserver extends ContentObserver {
125         public MyContentObserver() {
126             super(new Handler());
127         }
128
129         @Override
130         public void onChange(boolean selfChange) {
131             handleDownloadsChanged();
132         }
133     }
134
135     private class MyDataSetObserver extends DataSetObserver {
136         @Override
137         public void onChanged() {
138             // ignore change notification if there are selections
139             if (mSelectedIds.size() > 0) {
140                 return;
141             }
142             // may need to switch to or from the empty view
143             chooseListToShow();
144             ensureSomeGroupIsExpanded();
145         }
146     }
147
148     @Override
149     public void onCreate(Bundle icicle) {
150         super.onCreate(icicle);
151
152         // Trampoline over to new management UI
153         final Intent intent = new Intent(DocumentsContract.ACTION_MANAGE_ROOT);
154         intent.setData(DocumentsContract.buildRootUri(
155                 Constants.STORAGE_AUTHORITY, Constants.STORAGE_ROOT_ID));
156         startActivity(intent);
157         finish();
158     }
159
160     public void onCreateLegacy(Bundle icicle) {
161         super.onCreate(icicle);
162         setFinishOnTouchOutside(true);
163         setupViews();
164
165         mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
166         mDownloadManager.setAccessAllDownloads(true);
167         DownloadManager.Query baseQuery = new DownloadManager.Query()
168                 .setOnlyIncludeVisibleInDownloadsUi(true);
169         //TODO don't do both queries - do them as needed
170         mDateSortedCursor = mDownloadManager.query(baseQuery);
171         mSizeSortedCursor = mDownloadManager.query(baseQuery
172                                                   .orderBy(DownloadManager.COLUMN_TOTAL_SIZE_BYTES,
173                                                           DownloadManager.Query.ORDER_DESCENDING));
174
175         // only attach everything to the listbox if we can access the download database. Otherwise,
176         // just show it empty
177         if (haveCursors()) {
178             startManagingCursor(mDateSortedCursor);
179             startManagingCursor(mSizeSortedCursor);
180
181             mStatusColumnId =
182                     mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
183             mIdColumnId =
184                     mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
185             mLocalUriColumnId =
186                     mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
187             mMediaTypeColumnId =
188                     mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE);
189             mReasonColumndId =
190                     mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON);
191
192             mDateSortedAdapter = new DateSortedDownloadAdapter(this, mDateSortedCursor);
193             mDateOrderedListView.setAdapter(mDateSortedAdapter);
194             mSizeSortedAdapter = new DownloadAdapter(this, mSizeSortedCursor);
195             mSizeOrderedListView.setAdapter(mSizeSortedAdapter);
196
197             ensureSomeGroupIsExpanded();
198         }
199
200         // did the caller want  to display the data sorted by size?
201         Bundle extras = getIntent().getExtras();
202         if (extras != null &&
203                 extras.getBoolean(DownloadManager.INTENT_EXTRAS_SORT_BY_SIZE, false)) {
204             mIsSortedBySize = true;
205         }
206         mSortOption = (Button) findViewById(R.id.sort_button);
207         mSortOption.setOnClickListener(new OnClickListener() {
208             @Override
209             public void onClick(View v) {
210                 // flip the view
211                 mIsSortedBySize = !mIsSortedBySize;
212                 // clear all selections
213                 mSelectedIds.clear();
214                 chooseListToShow();
215             }
216         });
217
218         chooseListToShow();
219         mSelectedCountFormat = getString(R.string.selected_count);
220     }
221
222     /**
223      * If no group is expanded in the date-sorted list, expand the first one.
224      */
225     private void ensureSomeGroupIsExpanded() {
226         mDateOrderedListView.post(new Runnable() {
227             public void run() {
228                 if (mDateSortedAdapter.getGroupCount() == 0) {
229                     return;
230                 }
231                 for (int group = 0; group < mDateSortedAdapter.getGroupCount(); group++) {
232                     if (mDateOrderedListView.isGroupExpanded(group)) {
233                         return;
234                     }
235                 }
236                 mDateOrderedListView.expandGroup(0);
237             }
238         });
239     }
240
241     private void setupViews() {
242         setContentView(R.layout.download_list);
243         ModeCallback modeCallback = new ModeCallback(this);
244
245         //TODO don't create both views. create only the one needed.
246         mDateOrderedListView = (ExpandableListView) findViewById(R.id.date_ordered_list);
247         mDateOrderedListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
248         mDateOrderedListView.setMultiChoiceModeListener(modeCallback);
249         mDateOrderedListView.setOnChildClickListener(new OnChildClickListener() {
250             // called when a child is clicked on (this is NOT the checkbox click)
251             @Override
252             public boolean onChildClick(ExpandableListView parent, View v,
253                     int groupPosition, int childPosition, long id) {
254                 if (!(v instanceof DownloadItem)) {
255                     // can this even happen?
256                     return false;
257                 }
258                 if (mSelectedIds.size() > 0) {
259                     ((DownloadItem)v).setChecked(true);
260                 } else {
261                     mDateSortedAdapter.moveCursorToChildPosition(groupPosition, childPosition);
262                     handleItemClick(mDateSortedCursor);
263                 }
264                 return true;
265             }
266         });
267         mSizeOrderedListView = (ListView) findViewById(R.id.size_ordered_list);
268         mSizeOrderedListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
269         mSizeOrderedListView.setMultiChoiceModeListener(modeCallback);
270         mSizeOrderedListView.setOnItemClickListener(new OnItemClickListener() {
271             // handle a click from the size-sorted list. (this is NOT the checkbox click)
272             @Override
273             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
274                 mSizeSortedCursor.moveToPosition(position);
275                 handleItemClick(mSizeSortedCursor);
276             }
277         });
278         mEmptyView = findViewById(R.id.empty);
279     }
280
281     private static class ModeCallback implements MultiChoiceModeListener {
282         private final DownloadList mDownloadList;
283
284         public ModeCallback(DownloadList downloadList) {
285             mDownloadList = downloadList;
286         }
287
288         @Override public void onDestroyActionMode(ActionMode mode) {
289             mDownloadList.mSelectedIds.clear();
290             mDownloadList.mActionMode = null;
291         }
292
293         @Override
294         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
295             return true;
296         }
297
298         @Override
299         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
300             if (mDownloadList.haveCursors()) {
301                 final MenuInflater inflater = mDownloadList.getMenuInflater();
302                 inflater.inflate(R.menu.download_menu, menu);
303             }
304             mDownloadList.mActionMode = mode;
305             return true;
306         }
307
308         @Override
309         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
310             if (mDownloadList.mSelectedIds.size() == 0) {
311                 // nothing selected.
312                 return true;
313             }
314             switch (item.getItemId()) {
315                 case R.id.delete_download:
316                     for (Long downloadId : mDownloadList.mSelectedIds.keySet()) {
317                         mDownloadList.deleteDownload(downloadId);
318                     }
319                     // uncheck all checked items
320                     ListView lv = mDownloadList.getCurrentView();
321                     SparseBooleanArray checkedPositionList = lv.getCheckedItemPositions();
322                     int checkedPositionListSize = checkedPositionList.size();
323                     ArrayList<DownloadItem> sharedFiles = null;
324                     for (int i = 0; i < checkedPositionListSize; i++) {
325                         int position = checkedPositionList.keyAt(i);
326                         if (checkedPositionList.get(position, false)) {
327                             lv.setItemChecked(position, false);
328                             onItemCheckedStateChanged(mode, position, 0, false);
329                         }
330                     }
331                     mDownloadList.mSelectedIds.clear();
332                     // update the subtitle
333                     onItemCheckedStateChanged(mode, 1, 0, false);
334                     break;
335                 case R.id.share_download:
336                     mDownloadList.shareDownloadedFiles();
337                     break;
338             }
339             return true;
340         }
341
342         @Override
343         public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
344                 boolean checked) {
345             // ignore long clicks on groups
346             if (mDownloadList.isCurrentViewExpandableListView()) {
347                 ExpandableListView ev = mDownloadList.getExpandableListView();
348                 long pos = ev.getExpandableListPosition(position);
349                 if (checked && (ExpandableListView.getPackedPositionType(pos) ==
350                         ExpandableListView.PACKED_POSITION_TYPE_GROUP)) {
351                     // ignore this click
352                     ev.setItemChecked(position, false);
353                     return;
354                 }
355             }
356             mDownloadList.setActionModeTitle(mode);
357         }
358     }
359
360     void setActionModeTitle(ActionMode mode) {
361         int numSelected = mSelectedIds.size();
362         if (numSelected > 0) {
363             mode.setTitle(String.format(mSelectedCountFormat, numSelected,
364                     mCurrentCursor.getCount()));
365         } else {
366             mode.setTitle("");
367         }
368     }
369
370     private boolean haveCursors() {
371         return mDateSortedCursor != null && mSizeSortedCursor != null;
372     }
373
374     @Override
375     protected void onResume() {
376         super.onResume();
377         if (haveCursors()) {
378             mDateSortedCursor.registerContentObserver(mContentObserver);
379             mDateSortedCursor.registerDataSetObserver(mDataSetObserver);
380             refresh();
381         }
382     }
383
384     @Override
385     protected void onPause() {
386         super.onPause();
387         if (haveCursors()) {
388             mDateSortedCursor.unregisterContentObserver(mContentObserver);
389             mDateSortedCursor.unregisterDataSetObserver(mDataSetObserver);
390         }
391     }
392
393     private static final String BUNDLE_SAVED_DOWNLOAD_IDS = "download_ids";
394     private static final String BUNDLE_SAVED_FILENAMES = "filenames";
395     private static final String BUNDLE_SAVED_MIMETYPES = "mimetypes";
396     @Override
397     protected void onSaveInstanceState(Bundle outState) {
398         super.onSaveInstanceState(outState);
399         outState.putBoolean("isSortedBySize", mIsSortedBySize);
400         int len = mSelectedIds.size();
401         if (len == 0) {
402             return;
403         }
404         long[] selectedIds = new long[len];
405         String[] fileNames = new String[len];
406         String[] mimeTypes = new String[len];
407         int i = 0;
408         for (long id : mSelectedIds.keySet()) {
409             selectedIds[i] = id;
410             SelectionObjAttrs obj = mSelectedIds.get(id);
411             fileNames[i] = obj.getFileName();
412             mimeTypes[i] = obj.getMimeType();
413             i++;
414         }
415         outState.putLongArray(BUNDLE_SAVED_DOWNLOAD_IDS, selectedIds);
416         outState.putStringArray(BUNDLE_SAVED_FILENAMES, fileNames);
417         outState.putStringArray(BUNDLE_SAVED_MIMETYPES, mimeTypes);
418     }
419
420     @Override
421     protected void onRestoreInstanceState(Bundle savedInstanceState) {
422         super.onRestoreInstanceState(savedInstanceState);
423         mIsSortedBySize = savedInstanceState.getBoolean("isSortedBySize");
424         mSelectedIds.clear();
425         long[] selectedIds = savedInstanceState.getLongArray(BUNDLE_SAVED_DOWNLOAD_IDS);
426         String[] fileNames = savedInstanceState.getStringArray(BUNDLE_SAVED_FILENAMES);
427         String[] mimeTypes = savedInstanceState.getStringArray(BUNDLE_SAVED_MIMETYPES);
428         if (selectedIds != null && selectedIds.length > 0) {
429             for (int i = 0; i < selectedIds.length; i++) {
430                 mSelectedIds.put(selectedIds[i], new SelectionObjAttrs(fileNames[i], mimeTypes[i]));
431             }
432         }
433         chooseListToShow();
434     }
435
436     /**
437      * Show the correct ListView and hide the other, or hide both and show the empty view.
438      */
439     private void chooseListToShow() {
440         mDateOrderedListView.setVisibility(View.GONE);
441         mSizeOrderedListView.setVisibility(View.GONE);
442
443         if (mDateSortedCursor == null || mDateSortedCursor.getCount() == 0) {
444             mEmptyView.setVisibility(View.VISIBLE);
445             mSortOption.setVisibility(View.GONE);
446         } else {
447             mEmptyView.setVisibility(View.GONE);
448             mSortOption.setVisibility(View.VISIBLE);
449             ListView lv = activeListView();
450             lv.setVisibility(View.VISIBLE);
451             lv.invalidateViews(); // ensure checkboxes get updated
452         }
453         // restore the ActionMode title if there are selections
454         if (mActionMode != null) {
455             setActionModeTitle(mActionMode);
456         }
457     }
458
459     ListView getCurrentView() {
460         return mCurrentView;
461     }
462
463     ExpandableListView getExpandableListView() {
464         return mDateOrderedListView;
465     }
466
467     boolean isCurrentViewExpandableListView() {
468         return mCurrentViewIsExpandableListView;
469     }
470
471     private ListView activeListView() {
472         if (mIsSortedBySize) {
473             mCurrentCursor = mSizeSortedCursor;
474             mCurrentView = mSizeOrderedListView;
475             setTitle(R.string.download_title_sorted_by_size);
476             mSortOption.setText(R.string.button_sort_by_date);
477             mCurrentViewIsExpandableListView = false;
478         } else {
479             mCurrentCursor = mDateSortedCursor;
480             mCurrentView = mDateOrderedListView;
481             setTitle(R.string.download_title_sorted_by_date);
482             mSortOption.setText(R.string.button_sort_by_size);
483             mCurrentViewIsExpandableListView = true;
484         }
485         if (mActionMode != null) {
486             mActionMode.finish();
487         }
488         return mCurrentView;
489     }
490
491     /**
492      * @return an OnClickListener to delete the given downloadId from the Download Manager
493      */
494     private DialogInterface.OnClickListener getDeleteClickHandler(final long downloadId) {
495         return new DialogInterface.OnClickListener() {
496             @Override
497             public void onClick(DialogInterface dialog, int which) {
498                 deleteDownload(downloadId);
499             }
500         };
501     }
502
503     /**
504      * @return an OnClickListener to restart the given downloadId in the Download Manager
505      */
506     private DialogInterface.OnClickListener getRestartClickHandler(final long downloadId) {
507         return new DialogInterface.OnClickListener() {
508             @Override
509             public void onClick(DialogInterface dialog, int which) {
510                 mDownloadManager.restartDownload(downloadId);
511             }
512         };
513     }
514
515     /**
516      * Send an Intent to open the download currently pointed to by the given cursor.
517      */
518     private void openCurrentDownload(Cursor cursor) {
519         final Uri localUri = Uri.parse(cursor.getString(mLocalUriColumnId));
520         try {
521             getContentResolver().openFileDescriptor(localUri, "r").close();
522         } catch (FileNotFoundException exc) {
523             Log.d(LOG_TAG, "Failed to open download " + cursor.getLong(mIdColumnId), exc);
524             showFailedDialog(cursor.getLong(mIdColumnId),
525                     getString(R.string.dialog_file_missing_body));
526             return;
527         } catch (IOException exc) {
528             // close() failed, not a problem
529         }
530
531         final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID));
532         if (!OpenHelper.startViewIntent(this, id, 0)) {
533             Toast.makeText(this, R.string.download_no_application_title, Toast.LENGTH_SHORT).show();
534         }
535     }
536
537     private void handleItemClick(Cursor cursor) {
538         long id = cursor.getInt(mIdColumnId);
539         switch (cursor.getInt(mStatusColumnId)) {
540             case DownloadManager.STATUS_PENDING:
541             case DownloadManager.STATUS_RUNNING:
542                 sendRunningDownloadClickedBroadcast(id);
543                 break;
544
545             case DownloadManager.STATUS_PAUSED:
546                 if (isPausedForWifi(cursor)) {
547                     mQueuedDownloadId = id;
548                     mQueuedDialog = new AlertDialog.Builder(this)
549                             .setTitle(R.string.dialog_title_queued_body)
550                             .setMessage(R.string.dialog_queued_body)
551                             .setPositiveButton(R.string.keep_queued_download, null)
552                             .setNegativeButton(R.string.remove_download, getDeleteClickHandler(id))
553                             .setOnCancelListener(new DialogInterface.OnCancelListener() {
554                                 /**
555                                  * Called when a dialog for a pending download is canceled.
556                                  */
557                                 @Override
558                                 public void onCancel(DialogInterface dialog) {
559                                     mQueuedDownloadId = null;
560                                     mQueuedDialog = null;
561                                 }
562                             })
563                             .show();
564                 } else {
565                     sendRunningDownloadClickedBroadcast(id);
566                 }
567                 break;
568
569             case DownloadManager.STATUS_SUCCESSFUL:
570                 openCurrentDownload(cursor);
571                 break;
572
573             case DownloadManager.STATUS_FAILED:
574                 showFailedDialog(id, getErrorMessage(cursor));
575                 break;
576         }
577     }
578
579     /**
580      * @return the appropriate error message for the failed download pointed to by cursor
581      */
582     private String getErrorMessage(Cursor cursor) {
583         switch (cursor.getInt(mReasonColumndId)) {
584             case DownloadManager.ERROR_FILE_ALREADY_EXISTS:
585                 if (isOnExternalStorage(cursor)) {
586                     return getString(R.string.dialog_file_already_exists);
587                 } else {
588                     // the download manager should always find a free filename for cache downloads,
589                     // so this indicates a strange internal error
590                     return getUnknownErrorMessage();
591                 }
592
593             case DownloadManager.ERROR_INSUFFICIENT_SPACE:
594                 if (isOnExternalStorage(cursor)) {
595                     return getString(R.string.dialog_insufficient_space_on_external);
596                 } else {
597                     return getString(R.string.dialog_insufficient_space_on_cache);
598                 }
599
600             case DownloadManager.ERROR_DEVICE_NOT_FOUND:
601                 return getString(R.string.dialog_media_not_found);
602
603             case DownloadManager.ERROR_CANNOT_RESUME:
604                 return getString(R.string.dialog_cannot_resume);
605
606             default:
607                 return getUnknownErrorMessage();
608         }
609     }
610
611     private boolean isOnExternalStorage(Cursor cursor) {
612         String localUriString = cursor.getString(mLocalUriColumnId);
613         if (localUriString == null) {
614             return false;
615         }
616         Uri localUri = Uri.parse(localUriString);
617         if (!localUri.getScheme().equals("file")) {
618             return false;
619         }
620         String path = localUri.getPath();
621         String externalRoot = Environment.getExternalStorageDirectory().getPath();
622         return path.startsWith(externalRoot);
623     }
624
625     private String getUnknownErrorMessage() {
626         return getString(R.string.dialog_failed_body);
627     }
628
629     private void showFailedDialog(long downloadId, String dialogBody) {
630         new AlertDialog.Builder(this)
631                 .setTitle(R.string.dialog_title_not_available)
632                 .setMessage(dialogBody)
633                 .setNegativeButton(R.string.delete_download, getDeleteClickHandler(downloadId))
634                 .setPositiveButton(R.string.retry_download, getRestartClickHandler(downloadId))
635                 .show();
636     }
637
638     private void sendRunningDownloadClickedBroadcast(long id) {
639         final Intent intent = new Intent(Constants.ACTION_LIST);
640         intent.setPackage(Constants.PROVIDER_PACKAGE_NAME);
641         intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
642                 new long[] { id });
643         sendBroadcast(intent);
644     }
645
646     // handle a click on one of the download item checkboxes
647     public void onDownloadSelectionChanged(long downloadId, boolean isSelected,
648             String fileName, String mimeType) {
649         if (isSelected) {
650             mSelectedIds.put(downloadId, new SelectionObjAttrs(fileName, mimeType));
651         } else {
652             mSelectedIds.remove(downloadId);
653         }
654     }
655
656     /**
657      * Requery the database and update the UI.
658      */
659     private void refresh() {
660         mDateSortedCursor.requery();
661         mSizeSortedCursor.requery();
662         // Adapters get notification of changes and update automatically
663     }
664
665     /**
666      * Delete a download from the Download Manager.
667      */
668     private void deleteDownload(long downloadId) {
669         // let DownloadService do the job of cleaning up the downloads db, mediaprovider db,
670         // and removal of file from sdcard
671         // TODO do the following in asynctask - not on main thread.
672         mDownloadManager.markRowDeleted(downloadId);
673     }
674
675     public boolean isDownloadSelected(long id) {
676         return mSelectedIds.containsKey(id);
677     }
678
679     /**
680      * Called when there's a change to the downloads database.
681      */
682     void handleDownloadsChanged() {
683         checkSelectionForDeletedEntries();
684
685         if (mQueuedDownloadId != null && moveToDownload(mQueuedDownloadId)) {
686             if (mDateSortedCursor.getInt(mStatusColumnId) != DownloadManager.STATUS_PAUSED
687                     || !isPausedForWifi(mDateSortedCursor)) {
688                 mQueuedDialog.cancel();
689             }
690         }
691     }
692
693     private boolean isPausedForWifi(Cursor cursor) {
694         return cursor.getInt(mReasonColumndId) == DownloadManager.PAUSED_QUEUED_FOR_WIFI;
695     }
696
697     /**
698      * Check if any of the selected downloads have been deleted from the downloads database, and
699      * remove such downloads from the selection.
700      */
701     private void checkSelectionForDeletedEntries() {
702         // gather all existing IDs...
703         Set<Long> allIds = new HashSet<Long>();
704         for (mDateSortedCursor.moveToFirst(); !mDateSortedCursor.isAfterLast();
705                 mDateSortedCursor.moveToNext()) {
706             allIds.add(mDateSortedCursor.getLong(mIdColumnId));
707         }
708
709         // ...and check if any selected IDs are now missing
710         for (Iterator<Long> iterator = mSelectedIds.keySet().iterator(); iterator.hasNext(); ) {
711             if (!allIds.contains(iterator.next())) {
712                 iterator.remove();
713             }
714         }
715     }
716
717     /**
718      * Move {@link #mDateSortedCursor} to the download with the given ID.
719      * @return true if the specified download ID was found; false otherwise
720      */
721     private boolean moveToDownload(long downloadId) {
722         for (mDateSortedCursor.moveToFirst(); !mDateSortedCursor.isAfterLast();
723                 mDateSortedCursor.moveToNext()) {
724             if (mDateSortedCursor.getLong(mIdColumnId) == downloadId) {
725                 return true;
726             }
727         }
728         return false;
729     }
730
731     /**
732      * handle share menu button click when one more files are selected for sharing
733      */
734     public boolean shareDownloadedFiles() {
735         Intent intent = new Intent();
736         if (mSelectedIds.size() > 1) {
737             intent.setAction(Intent.ACTION_SEND_MULTIPLE);
738             ArrayList<Parcelable> attachments = new ArrayList<Parcelable>();
739             ArrayList<String> mimeTypes = new ArrayList<String>();
740             for (Map.Entry<Long, SelectionObjAttrs> item : mSelectedIds.entrySet()) {
741                 final Uri uri = ContentUris.withAppendedId(
742                         Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, item.getKey());
743                 final String mimeType = item.getValue().getMimeType();
744                 attachments.add(uri);
745                 if (mimeType != null) {
746                     mimeTypes.add(mimeType);
747                 }
748             }
749             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
750             intent.setType(findCommonMimeType(mimeTypes));
751         } else {
752             // get the entry
753             // since there is ONLY one entry in this, we can do the following
754             for (Map.Entry<Long, SelectionObjAttrs> item : mSelectedIds.entrySet()) {
755                 final Uri uri = ContentUris.withAppendedId(
756                         Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, item.getKey());
757                 final String mimeType = item.getValue().getMimeType();
758                 intent.setAction(Intent.ACTION_SEND);
759                 intent.putExtra(Intent.EXTRA_STREAM, uri);
760                 intent.setType(mimeType);
761             }
762         }
763         intent = Intent.createChooser(intent, getText(R.string.download_share_dialog));
764         startActivity(intent);
765         return true;
766     }
767
768     private String findCommonMimeType(ArrayList<String> mimeTypes) {
769         // are all mimeypes the same?
770         String str = findCommonString(mimeTypes);
771         if (str != null) {
772             return str;
773         }
774
775         // are all prefixes of the given mimetypes the same?
776         ArrayList<String> mimeTypePrefixes = new ArrayList<String>();
777         for (String s : mimeTypes) {
778             if (s != null) {
779                 mimeTypePrefixes.add(s.substring(0, s.indexOf('/')));
780             }
781         }
782         str = findCommonString(mimeTypePrefixes);
783         if (str != null) {
784             return str + "/*";
785         }
786
787         // return generic mimetype
788         return "*/*";
789     }
790     private String findCommonString(Collection<String> set) {
791         String str = null;
792         boolean found = true;
793         for (String s : set) {
794             if (str == null) {
795                 str = s;
796             } else if (!str.equals(s)) {
797                 found = false;
798                 break;
799             }
800         }
801         return (found) ? str : null;
802     }
803 }