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