// 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.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import org.chromium.base.ObserverList;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.chrome.browser.compositor.LayerTitleCache;
import org.chromium.chrome.browser.compositor.layouts.Layout.Orientation;
import org.chromium.chrome.browser.compositor.layouts.Layout.SizingFlags;
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.EdgeSwipeHandler;
import org.chromium.chrome.browser.compositor.layouts.eventfilter.EventFilter;
import org.chromium.chrome.browser.compositor.layouts.eventfilter.EventFilterHost;
import org.chromium.chrome.browser.compositor.scene_layer.SceneLayer;
import org.chromium.chrome.browser.contextualsearch.ContextualSearchManagementDelegate;
import org.chromium.chrome.browser.dom_distiller.ReaderModeManagerDelegate;
import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.tabmodel.EmptyTabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.content.browser.SPenSupport;
import org.chromium.ui.resources.ResourceManager;
import org.chromium.ui.resources.dynamics.DynamicResourceLoader;
import java.util.List;
/**
* A class that is responsible for managing an active {@link Layout} to show to the screen. This
* includes lifecycle managment like showing/hiding this {@link Layout}.
*/
public abstract class LayoutManager implements LayoutUpdateHost, LayoutProvider, EventFilterHost {
/** Sampling at 60 fps. */
private static final long FRAME_DELTA_TIME_MS = 16;
/** Used to convert pixels to dp. */
protected final float mPxToDp;
/** The {@link LayoutManagerHost}, who is responsible for showing the active {@link Layout}. */
protected final LayoutManagerHost mHost;
/** The last X coordinate of the last {@link MotionEvent#ACTION_DOWN} event. */
protected int mLastTapX;
/** The last Y coordinate of the last {@link MotionEvent#ACTION_DOWN} event. */
protected int mLastTapY;
// External Dependencies
private TabModelSelector mTabModelSelector;
private ViewGroup mContentContainer;
// External Observers
private final ObserverList<SceneChangeObserver> mSceneChangeObservers;
// Current Layout State
private Layout mActiveLayout;
private Layout mNextActiveLayout;
// Current Event Fitler State
private EventFilter mActiveEventFilter;
// Internal State
private int mFullscreenToken = FullscreenManager.INVALID_TOKEN;
private boolean mUpdateRequested;
// Sizing State
private final Rect mLastViewportPx = new Rect();
private final Rect mLastVisibleViewportPx = new Rect();
private final Rect mLastFullscreenViewportPx = new Rect();
protected final RectF mLastViewportDp = new RectF();
protected final RectF mLastVisibleViewportDp = new RectF();
protected final RectF mLastFullscreenViewportDp = new RectF();
protected float mLastContentWidthDp;
protected float mLastContentHeightDp;
protected float mLastHeightMinusTopControlsDp;
private final RectF mCachedRectF = new RectF();
private final Rect mCachedRect = new Rect();
private final Point mCachedPoint = new Point();
/**
* Creates a {@link LayoutManager} instance.
* @param host A {@link LayoutManagerHost} instance.
*/
public LayoutManager(LayoutManagerHost host) {
mHost = host;
mPxToDp = 1.f / mHost.getContext().getResources().getDisplayMetrics().density;
mSceneChangeObservers = new ObserverList<SceneChangeObserver>();
int hostWidth = host.getWidth();
int hostHeight = host.getHeight();
mLastViewportPx.set(0, 0, hostWidth, hostHeight);
mLastVisibleViewportPx.set(0, 0, hostWidth, hostHeight);
mLastFullscreenViewportPx.set(0, 0, hostWidth, hostHeight);
mLastContentWidthDp = hostWidth * mPxToDp;
mLastContentHeightDp = hostHeight * mPxToDp;
mLastViewportDp.set(0, 0, mLastContentWidthDp, mLastContentHeightDp);
mLastVisibleViewportDp.set(0, 0, mLastContentWidthDp, mLastContentHeightDp);
mLastFullscreenViewportDp.set(0, 0, mLastContentWidthDp, mLastContentHeightDp);
mLastHeightMinusTopControlsDp = mLastContentHeightDp;
}
/**
* @return The actual current time of the app in ms.
*/
public static long time() {
return SystemClock.uptimeMillis();
}
/**
* Gives the {@link LayoutManager} a chance to intercept and process touch events from the
* Android {@link View} system.
* @param e The {@link MotionEvent} that might be intercepted.
* @param isKeyboardShowing Whether or not the keyboard is showing.
* @return Whether or not this current touch gesture should be intercepted and
* continually forwarded to this class.
*/
public boolean onInterceptTouchEvent(MotionEvent e, boolean isKeyboardShowing) {
if (mActiveLayout == null) return false;
if (e.getAction() == MotionEvent.ACTION_DOWN) {
mLastTapX = (int) e.getX();
mLastTapY = (int) e.getY();
}
Point offsets = getMotionOffsets(e);
mActiveEventFilter =
mActiveLayout.findInterceptingEventFilter(e, offsets, isKeyboardShowing);
if (mActiveEventFilter != null) mActiveLayout.unstallImmediately();
return mActiveEventFilter != null;
}
/**
* Gives the {@link LayoutManager} a chance to process the touch events from the Android
* {@link View} system.
* @param e A {@link MotionEvent} instance.
* @return Whether or not {@code e} was consumed.
*/
public boolean onTouchEvent(MotionEvent e) {
if (mActiveEventFilter == null) return false;
boolean consumed = mActiveEventFilter.onTouchEvent(e);
Point offsets = getMotionOffsets(e);
if (offsets != null) mActiveEventFilter.setCurrentMotionEventOffsets(offsets.x, offsets.y);
return consumed;
}
@Override
public boolean propagateEvent(MotionEvent e) {
if (e == null) return false;
View view = getActiveLayout().getViewForInteraction();
if (view == null) return false;
e.offsetLocation(-view.getLeft(), -view.getTop());
return view.dispatchTouchEvent(e);
}
@Override
public int getViewportWidth() {
return mHost.getWidth();
}
private Point getMotionOffsets(MotionEvent e) {
int actionMasked = e.getActionMasked();
if (SPenSupport.isSPenSupported(mHost.getContext())) {
actionMasked = SPenSupport.convertSPenEventAction(actionMasked);
}
if (actionMasked == MotionEvent.ACTION_DOWN
|| actionMasked == MotionEvent.ACTION_HOVER_ENTER) {
getViewportPixel(mCachedRect);
mCachedPoint.set(-mCachedRect.left, -mCachedRect.top);
return mCachedPoint;
} else if (actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_CANCEL
|| actionMasked == MotionEvent.ACTION_HOVER_EXIT) {
mCachedPoint.set(0, 0);
return mCachedPoint;
}
return null;
}
/**
* Updates the state of the active {@link Layout} if needed. This updates the animations and
* cascades the changes to the tabs.
*/
public void onUpdate() {
TraceEvent.begin("LayoutDriver:onUpdate");
onUpdate(time(), FRAME_DELTA_TIME_MS);
TraceEvent.end("LayoutDriver:onUpdate");
}
/**
* Updates the state of the layout.
* @param timeMs The time in milliseconds.
* @param dtMs The delta time since the last update in milliseconds.
* @return Whether or not the {@link LayoutManager} needs more updates.
*/
@VisibleForTesting
public boolean onUpdate(long timeMs, long dtMs) {
if (!mUpdateRequested) return false;
mUpdateRequested = false;
final Layout layout = getActiveLayout();
if (layout != null && layout.onUpdate(timeMs, dtMs) && layout.isHiding()) {
layout.doneHiding();
}
return mUpdateRequested;
}
/**
* Initializes the {@link LayoutManager}. Must be called before using this object.
* @param selector A {@link TabModelSelector} instance.
* @param creator A {@link TabCreatorManager} instance.
* @param content A {@link TabContentManager} instance.
* @param androidContentContainer A {@link ViewGroup} for Android views to be bound to.
* @param contextualSearchDelegate A {@link ContextualSearchDelegate} instance.
* @param readerModeDelegate A {@link ReaderModeManagerDelegate} instance.
* @param dynamicResourceLoader A {@link DynamicResourceLoader} instance.
*/
public void init(TabModelSelector selector, TabCreatorManager creator,
TabContentManager content, ViewGroup androidContentContainer,
ContextualSearchManagementDelegate contextualSearchDelegate,
ReaderModeManagerDelegate readerModeDelegate,
DynamicResourceLoader dynamicResourceLoader) {
mTabModelSelector = selector;
mContentContainer = androidContentContainer;
if (mNextActiveLayout != null) startShowing(mNextActiveLayout, true);
updateLayoutForTabModelSelector();
}
/**
* Cleans up and destroys this object. It should not be used after this.
*/
public void destroy() {
mSceneChangeObservers.clear();
}
/**
* @param observer Adds {@code observer} to be notified when the active {@code Layout} changes.
*/
public void addSceneChangeObserver(SceneChangeObserver observer) {
mSceneChangeObservers.addObserver(observer);
}
/**
* @param observer Removes {@code observer}.
*/
public void removeSceneChangeObserver(SceneChangeObserver observer) {
mSceneChangeObservers.removeObserver(observer);
}
@Override
public SceneLayer getUpdatedActiveSceneLayer(Rect viewport, Rect contentViewport,
LayerTitleCache layerTitleCache, TabContentManager tabContentManager,
ResourceManager resourceManager, ChromeFullscreenManager fullscreenManager) {
return mActiveLayout.getUpdatedSceneLayer(viewport, contentViewport, layerTitleCache,
tabContentManager, resourceManager, fullscreenManager);
}
/**
* Called when the viewport has been changed. Override this to be notified when
* {@link #pushNewViewport(Rect, Rect, int)} calls actually change the current viewport.
* @param viewportDp The new viewport in dp.
*/
protected void onViewportChanged(RectF viewportDp) {
if (getActiveLayout() != null) {
getActiveLayout().sizeChanged(mLastVisibleViewportDp, mLastFullscreenViewportDp,
mLastHeightMinusTopControlsDp, getOrientation());
}
}
/**
* Should be called from an external source when the viewport changes. {@code viewport} and
* {@code visibleViewport} are different, as the top controls might be covering part of the
* viewport but a {@link Layout} might want to consume the whole space (or not).
* @param viewport The new viewport in px.
* @param visibleViewport The new visible viewport in px.
* @param heightMinusTopControls The height of the viewport minus the top controls.
*/
public final void pushNewViewport(
Rect viewport, Rect visibleViewport, int heightMinusTopControls) {
mLastViewportPx.set(viewport);
mLastVisibleViewportPx.set(visibleViewport);
mLastViewportDp.set(viewport.left * mPxToDp, viewport.top * mPxToDp,
viewport.right * mPxToDp, viewport.bottom * mPxToDp);
mLastVisibleViewportDp.set(visibleViewport.left * mPxToDp, visibleViewport.top * mPxToDp,
visibleViewport.right * mPxToDp, visibleViewport.bottom * mPxToDp);
mLastFullscreenViewportDp.set(0, 0, viewport.right * mPxToDp, viewport.bottom * mPxToDp);
mLastHeightMinusTopControlsDp = heightMinusTopControls * mPxToDp;
propagateViewportToActiveLayout();
}
/**
* @return The default {@link Layout} to show when {@link Layout}s get hidden and the next
* {@link Layout} to show isn't known.
*/
protected abstract Layout getDefaultLayout();
// TODO(dtrainor): Remove these from this control class. Split the interface?
@Override public abstract void initLayoutTabFromHost(final int tabId);
@Override
public abstract LayoutTab createLayoutTab(int id, boolean incognito, boolean showCloseButton,
boolean isTitleNeeded, float maxContentWidth, float maxContentHeight);
@Override public abstract void releaseTabLayout(int id);
/**
* @return The {@link TabModelSelector} instance this class knows about.
*/
protected TabModelSelector getTabModelSelector() {
return mTabModelSelector;
}
/**
* @return The next {@link Layout} that will be shown. If no {@link Layout} has been set
* since the last time {@link #startShowing(Layout, boolean)} was called, this will be
* {@link #getDefaultLayout()}.
*/
protected Layout getNextLayout() {
return mNextActiveLayout != null ? mNextActiveLayout : getDefaultLayout();
}
@Override
public Layout getActiveLayout() {
return mActiveLayout;
}
@Override
public RectF getViewportDp(RectF rect) {
if (rect == null) rect = new RectF();
if (getActiveLayout() == null) {
rect.set(mLastViewportDp);
return rect;
}
final int flags = getActiveLayout().getSizingFlags();
if ((flags & SizingFlags.REQUIRE_FULLSCREEN_SIZE) != 0) {
rect.set(mLastFullscreenViewportDp);
} else if ((flags & SizingFlags.ALLOW_TOOLBAR_HIDE) != 0) {
rect.set(mLastViewportDp);
} else {
rect.set(mLastVisibleViewportDp);
}
return rect;
}
@Override
public Rect getViewportPixel(Rect rect) {
if (rect == null) rect = new Rect();
if (getActiveLayout() == null) {
rect.set(mLastViewportPx);
return rect;
}
final int flags = getActiveLayout().getSizingFlags();
if ((flags & SizingFlags.REQUIRE_FULLSCREEN_SIZE) != 0) {
rect.set(mLastFullscreenViewportPx);
} else if ((flags & SizingFlags.ALLOW_TOOLBAR_HIDE) != 0) {
rect.set(mLastViewportPx);
} else {
rect.set(mLastVisibleViewportPx);
}
return rect;
}
@Override
public ChromeFullscreenManager getFullscreenManager() {
return mHost != null ? mHost.getFullscreenManager() : null;
}
@Override
public void requestUpdate() {
if (!mUpdateRequested) mHost.requestRender();
mUpdateRequested = true;
}
@Override
public void startHiding(int nextTabId, boolean hintAtTabSelection) {
requestUpdate();
if (hintAtTabSelection) {
for (SceneChangeObserver observer : mSceneChangeObservers) {
observer.onTabSelectionHinted(nextTabId);
}
}
}
@SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
@Override
public void doneHiding() {
// TODO: If next layout is default layout clear caches (should this be a sub layout thing?)
assert mNextActiveLayout != null : "Need to have a next active layout.";
if (mNextActiveLayout != null) {
startShowing(mNextActiveLayout, true);
}
}
@Override
public void doneShowing() {}
/**
* Should be called by control logic to show a new {@link Layout}.
*
* TODO(dtrainor, clholgat): Clean up the show logic to guarantee startHiding/doneHiding get
* called.
*
* @param layout The new {@link Layout} to show.
* @param animate Whether or not {@code layout} should animate as it shows.
*/
protected void startShowing(Layout layout, boolean animate) {
assert mTabModelSelector != null : "init() must be called first.";
assert layout != null : "Can't show a null layout.";
// Set the new layout
setNextLayout(null);
Layout oldLayout = getActiveLayout();
if (oldLayout != layout) {
if (oldLayout != null) {
oldLayout.detachViews();
}
layout.contextChanged(mHost.getContext());
layout.attachViews(mContentContainer);
mActiveLayout = layout;
}
ChromeFullscreenManager fullscreenManager = mHost.getFullscreenManager();
if (fullscreenManager != null) {
// Release any old fullscreen token we were holding.
fullscreenManager.hideControlsPersistent(mFullscreenToken);
mFullscreenToken = FullscreenManager.INVALID_TOKEN;
// Grab a new fullscreen token if this layout can't be in fullscreen.
final int flags = getActiveLayout().getSizingFlags();
if ((flags & SizingFlags.ALLOW_TOOLBAR_HIDE) == 0) {
mFullscreenToken = fullscreenManager.showControlsPersistent();
}
// Hide the toolbar immediately if the layout wants it gone quickly.
fullscreenManager.setTopControlsPermamentlyHidden(
flags == SizingFlags.HELPER_HIDE_TOOLBAR_IMMEDIATE);
}
propagateViewportToActiveLayout();
getActiveLayout().show(time(), animate);
mHost.setContentOverlayVisibility(getActiveLayout().shouldDisplayContentOverlay());
mHost.requestRender();
// Notify observers about the new scene.
for (SceneChangeObserver observer : mSceneChangeObservers) {
observer.onSceneChange(getActiveLayout());
}
}
/**
* Sets the next {@link Layout} to show after the current {@link Layout} is finished and is done
* hiding.
* @param layout The new {@link Layout} to show.
*/
public void setNextLayout(Layout layout) {
mNextActiveLayout = (layout == null) ? getDefaultLayout() : layout;
}
@Override
public boolean isActiveLayout(Layout layout) {
return layout == mActiveLayout;
}
/**
* Get a list of virtual views for accessibility.
*
* @param views A List to populate with virtual views.
*/
public abstract void getVirtualViews(List<VirtualView> views);
/**
* @return The {@link EdgeSwipeHandler} responsible for processing swipe events for the toolbar.
*/
public abstract EdgeSwipeHandler getTopSwipeHandler();
/**
* 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}.
*/
public abstract boolean onBackPressed();
private void propagateViewportToActiveLayout() {
getViewportDp(mCachedRectF);
float width = mCachedRectF.width();
float height = mCachedRectF.height();
mLastContentWidthDp = width;
mLastContentHeightDp = height;
onViewportChanged(mCachedRectF);
}
private int getOrientation() {
if (mLastContentWidthDp > mLastContentHeightDp) {
return Orientation.LANDSCAPE;
} else {
return Orientation.PORTRAIT;
}
}
/**
* Updates the Layout for the state of the {@link TabModelSelector} after initialization.
* If the TabModelSelector is not yet initialized when this function is called, a
* {@link TabModelSelectorObserver} is created to listen for when it is ready.
*/
private void updateLayoutForTabModelSelector() {
if (mTabModelSelector.isTabStateInitialized() && getActiveLayout() != null) {
getActiveLayout().onTabStateInitialized();
} else {
mTabModelSelector.addObserver(new EmptyTabModelSelectorObserver() {
@Override
public void onTabStateInitialized() {
if (getActiveLayout() != null) getActiveLayout().onTabStateInitialized();
final EmptyTabModelSelectorObserver observer = this;
new Handler().post(new Runnable() {
@Override
public void run() {
mTabModelSelector.removeObserver(observer);
}
});
}
});
}
}
}