blob: 1b87a6cafc6cf5f4061679af5d844cfcc2ae7a3f [file] [log] [blame]
// Copyright 2018 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;
import androidx.annotation.CallSuper;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ObserverList;
import org.chromium.base.ObserverList.RewindableIterator;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.layouts.LayoutStateProvider.LayoutStateObserver;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver;
/**
* A class that provides the current {@link Tab} for various states of the browser's activity.
*/
public class ActivityTabProvider implements Supplier<Tab> {
/** An interface to track the visible tab for the activity. */
public interface ActivityTabObserver {
/**
* A notification that the activity's tab has changed. This will be triggered whenever a
* different tab is selected by the active {@link TabModel} and when that tab is
* interactive (i.e. not in a tab switching mode). When switching to toolbar swipe or tab
* switcher, this method will be called with {@code null} to indicate that there is no
* single activity tab (observers may or may not choose to ignore this event).
* @param tab The {@link Tab} that became visible or null if not in {@link StaticLayout}.
* @param hint Whether the change event is a hint that a tab change is likely. If true, the
* provided tab may still be frozen and is not yet selected.
*/
void onActivityTabChanged(Tab tab, boolean hint);
}
/** An {@link ActivityTabObserver} that can be used to explicitly watch non-hint events. */
public abstract static class HintlessActivityTabObserver implements ActivityTabObserver {
@Override
public final void onActivityTabChanged(Tab tab, boolean hint) {
// Only pass the event through if it isn't a hint.
if (!hint) onActivityTabChanged(tab);
}
/**
* A notification that the {@link Tab} in the {@link StaticLayout} has changed.
* @param tab The activity's tab.
*/
public abstract void onActivityTabChanged(Tab tab);
}
/**
* A utility class for observing the activity tab via {@link TabObserver}. When the activity
* tab changes, the observer is switched to that tab.
*/
public static class ActivityTabTabObserver extends EmptyTabObserver {
/** A handle to the activity tab provider. */
private final ActivityTabProvider mTabProvider;
/** An observer to watch for a changing activity tab and move this tab observer. */
private final ActivityTabObserver mActivityTabObserver;
/** The current activity tab. */
private Tab mTab;
/**
* Create a new {@link TabObserver} that only observes the activity tab. It doesn't trigger
* for the initial tab being attached to after creation.
* @param tabProvider An {@link ActivityTabProvider} to get the activity tab.
*/
public ActivityTabTabObserver(ActivityTabProvider tabProvider) {
this(tabProvider, false);
}
/**
* Create a new {@link TabObserver} that only observes the activity tab. This constructor
* allows the option of triggering for the initial tab being attached to after creation.
* @param tabProvider An {@link ActivityTabProvider} to get the activity tab.
* @param shouldTrigger Whether the observer should be triggered for the initial tab after
* creation.
*/
public ActivityTabTabObserver(ActivityTabProvider tabProvider, boolean shouldTrigger) {
mTabProvider = tabProvider;
mActivityTabObserver = (tab, hint) -> {
updateObservedTab(tab);
onObservingDifferentTab(tab, hint);
};
if (shouldTrigger) {
mTabProvider.addObserverAndTrigger(mActivityTabObserver);
} else {
mTabProvider.addObserver(mActivityTabObserver);
}
updateObservedTab(mTabProvider.get());
}
/**
* Update the tab being observed.
* @param newTab The new tab to observe.
*/
private void updateObservedTab(Tab newTab) {
if (mTab != null) mTab.removeObserver(ActivityTabTabObserver.this);
mTab = newTab;
if (mTab != null) mTab.addObserver(ActivityTabTabObserver.this);
}
/**
* A notification that the observer has switched to observing a different tab. This can be
* called a first time with the {@code hint} parameter set to true, indicating that a new
* tab is going to be selected.
* @param tab The tab that the observer is now observing. This can be null.
* @param hint Whether the change event is a hint that a tab change is likely. If true, the
* provided tab may still be frozen and is not yet selected.
*/
protected void onObservingDifferentTab(Tab tab, boolean hint) {}
/**
* Clean up any state held by this observer.
*/
@CallSuper
public void destroy() {
if (mTab != null) {
mTab.removeObserver(this);
mTab = null;
}
mTabProvider.removeObserver(mActivityTabObserver);
}
}
/** The list of observers to send events to. */
private final ObserverList<ActivityTabObserver> mObservers = new ObserverList<>();
/**
* A single rewindable iterator bound to {@link #mObservers} to prevent constant allocation of
* new iterators.
*/
private final RewindableIterator<ActivityTabObserver> mRewindableIterator;
/** The {@link Tab} that is considered to be the activity's tab. */
private Tab mActivityTab;
/** A handle to the {@link LayoutStateProvider} to get the active layout. */
private LayoutStateProvider mLayoutStateProvider;
/** The observer watching scene changes in the active layout. */
private LayoutStateObserver mLayoutStateObserver;
/** A handle to the {@link TabModelSelector}. */
private TabModelSelector mTabModelSelector;
/** An observer for watching tab creation and switching events. */
private TabModelSelectorTabModelObserver mTabModelObserver;
/** An observer for watching tab model switching event. */
private TabModelSelectorObserver mTabModelSelectorObserver;
/** The last tab ID that was hinted. This is reset when the activity tab actually changes. */
private int mLastHintedTabId;
/**
* Default constructor.
*/
public ActivityTabProvider() {
mRewindableIterator = mObservers.rewindableIterator();
mLayoutStateObserver = new LayoutStateObserver() {
@Override
public void onTabSelectionHinted(int tabId) {
if (mTabModelSelector == null || mLastHintedTabId == tabId) return;
Tab tab = mTabModelSelector.getTabById(tabId);
mLastHintedTabId = tabId;
mRewindableIterator.rewind();
while (mRewindableIterator.hasNext()) {
mRewindableIterator.next().onActivityTabChanged(tab, true);
}
}
@Override
public void onStartedShowing(@LayoutType int layout, boolean showToolbar) {
// The {@link SimpleAnimationLayout} is a special case, the intent is not to switch
// tabs, but to merely run an animation. In this case, do nothing. If the animation
// layout does result in a new tab {@link TabModelObserver#didSelectTab} will
// trigger the event instead. If the tab does not change, the event will no
if (LayoutType.SIMPLE_ANIMATION == layout) return;
Tab tab = mTabModelSelector.getCurrentTab();
if (layout != LayoutType.BROWSING) tab = null;
triggerActivityTabChangeEvent(tab);
}
};
}
/**
* @return The activity's current tab.
*/
@Override
public Tab get() {
return mActivityTab;
}
/**
* @param selector A {@link TabModelSelector} for watching for changes in tabs.
*/
public void setTabModelSelector(TabModelSelector selector) {
assert mTabModelSelector == null;
mTabModelSelector = selector;
mTabModelObserver = new TabModelSelectorTabModelObserver(mTabModelSelector) {
@Override
public void didSelectTab(Tab tab, @TabSelectionType int type, int lastId) {
triggerActivityTabChangeEvent(tab);
}
@Override
public void willCloseTab(Tab tab, boolean animate) {
// If this is the last tab to close, make sure a signal is sent to the observers.
if (mTabModelSelector.getCurrentModel().getCount() <= 1) {
triggerActivityTabChangeEvent(null);
}
}
};
mTabModelSelectorObserver = new TabModelSelectorObserver() {
@Override
public void onTabModelSelected(TabModel newModel, TabModel oldModel) {
// Send a signal with null tab if a new model has no tab. Other cases
// are taken care of by TabModelSelectorTabModelObserver#didSelectTab.
if (newModel.getCount() == 0) triggerActivityTabChangeEvent(null);
}
};
mTabModelSelector.addObserver(mTabModelSelectorObserver);
}
/**
* @param layoutStateProvider A {@link LayoutStateProvider} for watching for scene changes.
*/
public void setLayoutStateProvider(LayoutStateProvider layoutStateProvider) {
assert mLayoutStateProvider == null;
mLayoutStateProvider = layoutStateProvider;
mLayoutStateProvider.addObserver(mLayoutStateObserver);
}
/**
* Check if the interactive tab change event needs to be triggered based on the provided tab.
* @param tab The activity's tab.
*/
private void triggerActivityTabChangeEvent(Tab tab) {
// Allow the event to trigger before native is ready (before the layout manager is set).
if (mLayoutStateProvider != null
&& !(mLayoutStateProvider.isLayoutVisible(LayoutType.BROWSING)
|| mLayoutStateProvider.isLayoutVisible(LayoutType.SIMPLE_ANIMATION))
&& tab != null) {
return;
}
if (mActivityTab == tab) return;
mActivityTab = tab;
mLastHintedTabId = Tab.INVALID_TAB_ID;
mRewindableIterator.rewind();
while (mRewindableIterator.hasNext()) {
mRewindableIterator.next().onActivityTabChanged(tab, false);
}
}
/**
* Add an observer but do not immediately trigger the event. This should only be used in
* extremely specific cases where the observer would trigger an event from the constructor of
* the implementing class (see {@link ActivityTabTabObserver}).
* @param observer The observer to be added.
*
* TODO(fgorski): Find a different way to mock this in tests for {@link LoadProgressMediator}.
*/
@VisibleForTesting
@Deprecated
public void addObserver(ActivityTabObserver observer) {
mObservers.addObserver(observer);
}
/**
* @param observer The {@link ActivityTabObserver} to add to the activity. This will trigger the
* {@link ActivityTabObserver#onActivityTabChanged(Tab, boolean)} event to be
* called on the added observer, providing access to the current tab.
*/
public void addObserverAndTrigger(ActivityTabObserver observer) {
mObservers.addObserver(observer);
observer.onActivityTabChanged(mActivityTab, false);
}
/**
* @param observer The {@link ActivityTabObserver} to remove from the activity.
*/
public void removeObserver(ActivityTabObserver observer) {
mObservers.removeObserver(observer);
}
/** Clean up and detach any observers this object created. */
public void destroy() {
mObservers.clear();
if (mLayoutStateProvider != null) mLayoutStateProvider.removeObserver(mLayoutStateObserver);
mLayoutStateProvider = null;
if (mTabModelObserver != null) mTabModelObserver.destroy();
if (mTabModelSelectorObserver != null) {
mTabModelSelector.removeObserver(mTabModelSelectorObserver);
mTabModelSelectorObserver = null;
}
mTabModelSelector = null;
}
}