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