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