Commit 11359108 authored by Matthew Jones's avatar Matthew Jones Committed by Commit Bot

Reland "Add ActivityTabProvider to ChromeActivity"

This is a reland of f7f910d0 with
the changes from https://chromium-review.googlesource.com/c/chromium/src/+/1208293
applied.

Original change's description:
> Add ActivityTabProvider to ChromeActivity
>
> This patch adds ActivityTabProvider to ChromeActivity. This class
> provides an easy mechanism for classes that care about the tab
> that is currently in the foreground (i.e. not in the tab
> switcher/toolbar swipe/animation layout).
>
> The observer provides either the activity's tab or null depending
> on the state of the browser. The null event can be ignored depending
> on the listener.
>
> The ActivityTabObserver interface only has a single method:
> onActivityTabChanged(Tab t)
>
> When an observer is attached to the provider, onActivityTabChanged is
> called on only that observer with the current tab (giving it access
> without requiring a handle to a larger object).
>
> Bug: 871279
> Change-Id: Ice961224c6690dc79c38f5d6cb255fed4730c8ce
> Reviewed-on: https://chromium-review.googlesource.com/1165489
> Commit-Queue: Matthew Jones <mdjones@chromium.org>
> Reviewed-by: Ted Choc <tedchoc@chromium.org>
> Reviewed-by: Theresa <twellington@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#588872}

