// 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.compositor.layouts;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.RectF;
import android.os.Build;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.ViewGroup;
import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelContentViewDelegate;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager;
import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel;
import org.chromium.chrome.browser.compositor.bottombar.readermode.ReaderModePanel;
import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab;
import org.chromium.chrome.browser.compositor.layouts.components.VirtualView;
import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
import org.chromium.chrome.browser.compositor.layouts.eventfilter.EdgeSwipeEventFilter;
import org.chromium.chrome.browser.compositor.layouts.eventfilter.EdgeSwipeEventFilter.ScrollDirection;
import org.chromium.chrome.browser.compositor.layouts.eventfilter.EdgeSwipeHandler;
import org.chromium.chrome.browser.compositor.layouts.eventfilter.EmptyEdgeSwipeHandler;
import org.chromium.chrome.browser.compositor.layouts.eventfilter.GestureHandler;
import org.chromium.chrome.browser.compositor.overlays.SceneOverlay;
import org.chromium.chrome.browser.compositor.scene_layer.ToolbarSceneLayer;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchManagementDelegate;
import org.chromium.chrome.browser.device.DeviceClassManager;
import org.chromium.chrome.browser.dom_distiller.ReaderModeManagerDelegate;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelSelector;
import org.chromium.chrome.browser.util.ColorUtils;
import org.chromium.chrome.browser.util.FeatureUtilities;
import org.chromium.content.browser.ContentViewCore;
import org.chromium.ui.resources.dynamics.DynamicResourceLoader;
import java.util.List;
/**
* A {@link Layout} controller for a simple document use case. This class is responsible for
* driving all {@link Layout}s that get shown via the {@link LayoutManager}.
*/
public class LayoutManagerDocument extends LayoutManager
implements OverlayPanelContentViewDelegate {
// Layouts
/** A {@link Layout} used for showing a normal web page. */
protected final StaticLayout mStaticLayout;
// Event Filters
private final EdgeSwipeEventFilter mStaticEdgeEventFilter;
private final EdgeSwipeHandler mToolbarSwipeHandler;
// Event Filter Handlers
/** A {@link GestureHandler} that will delegate all events to {@link #getActiveLayout()}. */
protected final GestureHandler mGestureHandler;
// Internal State
private final SparseArray<LayoutTab> mTabCache = new SparseArray<LayoutTab>();
private final ContextualSearchPanel mContextualSearchPanel;
private final ReaderModePanel mReaderModePanel;
private final OverlayPanelManager mOverlayPanelManager;
private final ToolbarSceneLayer mToolbarOverlay;
/** A delegate for interacting with the Contextual Search manager. */
protected ContextualSearchManagementDelegate mContextualSearchDelegate;
private final Context mContext;
@SuppressWarnings("unused") private TabModelSelectorTabObserver mTabModelSelectorTabObserver;
/**
* Creates a {@link LayoutManagerDocument} instance.
* @param host A {@link LayoutManagerHost} instance.
*/
public LayoutManagerDocument(LayoutManagerHost host) {
super(host);
mContext = host.getContext();
LayoutRenderHost renderHost = host.getLayoutRenderHost();
mToolbarOverlay = new ToolbarSceneLayer(mContext, this, renderHost);
// Build Event Filter Handlers
mGestureHandler = new GestureHandlerLayoutDelegate(this);
mToolbarSwipeHandler = new ToolbarSwipeHandler(this);
// Build Event Filters
mStaticEdgeEventFilter =
new EdgeSwipeEventFilter(mContext, this, new StaticEdgeSwipeHandler());
mOverlayPanelManager = new OverlayPanelManager();
// Build Layouts
mStaticLayout = new StaticLayout(
mContext, this, renderHost, mStaticEdgeEventFilter, mOverlayPanelManager);
// Contextual Search scene overlay.
mContextualSearchPanel =
new ContextualSearchPanel(mContext, this, this, mOverlayPanelManager);
// Reader Mode scene overlay.
mReaderModePanel = new ReaderModePanel(mContext, this, this, mOverlayPanelManager, this);
// Set up layout parameters
mStaticLayout.setLayoutHandlesTabLifecycles(true);
setNextLayout(null);
}
@Override
public void init(TabModelSelector selector, TabCreatorManager creator,
TabContentManager content, ViewGroup androidContentContainer,
ContextualSearchManagementDelegate contextualSearchDelegate,
ReaderModeManagerDelegate readerModeDelegate,
DynamicResourceLoader dynamicResourceLoader) {
// Add any SceneOverlays to a layout.
addAllSceneOverlays();
// Save state
mContextualSearchDelegate = contextualSearchDelegate;
// Initialize Event Filters
mStaticEdgeEventFilter.setTabModelSelector(selector);
// Initialize Layouts
mStaticLayout.setTabModelSelector(selector, content);
// Initialize Contextual Search Panel
mContextualSearchPanel.setManagementDelegate(contextualSearchDelegate);
// Set back flow communication
if (contextualSearchDelegate != null) {
contextualSearchDelegate.setContextualSearchPanel(mContextualSearchPanel);
}
mReaderModePanel.setManagerDelegate(readerModeDelegate);
if (readerModeDelegate != null) {
readerModeDelegate.setReaderModePanel(mReaderModePanel);
}
// Set the dynamic resource loader for all overlay panels.
mOverlayPanelManager.setDynamicResourceLoader(dynamicResourceLoader);
mOverlayPanelManager.setContainerView(androidContentContainer);
mTabModelSelectorTabObserver = new TabModelSelectorTabObserver(selector) {
@Override
public void onContentChanged(Tab tab) {
initLayoutTabFromHost(tab.getId());
}
@Override
public void onBackgroundColorChanged(Tab tab, int color) {
initLayoutTabFromHost(tab.getId());
}
@Override
public void onDidChangeThemeColor(Tab tab, int color) {
initLayoutTabFromHost(tab.getId());
}
};
super.init(selector, creator, content, androidContentContainer, contextualSearchDelegate,
readerModeDelegate, dynamicResourceLoader);
}
@Override
public void destroy() {
super.destroy();
if (mStaticLayout != null) mStaticLayout.destroy();
if (mOverlayPanelManager != null) mOverlayPanelManager.destroy();
if (mTabModelSelectorTabObserver != null) mTabModelSelectorTabObserver.destroy();
}
@Override
public void getVirtualViews(List<VirtualView> views) {
// Nothing to do here yet.
}
@Override
protected void onViewportChanged(RectF viewportDp) {
super.onViewportChanged(viewportDp);
for (int i = 0; i < mTabCache.size(); i++) {
// This assumes that the content width/height is always the size of the host.
mTabCache.valueAt(i).setContentSize(viewportDp.width(), viewportDp.height());
}
}
/**
* @return The {@link EdgeSwipeHandler} responsible for processing swipe events for the toolbar.
*/
@Override
public EdgeSwipeHandler getTopSwipeHandler() {
return mToolbarSwipeHandler;
}
/**
* Clears all content associated with {@code tabId} from the internal caches.
* @param tabId The id of the tab to clear.
*/
protected void emptyCachesExcept(int tabId) {
LayoutTab tab = mTabCache.get(tabId);
mTabCache.clear();
if (tab != null) mTabCache.put(tabId, tab);
}
/**
* Adds the {@link SceneOverlay} across all {@link Layout}s owned by this class.
* @param helper A {@link SceneOverlay} instance.
*/
protected void addGlobalSceneOverlay(SceneOverlay helper) {
mStaticLayout.addSceneOverlay(helper);
}
/**
* Add any {@link SceneOverlay}s to the layout. This can be used to add the overlays in a
* particular order.
* Classes that override this method should be careful about the order that
* overlays are added and when super is called (i.e. cases where one overlay needs to be
* on top of another positioned.
*/
protected void addAllSceneOverlays() {
addGlobalSceneOverlay(mToolbarOverlay);
mStaticLayout.addSceneOverlay(mContextualSearchPanel);
mStaticLayout.addSceneOverlay(mReaderModePanel);
}
/**
* @param tabId The id of the tab represented by a {@link LayoutTab}.
* @return A {@link LayoutTab} if one exists or {@code null} if none can be found.
*/
protected LayoutTab getExistingLayoutTab(int tabId) {
return mTabCache.get(tabId);
}
@Override
protected Layout getDefaultLayout() {
return mStaticLayout;
}
@Override
public void initLayoutTabFromHost(final int tabId) {
if (getTabModelSelector() == null || getActiveLayout() == null) return;
TabModelSelector selector = getTabModelSelector();
Tab tab = selector.getTabById(tabId);
if (tab == null) return;
LayoutTab layoutTab = mTabCache.get(tabId);
if (layoutTab == null) return;
String url = tab.getUrl();
boolean isNativePage = url != null && url.startsWith(UrlConstants.CHROME_NATIVE_SCHEME);
int themeColor = tab.getThemeColor();
// TODO(xingliu): Remove this override themeColor for Blimp tabs. See crbug.com/644774.
if (tab.isBlimpTab() && tab.getBlimpContents() != null) {
themeColor = tab.getBlimpContents().getThemeColor();
}
boolean canUseLiveTexture = tab.isBlimpTab()
|| tab.getContentViewCore() != null && !tab.isShowingSadTab() && !isNativePage;
boolean needsUpdate = layoutTab.initFromHost(tab.getBackgroundColor(), tab.shouldStall(),
canUseLiveTexture, themeColor, ColorUtils.getTextBoxColorForToolbarBackground(
mContext.getResources(), tab, themeColor),
ColorUtils.getTextBoxAlphaForToolbarBackground(tab));
if (needsUpdate) requestUpdate();
mHost.requestRender();
}
@Override
public LayoutTab createLayoutTab(int id, boolean incognito, boolean showCloseButton,
boolean isTitleNeeded, float maxContentWidth, float maxContentHeight) {
LayoutTab tab = mTabCache.get(id);
if (tab == null) {
tab = new LayoutTab(id, incognito, mLastContentWidthDp, mLastContentHeightDp,
showCloseButton, isTitleNeeded);
mTabCache.put(id, tab);
} else {
tab.init(mLastContentWidthDp, mLastContentHeightDp, showCloseButton, isTitleNeeded);
}
if (maxContentWidth > 0.f) tab.setMaxContentWidth(maxContentWidth);
if (maxContentHeight > 0.f) tab.setMaxContentHeight(maxContentHeight);
return tab;
}
@Override
public void releaseTabLayout(int id) {
mTabCache.remove(id);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e, boolean isKeyboardShowing) {
boolean intercepted = super.onInterceptTouchEvent(e, isKeyboardShowing);
if (intercepted) getActiveLayout().unstallImmediately();
return intercepted;
}
/**
* Should be called when the user presses the back button on the phone.
* @return Whether or not the back button was consumed by the active {@link Layout}.
*/
@Override
public boolean onBackPressed() {
return getActiveLayout() != null && getActiveLayout().onBackPressed();
}
@Override
public void setOverlayPanelContentViewCore(ContentViewCore contentViewCore) {
mHost.onContentViewCoreAdded(contentViewCore);
}
@Override
public void releaseOverlayPanelContentViewCore() {
if (getTabModelSelector() == null) return;
Tab tab = getTabModelSelector().getCurrentTab();
if (tab != null) tab.updateFullscreenEnabledState();
}
private ContentViewCore getCurrentTabContentViewCore() {
if (getTabModelSelector() == null) return null;
Tab tab = getTabModelSelector().getCurrentTab();
if (tab == null) return null;
ContentViewCore cvc = tab.getContentViewCore();
return cvc;
}
private class StaticEdgeSwipeHandler extends EmptyEdgeSwipeHandler {
@Override
public void swipeStarted(ScrollDirection direction, float x, float y) {
}
@Override
public boolean isSwipeEnabled(ScrollDirection direction) {
FullscreenManager fullscreenManager = mHost.getFullscreenManager();
return direction == ScrollDirection.DOWN && fullscreenManager != null
&& fullscreenManager.getPersistentFullscreenMode();
}
}
/**
* A {@link EdgeSwipeHandler} meant to respond to edge events for the toolbar.
*/
private class ToolbarSwipeHandler extends EdgeSwipeHandlerLayoutDelegate {
private ScrollDirection mLastScroll;
/**
* Creates an instance of the {@link ToolbarSwipeHandler}.
* @param provider A {@link LayoutProvider} instance.
*/
public ToolbarSwipeHandler(LayoutProvider provider) {
super(provider);
}
@Override
public void swipeStarted(ScrollDirection direction, float x, float y) {
super.swipeStarted(direction, x, y);
mLastScroll = direction;
}
@Override
public void swipeFinished() {
super.swipeFinished();
changeTabs();
}
@Override
public void swipeFlingOccurred(float x, float y, float tx, float ty, float vx, float vy) {
super.swipeFlingOccurred(x, y, tx, ty, vx, vy);
changeTabs();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void changeTabs() {
DocumentTabModelSelector selector =
ChromeApplication.getDocumentTabModelSelector();
TabModel tabModel = selector.getCurrentModel();
int currentIndex = tabModel.index();
if (mLastScroll == ScrollDirection.LEFT) {
if (currentIndex < tabModel.getCount() - 1) {
TabModelUtils.setIndex(tabModel, currentIndex + 1);
}
} else {
if (currentIndex > 0) {
TabModelUtils.setIndex(tabModel, currentIndex - 1);
}
}
}
@Override
public boolean isSwipeEnabled(ScrollDirection direction) {
FullscreenManager manager = mHost.getFullscreenManager();
if (getActiveLayout() != mStaticLayout
|| !FeatureUtilities.isDocumentModeEligible(mHost.getContext())
|| !DeviceClassManager.enableToolbarSwipe()
|| (manager != null && manager.getPersistentFullscreenMode())) {
return false;
}
return direction == ScrollDirection.LEFT || direction == ScrollDirection.RIGHT;
}
}
}