// 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; } }