// 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;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.widget.ExploreByTouchHelper;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.DragEvent;
import android.view.MotionEvent;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import org.chromium.base.SysUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.compositor.Invalidator.Client;
import org.chromium.chrome.browser.compositor.layouts.LayoutManager;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerHost;
import org.chromium.chrome.browser.compositor.layouts.LayoutRenderHost;
import org.chromium.chrome.browser.compositor.layouts.components.VirtualView;
import org.chromium.chrome.browser.compositor.layouts.content.ContentOffsetProvider;
import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
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.ChromeFullscreenManager;
import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager.FullscreenListener;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabContentViewParent;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tabmodel.EmptyTabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.util.ColorUtils;
import org.chromium.chrome.browser.widget.ClipDrawableProgressBar.DrawingInfo;
import org.chromium.chrome.browser.widget.ControlContainer;
import org.chromium.content.browser.ContentViewClient;
import org.chromium.content.browser.ContentViewCore;
import org.chromium.content.browser.SPenSupport;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.resources.ResourceManager;
import org.chromium.ui.resources.dynamics.DynamicResourceLoader;
import java.util.ArrayList;
import java.util.List;
/**
* This class holds a {@link CompositorView}. This level of indirection is needed to benefit from
* the {@link android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)} capability on
* available on {@link android.view.ViewGroup}s.
* This class also holds the {@link LayoutManager} responsible to describe the items to be
* drawn by the UI compositor on the native side.
*/
public class CompositorViewHolder extends CoordinatorLayout
implements LayoutManagerHost, LayoutRenderHost, Invalidator.Host, FullscreenListener {
private boolean mIsKeyboardShowing = false;
private final Invalidator mInvalidator = new Invalidator();
private LayoutManager mLayoutManager;
private LayerTitleCache mLayerTitleCache;
private CompositorView mCompositorView;
private boolean mContentOverlayVisiblity = true;
private int mPendingSwapBuffersCount;
private final ArrayList<Invalidator.Client> mPendingInvalidations =
new ArrayList<Invalidator.Client>();
private boolean mSkipInvalidation = false;
/**
* A task to be performed after a resize event.
*/
private Runnable mPostHideKeyboardTask;
private TabModelSelector mTabModelSelector;
private ChromeFullscreenManager mFullscreenManager;
private View mAccessibilityView;
private CompositorAccessibilityProvider mNodeProvider;
private boolean mFullscreenTouchEvent = false;
private float mLastContentOffset = 0;
private float mLastVisibleContentOffset = 0;
/** The toolbar control container. **/
private ControlContainer mControlContainer;
/** The currently visible Tab. */
private Tab mTabVisible;
/** The currently attached View. */
private TabContentViewParent mView;
private TabObserver mTabObserver;
private boolean mEnableCompositorTabStrip;
// Cache objects that should not be created frequently.
private final Rect mCacheViewport = new Rect();
private final Rect mCacheVisibleViewport = new Rect();
private DrawingInfo mProgressBarDrawingInfo;
// If we've drawn at least one frame.
private boolean mHasDrawnOnce = false;
/**
* This view is created on demand to display debugging information.
*/
private static class DebugOverlay extends View {
private final List<Pair<Rect, Integer>> mRectangles = new ArrayList<Pair<Rect, Integer>>();
private final Paint mPaint = new Paint();
private boolean mFirstPush = true;
/**
* @param context The current Android's context.
*/
public DebugOverlay(Context context) {
super(context);
}
/**
* Pushes a rectangle to be drawn on the screen on top of everything.
*
* @param rect The rectangle to be drawn on screen
* @param color The color of the rectangle
*/
public void pushRect(Rect rect, int color) {
if (mFirstPush) {
mRectangles.clear();
mFirstPush = false;
}
mRectangles.add(new Pair<Rect, Integer>(rect, color));
invalidate();
}
@SuppressFBWarnings("NP_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD")
@Override
protected void onDraw(Canvas canvas) {
for (int i = 0; i < mRectangles.size(); i++) {
mPaint.setColor(mRectangles.get(i).second);
canvas.drawRect(mRectangles.get(i).first, mPaint);
}
mFirstPush = true;
}
}
private DebugOverlay mDebugOverlay;
private View mUrlBar;
/**
* Creates a {@link CompositorView}.
* @param c The Context to create this {@link CompositorView} in.
*/
public CompositorViewHolder(Context c) {
super(c);
internalInit();
}
/**
* Creates a {@link CompositorView}.
* @param c The Context to create this {@link CompositorView} in.
* @param attrs The AttributeSet used to create this {@link CompositorView}.
*/
public CompositorViewHolder(Context c, AttributeSet attrs) {
super(c, attrs);
internalInit();
}
private void internalInit() {
mTabObserver = new EmptyTabObserver() {
@Override
public void onContentChanged(Tab tab) {
CompositorViewHolder.this.onContentChanged();
}
};
mEnableCompositorTabStrip = DeviceFormFactor.isTablet(getContext());
addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
propagateViewportToLayouts(right - left, bottom - top);
// If there's an event that needs to occur after the keyboard is hidden, post
// it as a delayed event. Otherwise this happens in the midst of the
// ContentView's relayout, which causes the ContentView to relayout on top of the
// stack view. The 30ms is arbitrary, hoping to let the view get one repaint
// in so the full page is shown.
if (mPostHideKeyboardTask != null) {
new Handler().postDelayed(mPostHideKeyboardTask, 30);
mPostHideKeyboardTask = null;
}
}
});
mCompositorView = new CompositorView(getContext(), this);
// mCompositorView should always be the first child.
addView(mCompositorView, 0,
new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
/**
* @param layoutManager The {@link LayoutManager} instance that will be driving what
* shows in this {@link CompositorViewHolder}.
*/
public void setLayoutManager(LayoutManager layoutManager) {
mLayoutManager = layoutManager;
propagateViewportToLayouts(getWidth(), getHeight());
}
/**
* @param view The root view of the hierarchy.
*/
public void setRootView(View view) {
mCompositorView.setRootView(view);
}
/**
* @param controlContainer The ControlContainer.
*/
public void setControlContainer(ControlContainer controlContainer) {
DynamicResourceLoader loader = mCompositorView.getResourceManager() != null
? mCompositorView.getResourceManager().getDynamicResourceLoader()
: null;
if (loader != null && mControlContainer != null) {
loader.unregisterResource(R.id.control_container);
}
mControlContainer = controlContainer;
if (loader != null && mControlContainer != null) {
loader.registerResource(
R.id.control_container, mControlContainer.getToolbarResourceAdapter());
}
}
/**
* Reset command line flags. This gets called after the native library finishes
* loading.
*/
public void resetFlags() {
mCompositorView.resetFlags();
}
/**
* Should be called for cleanup when the CompositorView instance is no longer used.
*/
public void shutDown() {
setTab(null);
if (mLayerTitleCache != null) mLayerTitleCache.shutDown();
mCompositorView.shutDown();
}
/**
* This is called when the native library are ready.
*/
public void onNativeLibraryReady(
WindowAndroid windowAndroid, TabContentManager tabContentManager) {
assert mLayerTitleCache == null : "Should be called once";
if (DeviceClassManager.enableLayerDecorationCache()) {
mLayerTitleCache = new LayerTitleCache(getContext());
}
mCompositorView.initNativeCompositor(
SysUtils.isLowEndDevice(), windowAndroid, mLayerTitleCache, tabContentManager);
if (mLayerTitleCache != null) {
mLayerTitleCache.setResourceManager(getResourceManager());
}
if (mControlContainer != null) {
mCompositorView.getResourceManager().getDynamicResourceLoader().registerResource(
R.id.control_container, mControlContainer.getToolbarResourceAdapter());
}
}
/**
* Perform any initialization necessary for showing a reparented tab.
*/
public void prepareForTabReparenting() {
if (mHasDrawnOnce) return;
// Set the background to white while we wait for the first swap of buffers. This gets
// corrected inside the view.
mCompositorView.setBackgroundColor(Color.WHITE);
}
@Override
public ResourceManager getResourceManager() {
return mCompositorView.getResourceManager();
}
public ContentOffsetProvider getContentOffsetProvider() {
return mCompositorView;
}
/**
* @return The {@link DynamicResourceLoader} for registering resources.
*/
public DynamicResourceLoader getDynamicResourceLoader() {
return mCompositorView.getResourceManager().getDynamicResourceLoader();
}
/**
* @return The {@link Invalidator} instance that is driven by this {@link CompositorViewHolder}.
*/
public Invalidator getInvalidator() {
return mInvalidator;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
boolean consumedBySuper = super.onInterceptTouchEvent(e);
if (consumedBySuper) return true;
if (mLayoutManager == null) return false;
mFullscreenTouchEvent = false;
if (mFullscreenManager != null && mFullscreenManager.onInterceptMotionEvent(e)
&& !mEnableCompositorTabStrip) {
// Don't eat the event if the new tab strip is enabled.
mFullscreenTouchEvent = true;
return true;
}
setContentViewMotionEventOffsets(e, false);
return mLayoutManager.onInterceptTouchEvent(e, mIsKeyboardShowing);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
boolean consumedBySuper = super.onTouchEvent(e);
if (consumedBySuper) return true;
if (mFullscreenManager != null) mFullscreenManager.onMotionEvent(e);
if (mFullscreenTouchEvent) return true;
boolean consumed = mLayoutManager != null && mLayoutManager.onTouchEvent(e);
setContentViewMotionEventOffsets(e, true);
return consumed;
}
@Override
public boolean onInterceptHoverEvent(MotionEvent e) {
setContentViewMotionEventOffsets(e, true);
return super.onInterceptHoverEvent(e);
}
@Override
public boolean dispatchHoverEvent(MotionEvent e) {
if (mNodeProvider != null) {
if (mNodeProvider.dispatchHoverEvent(e)) {
return true;
}
}
return super.dispatchHoverEvent(e);
}
@Override
public boolean dispatchDragEvent(DragEvent e) {
ContentViewCore contentViewCore = mTabVisible.getContentViewCore();
if (contentViewCore == null) return false;
if (mLayoutManager != null) mLayoutManager.getViewportPixel(mCacheViewport);
contentViewCore.setCurrentTouchEventOffsets(-mCacheViewport.left, -mCacheViewport.top);
boolean ret = super.dispatchDragEvent(e);
int action = e.getAction();
if (action == DragEvent.ACTION_DRAG_EXITED || action == DragEvent.ACTION_DRAG_ENDED
|| action == DragEvent.ACTION_DROP) {
contentViewCore.setCurrentTouchEventOffsets(0.f, 0.f);
}
return ret;
}
/**
* @return The {@link LayoutManager} associated with this view.
*/
public LayoutManager getLayoutManager() {
return mLayoutManager;
}
/**
* @return The SurfaceView used by the Compositor.
*/
public SurfaceView getSurfaceView() {
return mCompositorView;
}
private View getActiveView() {
if (mLayoutManager == null || mTabModelSelector == null) return null;
Tab tab = mTabModelSelector.getCurrentTab();
return tab != null ? tab.getContentView() : null;
}
private ContentViewCore getActiveContent() {
if (mLayoutManager == null || mTabModelSelector == null) return null;
Tab tab = mTabModelSelector.getCurrentTab();
return tab != null ? tab.getActiveContentViewCore() : null;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
View view = getActiveView();
if (view != null && setSizeOfUnattachedView(view)) requestRender();
}
@Override
public void onPhysicalBackingSizeChanged(int width, int height) {
ContentViewCore content = getActiveContent();
if (content != null) adjustPhysicalBackingSize(content, width, height);
}
/**
* Called whenever the host activity is started.
*/
public void onStart() {
if (mFullscreenManager != null) {
mLastContentOffset = mFullscreenManager.getContentOffset();
mLastVisibleContentOffset = mFullscreenManager.getVisibleContentOffset();
mFullscreenManager.addListener(this);
}
requestRender();
}
/**
* Called whenever the host activity is stopped.
*/
public void onStop() {
if (mFullscreenManager != null) mFullscreenManager.removeListener(this);
}
@Override
public void onContentOffsetChanged(float offset) {
mLastContentOffset = offset;
propagateViewportToLayouts(getWidth(), getHeight());
}
@Override
public void onVisibleContentOffsetChanged(float offset, boolean needsAnimate) {
mLastVisibleContentOffset = offset;
propagateViewportToLayouts(getWidth(), getHeight());
if (needsAnimate) requestRender();
}
@Override
public void onToggleOverlayVideoMode(boolean enabled) {
if (mCompositorView != null) {
mCompositorView.setOverlayVideoMode(enabled);
}
}
private void setContentViewMotionEventOffsets(MotionEvent e, boolean canClear) {
// TODO(dtrainor): Factor this out to LayoutDriver.
if (e == null || mTabVisible == null) return;
ContentViewCore contentViewCore = mTabVisible.getContentViewCore();
if (contentViewCore == null) return;
int actionMasked = e.getActionMasked();
if (SPenSupport.isSPenSupported(getContext())) {
actionMasked = SPenSupport.convertSPenEventAction(actionMasked);
}
if (actionMasked == MotionEvent.ACTION_DOWN
|| actionMasked == MotionEvent.ACTION_HOVER_ENTER) {
if (mLayoutManager != null) mLayoutManager.getViewportPixel(mCacheViewport);
contentViewCore.setCurrentTouchEventOffsets(-mCacheViewport.left, -mCacheViewport.top);
} else if (canClear && (actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_CANCEL
|| actionMasked == MotionEvent.ACTION_HOVER_EXIT)) {
contentViewCore.setCurrentTouchEventOffsets(0.f, 0.f);
}
}
private void propagateViewportToLayouts(int contentWidth, int contentHeight) {
int heightMinusTopControls = contentHeight - getTopControlsHeightPixels();
mCacheViewport.set(0, (int) mLastContentOffset, contentWidth, contentHeight);
mCacheVisibleViewport.set(0, (int) mLastVisibleContentOffset, contentWidth, contentHeight);
// TODO(changwan): check if this can be merged with setContentMotionEventOffsets.
if (mTabVisible != null && mTabVisible.getContentViewCore() != null) {
mTabVisible.getContentViewCore().setSmartClipOffsets(
-mCacheViewport.left, -mCacheViewport.top);
}
if (mLayoutManager != null) {
mLayoutManager.pushNewViewport(
mCacheViewport, mCacheVisibleViewport, heightMinusTopControls);
}
}
/**
* To be called once a frame before commit.
*/
@Override
public void onCompositorLayout() {
TraceEvent.begin("CompositorViewHolder:layout");
if (mLayoutManager != null) {
mLayoutManager.onUpdate();
if (!DeviceFormFactor.isTablet(getContext()) && mControlContainer != null) {
if (mProgressBarDrawingInfo == null) mProgressBarDrawingInfo = new DrawingInfo();
mControlContainer.getProgressBarDrawingInfo(mProgressBarDrawingInfo);
} else {
assert mProgressBarDrawingInfo == null;
}
mCompositorView.finalizeLayers(mLayoutManager, false,
mProgressBarDrawingInfo);
}
TraceEvent.end("CompositorViewHolder:layout");
}
@Override
public void requestRender() {
mCompositorView.requestRender();
}
@Override
public void onSurfaceCreated() {
mPendingSwapBuffersCount = 0;
flushInvalidation();
}
@Override
public void onSwapBuffersCompleted(int pendingSwapBuffersCount) {
TraceEvent.instant("onSwapBuffersCompleted");
// Wait until the second frame to turn off the placeholder background on
// tablets so the tab strip has time to start drawing.
final ViewGroup controlContainer = (ViewGroup) mControlContainer;
if (controlContainer != null && controlContainer.getBackground() != null && mHasDrawnOnce) {
post(new Runnable() {
@Override
public void run() {
controlContainer.setBackgroundResource(0);
}
});
}
mHasDrawnOnce = true;
mPendingSwapBuffersCount = pendingSwapBuffersCount;
if (!mSkipInvalidation || pendingSwapBuffersCount == 0) flushInvalidation();
mSkipInvalidation = !mSkipInvalidation;
}
@Override
public void setContentOverlayVisibility(boolean show) {
if (show != mContentOverlayVisiblity) {
mContentOverlayVisiblity = show;
updateContentOverlayVisibility(mContentOverlayVisiblity);
}
}
@Override
public LayoutRenderHost getLayoutRenderHost() {
return this;
}
@Override
public int getLayoutTabsDrawnCount() {
return mCompositorView.getLastLayerCount();
}
@Override
public void pushDebugRect(Rect rect, int color) {
if (mDebugOverlay == null) {
mDebugOverlay = new DebugOverlay(getContext());
addView(mDebugOverlay);
}
mDebugOverlay.pushRect(rect, color);
}
@Override
public void loadPersitentTextureDataIfNeeded() {}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mIsKeyboardShowing = UiUtils.isKeyboardShowing(getContext(), this);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
propagateViewportToLayouts(r - l, b - t);
}
super.onLayout(changed, l, t, r, b);
invalidateAccessibilityProvider();
}
@Override
public void clearChildFocus(View child) {
// Override this method so that the ViewRoot doesn't go looking for a new
// view to take focus. It will find the URL Bar, focus it, then refocus this
// later, causing a keyboard flicker.
}
@Override
public ChromeFullscreenManager getFullscreenManager() {
return mFullscreenManager;
}
/**
* Sets a fullscreen handler.
* @param fullscreen A fullscreen handler.
*/
public void setFullscreenHandler(ChromeFullscreenManager fullscreen) {
mFullscreenManager = fullscreen;
if (mFullscreenManager != null) {
mLastContentOffset = mFullscreenManager.getContentOffset();
mLastVisibleContentOffset = mFullscreenManager.getVisibleContentOffset();
mFullscreenManager.addListener(this);
}
propagateViewportToLayouts(getWidth(), getHeight());
}
/**
* Note that the returned rect is reused for other calls.
*/
@Override
public Rect getVisibleViewport(Rect rect) {
if (rect == null) rect = new Rect();
rect.set(0, (int) mLastVisibleContentOffset, getWidth(), getHeight());
return rect;
}
@Override
public int getTopControlsBackgroundColor() {
return mTabVisible == null ? Color.WHITE : mTabVisible.getThemeColor();
}
@Override
public float getTopControlsUrlBarAlpha() {
return mTabVisible == null
? 1.f
: ColorUtils.getTextBoxAlphaForToolbarBackground(mTabVisible);
}
@Override
public boolean areTopControlsPermanentlyHidden() {
return mFullscreenManager != null && mFullscreenManager.areTopControlsPermanentlyHidden();
}
@Override
public int getTopControlsHeightPixels() {
return mFullscreenManager != null ? mFullscreenManager.getTopControlsHeight() : 0;
}
/**
* Sets the URL bar. This is needed so that the ContentViewHolder can find out
* whether it can claim focus.
*/
public void setUrlBar(View urlBar) {
mUrlBar = urlBar;
}
@Override
public void onAttachedToWindow() {
mInvalidator.set(this);
super.onAttachedToWindow();
}
@Override
public void onDetachedFromWindow() {
if (mLayoutManager != null) mLayoutManager.destroy();
flushInvalidation();
mInvalidator.set(null);
super.onDetachedFromWindow();
// Removes the accessibility node provider from this view.
if (mNodeProvider != null) {
mAccessibilityView.setAccessibilityDelegate(null);
mNodeProvider = null;
removeView(mAccessibilityView);
mAccessibilityView = null;
}
}
/**
* @return True if the currently active content view is shown in the normal interactive mode.
*/
public boolean isTabInteractive() {
return mLayoutManager != null && mLayoutManager.getActiveLayout() != null
&& mLayoutManager.getActiveLayout().isTabInteractive() && mContentOverlayVisiblity
&& mView != null;
}
@Override
public void hideKeyboard(Runnable postHideTask) {
// When this is called we actually want to hide the keyboard whatever owns it.
// This includes hiding the keyboard, and dropping focus from the URL bar.
// See http://crbug/236424
// TODO(aberent) Find a better place to put this, possibly as part of a wider
// redesign of focus control.
if (mUrlBar != null) mUrlBar.clearFocus();
boolean wasVisible = false;
if (hasFocus()) {
wasVisible = UiUtils.hideKeyboard(this);
}
if (wasVisible) {
mPostHideKeyboardTask = postHideTask;
} else {
postHideTask.run();
}
}
/**
* Sets the appropriate objects this class should represent.
* @param tabModelSelector The {@link TabModelSelector} this View should hold and
* represent.
* @param tabCreatorManager The {@link TabCreatorManager} for this view.
* @param tabContentManager The {@link TabContentManager} for the tabs.
* @param androidContentContainer The {@link ViewGroup} the {@link LayoutManager} should bind
* Android content to.
* @param contextualSearchManager A {@link ContextualSearchManagementDelegate} instance.
* @param readerModeManager A {@link ReaderModeManagerDelegate} instance.
*/
public void onFinishNativeInitialization(TabModelSelector tabModelSelector,
TabCreatorManager tabCreatorManager, TabContentManager tabContentManager,
ViewGroup androidContentContainer,
ContextualSearchManagementDelegate contextualSearchManager,
ReaderModeManagerDelegate readerModeManager) {
assert mLayoutManager != null;
mLayoutManager.init(tabModelSelector, tabCreatorManager, tabContentManager,
androidContentContainer, contextualSearchManager, readerModeManager,
mCompositorView.getResourceManager().getDynamicResourceLoader());
mTabModelSelector = tabModelSelector;
tabModelSelector.addObserver(new EmptyTabModelSelectorObserver() {
@Override
public void onChange() {
onContentChanged();
}
@Override
public void onNewTabCreated(Tab tab) {
initializeTab(tab);
}
});
mLayerTitleCache.setTabModelSelector(mTabModelSelector);
onContentChanged();
}
private void updateContentOverlayVisibility(boolean show) {
if (mView == null) return;
ContentViewCore content = getActiveContent();
if (show) {
if (mView.getParent() != this) {
// During tab creation, we temporarily add the new tab's view to a FrameLayout to
// measure and lay it out. This way we could show the animation in the stack view.
// Therefore we should remove the view from that temporary FrameLayout here.
UiUtils.removeViewFromParent(mView);
if (content != null) {
assert content.isAlive();
content.getContainerView().setVisibility(View.VISIBLE);
if (mFullscreenManager != null) {
mFullscreenManager.updateContentViewViewportSize(content);
}
}
CoordinatorLayout.LayoutParams layoutParams;
if (mView.getLayoutParams() instanceof CoordinatorLayout.LayoutParams) {
layoutParams = (CoordinatorLayout.LayoutParams) mView.getLayoutParams();
} else {
layoutParams = new CoordinatorLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
layoutParams.setBehavior(mView.getBehavior());
// CompositorView has index of 0; TabContentViewParent has index of 1; omnibox
// result container (the scrim) has index of 2, Snackbar (if any) has index of 3.
// Setting index here explicitly to avoid TabContentViewParent hiding the scrim.
// TODO(ianwen): Use more advanced technologies to ensure z-order of the children of
// this class, instead of hard-coding.
addView(mView, 1, layoutParams);
setFocusable(false);
setFocusableInTouchMode(false);
// Claim focus for the new view unless the user is currently using the URL bar.
if (mUrlBar == null || !mUrlBar.hasFocus()) mView.requestFocus();
}
} else {
if (mView.getParent() == this) {
setFocusable(true);
setFocusableInTouchMode(true);
if (content != null) {
if (content.isAlive()) content.getContainerView().setVisibility(View.INVISIBLE);
}
removeView(mView);
}
}
}
@Override
public void onContentChanged() {
if (mTabModelSelector == null) {
// Not yet initialized, onContentChanged() will eventually get called by
// setTabModelSelector.
return;
}
Tab tab = mTabModelSelector.getCurrentTab();
setTab(tab);
}
@Override
public void onContentViewCoreAdded(ContentViewCore content) {
// TODO(dtrainor): Look into rolling this into onContentChanged().
initializeContentViewCore(content);
setSizeOfUnattachedView(content.getContainerView());
}
private void setTab(Tab tab) {
if (tab != null) tab.loadIfNeeded();
TabContentViewParent newView = tab != null ? tab.getView() : null;
if (mView == newView) return;
// TODO(dtrainor): Look into changing this only if the views differ, but still parse the
// ContentViewCore list even if they're the same.
updateContentOverlayVisibility(false);
if (mTabVisible != tab) {
if (mTabVisible != null) mTabVisible.removeObserver(mTabObserver);
if (tab != null) tab.addObserver(mTabObserver);
}
mTabVisible = tab;
mView = newView;
updateContentOverlayVisibility(mContentOverlayVisiblity);
if (mTabVisible != null) initializeTab(mTabVisible);
}
/**
* Sets the correct size for {@link View} on {@code tab} and sets the correct rendering
* parameters on {@link ContentViewCore} on {@code tab}.
* @param tab The {@link Tab} to initialize.
*/
private void initializeTab(Tab tab) {
ContentViewCore content = tab.getActiveContentViewCore();
if (content != null) initializeContentViewCore(content);
View view = tab.getContentView();
if (view != tab.getView() || !tab.isNativePage()) setSizeOfUnattachedView(view);
}
/**
* Initializes the rendering surface parameters of {@code contentViewCore}. Note that this does
* not size the actual {@link ContentViewCore}.
* @param contentViewCore The {@link ContentViewCore} to initialize.
*/
private void initializeContentViewCore(ContentViewCore contentViewCore) {
contentViewCore.setCurrentTouchEventOffsets(0.f, 0.f);
contentViewCore.setTopControlsHeight(
getTopControlsHeightPixels(), contentViewCore.doTopControlsShrinkBlinkSize());
adjustPhysicalBackingSize(contentViewCore,
mCompositorView.getWidth(), mCompositorView.getHeight());
}
/**
* Adjusts the physical backing size of a given ContentViewCore. This method will first check
* if the ContentViewCore's client wants to override the size and, if so, it will use the
* values provided by the {@link ContentViewClient#getDesiredWidthMeasureSpec()} and
* {@link ContentViewClient#getDesiredHeightMeasureSpec()} methods. If no value is provided
* in one of these methods, the values from the |width| and |height| arguments will be
* used instead.
*
* @param contentViewCore The {@link ContentViewCore} to resize.
* @param width The default width.
* @param height The default height.
*/
private void adjustPhysicalBackingSize(ContentViewCore contentViewCore, int width, int height) {
ContentViewClient client = contentViewCore.getContentViewClient();
int desiredWidthMeasureSpec = client.getDesiredWidthMeasureSpec();
if (MeasureSpec.getMode(desiredWidthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
width = MeasureSpec.getSize(desiredWidthMeasureSpec);
}
int desiredHeightMeasureSpec = client.getDesiredHeightMeasureSpec();
if (MeasureSpec.getMode(desiredHeightMeasureSpec) != MeasureSpec.UNSPECIFIED) {
height = MeasureSpec.getSize(desiredHeightMeasureSpec);
}
contentViewCore.onPhysicalBackingSizeChanged(width, height);
}
/**
* Resize {@code view} to match the size of this {@link FrameLayout}. This will only happen if
* {@code view} is not {@code null} and if {@link View#getWindowToken()} returns {@code null}
* (the {@link View} is not part of the view hierarchy).
* @param view The {@link View} to resize.
* @return Whether or not {@code view} was resized.
*/
private boolean setSizeOfUnattachedView(View view) {
// Need to call layout() for the following View if it is not attached to the view hierarchy.
// Calling onSizeChanged() is dangerous because if the View has a different size than the
// ContentViewCore it might think a future size update is a NOOP and not call
// onSizeChanged() on the ContentViewCore.
if (view == null || view.getWindowToken() != null) return false;
int width = getWidth();
int height = getHeight();
view.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
return true;
}
@Override
public TitleCache getTitleCache() {
return mLayerTitleCache;
}
@Override
public void deferInvalidate(Client client) {
if (mPendingSwapBuffersCount <= 0) {
client.doInvalidate();
} else if (!mPendingInvalidations.contains(client)) {
mPendingInvalidations.add(client);
}
}
private void flushInvalidation() {
if (mPendingInvalidations.isEmpty()) return;
TraceEvent.instant("CompositorViewHolder.flushInvalidation");
for (int i = 0; i < mPendingInvalidations.size(); i++) {
mPendingInvalidations.get(i).doInvalidate();
}
mPendingInvalidations.clear();
}
@Override
public void invalidateAccessibilityProvider() {
if (mNodeProvider != null) {
mNodeProvider.sendEventForVirtualView(mNodeProvider.getFocusedVirtualView(),
AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
mNodeProvider.invalidateRoot();
}
}
/**
* Called when the accessibility enabled state changes.
* @param enabled Whether accessibility is enabled.
*/
public void onAccessibilityStatusChanged(boolean enabled) {
// Instantiate and install the accessibility node provider on this view if necessary.
// This overrides any hover event listeners or accessibility delegates
// that may have been added elsewhere.
if (enabled && (mNodeProvider == null)) {
mAccessibilityView = new View(getContext());
addView(mAccessibilityView);
mNodeProvider = new CompositorAccessibilityProvider(mAccessibilityView);
ViewCompat.setAccessibilityDelegate(mAccessibilityView, mNodeProvider);
}
}
/**
* Class used to provide a virtual view hierarchy to the Accessibility
* framework for this view and its contained items.
* <p>
* <strong>NOTE:</strong> This class is fully backwards compatible for
* compilation, but will only provide touch exploration on devices running
* Ice Cream Sandwich and above.
* </p>
*/
private class CompositorAccessibilityProvider extends ExploreByTouchHelper {
private final float mDpToPx;
List<VirtualView> mVirtualViews = new ArrayList<VirtualView>();
private final Rect mPlaceHolderRect = new Rect(0, 0, 1, 1);
private static final String PLACE_HOLDER_STRING = "";
private final RectF mTouchTarget = new RectF();
private final Rect mPixelRect = new Rect();
public CompositorAccessibilityProvider(View forView) {
super(forView);
mDpToPx = getContext().getResources().getDisplayMetrics().density;
}
@Override
protected int getVirtualViewAt(float x, float y) {
if (mVirtualViews == null) return INVALID_ID;
for (int i = 0; i < mVirtualViews.size(); i++) {
if (mVirtualViews.get(i).checkClicked(x / mDpToPx, y / mDpToPx)) {
return i;
}
}
return INVALID_ID;
}
@Override
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
if (mLayoutManager == null) return;
mVirtualViews.clear();
mLayoutManager.getVirtualViews(mVirtualViews);
for (int i = 0; i < mVirtualViews.size(); i++) {
virtualViewIds.add(i);
}
}
@Override
protected boolean onPerformActionForVirtualView(
int virtualViewId, int action, Bundle arguments) {
switch (action) {
case AccessibilityNodeInfoCompat.ACTION_CLICK:
return true;
}
return false;
}
@Override
protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
if (mVirtualViews == null || mVirtualViews.size() <= virtualViewId) {
// TODO(clholgat): Remove this work around when the Android bug is fixed.
// crbug.com/420177
event.setContentDescription(PLACE_HOLDER_STRING);
return;
}
VirtualView view = mVirtualViews.get(virtualViewId);
event.setContentDescription(view.getAccessibilityDescription());
event.setClassName(CompositorViewHolder.class.getName());
}
@Override
protected void onPopulateNodeForVirtualView(
int virtualViewId, AccessibilityNodeInfoCompat node) {
if (mVirtualViews == null || mVirtualViews.size() <= virtualViewId) {
// TODO(clholgat): Remove this work around when the Android bug is fixed.
// crbug.com/420177
node.setBoundsInParent(mPlaceHolderRect);
node.setContentDescription(PLACE_HOLDER_STRING);
return;
}
VirtualView view = mVirtualViews.get(virtualViewId);
view.getTouchTarget(mTouchTarget);
node.setBoundsInParent(rectToPx(mTouchTarget));
node.setContentDescription(view.getAccessibilityDescription());
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
node.addAction(AccessibilityNodeInfoCompat.ACTION_FOCUS);
node.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
}
private Rect rectToPx(RectF rect) {
rect.roundOut(mPixelRect);
mPixelRect.left = (int) (mPixelRect.left * mDpToPx);
mPixelRect.top = (int) (mPixelRect.top * mDpToPx);
mPixelRect.right = (int) (mPixelRect.right * mDpToPx);
mPixelRect.bottom = (int) (mPixelRect.bottom * mDpToPx);
// Don't let any zero sized rects through, they'll cause parent
// size errors in L.
if (mPixelRect.width() == 0) {
mPixelRect.right = mPixelRect.left + 1;
}
if (mPixelRect.height() == 0) {
mPixelRect.bottom = mPixelRect.top + 1;
}
return mPixelRect;
}
}
}