• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
SelectionObjAttrs(String fileName, String mimeType)98         SelectionObjAttrs(String fileName, String mimeType) {
99             mFileName = fileName;
100             mMimeType = mimeType;
101         }
getFileName()102         String getFileName() {
103             return mFileName;
104         }
getMimeType()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 {
MyContentObserver()125         public MyContentObserver() {
126             super(new Handler());
127         }
128 
129         @Override
onChange(boolean selfChange)130         public void onChange(boolean selfChange) {
131             handleDownloadsChanged();
132         }
133     }
134 
135     private class MyDataSetObserver extends DataSetObserver {
136         @Override
onChanged()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
onCreate(Bundle icicle)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      */
ensureSomeGroupIsExpanded()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 
setupViews()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 
ModeCallback(DownloadList downloadList)273         public ModeCallback(DownloadList downloadList) {
274             mDownloadList = downloadList;
275         }
276 
onDestroyActionMode(ActionMode mode)277         @Override public void onDestroyActionMode(ActionMode mode) {
278             mDownloadList.mSelectedIds.clear();
279             mDownloadList.mActionMode = null;
280         }
281 
282         @Override
onPrepareActionMode(ActionMode mode, Menu menu)283         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
284             return true;
285         }
286 
287         @Override
onCreateActionMode(ActionMode mode, Menu menu)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
onActionItemClicked(ActionMode mode, MenuItem item)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
onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked)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 
setActionModeTitle(ActionMode mode)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 
haveCursors()359     private boolean haveCursors() {
360         return mDateSortedCursor != null && mSizeSortedCursor != null;
361     }
362 
363     @Override
onResume()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
onPause()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
onSaveInstanceState(Bundle outState)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
onRestoreInstanceState(Bundle savedInstanceState)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      */
chooseListToShow()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 
getCurrentView()446     ListView getCurrentView() {
447         return mCurrentView;
448     }
449 
getExpandableListView()450     ExpandableListView getExpandableListView() {
451         return mDateOrderedListView;
452     }
453 
isCurrentViewExpandableListView()454     boolean isCurrentViewExpandableListView() {
455         return mCurrentViewIsExpandableListView;
456     }
457 
activeListView()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      */
getDeleteClickHandler(final long downloadId)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                 mimeTypes.add(mimeType);
736             }
737             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
738             intent.setType(findCommonMimeType(mimeTypes));
739         } else {
740             // get the entry
741             // since there is ONLY one entry in this, we can do the following
742             for (Map.Entry<Long, SelectionObjAttrs> item : mSelectedIds.entrySet()) {
743                 final Uri uri = ContentUris.withAppendedId(
744                         Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, item.getKey());
745                 final String mimeType = item.getValue().getMimeType();
746                 intent.setAction(Intent.ACTION_SEND);
747                 intent.putExtra(Intent.EXTRA_STREAM, uri);
748                 intent.setType(mimeType);
749             }
750         }
751         intent = Intent.createChooser(intent, getText(R.string.download_share_dialog));
752         startActivity(intent);
753         return true;
754     }
755 
756     private String findCommonMimeType(ArrayList<String> mimeTypes) {
757         // are all mimeypes the same?
758         String str = findCommonString(mimeTypes);
759         if (str != null) {
760             return str;
761         }
762 
763         // are all prefixes of the given mimetypes the same?
764         ArrayList<String> mimeTypePrefixes = new ArrayList<String>();
765         for (String s : mimeTypes) {
766             mimeTypePrefixes.add(s.substring(0, s.indexOf('/')));
767         }
768         str = findCommonString(mimeTypePrefixes);
769         if (str != null) {
770             return str + "/*";
771         }
772 
773         // return generic mimetype
774         return "*/*";
775     }
776     private String findCommonString(Collection<String> set) {
777         String str = null;
778         boolean found = true;
779         for (String s : set) {
780             if (str == null) {
781                 str = s;
782             } else if (!str.equals(s)) {
783                 found = false;
784                 break;
785             }
786         }
787         return (found) ? str : null;
788     }
789 }
790