// Copyright 2015 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.snackbar.undo; import android.content.Context; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.R; import org.chromium.chrome.browser.device.DeviceClassManager; import org.chromium.chrome.browser.snackbar.Snackbar; import org.chromium.chrome.browser.snackbar.SnackbarManager; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tabmodel.EmptyTabModelObserver; import org.chromium.chrome.browser.tabmodel.TabModel; import org.chromium.chrome.browser.tabmodel.TabModelObserver; import org.chromium.chrome.browser.tabmodel.TabModelSelector; import java.util.List; import java.util.Locale; /** * A controller that listens to and visually represents cancelable tab closures. * <p/> * Each time a tab is undoably closed via {@link TabModelObserver#tabPendingClosure(Tab)}, * this controller saves that tab id and title to the stack of SnackbarManager. It will then let * SnackbarManager to show a snackbar representing the top entry in of stack. Each added entry * resets the timeout that tracks when to commit the undoable actions. * <p/> * When the undo button is clicked, it will cancel the tab closure if any. all pending closing will * be committed. * <p/> * This class also responds to external changes to the undo state by monitoring * {@link TabModelObserver#tabClosureUndone(Tab)} and * {@link TabModelObserver#tabClosureCommitted(Tab)} to properly keep it's internal state * in sync with the model. */ public class UndoBarController implements SnackbarManager.SnackbarController { // AndroidTabCloseUndoToastEvent defined in tools/metrics/histograms/histograms.xml. private static final int TAB_CLOSE_UNDO_TOAST_SHOWN_COLD = 0; private static final int TAB_CLOSE_UNDO_TOAST_SHOWN_WARM = 1; private static final int TAB_CLOSE_UNDO_TOAST_PRESSED = 2; private static final int TAB_CLOSE_UNDO_TOAST_COUNT = 5; private final TabModelSelector mTabModelSelector; private final TabModelObserver mTabModelObserver; private final SnackbarManager mSnackbarManager; private final Context mContext; /** * Creates an instance of a {@link UndoBarController}. * @param context The {@link Context} in which snackbar is shown. * @param selector The {@link TabModelSelector} that will be used to commit and undo tab * closures. * @param snackbarManager The manager that helps to show up snackbar. */ public UndoBarController(Context context, TabModelSelector selector, SnackbarManager snackbarManager) { mSnackbarManager = snackbarManager; mTabModelSelector = selector; mContext = context; mTabModelObserver = new EmptyTabModelObserver() { private boolean disableUndo() { return DeviceClassManager.isAccessibilityModeEnabled(mContext) || DeviceClassManager.enableAccessibilityLayout(); } @Override public void tabPendingClosure(Tab tab) { if (disableUndo()) return; showUndoBar(tab.getId(), tab.getTitle()); } @Override public void tabClosureUndone(Tab tab) { if (disableUndo()) return; mSnackbarManager.dismissSnackbars(UndoBarController.this, tab.getId()); } @Override public void tabClosureCommitted(Tab tab) { if (disableUndo()) return; mSnackbarManager.dismissSnackbars(UndoBarController.this, tab.getId()); } @Override public void allTabsPendingClosure(List<Integer> tabIds) { if (disableUndo()) return; showUndoCloseAllBar(tabIds); } @Override public void allTabsClosureCommitted() { if (disableUndo()) return; mSnackbarManager.dismissSnackbars(UndoBarController.this); } }; } /** * Carry out native library dependent operations like registering observers and notifications. */ public void initialize() { mTabModelSelector.getModel(false).addObserver(mTabModelObserver); } /** * Cleans up this class, unregistering for application notifications from the * {@link TabModelSelector}. */ public void destroy() { TabModel model = mTabModelSelector.getModel(false); if (model != null) model.removeObserver(mTabModelObserver); } /** * Shows an undo bar. Based on user actions, this will cause a call to either * {@link TabModel#commitTabClosure(int)} or {@link TabModel#cancelTabClosure(int)} to be called * for {@code tabId}. * * @param tabId The id of the tab. * @param content The title of the tab. */ private void showUndoBar(int tabId, String content) { RecordHistogram.recordEnumeratedHistogram("AndroidTabCloseUndo.Toast", mSnackbarManager.isShowing() ? TAB_CLOSE_UNDO_TOAST_SHOWN_WARM : TAB_CLOSE_UNDO_TOAST_SHOWN_COLD, TAB_CLOSE_UNDO_TOAST_COUNT); mSnackbarManager.showSnackbar( Snackbar.make(content, this, Snackbar.TYPE_ACTION, Snackbar.UMA_TAB_CLOSE_UNDO) .setTemplateText(mContext.getString(R.string.undo_bar_close_message)) .setAction(mContext.getString(R.string.undo), tabId)); } /** * Shows an undo close all bar. Based on user actions, this will cause a call to either * {@link TabModel#commitTabClosure(int)} or {@link TabModel#cancelTabClosure(int)} to be called * for each tab in {@code closedTabIds}. This will happen unless * {@code SnackbarManager#removeFromStackForData(Object)} is called. * * @param closedTabIds A list of ids corresponding to tabs that were closed */ private void showUndoCloseAllBar(List<Integer> closedTabIds) { String content = String.format(Locale.getDefault(), "%d", closedTabIds.size()); mSnackbarManager.showSnackbar( Snackbar.make(content, this, Snackbar.TYPE_ACTION, Snackbar.UMA_TAB_CLOSE_ALL_UNDO) .setTemplateText(mContext.getString(R.string.undo_bar_close_all_message)) .setAction(mContext.getString(R.string.undo), closedTabIds)); } /** * Calls {@link TabModel#cancelTabClosure(int)} for the tab or for each tab in * the list of closed tabs. */ @SuppressWarnings("unchecked") @Override public void onAction(Object actionData) { RecordHistogram.recordEnumeratedHistogram("AndroidTabCloseUndo.Toast", TAB_CLOSE_UNDO_TOAST_PRESSED, TAB_CLOSE_UNDO_TOAST_COUNT); if (actionData instanceof Integer) { cancelTabClosure((Integer) actionData); } else { for (Integer id : (List<Integer>) actionData) { cancelTabClosure(id); } } } private void cancelTabClosure(int tabId) { TabModel model = mTabModelSelector.getModelForTabId(tabId); if (model != null) model.cancelTabClosure(tabId); } /** * Calls {@link TabModel#commitTabClosure(int)} for the tab or for each tab in * the list of closed tabs. */ @SuppressWarnings("unchecked") @Override public void onDismissNoAction(Object actionData) { if (actionData instanceof Integer) { commitTabClosure((Integer) actionData); } else { for (Integer tabId : (List<Integer>) actionData) { commitTabClosure(tabId); } } } private void commitTabClosure(int tabId) { TabModel model = mTabModelSelector.getModelForTabId(tabId); if (model != null) model.commitTabClosure(tabId); } }