blob: dfd5ffc99a768b53cc622254b27f574aa9c7cfcd [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.downloads.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.ContentUris;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.Button;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ListView;
import android.widget.Toast;
import com.android.providers.downloads.ui.DownloadItem.DownloadSelectListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* View showing a list of all downloads the Download Manager knows about.
*/
public class DownloadList extends Activity
implements OnChildClickListener, OnItemClickListener, DownloadSelectListener,
OnClickListener, OnCancelListener {
private static final String LOG_TAG = "DownloadList";
private ExpandableListView mDateOrderedListView;
private ListView mSizeOrderedListView;
private View mEmptyView;
private ViewGroup mSelectionMenuView;
private Button mSelectionDeleteButton;
private DownloadManager mDownloadManager;
private Cursor mDateSortedCursor;
private DateSortedDownloadAdapter mDateSortedAdapter;
private Cursor mSizeSortedCursor;
private DownloadAdapter mSizeSortedAdapter;
private MyContentObserver mContentObserver = new MyContentObserver();
private MyDataSetObserver mDataSetObserver = new MyDataSetObserver();
private int mStatusColumnId;
private int mIdColumnId;
private int mLocalUriColumnId;
private int mMediaTypeColumnId;
private int mReasonColumndId;
private int mMediaProviderUriId;
private boolean mIsSortedBySize = false;
private Set<Long> mSelectedIds = new HashSet<Long>();
/**
* We keep track of when a dialog is being displayed for a pending download, because if that
* download starts running, we want to immediately hide the dialog.
*/
private Long mQueuedDownloadId = null;
private AlertDialog mQueuedDialog;
private class MyContentObserver extends ContentObserver {
public MyContentObserver() {
super(new Handler());
}
@Override
public void onChange(boolean selfChange) {
handleDownloadsChanged();
}
}
private class MyDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
// may need to switch to or from the empty view
chooseListToShow();
ensureSomeGroupIsExpanded();
}
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setupViews();
mDownloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
mDownloadManager.setAccessAllDownloads(true);
DownloadManager.Query baseQuery = new DownloadManager.Query()
.setOnlyIncludeVisibleInDownloadsUi(true);
mDateSortedCursor = mDownloadManager.query(baseQuery);
mSizeSortedCursor = mDownloadManager.query(baseQuery
.orderBy(DownloadManager.COLUMN_TOTAL_SIZE_BYTES,
DownloadManager.Query.ORDER_DESCENDING));
// only attach everything to the listbox if we can access the download database. Otherwise,
// just show it empty
if (haveCursors()) {
startManagingCursor(mDateSortedCursor);
startManagingCursor(mSizeSortedCursor);
mStatusColumnId =
mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS);
mIdColumnId =
mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID);
mLocalUriColumnId =
mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI);
mMediaTypeColumnId =
mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE);
mReasonColumndId =
mDateSortedCursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON);
mMediaProviderUriId =
mDateSortedCursor.getColumnIndexOrThrow(
DownloadManager.COLUMN_MEDIAPROVIDER_URI);
mDateSortedAdapter = new DateSortedDownloadAdapter(this, mDateSortedCursor, this);
mDateOrderedListView.setAdapter(mDateSortedAdapter);
mSizeSortedAdapter = new DownloadAdapter(this, mSizeSortedCursor, this);
mSizeOrderedListView.setAdapter(mSizeSortedAdapter);
ensureSomeGroupIsExpanded();
}
chooseListToShow();
}
/**
* If no group is expanded in the date-sorted list, expand the first one.
*/
private void ensureSomeGroupIsExpanded() {
mDateOrderedListView.post(new Runnable() {
public void run() {
if (mDateSortedAdapter.getGroupCount() == 0) {
return;
}
for (int group = 0; group < mDateSortedAdapter.getGroupCount(); group++) {
if (mDateOrderedListView.isGroupExpanded(group)) {
return;
}
}
mDateOrderedListView.expandGroup(0);
}
});
}
private void setupViews() {
setContentView(R.layout.download_list);
setTitle(getText(R.string.download_title));
mDateOrderedListView = (ExpandableListView) findViewById(R.id.date_ordered_list);
mDateOrderedListView.setOnChildClickListener(this);
mSizeOrderedListView = (ListView) findViewById(R.id.size_ordered_list);
mSizeOrderedListView.setOnItemClickListener(this);
mEmptyView = findViewById(R.id.empty);
mSelectionMenuView = (ViewGroup) findViewById(R.id.selection_menu);
mSelectionDeleteButton = (Button) findViewById(R.id.selection_delete);
mSelectionDeleteButton.setOnClickListener(this);
((Button) findViewById(R.id.deselect_all)).setOnClickListener(this);
}
private boolean haveCursors() {
return mDateSortedCursor != null && mSizeSortedCursor != null;
}
@Override
protected void onResume() {
super.onResume();
if (haveCursors()) {
mDateSortedCursor.registerContentObserver(mContentObserver);
mDateSortedCursor.registerDataSetObserver(mDataSetObserver);
refresh();
}
}
@Override
protected void onPause() {
super.onPause();
if (haveCursors()) {
mDateSortedCursor.unregisterContentObserver(mContentObserver);
mDateSortedCursor.unregisterDataSetObserver(mDataSetObserver);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("isSortedBySize", mIsSortedBySize);
outState.putLongArray("selection", getSelectionAsArray());
}
private long[] getSelectionAsArray() {
long[] selectedIds = new long[mSelectedIds.size()];
Iterator<Long> iterator = mSelectedIds.iterator();
for (int i = 0; i < selectedIds.length; i++) {
selectedIds[i] = iterator.next();
}
return selectedIds;
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mIsSortedBySize = savedInstanceState.getBoolean("isSortedBySize");
mSelectedIds.clear();
for (long selectedId : savedInstanceState.getLongArray("selection")) {
mSelectedIds.add(selectedId);
}
chooseListToShow();
showOrHideSelectionMenu();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (haveCursors()) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.download_menu, menu);
}
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.download_menu_sort_by_size).setVisible(!mIsSortedBySize);
menu.findItem(R.id.download_menu_sort_by_date).setVisible(mIsSortedBySize);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.download_menu_sort_by_size:
mIsSortedBySize = true;
chooseListToShow();
return true;
case R.id.download_menu_sort_by_date:
mIsSortedBySize = false;
chooseListToShow();
return true;
}
return false;
}
/**
* Show the correct ListView and hide the other, or hide both and show the empty view.
*/
private void chooseListToShow() {
mDateOrderedListView.setVisibility(View.GONE);
mSizeOrderedListView.setVisibility(View.GONE);
if (mDateSortedCursor == null || mDateSortedCursor.getCount() == 0) {
mEmptyView.setVisibility(View.VISIBLE);
} else {
mEmptyView.setVisibility(View.GONE);
activeListView().setVisibility(View.VISIBLE);
activeListView().invalidateViews(); // ensure checkboxes get updated
}
}
/**
* @return the ListView that should currently be visible.
*/
private ListView activeListView() {
if (mIsSortedBySize) {
return mSizeOrderedListView;
}
return mDateOrderedListView;
}
/**
* @return an OnClickListener to delete the given downloadId from the Download Manager
*/
private DialogInterface.OnClickListener getDeleteClickHandler(final long downloadId) {
return new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
deleteDownload(downloadId);
}
};
}
/**
* @return an OnClickListener to restart the given downloadId in the Download Manager
*/
private DialogInterface.OnClickListener getRestartClickHandler(final long downloadId) {
return new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mDownloadManager.restartDownload(downloadId);
}
};
}
/**
* Send an Intent to open the download currently pointed to by the given cursor.
*/
private void openCurrentDownload(Cursor cursor) {
Uri localUri = Uri.parse(cursor.getString(mLocalUriColumnId));
try {
getContentResolver().openFileDescriptor(localUri, "r").close();
} catch (FileNotFoundException exc) {
Log.d(LOG_TAG, "Failed to open download " + cursor.getLong(mIdColumnId), exc);
showFailedDialog(cursor.getLong(mIdColumnId),
getString(R.string.dialog_file_missing_body));
return;
} catch (IOException exc) {
// close() failed, not a problem
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(localUri, cursor.getString(mMediaTypeColumnId));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
startActivity(intent);
} catch (ActivityNotFoundException ex) {
Toast.makeText(this, R.string.download_no_application_title, Toast.LENGTH_LONG).show();
}
}
private void handleItemClick(Cursor cursor) {
long id = cursor.getInt(mIdColumnId);
switch (cursor.getInt(mStatusColumnId)) {
case DownloadManager.STATUS_PENDING:
case DownloadManager.STATUS_RUNNING:
sendRunningDownloadClickedBroadcast(id);
break;
case DownloadManager.STATUS_PAUSED:
if (isPausedForWifi(cursor)) {
mQueuedDownloadId = id;
mQueuedDialog = new AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_queued_body)
.setMessage(R.string.dialog_queued_body)
.setPositiveButton(R.string.keep_queued_download, null)
.setNegativeButton(R.string.remove_download, getDeleteClickHandler(id))
.setOnCancelListener(this)
.show();
} else {
sendRunningDownloadClickedBroadcast(id);
}
break;
case DownloadManager.STATUS_SUCCESSFUL:
openCurrentDownload(cursor);
break;
case DownloadManager.STATUS_FAILED:
showFailedDialog(id, getErrorMessage(cursor));
break;
}
}
/**
* @return the appropriate error message for the failed download pointed to by cursor
*/
private String getErrorMessage(Cursor cursor) {
switch (cursor.getInt(mReasonColumndId)) {
case DownloadManager.ERROR_FILE_ALREADY_EXISTS:
if (isOnExternalStorage(cursor)) {
return getString(R.string.dialog_file_already_exists);
} else {
// the download manager should always find a free filename for cache downloads,
// so this indicates a strange internal error
return getUnknownErrorMessage();
}
case DownloadManager.ERROR_INSUFFICIENT_SPACE:
if (isOnExternalStorage(cursor)) {
return getString(R.string.dialog_insufficient_space_on_external);
} else {
return getString(R.string.dialog_insufficient_space_on_cache);
}
case DownloadManager.ERROR_DEVICE_NOT_FOUND:
return getString(R.string.dialog_media_not_found);
case DownloadManager.ERROR_CANNOT_RESUME:
return getString(R.string.dialog_cannot_resume);
default:
return getUnknownErrorMessage();
}
}
private boolean isOnExternalStorage(Cursor cursor) {
String localUriString = cursor.getString(mLocalUriColumnId);
if (localUriString == null) {
return false;
}
Uri localUri = Uri.parse(localUriString);
if (!localUri.getScheme().equals("file")) {
return false;
}
String path = localUri.getPath();
String externalRoot = Environment.getExternalStorageDirectory().getPath();
return path.startsWith(externalRoot);
}
private String getUnknownErrorMessage() {
return getString(R.string.dialog_failed_body);
}
private void showFailedDialog(long downloadId, String dialogBody) {
new AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_not_available)
.setMessage(dialogBody)
.setNegativeButton(R.string.delete_download, getDeleteClickHandler(downloadId))
.setPositiveButton(R.string.retry_download, getRestartClickHandler(downloadId))
.show();
}
/**
* TODO use constants/shared code?
*/
private void sendRunningDownloadClickedBroadcast(long id) {
Intent intent = new Intent("android.intent.action.DOWNLOAD_LIST");
intent.setClassName("com.android.providers.downloads",
"com.android.providers.downloads.DownloadReceiver");
intent.setData(ContentUris.withAppendedId(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, id));
intent.putExtra("multiple", false);
sendBroadcast(intent);
}
// handle a click from the date-sorted list
@Override
public boolean onChildClick(ExpandableListView parent, View v,
int groupPosition, int childPosition, long id) {
mDateSortedAdapter.moveCursorToChildPosition(groupPosition, childPosition);
handleItemClick(mDateSortedCursor);
return true;
}
// handle a click from the size-sorted list
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mSizeSortedCursor.moveToPosition(position);
handleItemClick(mSizeSortedCursor);
}
// handle a click on one of the download item checkboxes
@Override
public void onDownloadSelectionChanged(long downloadId, boolean isSelected) {
if (isSelected) {
mSelectedIds.add(downloadId);
} else {
mSelectedIds.remove(downloadId);
}
showOrHideSelectionMenu();
}
private void showOrHideSelectionMenu() {
boolean shouldBeVisible = !mSelectedIds.isEmpty();
boolean isVisible = mSelectionMenuView.getVisibility() == View.VISIBLE;
if (shouldBeVisible) {
updateSelectionMenu();
if (!isVisible) {
// show menu
mSelectionMenuView.setVisibility(View.VISIBLE);
mSelectionMenuView.startAnimation(
AnimationUtils.loadAnimation(this, R.anim.footer_appear));
}
} else if (!shouldBeVisible && isVisible) {
// hide menu
mSelectionMenuView.setVisibility(View.GONE);
mSelectionMenuView.startAnimation(
AnimationUtils.loadAnimation(this, R.anim.footer_disappear));
}
}
/**
* Set up the contents of the selection menu based on the current selection.
*/
private void updateSelectionMenu() {
int deleteButtonStringId = R.string.delete_download;
if (mSelectedIds.size() == 1) {
Cursor cursor = mDownloadManager.query(new DownloadManager.Query()
.setFilterById(mSelectedIds.iterator().next()));
try {
cursor.moveToFirst();
switch (cursor.getInt(mStatusColumnId)) {
case DownloadManager.STATUS_FAILED:
deleteButtonStringId = R.string.delete_download;
break;
case DownloadManager.STATUS_PENDING:
deleteButtonStringId = R.string.remove_download;
break;
case DownloadManager.STATUS_PAUSED:
case DownloadManager.STATUS_RUNNING:
deleteButtonStringId = R.string.cancel_running_download;
break;
}
} finally {
cursor.close();
}
}
mSelectionDeleteButton.setText(deleteButtonStringId);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.selection_delete:
for (Long downloadId : mSelectedIds) {
deleteDownload(downloadId);
}
clearSelection();
break;
case R.id.deselect_all:
clearSelection();
break;
}
}
/**
* Requery the database and update the UI.
*/
private void refresh() {
mDateSortedCursor.requery();
mSizeSortedCursor.requery();
// Adapters get notification of changes and update automatically
}
private void clearSelection() {
mSelectedIds.clear();
showOrHideSelectionMenu();
}
/**
* Delete a download from the Download Manager.
*/
private void deleteDownload(long downloadId) {
if (moveToDownload(downloadId)) {
int status = mDateSortedCursor.getInt(mStatusColumnId);
boolean isComplete = status == DownloadManager.STATUS_SUCCESSFUL
|| status == DownloadManager.STATUS_FAILED;
String localUri = mDateSortedCursor.getString(mLocalUriColumnId);
if (isComplete && localUri != null) {
String path = Uri.parse(localUri).getPath();
if (path.startsWith(Environment.getExternalStorageDirectory().getPath())) {
String mediaProviderUri = mDateSortedCursor.getString(mMediaProviderUriId);
if (TextUtils.isEmpty(mediaProviderUri)) {
// downloads database doesn't have the mediaprovider_uri. It means
// this download occurred before mediaprovider_uri column existed
// in downloads table. Since MediaProvider needs the mediaprovider_uri to
// delete this download, just set the 'deleted' flag to 1 on this row
// in the database. DownloadService, upon seeing this flag set to 1, will
// re-scan the file and get the MediaProviderUri and then delete the file
mDownloadManager.markRowDeleted(downloadId);
return;
} else {
getContentResolver().delete(Uri.parse(mediaProviderUri), null, null);
// sometimes mediaprovider doesn't delete file from sdcard after deleting it
// from its db. delete it now
try {
File file = new File(path);
file.delete();
} catch (Exception e) {
Log.w(LOG_TAG, "file: '" + path + "' couldn't be deleted", e);
}
}
}
}
}
mDownloadManager.remove(downloadId);
}
@Override
public boolean isDownloadSelected(long id) {
return mSelectedIds.contains(id);
}
/**
* Called when there's a change to the downloads database.
*/
void handleDownloadsChanged() {
checkSelectionForDeletedEntries();
if (mQueuedDownloadId != null && moveToDownload(mQueuedDownloadId)) {
if (mDateSortedCursor.getInt(mStatusColumnId) != DownloadManager.STATUS_PAUSED
|| !isPausedForWifi(mDateSortedCursor)) {
mQueuedDialog.cancel();
}
}
}
private boolean isPausedForWifi(Cursor cursor) {
return cursor.getInt(mReasonColumndId) == DownloadManager.PAUSED_QUEUED_FOR_WIFI;
}
/**
* Check if any of the selected downloads have been deleted from the downloads database, and
* remove such downloads from the selection.
*/
private void checkSelectionForDeletedEntries() {
// gather all existing IDs...
Set<Long> allIds = new HashSet<Long>();
for (mDateSortedCursor.moveToFirst(); !mDateSortedCursor.isAfterLast();
mDateSortedCursor.moveToNext()) {
allIds.add(mDateSortedCursor.getLong(mIdColumnId));
}
// ...and check if any selected IDs are now missing
for (Iterator<Long> iterator = mSelectedIds.iterator(); iterator.hasNext(); ) {
if (!allIds.contains(iterator.next())) {
iterator.remove();
}
}
}
/**
* Move {@link #mDateSortedCursor} to the download with the given ID.
* @return true if the specified download ID was found; false otherwise
*/
private boolean moveToDownload(long downloadId) {
for (mDateSortedCursor.moveToFirst(); !mDateSortedCursor.isAfterLast();
mDateSortedCursor.moveToNext()) {
if (mDateSortedCursor.getLong(mIdColumnId) == downloadId) {
return true;
}
}
return false;
}
/**
* Called when a dialog for a pending download is canceled.
*/
@Override
public void onCancel(DialogInterface dialog) {
mQueuedDownloadId = null;
mQueuedDialog = null;
}
}