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