2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.providers.downloads.ui;
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.DialogInterface.OnCancelListener;
27 import android.content.Intent;
28 import android.database.ContentObserver;
29 import android.database.Cursor;
30 import android.database.DataSetObserver;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.Environment;
34 import android.os.Handler;
35 import android.provider.Downloads;
36 import android.text.TextUtils;
37 import android.util.Log;
38 import android.view.Menu;
39 import android.view.MenuInflater;
40 import android.view.MenuItem;
41 import android.view.View;
42 import android.view.View.OnClickListener;
43 import android.view.ViewGroup;
44 import android.view.animation.AnimationUtils;
45 import android.widget.AdapterView;
46 import android.widget.AdapterView.OnItemClickListener;
47 import android.widget.Button;
48 import android.widget.ExpandableListView;
49 import android.widget.ExpandableListView.OnChildClickListener;
50 import android.widget.ListView;
51 import android.widget.Toast;
53 import com.android.providers.downloads.ui.DownloadItem.DownloadSelectListener;
56 import java.io.FileNotFoundException;
57 import java.io.IOException;
58 import java.util.HashSet;
59 import java.util.Iterator;
63 * View showing a list of all downloads the Download Manager knows about.
65 public class DownloadList extends Activity
66 implements OnChildClickListener, OnItemClickListener, DownloadSelectListener,
67 OnClickListener, OnCancelListener {
68 private static final String LOG_TAG = "DownloadList";
70 private ExpandableListView mDateOrderedListView;
71 private ListView mSizeOrderedListView;
72 private View mEmptyView;
73 private ViewGroup mSelectionMenuView;
74 private Button mSelectionDeleteButton;
76 private DownloadManager mDownloadManager;
77 private Cursor mDateSortedCursor;
78 private DateSortedDownloadAdapter mDateSortedAdapter;
79 private Cursor mSizeSortedCursor;
80 private DownloadAdapter mSizeSortedAdapter;
81 private MyContentObserver mContentObserver = new MyContentObserver();
82 private MyDataSetObserver mDataSetObserver = new MyDataSetObserver();
84 private int mStatusColumnId;
85 private int mIdColumnId;
86 private int mLocalUriColumnId;
87 private int mMediaTypeColumnId;
88 private int mReasonColumndId;
89 private int mMediaProviderUriId;
91 private boolean mIsSortedBySize = false;
92 private Set<Long> mSelectedIds = new HashSet<Long>();
95 * We keep track of when a dialog is being displayed for a pending download, because if that
96 * download starts running, we want to immediately hide the dialog.
98 private Long mQueuedDownloadId = null;
99 private AlertDialog mQueuedDialog;
102 private class MyContentObserver extends ContentObserver {
103 public MyContentObserver() {
104 super(new Handler());
108 public void onChange(boolean selfChange) {
109 handleDownloadsChanged();
113 private class MyDataSetObserver extends DataSetObserver {
115 public void onChanged() {
116 // may need to switch to or from the empty view
118 ensureSomeGroupIsExpanded();
123 public void onCreate(Bundle icicle) {
124 super.onCreate(icicle);
127 mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
128 mDownloadManager.setAccessAllDownloads(true);
129 DownloadManager.Query baseQuery = new DownloadManager.Query()
130 .setOnlyIncludeVisibleInDownloadsUi(true);
131 mDateSortedCursor = mDownloadManager.query(baseQuery);
132 mSizeSortedCursor = mDownloadManager.query(baseQuery
133 .orderBy(DownloadManager.COLUMN_TOTAL_SIZE_BYTES,
134 DownloadManager.Query.ORDER_DESCENDING));
136 // only attach everything to the listbox if we can access the download database. Otherwise,
137 // just show it empty
139 startManagingCursor(mDateSortedCursor);
140 startManagingCursor(mSizeSortedCursor);
143 mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
145 mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
147 mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
149 mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE);
151 mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON);
152 mMediaProviderUriId =
153 mDateSortedCursor.getColumnIndexOrThrow(
154 DownloadManager.COLUMN_MEDIAPROVIDER_URI);
156 mDateSortedAdapter = new DateSortedDownloadAdapter(this, mDateSortedCursor, this);
157 mDateOrderedListView.setAdapter(mDateSortedAdapter);
158 mSizeSortedAdapter = new DownloadAdapter(this, mSizeSortedCursor, this);
159 mSizeOrderedListView.setAdapter(mSizeSortedAdapter);
161 ensureSomeGroupIsExpanded();
168 * If no group is expanded in the date-sorted list, expand the first one.
170 private void ensureSomeGroupIsExpanded() {
171 mDateOrderedListView.post(new Runnable() {
173 if (mDateSortedAdapter.getGroupCount() == 0) {
176 for (int group = 0; group < mDateSortedAdapter.getGroupCount(); group++) {
177 if (mDateOrderedListView.isGroupExpanded(group)) {
181 mDateOrderedListView.expandGroup(0);
186 private void setupViews() {
187 setContentView(R.layout.download_list);
188 setTitle(getText(R.string.download_title));
190 mDateOrderedListView = (ExpandableListView) findViewById(R.id.date_ordered_list);
191 mDateOrderedListView.setOnChildClickListener(this);
192 mSizeOrderedListView = (ListView) findViewById(R.id.size_ordered_list);
193 mSizeOrderedListView.setOnItemClickListener(this);
194 mEmptyView = findViewById(R.id.empty);
196 mSelectionMenuView = (ViewGroup) findViewById(R.id.selection_menu);
197 mSelectionDeleteButton = (Button) findViewById(R.id.selection_delete);
198 mSelectionDeleteButton.setOnClickListener(this);
200 ((Button) findViewById(R.id.deselect_all)).setOnClickListener(this);
203 private boolean haveCursors() {
204 return mDateSortedCursor != null && mSizeSortedCursor != null;
208 protected void onResume() {
211 mDateSortedCursor.registerContentObserver(mContentObserver);
212 mDateSortedCursor.registerDataSetObserver(mDataSetObserver);
218 protected void onPause() {
221 mDateSortedCursor.unregisterContentObserver(mContentObserver);
222 mDateSortedCursor.unregisterDataSetObserver(mDataSetObserver);
227 protected void onSaveInstanceState(Bundle outState) {
228 super.onSaveInstanceState(outState);
229 outState.putBoolean("isSortedBySize", mIsSortedBySize);
230 outState.putLongArray("selection", getSelectionAsArray());
233 private long[] getSelectionAsArray() {
234 long[] selectedIds = new long[mSelectedIds.size()];
235 Iterator<Long> iterator = mSelectedIds.iterator();
236 for (int i = 0; i < selectedIds.length; i++) {
237 selectedIds[i] = iterator.next();
243 protected void onRestoreInstanceState(Bundle savedInstanceState) {
244 super.onRestoreInstanceState(savedInstanceState);
245 mIsSortedBySize = savedInstanceState.getBoolean("isSortedBySize");
246 mSelectedIds.clear();
247 for (long selectedId : savedInstanceState.getLongArray("selection")) {
248 mSelectedIds.add(selectedId);
251 showOrHideSelectionMenu();
255 public boolean onCreateOptionsMenu(Menu menu) {
257 MenuInflater inflater = getMenuInflater();
258 inflater.inflate(R.menu.download_menu, menu);
264 public boolean onPrepareOptionsMenu(Menu menu) {
265 menu.findItem(R.id.download_menu_sort_by_size).setVisible(!mIsSortedBySize);
266 menu.findItem(R.id.download_menu_sort_by_date).setVisible(mIsSortedBySize);
267 return super.onPrepareOptionsMenu(menu);
271 public boolean onOptionsItemSelected(MenuItem item) {
272 switch (item.getItemId()) {
273 case R.id.download_menu_sort_by_size:
274 mIsSortedBySize = true;
277 case R.id.download_menu_sort_by_date:
278 mIsSortedBySize = false;
286 * Show the correct ListView and hide the other, or hide both and show the empty view.
288 private void chooseListToShow() {
289 mDateOrderedListView.setVisibility(View.GONE);
290 mSizeOrderedListView.setVisibility(View.GONE);
292 if (mDateSortedCursor == null || mDateSortedCursor.getCount() == 0) {
293 mEmptyView.setVisibility(View.VISIBLE);
295 mEmptyView.setVisibility(View.GONE);
296 activeListView().setVisibility(View.VISIBLE);
297 activeListView().invalidateViews(); // ensure checkboxes get updated
302 * @return the ListView that should currently be visible.
304 private ListView activeListView() {
305 if (mIsSortedBySize) {
306 return mSizeOrderedListView;
308 return mDateOrderedListView;
312 * @return an OnClickListener to delete the given downloadId from the Download Manager
314 private DialogInterface.OnClickListener getDeleteClickHandler(final long downloadId) {
315 return new DialogInterface.OnClickListener() {
317 public void onClick(DialogInterface dialog, int which) {
318 deleteDownload(downloadId);
324 * @return an OnClickListener to restart the given downloadId in the Download Manager
326 private DialogInterface.OnClickListener getRestartClickHandler(final long downloadId) {
327 return new DialogInterface.OnClickListener() {
329 public void onClick(DialogInterface dialog, int which) {
330 mDownloadManager.restartDownload(downloadId);
336 * Send an Intent to open the download currently pointed to by the given cursor.
338 private void openCurrentDownload(Cursor cursor) {
339 Uri localUri = Uri.parse(cursor.getString(mLocalUriColumnId));
341 getContentResolver().openFileDescriptor(localUri, "r").close();
342 } catch (FileNotFoundException exc) {
343 Log.d(LOG_TAG, "Failed to open download " + cursor.getLong(mIdColumnId), exc);
344 showFailedDialog(cursor.getLong(mIdColumnId),
345 getString(R.string.dialog_file_missing_body));
347 } catch (IOException exc) {
348 // close() failed, not a problem
351 Intent intent = new Intent(Intent.ACTION_VIEW);
352 intent.setDataAndType(localUri, cursor.getString(mMediaTypeColumnId));
353 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
355 startActivity(intent);
356 } catch (ActivityNotFoundException ex) {
357 Toast.makeText(this, R.string.download_no_application_title, Toast.LENGTH_LONG).show();
361 private void handleItemClick(Cursor cursor) {
362 long id = cursor.getInt(mIdColumnId);
363 switch (cursor.getInt(mStatusColumnId)) {
364 case DownloadManager.STATUS_PENDING:
365 case DownloadManager.STATUS_RUNNING:
366 sendRunningDownloadClickedBroadcast(id);
369 case DownloadManager.STATUS_PAUSED:
370 if (isPausedForWifi(cursor)) {
371 mQueuedDownloadId = id;
372 mQueuedDialog = new AlertDialog.Builder(this)
373 .setTitle(R.string.dialog_title_queued_body)
374 .setMessage(R.string.dialog_queued_body)
375 .setPositiveButton(R.string.keep_queued_download, null)
376 .setNegativeButton(R.string.remove_download, getDeleteClickHandler(id))
377 .setOnCancelListener(this)
380 sendRunningDownloadClickedBroadcast(id);
384 case DownloadManager.STATUS_SUCCESSFUL:
385 openCurrentDownload(cursor);
388 case DownloadManager.STATUS_FAILED:
389 showFailedDialog(id, getErrorMessage(cursor));
395 * @return the appropriate error message for the failed download pointed to by cursor
397 private String getErrorMessage(Cursor cursor) {
398 switch (cursor.getInt(mReasonColumndId)) {
399 case DownloadManager.ERROR_FILE_ALREADY_EXISTS:
400 if (isOnExternalStorage(cursor)) {
401 return getString(R.string.dialog_file_already_exists);
403 // the download manager should always find a free filename for cache downloads,
404 // so this indicates a strange internal error
405 return getUnknownErrorMessage();
408 case DownloadManager.ERROR_INSUFFICIENT_SPACE:
409 if (isOnExternalStorage(cursor)) {
410 return getString(R.string.dialog_insufficient_space_on_external);
412 return getString(R.string.dialog_insufficient_space_on_cache);
415 case DownloadManager.ERROR_DEVICE_NOT_FOUND:
416 return getString(R.string.dialog_media_not_found);
418 case DownloadManager.ERROR_CANNOT_RESUME:
419 return getString(R.string.dialog_cannot_resume);
422 return getUnknownErrorMessage();
426 private boolean isOnExternalStorage(Cursor cursor) {
427 String localUriString = cursor.getString(mLocalUriColumnId);
428 if (localUriString == null) {
431 Uri localUri = Uri.parse(localUriString);
432 if (!localUri.getScheme().equals("file")) {
435 String path = localUri.getPath();
436 String externalRoot = Environment.getExternalStorageDirectory().getPath();
437 return path.startsWith(externalRoot);
440 private String getUnknownErrorMessage() {
441 return getString(R.string.dialog_failed_body);
444 private void showFailedDialog(long downloadId, String dialogBody) {
445 new AlertDialog.Builder(this)
446 .setTitle(R.string.dialog_title_not_available)
447 .setMessage(dialogBody)
448 .setNegativeButton(R.string.delete_download, getDeleteClickHandler(downloadId))
449 .setPositiveButton(R.string.retry_download, getRestartClickHandler(downloadId))
454 * TODO use constants/shared code?
456 private void sendRunningDownloadClickedBroadcast(long id) {
457 Intent intent = new Intent("android.intent.action.DOWNLOAD_LIST");
458 intent.setClassName("com.android.providers.downloads",
459 "com.android.providers.downloads.DownloadReceiver");
460 intent.setData(ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id));
461 intent.putExtra("multiple", false);
462 sendBroadcast(intent);
465 // handle a click from the date-sorted list
467 public boolean onChildClick(ExpandableListView parent, View v,
468 int groupPosition, int childPosition, long id) {
469 mDateSortedAdapter.moveCursorToChildPosition(groupPosition, childPosition);
470 handleItemClick(mDateSortedCursor);
474 // handle a click from the size-sorted list
476 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
477 mSizeSortedCursor.moveToPosition(position);
478 handleItemClick(mSizeSortedCursor);
481 // handle a click on one of the download item checkboxes
483 public void onDownloadSelectionChanged(long downloadId, boolean isSelected) {
485 mSelectedIds.add(downloadId);
487 mSelectedIds.remove(downloadId);
489 showOrHideSelectionMenu();
492 private void showOrHideSelectionMenu() {
493 boolean shouldBeVisible = !mSelectedIds.isEmpty();
494 boolean isVisible = mSelectionMenuView.getVisibility() == View.VISIBLE;
495 if (shouldBeVisible) {
496 updateSelectionMenu();
499 mSelectionMenuView.setVisibility(View.VISIBLE);
500 mSelectionMenuView.startAnimation(
501 AnimationUtils.loadAnimation(this, R.anim.footer_appear));
503 } else if (!shouldBeVisible && isVisible) {
505 mSelectionMenuView.setVisibility(View.GONE);
506 mSelectionMenuView.startAnimation(
507 AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
512 * Set up the contents of the selection menu based on the current selection.
514 private void updateSelectionMenu() {
515 int deleteButtonStringId = R.string.delete_download;
516 if (mSelectedIds.size() == 1) {
517 Cursor cursor = mDownloadManager.query(new DownloadManager.Query()
518 .setFilterById(mSelectedIds.iterator().next()));
520 cursor.moveToFirst();
521 switch (cursor.getInt(mStatusColumnId)) {
522 case DownloadManager.STATUS_FAILED:
523 deleteButtonStringId = R.string.delete_download;
526 case DownloadManager.STATUS_PENDING:
527 deleteButtonStringId = R.string.remove_download;
530 case DownloadManager.STATUS_PAUSED:
531 case DownloadManager.STATUS_RUNNING:
532 deleteButtonStringId = R.string.cancel_running_download;
539 mSelectionDeleteButton.setText(deleteButtonStringId);
543 public void onClick(View v) {
545 case R.id.selection_delete:
546 for (Long downloadId : mSelectedIds) {
547 deleteDownload(downloadId);
552 case R.id.deselect_all:
559 * Requery the database and update the UI.
561 private void refresh() {
562 mDateSortedCursor.requery();
563 mSizeSortedCursor.requery();
564 // Adapters get notification of changes and update automatically
567 private void clearSelection() {
568 mSelectedIds.clear();
569 showOrHideSelectionMenu();
573 * Delete a download from the Download Manager.
575 private void deleteDownload(long downloadId) {
576 if (moveToDownload(downloadId)) {
577 int status = mDateSortedCursor.getInt(mStatusColumnId);
578 boolean isComplete = status == DownloadManager.STATUS_SUCCESSFUL
579 || status == DownloadManager.STATUS_FAILED;
580 String localUri = mDateSortedCursor.getString(mLocalUriColumnId);
581 if (isComplete && localUri != null) {
582 String path = Uri.parse(localUri).getPath();
583 if (path.startsWith(Environment.getExternalStorageDirectory().getPath())) {
584 String mediaProviderUri = mDateSortedCursor.getString(mMediaProviderUriId);
585 if (TextUtils.isEmpty(mediaProviderUri)) {
586 // downloads database doesn't have the mediaprovider_uri. It means
587 // this download occurred before mediaprovider_uri column existed
588 // in downloads table. Since MediaProvider needs the mediaprovider_uri to
589 // delete this download, just set the 'deleted' flag to 1 on this row
590 // in the database. DownloadService, upon seeing this flag set to 1, will
591 // re-scan the file and get the MediaProviderUri and then delete the file
592 mDownloadManager.markRowDeleted(downloadId);
595 getContentResolver().delete(Uri.parse(mediaProviderUri), null, null);
596 // sometimes mediaprovider doesn't delete file from sdcard after deleting it
597 // from its db. delete it now
599 File file = new File(path);
601 } catch (Exception e) {
602 Log.w(LOG_TAG, "file: '" + path + "' couldn't be deleted", e);
608 mDownloadManager.remove(downloadId);
612 public boolean isDownloadSelected(long id) {
613 return mSelectedIds.contains(id);
617 * Called when there's a change to the downloads database.
619 void handleDownloadsChanged() {
620 checkSelectionForDeletedEntries();
622 if (mQueuedDownloadId != null && moveToDownload(mQueuedDownloadId)) {
623 if (mDateSortedCursor.getInt(mStatusColumnId) != DownloadManager.STATUS_PAUSED
624 || !isPausedForWifi(mDateSortedCursor)) {
625 mQueuedDialog.cancel();
630 private boolean isPausedForWifi(Cursor cursor) {
631 return cursor.getInt(mReasonColumndId) == DownloadManager.PAUSED_QUEUED_FOR_WIFI;
635 * Check if any of the selected downloads have been deleted from the downloads database, and
636 * remove such downloads from the selection.
638 private void checkSelectionForDeletedEntries() {
639 // gather all existing IDs...
640 Set<Long> allIds = new HashSet<Long>();
641 for (mDateSortedCursor.moveToFirst(); !mDateSortedCursor.isAfterLast();
642 mDateSortedCursor.moveToNext()) {
643 allIds.add(mDateSortedCursor.getLong(mIdColumnId));
646 // ...and check if any selected IDs are now missing
647 for (Iterator<Long> iterator = mSelectedIds.iterator(); iterator.hasNext(); ) {
648 if (!allIds.contains(iterator.next())) {
655 * Move {@link #mDateSortedCursor} to the download with the given ID.
656 * @return true if the specified download ID was found; false otherwise
658 private boolean moveToDownload(long downloadId) {
659 for (mDateSortedCursor.moveToFirst(); !mDateSortedCursor.isAfterLast();
660 mDateSortedCursor.moveToNext()) {
661 if (mDateSortedCursor.getLong(mIdColumnId) == downloadId) {
669 * Called when a dialog for a pending download is canceled.
672 public void onCancel(DialogInterface dialog) {
673 mQueuedDownloadId = null;
674 mQueuedDialog = null;