// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.download.ui;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.DrawerLayout.DrawerListener;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.AdapterDataObserver;
import android.support.v7.widget.Toolbar.OnMenuItemClickListener;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.FileUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.BasicNativePage;
import org.chromium.chrome.browser.download.DownloadManagerService;
import org.chromium.chrome.browser.download.DownloadUtils;
import org.chromium.chrome.browser.offlinepages.downloads.OfflinePageDownloadBridge;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.snackbar.Snackbar;
import org.chromium.chrome.browser.snackbar.SnackbarManager;
import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController;
import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarManageable;
import org.chromium.chrome.browser.widget.FadingShadow;
import org.chromium.chrome.browser.widget.FadingShadowView;
import org.chromium.chrome.browser.widget.LoadingView;
import org.chromium.chrome.browser.widget.selection.SelectionDelegate;
import org.chromium.ui.base.DeviceFormFactor;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* Displays and manages the UI for the download manager.
*/
public class DownloadManagerUi implements OnMenuItemClickListener {
/**
* Interface to observe the changes in the download manager ui. This should be implemented by
* the ui components that is shown, in order to let them get proper notifications.
*/
public interface DownloadUiObserver {
/**
* Called when the filter has been changed by the user.
*/
public void onFilterChanged(int filter);
/**
* Called when the download manager is not shown anymore.
*/
public void onManagerDestroyed();
}
private static class DownloadBackendProvider implements BackendProvider {
private OfflinePageDownloadBridge mOfflinePageBridge;
private SelectionDelegate<DownloadHistoryItemWrapper> mSelectionDelegate;
private ThumbnailProvider mThumbnailProvider;
DownloadBackendProvider() {
Resources resources = ContextUtils.getApplicationContext().getResources();
int iconSize = resources.getDimensionPixelSize(R.dimen.downloads_item_icon_size);
mOfflinePageBridge = new OfflinePageDownloadBridge(
Profile.getLastUsedProfile().getOriginalProfile());
mSelectionDelegate = new SelectionDelegate<DownloadHistoryItemWrapper>();
mThumbnailProvider = new ThumbnailProviderImpl(iconSize);
}
@Override
public DownloadDelegate getDownloadDelegate() {
return DownloadManagerService.getDownloadManagerService(
ContextUtils.getApplicationContext());
}
@Override
public OfflinePageDownloadBridge getOfflinePageBridge() {
return mOfflinePageBridge;
}
@Override
public ThumbnailProvider getThumbnailProvider() {
return mThumbnailProvider;
}
@Override
public SelectionDelegate<DownloadHistoryItemWrapper> getSelectionDelegate() {
return mSelectionDelegate;
}
@Override
public void destroy() {
getOfflinePageBridge().destroy();
mThumbnailProvider.destroy();
mThumbnailProvider = null;
}
}
private class UndoDeletionSnackbarController implements SnackbarController {
@Override
public void onAction(Object actionData) {
@SuppressWarnings("unchecked")
List<DownloadHistoryItemWrapper> items = (List<DownloadHistoryItemWrapper>) actionData;
// Deletion was undone. Add items back to the adapter.
mHistoryAdapter.addItemsToAdapter(items);
RecordUserAction.record("Android.DownloadManager.UndoDelete");
}
@Override
public void onDismissNoAction(Object actionData) {
@SuppressWarnings("unchecked")
List<DownloadHistoryItemWrapper> items = (List<DownloadHistoryItemWrapper>) actionData;
// Deletion was not undone. Remove downloads from backend.
final ArrayList<File> filesToDelete = new ArrayList<>();
// Some types of DownloadHistoryItemWrappers delete their own files when #remove()
// is called. Determine which files are not deleted by the #remove() call.
for (int i = 0; i < items.size(); i++) {
DownloadHistoryItemWrapper wrappedItem = items.get(i);
if (!wrappedItem.remove()) filesToDelete.add(wrappedItem.getFile());
}
// Delete the files associated with the download items (if necessary) using a single
// AsyncTask that batch deletes all of the files. The thread pool has a finite
// number of tasks that can be queued at once. If too many tasks are queued an
// exception is thrown. See crbug.com/643811.
if (filesToDelete.size() != 0) {
new AsyncTask<Void, Void, Void>() {
@Override
public Void doInBackground(Void... params) {
FileUtils.batchDeleteFiles(filesToDelete);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
RecordUserAction.record("Android.DownloadManager.Delete");
}
}
private static BackendProvider sProviderForTests;
private final DownloadHistoryAdapter mHistoryAdapter;
private final FilterAdapter mFilterAdapter;
private final ObserverList<DownloadUiObserver> mObservers = new ObserverList<>();
private final BackendProvider mBackendProvider;
private final Activity mActivity;
private final ViewGroup mMainView;
private final DownloadManagerToolbar mToolbar;
private final SpaceDisplay mSpaceDisplay;
private final ListView mFilterView;
private final RecyclerView mRecyclerView;
private final TextView mEmptyView;
private final LoadingView mLoadingView;
private BasicNativePage mNativePage;
private UndoDeletionSnackbarController mUndoDeletionSnackbarController;
private final AdapterDataObserver mAdapterObserver = new AdapterDataObserver() {
@Override
public void onChanged() {
if (mHistoryAdapter.getItemCount() == 0) {
mEmptyView.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
} else {
mEmptyView.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
}
// At inflation, the RecyclerView is set to gone, and the loading view is visible. As
// long as the adapter data changes, we show the recycler view, and hide loading view.
mLoadingView.hideLoadingUI();
}
};
public DownloadManagerUi(
Activity activity, boolean isOffTheRecord, ComponentName parentComponent) {
mActivity = activity;
mBackendProvider =
sProviderForTests == null ? new DownloadBackendProvider() : sProviderForTests;
mMainView = (ViewGroup) LayoutInflater.from(activity).inflate(R.layout.download_main, null);
mEmptyView = (TextView) mMainView.findViewById(R.id.empty_view);
mEmptyView.setCompoundDrawablesWithIntrinsicBounds(null, VectorDrawableCompat
.create(activity.getResources(), R.drawable.downloads_big, activity.getTheme()),
null, null);
mLoadingView = (LoadingView) mMainView.findViewById(R.id.loading_view);
mLoadingView.showLoadingUI();
mHistoryAdapter = new DownloadHistoryAdapter(isOffTheRecord, parentComponent);
mHistoryAdapter.registerAdapterDataObserver(mAdapterObserver);
mHistoryAdapter.initialize(mBackendProvider);
addObserver(mHistoryAdapter);
mSpaceDisplay = new SpaceDisplay(mMainView, mHistoryAdapter);
mHistoryAdapter.registerAdapterDataObserver(mSpaceDisplay);
mSpaceDisplay.onChanged();
mFilterAdapter = new FilterAdapter();
mFilterAdapter.initialize(this);
addObserver(mFilterAdapter);
mToolbar = (DownloadManagerToolbar) mMainView.findViewById(R.id.action_bar);
mToolbar.setOnMenuItemClickListener(this);
DrawerLayout drawerLayout = null;
if (!DeviceFormFactor.isLargeTablet(activity)) {
drawerLayout = (DrawerLayout) mMainView;
addDrawerListener(drawerLayout);
}
mToolbar.initialize(mBackendProvider.getSelectionDelegate(), 0, drawerLayout,
R.id.normal_menu_group, R.id.selection_mode_menu_group);
addObserver(mToolbar);
mFilterView = (ListView) mMainView.findViewById(R.id.section_list);
mFilterView.setAdapter(mFilterAdapter);
mFilterView.setOnItemClickListener(mFilterAdapter);
mRecyclerView = (RecyclerView) mMainView.findViewById(R.id.recycler_view);
mRecyclerView.setAdapter(mHistoryAdapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
FadingShadowView shadow = (FadingShadowView) mMainView.findViewById(R.id.shadow);
if (DeviceFormFactor.isLargeTablet(mActivity)) {
shadow.setVisibility(View.GONE);
} else {
shadow.init(ApiCompatibilityUtils.getColor(mMainView.getResources(),
R.color.toolbar_shadow_color), FadingShadow.POSITION_TOP);
}
mToolbar.setTitle(R.string.menu_downloads);
mUndoDeletionSnackbarController = new UndoDeletionSnackbarController();
}
/**
* Sets the {@link BasicNativePage} that holds this manager.
*/
public void setBasicNativePage(BasicNativePage delegate) {
mNativePage = delegate;
}
/**
* Called when the activity/native page is destroyed.
*/
public void onDestroyed() {
for (DownloadUiObserver observer : mObservers) {
observer.onManagerDestroyed();
removeObserver(observer);
}
dismissUndoDeletionSnackbars();
mBackendProvider.destroy();
mHistoryAdapter.unregisterAdapterDataObserver(mAdapterObserver);
mHistoryAdapter.unregisterAdapterDataObserver(mSpaceDisplay);
}
/**
* Called when the UI needs to react to the back button being pressed.
*
* @return Whether the back button was handled.
*/
public boolean onBackPressed() {
if (mMainView instanceof DrawerLayout) {
DrawerLayout drawerLayout = (DrawerLayout) mMainView;
if (drawerLayout.isDrawerOpen(Gravity.START)) {
closeDrawer();
return true;
}
}
if (mBackendProvider.getSelectionDelegate().isSelectionEnabled()) {
mBackendProvider.getSelectionDelegate().clearSelection();
return true;
}
return false;
}
/**
* @return The view that shows the main download UI.
*/
public ViewGroup getView() {
return mMainView;
}
/**
* Sets the download manager to the state that the url represents.
*/
public void updateForUrl(String url) {
int filter = DownloadFilter.getFilterFromUrl(url);
onFilterChanged(filter);
}
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.close_menu_id && !DeviceFormFactor.isTablet(mActivity)) {
mActivity.finish();
return true;
} else if (item.getItemId() == R.id.selection_mode_delete_menu_id) {
deleteSelectedItems();
return true;
} else if (item.getItemId() == R.id.selection_mode_share_menu_id) {
shareSelectedItems();
return true;
}
return false;
}
/**
* @see DrawerLayout#openDrawer(int)
*/
@VisibleForTesting
public void openDrawer() {
if (mMainView instanceof DrawerLayout) {
((DrawerLayout) mMainView).openDrawer(GravityCompat.START);
}
}
/**
* Adds a {@link DownloadUiObserver} to observe the changes in the download manager.
*/
public void addObserver(DownloadUiObserver observer) {
mObservers.addObserver(observer);
}
/**
* Removes a {@link DownloadUiObserver} that were added in
* {@link #addObserver(DownloadUiObserver)}
*/
public void removeObserver(DownloadUiObserver observer) {
mObservers.removeObserver(observer);
}
/**
* @see DrawerLayout#closeDrawer(int)
*/
void closeDrawer() {
if (mMainView instanceof DrawerLayout) {
((DrawerLayout) mMainView).closeDrawer(GravityCompat.START);
}
}
/**
* @return The activity that holds the download UI.
*/
Activity getActivity() {
return mActivity;
}
/**
* @return The BackendProvider associated with the download UI.
*/
public BackendProvider getBackendProvider() {
return mBackendProvider;
}
/** Called when the filter has been changed by the user. */
void onFilterChanged(int filter) {
mBackendProvider.getSelectionDelegate().clearSelection();
for (DownloadUiObserver observer : mObservers) {
observer.onFilterChanged(filter);
}
if (mNativePage != null) {
mNativePage.onStateChange(DownloadFilter.getUrlForFilter(filter));
}
RecordHistogram.recordEnumeratedHistogram("Android.DownloadManager.Filter", filter,
DownloadFilter.FILTER_BOUNDARY);
}
private void shareSelectedItems() {
List<DownloadHistoryItemWrapper> selectedItems =
mBackendProvider.getSelectionDelegate().getSelectedItems();
assert selectedItems.size() > 0;
mActivity.startActivity(Intent.createChooser(createShareIntent(),
mActivity.getString(R.string.share_link_chooser_title)));
// TODO(twellington): ideally the intent chooser would be started with
// startActivityForResult() and the selection would only be cleared after
// receiving an OK response. See crbug.com/638916.
mBackendProvider.getSelectionDelegate().clearSelection();
}
/**
* @return An Intent to share the selected items.
*/
@VisibleForTesting
public Intent createShareIntent() {
List<DownloadHistoryItemWrapper> selectedItems =
mBackendProvider.getSelectionDelegate().getSelectedItems();
return DownloadUtils.createShareIntent(selectedItems);
}
private void deleteSelectedItems() {
List<DownloadHistoryItemWrapper> selectedItems =
mBackendProvider.getSelectionDelegate().getSelectedItems();
final List<DownloadHistoryItemWrapper> itemsToDelete = getItemsForDeletion();
mBackendProvider.getSelectionDelegate().clearSelection();
if (itemsToDelete.isEmpty()) return;
mHistoryAdapter.removeItemsFromAdapter(itemsToDelete);
dismissUndoDeletionSnackbars();
boolean singleItemDeleted = selectedItems.size() == 1;
String snackbarText = singleItemDeleted ? selectedItems.get(0).getDisplayFileName() :
String.format(Locale.getDefault(), "%d", selectedItems.size());
int snackbarTemplateId = singleItemDeleted ? R.string.undo_bar_delete_message
: R.string.undo_bar_multiple_downloads_delete_message;
Snackbar snackbar = Snackbar.make(snackbarText, mUndoDeletionSnackbarController,
Snackbar.TYPE_ACTION, Snackbar.UMA_DOWNLOAD_DELETE_UNDO);
snackbar.setAction(mActivity.getString(R.string.undo), itemsToDelete);
snackbar.setTemplateText(mActivity.getString(snackbarTemplateId));
((SnackbarManageable) mActivity).getSnackbarManager().showSnackbar(snackbar);
}
private List<DownloadHistoryItemWrapper> getItemsForDeletion() {
List<DownloadHistoryItemWrapper> selectedItems =
mBackendProvider.getSelectionDelegate().getSelectedItems();
List<DownloadHistoryItemWrapper> itemsToRemove = new ArrayList<>();
Set<String> filePathsToRemove = new HashSet<>();
for (DownloadHistoryItemWrapper item : selectedItems) {
if (!filePathsToRemove.contains(item.getFilePath())) {
List<DownloadHistoryItemWrapper> itemsForFilePath =
mHistoryAdapter.getItemsForFilePath(item.getFilePath());
if (itemsForFilePath != null) {
itemsToRemove.addAll(itemsForFilePath);
}
filePathsToRemove.add(item.getFilePath());
}
}
return itemsToRemove;
}
private void addDrawerListener(DrawerLayout drawer) {
drawer.addDrawerListener(new DrawerListener() {
@Override
public void onDrawerSlide(View drawerView, float slideOffset) {
}
@Override
public void onDrawerOpened(View drawerView) {
RecordUserAction.record("Android.DownloadManager.OpenDrawer");
}
@Override
public void onDrawerClosed(View drawerView) {
}
@Override
public void onDrawerStateChanged(int newState) {
}
});
}
private void dismissUndoDeletionSnackbars() {
((SnackbarManageable) mActivity).getSnackbarManager().dismissSnackbars(
mUndoDeletionSnackbarController);
}
@VisibleForTesting
public SnackbarManager getSnackbarManagerForTesting() {
return ((SnackbarManageable) mActivity).getSnackbarManager();
}
/** Returns the {@link DownloadManagerToolbar}. */
@VisibleForTesting
public DownloadManagerToolbar getDownloadManagerToolbarForTests() {
return mToolbar;
}
/** Returns the {@link DownloadHistoryAdapter}. */
@VisibleForTesting
public DownloadHistoryAdapter getDownloadHistoryAdapterForTests() {
return mHistoryAdapter;
}
/** Sets a BackendProvider that is used in place of a real one. */
@VisibleForTesting
public static void setProviderForTests(BackendProvider provider) {
sProviderForTests = provider;
}
}