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