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