05b5d75706e81703a3e72de92495141e2311315f
[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             mSortOption.setVisibility(View.GONE);
435         } else {
436             mEmptyView.setVisibility(View.GONE);
437             mSortOption.setVisibility(View.VISIBLE);
438             ListView lv = activeListView();
439             lv.setVisibility(View.VISIBLE);
440             lv.invalidateViews(); // ensure checkboxes get updated
441         }
442         // restore the ActionMode title if there are selections
443         if (mActionMode != null) {
444             setActionModeTitle(mActionMode);
445         }
446     }
447
448     ListView getCurrentView() {
449         return mCurrentView;
450     }
451
452     ExpandableListView getExpandableListView() {
453         return mDateOrderedListView;
454     }
455
456     boolean isCurrentViewExpandableListView() {
457         return mCurrentViewIsExpandableListView;
458     }
459
460     private ListView activeListView() {
461         if (mIsSortedBySize) {
462             mCurrentCursor = mSizeSortedCursor;
463             mCurrentView = mSizeOrderedListView;
464             setTitle(R.string.download_title_sorted_by_size);
465             mSortOption.setText(R.string.button_sort_by_date);
466             mCurrentViewIsExpandableListView = false;
467         } else {
468             mCurrentCursor = mDateSortedCursor;
469             mCurrentView = mDateOrderedListView;
470             setTitle(R.string.download_title_sorted_by_date);
471             mSortOption.setText(R.string.button_sort_by_size);
472             mCurrentViewIsExpandableListView = true;
473         }
474         if (mActionMode != null) {
475             mActionMode.finish();
476         }
477         return mCurrentView;
478     }
479
480     /**
481      * @return an OnClickListener to delete the given downloadId from the Download Manager
482      */
483     private DialogInterface.OnClickListener getDeleteClickHandler(final long downloadId) {
484         return new DialogInterface.OnClickListener() {
485             @Override
486             public void onClick(DialogInterface dialog, int which) {
487                 deleteDownload(downloadId);
488             }
489         };
490     }
491
492     /**
493      * @return an OnClickListener to restart the given downloadId in the Download Manager
494      */
495     private DialogInterface.OnClickListener getRestartClickHandler(final long downloadId) {
496         return new DialogInterface.OnClickListener() {
497             @Override
498             public void onClick(DialogInterface dialog, int which) {
499                 mDownloadManager.restartDownload(downloadId);
500             }
501         };
502     }
503
504     /**
505      * Send an Intent to open the download currently pointed to by the given cursor.
506      */
507     private void openCurrentDownload(Cursor cursor) {
508         final Uri localUri = Uri.parse(cursor.getString(mLocalUriColumnId));
509         try {
510             getContentResolver().openFileDescriptor(localUri, "r").close();
511         } catch (FileNotFoundException exc) {
512             Log.d(LOG_TAG, "Failed to open download " + cursor.getLong(mIdColumnId), exc);
513             showFailedDialog(cursor.getLong(mIdColumnId),
514                     getString(R.string.dialog_file_missing_body));
515             return;
516         } catch (IOException exc) {
517             // close() failed, not a problem
518         }
519
520         final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID));
521         final Intent intent = OpenHelper.buildViewIntent(this, id);
522         try {
523             startActivity(intent);
524         } catch (ActivityNotFoundException ex) {
525             Toast.makeText(this, R.string.download_no_application_title, Toast.LENGTH_LONG).show();
526         }
527     }
528
529     private void handleItemClick(Cursor cursor) {
530         long id = cursor.getInt(mIdColumnId);
531         switch (cursor.getInt(mStatusColumnId)) {
532             case DownloadManager.STATUS_PENDING:
533             case DownloadManager.STATUS_RUNNING:
534                 sendRunningDownloadClickedBroadcast(id);
535                 break;
536
537             case DownloadManager.STATUS_PAUSED:
538                 if (isPausedForWifi(cursor)) {
539                     mQueuedDownloadId = id;
540                     mQueuedDialog = new AlertDialog.Builder(this)
541                             .setTitle(R.string.dialog_title_queued_body)
542                             .setMessage(R.string.dialog_queued_body)
543                             .setPositiveButton(R.string.keep_queued_download, null)
544                             .setNegativeButton(R.string.remove_download, getDeleteClickHandler(id))
545                             .setOnCancelListener(new DialogInterface.OnCancelListener() {
546                                 /**
547                                  * Called when a dialog for a pending download is canceled.
548                                  */
549                                 @Override
550                                 public void onCancel(DialogInterface dialog) {
551                                     mQueuedDownloadId = null;
552                                     mQueuedDialog = null;
553                                 }
554                             })
555                             .show();
556                 } else {
557                     sendRunningDownloadClickedBroadcast(id);
558                 }
559                 break;
560
561             case DownloadManager.STATUS_SUCCESSFUL:
562                 openCurrentDownload(cursor);
563                 break;
564
565             case DownloadManager.STATUS_FAILED:
566                 showFailedDialog(id, getErrorMessage(cursor));
567                 break;
568         }
569     }
570
571     /**
572      * @return the appropriate error message for the failed download pointed to by cursor
573      */
574     private String getErrorMessage(Cursor cursor) {
575         switch (cursor.getInt(mReasonColumndId)) {
576             case DownloadManager.ERROR_FILE_ALREADY_EXISTS:
577                 if (isOnExternalStorage(cursor)) {
578                     return getString(R.string.dialog_file_already_exists);
579                 } else {
580                     // the download manager should always find a free filename for cache downloads,
581                     // so this indicates a strange internal error
582                     return getUnknownErrorMessage();
583                 }
584
585             case DownloadManager.ERROR_INSUFFICIENT_SPACE:
586                 if (isOnExternalStorage(cursor)) {
587                     return getString(R.string.dialog_insufficient_space_on_external);
588                 } else {
589                     return getString(R.string.dialog_insufficient_space_on_cache);
590                 }
591
592             case DownloadManager.ERROR_DEVICE_NOT_FOUND:
593                 return getString(R.string.dialog_media_not_found);
594
595             case DownloadManager.ERROR_CANNOT_RESUME:
596                 return getString(R.string.dialog_cannot_resume);
597
598             default:
599                 return getUnknownErrorMessage();
600         }
601     }
602
603     private boolean isOnExternalStorage(Cursor cursor) {
604         String localUriString = cursor.getString(mLocalUriColumnId);
605         if (localUriString == null) {
606             return false;
607         }
608         Uri localUri = Uri.parse(localUriString);
609         if (!localUri.getScheme().equals("file")) {
610             return false;
611         }
612         String path = localUri.getPath();
613         String externalRoot = Environment.getExternalStorageDirectory().getPath();
614         return path.startsWith(externalRoot);
615     }
616
617     private String getUnknownErrorMessage() {
618         return getString(R.string.dialog_failed_body);
619     }
620
621     private void showFailedDialog(long downloadId, String dialogBody) {
622         new AlertDialog.Builder(this)
623                 .setTitle(R.string.dialog_title_not_available)
624                 .setMessage(dialogBody)
625                 .setNegativeButton(R.string.delete_download, getDeleteClickHandler(downloadId))
626                 .setPositiveButton(R.string.retry_download, getRestartClickHandler(downloadId))
627                 .show();
628     }
629
630     private void sendRunningDownloadClickedBroadcast(long id) {
631         final Intent intent = new Intent(Constants.ACTION_LIST);
632         intent.setPackage(Constants.PROVIDER_PACKAGE_NAME);
633         intent.putExtra(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS,
634                 new long[] { id });
635         sendBroadcast(intent);
636     }
637
638     // handle a click on one of the download item checkboxes
639     public void onDownloadSelectionChanged(long downloadId, boolean isSelected,
640             String fileName, String mimeType) {
641         if (isSelected) {
642             mSelectedIds.put(downloadId, new SelectionObjAttrs(fileName, mimeType));
643         } else {
644             mSelectedIds.remove(downloadId);
645         }
646     }
647
648     /**
649      * Requery the database and update the UI.
650      */
651     private void refresh() {
652         mDateSortedCursor.requery();
653         mSizeSortedCursor.requery();
654         // Adapters get notification of changes and update automatically
655     }
656
657     /**
658      * Delete a download from the Download Manager.
659      */
660     private void deleteDownload(long downloadId) {
661         // let DownloadService do the job of cleaning up the downloads db, mediaprovider db,
662         // and removal of file from sdcard
663         // TODO do the following in asynctask - not on main thread.
664         mDownloadManager.markRowDeleted(downloadId);
665     }
666
667     public boolean isDownloadSelected(long id) {
668         return mSelectedIds.containsKey(id);
669     }
670
671     /**
672      * Called when there's a change to the downloads database.
673      */
674     void handleDownloadsChanged() {
675         checkSelectionForDeletedEntries();
676
677         if (mQueuedDownloadId != null && moveToDownload(mQueuedDownloadId)) {
678             if (mDateSortedCursor.getInt(mStatusColumnId) != DownloadManager.STATUS_PAUSED
679                     || !isPausedForWifi(mDateSortedCursor)) {
680                 mQueuedDialog.cancel();
681             }
682         }
683     }
684
685     private boolean isPausedForWifi(Cursor cursor) {
686         return cursor.getInt(mReasonColumndId) == DownloadManager.PAUSED_QUEUED_FOR_WIFI;
687     }
688
689     /**
690      * Check if any of the selected downloads have been deleted from the downloads database, and
691      * remove such downloads from the selection.
692      */
693     private void checkSelectionForDeletedEntries() {
694         // gather all existing IDs...
695         Set<Long> allIds = new HashSet<Long>();
696         for (mDateSortedCursor.moveToFirst(); !mDateSortedCursor.isAfterLast();
697                 mDateSortedCursor.moveToNext()) {
698             allIds.add(mDateSortedCursor.getLong(mIdColumnId));
699         }
700
701         // ...and check if any selected IDs are now missing
702         for (Iterator<Long> iterator = mSelectedIds.keySet().iterator(); iterator.hasNext(); ) {
703             if (!allIds.contains(iterator.next())) {
704                 iterator.remove();
705             }
706         }
707     }
708
709     /**
710      * Move {@link #mDateSortedCursor} to the download with the given ID.
711      * @return true if the specified download ID was found; false otherwise
712      */
713     private boolean moveToDownload(long downloadId) {
714         for (mDateSortedCursor.moveToFirst(); !mDateSortedCursor.isAfterLast();
715                 mDateSortedCursor.moveToNext()) {
716             if (mDateSortedCursor.getLong(mIdColumnId) == downloadId) {
717                 return true;
718             }
719         }
720         return false;
721     }
722
723     /**
724      * handle share menu button click when one more files are selected for sharing
725      */
726     public boolean shareDownloadedFiles() {
727         Intent intent = new Intent();
728         if (mSelectedIds.size() > 1) {
729             intent.setAction(Intent.ACTION_SEND_MULTIPLE);
730             ArrayList<Parcelable> attachments = new ArrayList<Parcelable>();
731             ArrayList<String> mimeTypes = new ArrayList<String>();
732             for (Map.Entry<Long, SelectionObjAttrs> item : mSelectedIds.entrySet()) {
733                 final Uri uri = ContentUris.withAppendedId(
734                         Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, item.getKey());
735                 final String mimeType = item.getValue().getMimeType();
736                 attachments.add(uri);
737                 if (mimeType != null) {
738                     mimeTypes.add(mimeType);
739                 }
740             }
741             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
742             intent.setType(findCommonMimeType(mimeTypes));
743         } else {
744             // get the entry
745             // since there is ONLY one entry in this, we can do the following
746             for (Map.Entry<Long, SelectionObjAttrs> item : mSelectedIds.entrySet()) {
747                 final Uri uri = ContentUris.withAppendedId(
748                         Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, item.getKey());
749                 final String mimeType = item.getValue().getMimeType();
750                 intent.setAction(Intent.ACTION_SEND);
751                 intent.putExtra(Intent.EXTRA_STREAM, uri);
752                 intent.setType(mimeType);
753             }
754         }
755         intent = Intent.createChooser(intent, getText(R.string.download_share_dialog));
756         startActivity(intent);
757         return true;
758     }
759
760     private String findCommonMimeType(ArrayList<String> mimeTypes) {
761         // are all mimeypes the same?
762         String str = findCommonString(mimeTypes);
763         if (str != null) {
764             return str;
765         }
766
767         // are all prefixes of the given mimetypes the same?
768         ArrayList<String> mimeTypePrefixes = new ArrayList<String>();
769         for (String s : mimeTypes) {
770             if (s != null) {
771                 mimeTypePrefixes.add(s.substring(0, s.indexOf('/')));
772             }
773         }
774         str = findCommonString(mimeTypePrefixes);
775         if (str != null) {
776             return str + "/*";
777         }
778
779         // return generic mimetype
780         return "*/*";
781     }
782     private String findCommonString(Collection<String> set) {
783         String str = null;
784         boolean found = true;
785         for (String s : set) {
786             if (str == null) {
787                 str = s;
788             } else if (!str.equals(s)) {
789                 found = false;
790                 break;
791             }
792         }
793         return (found) ? str : null;
794     }
795 }