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