TBR: tedchoc
Bug: 871279
Change-Id: I9c17590de19db025110cda2171cd1f8d9c1ffa57
Reviewed-on: https://chromium-review.googlesource.com/1208714Reviewed-by: default avatarMatthew Jones <mdjones@chromium.org>
Commit-Queue: Matthew Jones <mdjones@chromium.org>
Cr-Commit-Position: refs/heads/master@{#589018}
parent 2b04f7df
// 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 org.chromium.base.ObserverList;
import org.chromium.base.ObserverList.RewindableIterator;
import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.compositor.layouts.LayoutManager;
import org.chromium.chrome.browser.compositor.layouts.SceneChangeObserver;
import org.chromium.chrome.browser.compositor.layouts.StaticLayout;
import org.chromium.chrome.browser.compositor.layouts.phone.SimpleAnimationLayout;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
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 {
/** 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 static abstract 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);
}
/** 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 LayoutManager} to get the active layout. */
private LayoutManager mLayoutManager;
/** The observer watching scene changes in the {@link LayoutManager}. */
private SceneChangeObserver mSceneChangeObserver;
/** A handle to the {@link TabModelSelector}. */
private TabModelSelector mTabModelSelector;
/** An observer for watching tab creation and switching events. */
private TabModelSelectorTabModelObserver mTabModelObserver;
/** 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();
mSceneChangeObserver = new SceneChangeObserver() {
@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 onSceneChange(Layout layout) {
// 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 (layout instanceof SimpleAnimationLayout) return;
Tab tab = mTabModelSelector.getCurrentTab();
if (!(layout instanceof StaticLayout)) tab = null;
triggerActivityTabChangeEvent(tab);
}
};
}
/**
* @return The activity's current tab.
*/
public Tab getActivityTab() {
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, @TabModel.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.getTotalTabCount() <= 1) triggerActivityTabChangeEvent(null);
}
};
}
/**
* @param layoutManager A {@link LayoutManager} for watching for scene changes.
*/
public void setLayoutManager(LayoutManager layoutManager) {
assert mLayoutManager == null;
mLayoutManager = layoutManager;
mLayoutManager.addSceneChangeObserver(mSceneChangeObserver);
}
/**
* 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) {
if (mLayoutManager == null) return;
if (!(mLayoutManager.getActiveLayout() instanceof StaticLayout) && tab != null) return;
if (mActivityTab == tab) return;
mActivityTab = tab;
mLastHintedTabId = Tab.INVALID_TAB_ID;
mRewindableIterator.rewind();
while (mRewindableIterator.hasNext()) {
mRewindableIterator.next().onActivityTabChanged(tab, false);
}
}
/**
* @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 (mLayoutManager != null) mLayoutManager.removeSceneChangeObserver(mSceneChangeObserver);
mLayoutManager = null;
if (mTabModelObserver != null) mTabModelObserver.destroy();
mTabModelSelector = null;
}
}
...@@ -298,6 +298,9 @@ public abstract class ChromeActivity extends AsyncInitializationActivity ...@@ -298,6 +298,9 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
private ActivityTabStartupMetricsTracker mActivityTabStartupMetricsTracker; private ActivityTabStartupMetricsTracker mActivityTabStartupMetricsTracker;
/** A means of providing the foreground tab of the activity to different features. */
private ActivityTabProvider mActivityTabProvider = new ActivityTabProvider();
/** Whether or not the activity is in started state. */ /** Whether or not the activity is in started state. */
private boolean mStarted; private boolean mStarted;
...@@ -583,6 +586,8 @@ public abstract class ChromeActivity extends AsyncInitializationActivity ...@@ -583,6 +586,8 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
if (mTabModelsInitialized) return; if (mTabModelsInitialized) return;
mTabModelSelector = createTabModelSelector(); mTabModelSelector = createTabModelSelector();
mActivityTabProvider.setTabModelSelector(mTabModelSelector);
if (mTabModelSelector == null) { if (mTabModelSelector == null) {
assert isFinishing(); assert isFinishing();
mTabModelsInitialized = true; mTabModelsInitialized = true;
...@@ -1275,6 +1280,8 @@ public abstract class ChromeActivity extends AsyncInitializationActivity ...@@ -1275,6 +1280,8 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
manager.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); manager.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener);
} }
mActivityTabProvider.destroy();
super.onDestroy(); super.onDestroy();
} }
...@@ -1612,6 +1619,13 @@ public abstract class ChromeActivity extends AsyncInitializationActivity ...@@ -1612,6 +1619,13 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
return mTabModelSelector; return mTabModelSelector;
} }
/**
* @return The provider of the visible tab in the current activity.
*/
public ActivityTabProvider getActivityTabProvider() {
return mActivityTabProvider;
}
/** /**
* Returns the {@link InsetObserverView} that has the current system window * Returns the {@link InsetObserverView} that has the current system window
* insets information. * insets information.
...@@ -1667,6 +1681,9 @@ public abstract class ChromeActivity extends AsyncInitializationActivity ...@@ -1667,6 +1681,9 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
} }
/** /**
* DEPRECATED: Instead, use/hold a reference to {@link #mActivityTabProvider}. See
* https://crbug.com/871279 for more details.
*
* Returns the tab being displayed by this ChromeActivity instance. This allows differentiation * Returns the tab being displayed by this ChromeActivity instance. This allows differentiation
* between ChromeActivity subclasses that swap between multiple tabs (e.g. ChromeTabbedActivity) * between ChromeActivity subclasses that swap between multiple tabs (e.g. ChromeTabbedActivity)
* and subclasses that only display one Tab (e.g. FullScreenActivity and DocumentActivity). * and subclasses that only display one Tab (e.g. FullScreenActivity and DocumentActivity).
...@@ -1796,6 +1813,8 @@ public abstract class ChromeActivity extends AsyncInitializationActivity ...@@ -1796,6 +1813,8 @@ public abstract class ChromeActivity extends AsyncInitializationActivity
controlContainer.setSwipeHandler( controlContainer.setSwipeHandler(
getCompositorViewHolder().getLayoutManager().getToolbarSwipeHandler()); getCompositorViewHolder().getLayoutManager().getToolbarSwipeHandler());
} }
mActivityTabProvider.setLayoutManager(layoutManager);
} }
/** /**
......
...@@ -9,6 +9,7 @@ import("//chrome/android/feed/feed_java_sources.gni") ...@@ -9,6 +9,7 @@ import("//chrome/android/feed/feed_java_sources.gni")
chrome_java_sources = [ chrome_java_sources = [
"java/src/com/google/android/apps/chrome/appwidget/bookmarks/BookmarkThumbnailWidgetProvider.java", "java/src/com/google/android/apps/chrome/appwidget/bookmarks/BookmarkThumbnailWidgetProvider.java",
"java/src/org/chromium/chrome/browser/ActivityTabProvider.java",
"java/src/org/chromium/chrome/browser/ActivityTabTaskDescriptionHelper.java", "java/src/org/chromium/chrome/browser/ActivityTabTaskDescriptionHelper.java",
"java/src/org/chromium/chrome/browser/ActivityTaskDescriptionIconGenerator.java", "java/src/org/chromium/chrome/browser/ActivityTaskDescriptionIconGenerator.java",
"java/src/org/chromium/chrome/browser/AfterStartupTaskUtils.java", "java/src/org/chromium/chrome/browser/AfterStartupTaskUtils.java",
...@@ -1660,6 +1661,7 @@ if (enable_vr) { ...@@ -1660,6 +1661,7 @@ if (enable_vr) {
chrome_test_java_sources = [ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/compositor/CompositorVisibilityTest.java", "javatests/src/org/chromium/chrome/browser/compositor/CompositorVisibilityTest.java",
"javatests/src/org/chromium/chrome/browser/ActivityTabProviderTest.java",
"javatests/src/org/chromium/chrome/browser/AudioTest.java", "javatests/src/org/chromium/chrome/browser/AudioTest.java",
"javatests/src/org/chromium/chrome/browser/BackgroundSyncLauncherTest.java", "javatests/src/org/chromium/chrome/browser/BackgroundSyncLauncherTest.java",
"javatests/src/org/chromium/chrome/browser/BluetoothChooserDialogTest.java", "javatests/src/org/chromium/chrome/browser/BluetoothChooserDialogTest.java",
......
// 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Restriction;
import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.compositor.layouts.SceneChangeObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel.TabSelectionType;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.test.util.UiRestriction;
import java.util.concurrent.TimeoutException;
/**
* Tests for {@link ChromeActivity}'s {@link ActivityTabProvider}.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class ActivityTabProviderTest {
@Rule
public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
private ChromeTabbedActivity mActivity;
private ActivityTabProvider mProvider;
private Tab mActivityTab;
private CallbackHelper mActivityTabChangedHelper = new CallbackHelper();
private CallbackHelper mActivityTabChangedHintHelper = new CallbackHelper();
@Before
public void setUp() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
mActivity = mActivityTestRule.getActivity();
mProvider = mActivity.getActivityTabProvider();
mProvider.addObserverAndTrigger((tab, hint) -> {
if (hint) {
mActivityTabChangedHintHelper.notifyCalled();
} else {
mActivityTab = tab;
mActivityTabChangedHelper.notifyCalled();
}
});
assertEquals("Setup should have only triggered the event once.",
mActivityTabChangedHelper.getCallCount(), 1);
}
/**
* @return The {@link Tab} that the active model currently has selected.
*/
private Tab getModelSelectedTab() {
return mActivity.getTabModelSelector().getCurrentTab();
}
/**
* Test that the onActivityTabChanged event is triggered when the observer is attached for
* only that observer.
*/
@Test
@SmallTest
@Feature({"ActivityTabObserver"})
public void testTriggerOnAddObserver() throws InterruptedException, TimeoutException {
CallbackHelper helper = new CallbackHelper();
mProvider.addObserverAndTrigger((tab, hint) -> helper.notifyCalled());
helper.waitForCallback(0);
assertEquals("Only the added observer should have been triggered.",
mActivityTabChangedHelper.getCallCount(), 1);
assertEquals("The added observer should have only been triggered once.", 1,
helper.getCallCount());
}
/** Test that onActivityTabChanged is triggered when entering and exiting the tab switcher. */
@Test
@SmallTest
@Feature({"ActivityTabObserver"})
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
public void testTriggerWithTabSwitcher() throws InterruptedException, TimeoutException {
assertEquals("The activity tab should be the model's selected tab.", getModelSelectedTab(),
mActivityTab);
ThreadUtils.runOnUiThreadBlocking(() -> mActivity.getLayoutManager().showOverview(false));
mActivityTabChangedHelper.waitForCallback(1);
assertEquals("Entering the tab switcher should have triggered the event once.", 2,
mActivityTabChangedHelper.getCallCount());
assertEquals("The activity tab should be null.", null, mActivityTab);
ThreadUtils.runOnUiThreadBlocking(() -> mActivity.getLayoutManager().hideOverview(false));
mActivityTabChangedHelper.waitForCallback(2);
assertEquals("Exiting the tab switcher should have triggered the event once.", 3,
mActivityTabChangedHelper.getCallCount());
assertEquals("The activity tab should be the model's selected tab.", getModelSelectedTab(),
mActivityTab);
}
/** Test that the hint event triggers when exiting the tab switcher. */
@Test
@LargeTest
@Feature({"ActivityTabObserver"})
@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
public void testTriggerHintWithTabSwitcher() throws InterruptedException, TimeoutException {
assertEquals("The hint should not yet have triggered.", 0,
mActivityTabChangedHintHelper.getCallCount());
setTabSwitcherModeAndWait(true);
assertEquals("The hint should not yet have triggered.", 0,
mActivityTabChangedHintHelper.getCallCount());
setTabSwitcherModeAndWait(false);
mActivityTabChangedHintHelper.waitForCallback(0);
assertEquals("The hint should have triggerd once.", 1,
mActivityTabChangedHintHelper.getCallCount());
}
/**
* Test that onActivityTabChanged is triggered when switching to a new tab without switching
* layouts.
*/
@Test
@SmallTest
@Feature({"ActivityTabObserver"})
public void testTriggerWithTabSelection() throws InterruptedException, TimeoutException {
Tab startingTab = getModelSelectedTab();
ChromeTabUtils.fullyLoadUrlInNewTab(InstrumentationRegistry.getInstrumentation(), mActivity,
ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL, false);
assertNotEquals(
"A new tab should be in the foreground.", startingTab, getModelSelectedTab());
assertEquals("The activity tab should be the model's selected tab.", getModelSelectedTab(),
mActivityTab);
int callCount = mActivityTabChangedHelper.getCallCount();
ThreadUtils.runOnUiThreadBlocking(() -> {
// Select the original tab without switching layouts.
mActivity.getTabModelSelector().getCurrentModel().setIndex(
0, TabSelectionType.FROM_USER);
});
mActivityTabChangedHelper.waitForCallback(callCount);
assertEquals("Switching tabs should have triggered the event once.", callCount + 1,
mActivityTabChangedHelper.getCallCount());
}
/** Test that onActivityTabChanged is triggered when the last tab is closed. */
@Test
@SmallTest
@Feature({"ActivityTabObserver"})
public void testTriggerOnLastTabClosed() throws InterruptedException, TimeoutException {
int callCount = mActivityTabChangedHelper.getCallCount();
ThreadUtils.runOnUiThreadBlocking(
() -> { mActivity.getTabModelSelector().closeTab(getModelSelectedTab()); });
mActivityTabChangedHelper.waitForCallback(callCount);
assertEquals("Closing the last tab should have triggered the event once.", callCount + 1,
mActivityTabChangedHelper.getCallCount());
assertEquals("The activity's tab should be null.", null, mActivityTab);
}
/**
* Test that the correct tab is considered the activity tab when a different tab is closed on
* phone.
*/
@Test
@SmallTest
@Feature({"ActivityTabObserver"})
public void testCorrectTabAfterTabClosed() throws InterruptedException, TimeoutException {
Tab startingTab = getModelSelectedTab();
ChromeTabUtils.fullyLoadUrlInNewTab(InstrumentationRegistry.getInstrumentation(), mActivity,
ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL, false);
assertNotEquals("The starting tab should not be the selected tab.", getModelSelectedTab(),
startingTab);
assertEquals("The activity tab should be the model's selected tab.", getModelSelectedTab(),
mActivityTab);
Tab activityTabBefore = mActivityTab;
int callCount = mActivityTabChangedHelper.getCallCount();
ThreadUtils.runOnUiThreadBlocking(
() -> { mActivity.getTabModelSelector().closeTab(startingTab); });
assertEquals("The activity tab should not have changed.", activityTabBefore, mActivityTab);
}
/**
* Enter or exit the tab switcher with animations and wait for the scene to change.
* @param inSwitcher Whether to enter or exit the tab switcher.
*/
private void setTabSwitcherModeAndWait(boolean inSwitcher)
throws InterruptedException, TimeoutException {
final CallbackHelper sceneChangeHelper = new CallbackHelper();
SceneChangeObserver observer = new SceneChangeObserver() {
@Override
public void onTabSelectionHinted(int tabId) {}
@Override
public void onSceneChange(Layout layout) {
sceneChangeHelper.notifyCalled();
}
};
mActivity.getCompositorViewHolder().getLayoutManager().addSceneChangeObserver(observer);
int sceneChangeCount = sceneChangeHelper.getCallCount();
if (inSwitcher) {
ThreadUtils.runOnUiThreadBlocking(
() -> mActivity.getLayoutManager().showOverview(true));
} else {
ThreadUtils.runOnUiThreadBlocking(
() -> mActivity.getLayoutManager().hideOverview(true));
}
sceneChangeHelper.waitForCallback(sceneChangeCount);
mActivity.getCompositorViewHolder().getLayoutManager().removeSceneChangeObserver(observer);
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment