blob: 481391af84ccd556a7bda9b2ed92e89016a2124d [file] [log] [blame]
// Copyright 2016 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.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.widget.FrameLayout;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.compositor.layouts.LayoutProvider;
import org.chromium.chrome.browser.compositor.layouts.LayoutRenderHost;
import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
import org.chromium.chrome.browser.compositor.resources.StaticResourcePreloads;
import org.chromium.chrome.browser.compositor.scene_layer.SceneLayer;
import org.chromium.chrome.browser.externalnav.IntentWithGesturesHandler;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.tabmodel.TabModelImpl;
import org.chromium.chrome.browser.util.ColorUtils;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.resources.AndroidResourceType;
import org.chromium.ui.resources.ResourceManager;
import java.util.ArrayList;
import java.util.List;
/**
* The is the {@link View} displaying the ui compositor results; including webpages and tabswitcher.
*/
@JNINamespace("android")
public class CompositorView
extends FrameLayout implements CompositorSurfaceManager.SurfaceManagerCallbackTarget,
WindowAndroid.SelectionHandlesObserver {
private static final String TAG = "CompositorView";
// Cache objects that should not be created every frame
private final Rect mCacheAppRect = new Rect();
private final int[] mCacheViewPosition = new int[2];
private CompositorSurfaceManager mCompositorSurfaceManager;
private boolean mOverlayVideoEnabled;
private boolean mAlwaysTranslucent;
// Are we waiting to hide the outgoing surface until the foreground has something to display?
// If == 0, then no. If > 0, then yes. We'll hide when it transitions from one to zero.
private int mFramesUntilHideBackground;
private long mNativeCompositorView;
private final LayoutRenderHost mRenderHost;
private int mPreviousWindowTop = -1;
// Resource Management
private ResourceManager mResourceManager;
// Lazily populated as it is needed.
private View mRootActivityView;
private WindowAndroid mWindowAndroid;
private LayerTitleCache mLayerTitleCache;
private TabContentManager mTabContentManager;
private View mRootView;
private boolean mPreloadedResources;
private List<Runnable> mDrawingFinishedCallbacks;
private boolean mIsInVr;
private boolean mIsSurfaceControlEnabled;
private boolean mSelectionHandlesActive;
// On P and above, toggling the screen off gets us in a state where the Surface is destroyed but
// it is never recreated when it is turned on again. This is the only workaround that seems to
// be working, see crbug.com/931195.
class ScreenStateReceiverWorkaround extends BroadcastReceiver {
ScreenStateReceiverWorkaround() {
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
getContext().getApplicationContext().registerReceiver(this, filter);
}
void shutDown() {
getContext().getApplicationContext().unregisterReceiver(this);
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)
&& mCompositorSurfaceManager != null && !mIsInVr
&& mNativeCompositorView != 0) {
mCompositorSurfaceManager.shutDown();
createCompositorSurfaceManager();
}
}
}
private ScreenStateReceiverWorkaround mScreenStateReceiver;
/**
* Creates a {@link CompositorView}. This can be called only after the native library is
* properly loaded.
* @param c The Context to create this {@link CompositorView} in.
* @param host The renderer host owning this view.
*/
public CompositorView(Context c, LayoutRenderHost host) {
super(c);
mRenderHost = host;
initializeIfOnUiThread();
}
/**
* The {@link CompositorSurfaceManagerImpl} constructor creates a handler (inside the
* SurfaceView constructor on android N and before) and thus can only be called on the UI
* thread. If the layout is inflated on a background thread this fails, thus we only initialize
* the {@link CompositorSurfaceManager} in the constructor if on the UI thread (or we are
* running on android O+), otherwise it is initialized inside the first call to
* {@link #setRootView}.
*/
private void initializeIfOnUiThread() {
if (!ThreadUtils.runningOnUiThread() && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
mCompositorSurfaceManager = new CompositorSurfaceManagerImpl(this, this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
mScreenStateReceiver = new ScreenStateReceiverWorkaround();
}
// Cover the black surface before it has valid content. Set this placeholder view to
// visible, but don't yet make SurfaceView visible, in order to delay
// surfaceCreate/surfaceChanged calls until the native library is loaded.
setBackgroundColor(ColorUtils.getPrimaryBackgroundColor(getResources(), false));
super.setVisibility(View.VISIBLE);
// Request the opaque surface. We might need the translucent one, but
// we don't know yet. We'll switch back later if we discover that
// we're on a low memory device that always uses translucent.
mCompositorSurfaceManager.requestSurface(PixelFormat.OPAQUE);
}
/**
* @param view The root view of the hierarchy.
*/
public void setRootView(View view) {
// If layout was inflated on a background thread, then the CompositorView should be
// initialized now.
if (mCompositorSurfaceManager == null) {
ThreadUtils.assertOnUiThread();
initializeIfOnUiThread();
}
mRootView = view;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mRootView != null) {
mRootView.getWindowVisibleDisplayFrame(mCacheAppRect);
// Check whether the top position of the window has changed as we always must
// resize in that case to the specified height spec. On certain versions of
// Android when you change the top position (i.e. by leaving fullscreen) and
// do not shrink the SurfaceView, it will appear to be pinned to the top of
// the screen under the notification bar and all touch offsets will be wrong
// as well as a gap will appear at the bottom of the screen.
int windowTop = mCacheAppRect.top;
boolean topChanged = windowTop != mPreviousWindowTop;
mPreviousWindowTop = windowTop;
Activity activity = mWindowAndroid != null ? mWindowAndroid.getActivity().get() : null;
boolean isMultiWindow = MultiWindowUtils.getInstance().isLegacyMultiWindow(activity)
|| MultiWindowUtils.getInstance().isInMultiWindowMode(activity);
// If the measured width is the same as the allowed width (i.e. the orientation has
// not changed) and multi-window mode is off, use the largest measured height seen thus
// far. This will prevent surface resizes as a result of showing the keyboard.
if (!topChanged && !isMultiWindow
&& getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() > MeasureSpec.getSize(heightMeasureSpec)) {
heightMeasureSpec =
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY);
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mPreviousWindowTop = -1;
}
/**
* WindowAndroid.SelectionHandlesObserver impl.
*/
@Override
public void onSelectionHandlesStateChanged(boolean active) {
// If the feature is disabled or we're in Vr mode, we are already rendering directly to the
// SurfaceView.
if (!mIsSurfaceControlEnabled || mIsInVr) return;
if (mSelectionHandlesActive == active) return;
mSelectionHandlesActive = active;
// If selection handles are active, we need to switch to render to the SurfaceView so the
// Magnifier widget can copy from its buffers to show in the UI.
boolean switchToSurfaceView = mSelectionHandlesActive;
// Cache the backbuffer for the currently visible Surface in the GPU process, if we're going
// from SurfaceControl to SurfaceView. We need to preserve it until the new SurfaceView has
// content.
// When the CompositorImpl is switched to a new SurfaceView the Surface associated with the
// current SurfaceView is disconnected from GL and its EGLSurface is destroyed. But the
// buffers associated with that Surface are preserved (in android's internal BufferQueue)
// when rendering directly to the SurfaceView. So caching the SurfaceView is enough to
// preserve the old content.
// But with SurfaceControl, switching to a new SurfaceView evicts that content when
// destroying the GLSurface in the GPU process. So we need to explicitly preserve them in
// the GPU process during this transition.
if (switchToSurfaceView) {
CompositorViewJni.get().cacheBackBufferForCurrentSurface(
mNativeCompositorView, CompositorView.this);
}
// Trigger the creation of a new SurfaceView. CompositorSurfaceManager will handle caching
// the old one during the transition.
mCompositorSurfaceManager.requestSurface(getSurfacePixelFormat());
}
/**
* @return The ResourceManager.
*/
public ResourceManager getResourceManager() {
return mResourceManager;
}
/**
* @return The active {@link SurfaceView} of this compositor.
*/
public View getActiveSurfaceView() {
return mCompositorSurfaceManager.getActiveSurfaceView();
}
/**
* Should be called for cleanup when the CompositorView instance is no longer used.
*/
public void shutDown() {
mCompositorSurfaceManager.shutDown();
if (mScreenStateReceiver != null) {
mScreenStateReceiver.shutDown();
}
if (mNativeCompositorView != 0) {
CompositorViewJni.get().destroy(mNativeCompositorView, CompositorView.this);
}
mNativeCompositorView = 0;
}
/**
* Initializes the {@link CompositorView}'s native parts (e.g. the rendering parts).
* @param lowMemDevice If this is a low memory device.
* @param windowAndroid A {@link WindowAndroid} instance.
* @param layerTitleCache A {@link LayerTitleCache} instance.
* @param tabContentManager A {@link TabContentManager} instance.
*/
public void initNativeCompositor(boolean lowMemDevice, WindowAndroid windowAndroid,
LayerTitleCache layerTitleCache, TabContentManager tabContentManager) {
// https://crbug.com/802160. We can't call setWindowAndroid here because updating the window
// visibility here breaks exiting Reader Mode somehow.
mWindowAndroid = windowAndroid;
mWindowAndroid.addSelectionHandlesObserver(this);
mLayerTitleCache = layerTitleCache;
mTabContentManager = tabContentManager;
mNativeCompositorView = CompositorViewJni.get().init(CompositorView.this, lowMemDevice,
windowAndroid, layerTitleCache, tabContentManager);
// compositor_impl_android.cc will use 565 EGL surfaces if and only if we're using a low
// memory device, and no alpha channel is desired. Otherwise, it will use 8888. Since
// SurfaceFlinger doesn't need the eOpaque flag to optimize out alpha blending during
// composition if the buffer has no alpha channel, we can avoid using the extra background
// surface (and the memory it requires) in the low memory case. The output buffer will
// either have an alpha channel or not, depending on whether the compositor needs it. We
// can keep the surface translucent all the times without worrying about the impact on power
// usage during SurfaceFlinger composition. We might also want to set |mAlwaysTranslucent|
// on non-low memory devices, if we are running on hardware that implements efficient alpha
// blending.
mAlwaysTranslucent = lowMemDevice;
// In case we changed the requested format due to |lowMemDevice|,
// re-request the surface now.
mCompositorSurfaceManager.requestSurface(getSurfacePixelFormat());
setVisibility(View.VISIBLE);
// Grab the Resource Manager
mResourceManager = CompositorViewJni.get().getResourceManager(
mNativeCompositorView, CompositorView.this);
// Redraw in case there are callbacks pending |mDrawingFinishedCallbacks|.
CompositorViewJni.get().setNeedsComposite(mNativeCompositorView, CompositorView.this);
}
private void setWindowAndroid(WindowAndroid windowAndroid) {
assert mWindowAndroid != null;
mWindowAndroid.removeSelectionHandlesObserver(this);
mWindowAndroid = windowAndroid;
mWindowAndroid.addSelectionHandlesObserver(this);
onWindowVisibilityChangedInternal(getWindowVisibility());
}
@Override
public boolean onTouchEvent(MotionEvent e) {
return super.onTouchEvent(e);
}
/**
* Enables/disables overlay video mode. Affects alpha blending on this view.
* @param enabled Whether to enter or leave overlay video mode.
*/
public void setOverlayVideoMode(boolean enabled) {
CompositorViewJni.get().setOverlayVideoMode(
mNativeCompositorView, CompositorView.this, enabled);
mOverlayVideoEnabled = enabled;
// Request the new surface, even if it's the same as the old one. We'll get a synthetic
// destroy / create / changed callback in that case, possibly before this returns.
mCompositorSurfaceManager.requestSurface(getSurfacePixelFormat());
// Note that we don't know if we'll get a surfaceCreated / surfaceDestoyed for this surface.
// We do know that if we do get one, then it will be for the surface that we just requested.
}
private int getSurfacePixelFormat() {
if (mOverlayVideoEnabled || mAlwaysTranslucent) {
return PixelFormat.TRANSLUCENT;
}
if (mIsSurfaceControlEnabled) {
// In SurfaceControl mode, we can always use a translucent format since there is no
// buffer associated to the SurfaceView, and the buffers passed to the SurfaceControl
// API are correctly tagged with whether blending is needed in the GPU process itself.
// But if we need to temporarily render directly to a SurfaceView, then opaque format is
// needed.
// The transition between the 2 also relies on them using different Surfaces (through
// different format requests).
return canUseSurfaceControl() ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
}
return PixelFormat.OPAQUE;
}
private boolean canUseSurfaceControl() {
return !mIsInVr && !mSelectionHandlesActive;
}
@Override
public void surfaceRedrawNeededAsync(Runnable drawingFinished) {
if (mDrawingFinishedCallbacks == null) {
mDrawingFinishedCallbacks = new ArrayList<>();
}
mDrawingFinishedCallbacks.add(drawingFinished);
if (mNativeCompositorView != 0) {
CompositorViewJni.get().setNeedsComposite(mNativeCompositorView, CompositorView.this);
}
}
@Override
public void surfaceChanged(Surface surface, int format, int width, int height) {
if (mNativeCompositorView == 0) return;
CompositorViewJni.get().surfaceChanged(mNativeCompositorView, CompositorView.this, format,
width, height, canUseSurfaceControl(), surface);
mRenderHost.onSurfaceResized(width, height);
}
@Override
public void surfaceCreated(Surface surface) {
if (mNativeCompositorView == 0) return;
CompositorViewJni.get().surfaceCreated(mNativeCompositorView, CompositorView.this);
mFramesUntilHideBackground = 2;
mRenderHost.onSurfaceCreated();
}
@Override
public void surfaceDestroyed(Surface surface) {
if (mNativeCompositorView == 0) return;
CompositorViewJni.get().surfaceDestroyed(mNativeCompositorView, CompositorView.this);
}
@Override
public void unownedSurfaceDestroyed() {
CompositorViewJni.get().evictCachedBackBuffer(mNativeCompositorView, CompositorView.this);
}
@Override
public void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
onWindowVisibilityChangedInternal(visibility);
}
private void onWindowVisibilityChangedInternal(int visibility) {
if (mWindowAndroid == null) return;
if (visibility == View.GONE) {
mWindowAndroid.onVisibilityChanged(false);
} else if (visibility == View.VISIBLE) {
mWindowAndroid.onVisibilityChanged(true);
}
IntentWithGesturesHandler.getInstance().clear();
}
void onPhysicalBackingSizeChanged(WebContents webContents, int width, int height) {
CompositorViewJni.get().onPhysicalBackingSizeChanged(
mNativeCompositorView, CompositorView.this, webContents, width, height);
}
@CalledByNative
private void onCompositorLayout() {
mRenderHost.onCompositorLayout();
}
@CalledByNative
private void recreateSurface() {
mCompositorSurfaceManager.recreateSurface();
}
/**
* Request compositor view to render a frame.
*/
public void requestRender() {
if (mNativeCompositorView != 0) {
CompositorViewJni.get().setNeedsComposite(mNativeCompositorView, CompositorView.this);
}
}
@CalledByNative
private void didSwapFrame(int pendingFrameCount) {
mRenderHost.didSwapFrame(pendingFrameCount);
}
@CalledByNative
private void didSwapBuffers(boolean swappedCurrentSize) {
// If we're in the middle of a surface swap, then see if we've received a new frame yet for
// the new surface before hiding the outgoing surface.
if (mFramesUntilHideBackground > 1) {
// We need at least one more frame before we hide the outgoing surface. Make sure that
// there will be a frame.
mFramesUntilHideBackground--;
requestRender();
} else if (mFramesUntilHideBackground == 1) {
// We can hide the outgoing surface, since the incoming one has a frame. It's okay if
// we've don't have an unowned surface.
mFramesUntilHideBackground = 0;
// Evict the SurfaceView and the associated backbuffer now that the new SurfaceView is
// ready.
CompositorViewJni.get().evictCachedBackBuffer(
mNativeCompositorView, CompositorView.this);
mCompositorSurfaceManager.doneWithUnownedSurface();
}
// Only run our draw finished callbacks if the frame we swapped was the correct size.
if (swappedCurrentSize) {
runDrawFinishedCallbacks();
}
}
@CalledByNative
private void notifyWillUseSurfaceControl() {
mIsSurfaceControlEnabled = true;
}
/**
* Converts the layout into compositor layers. This is to be called on every frame the layout
* is changing.
* @param provider Provides the layout to be rendered.
* @param forRotation Whether or not this is a special draw during a rotation.
*/
public void finalizeLayers(final LayoutProvider provider, boolean forRotation) {
TraceEvent.begin("CompositorView:finalizeLayers");
Layout layout = provider.getActiveLayout();
if (layout == null || mNativeCompositorView == 0) {
TraceEvent.end("CompositorView:finalizeLayers");
return;
}
if (!mPreloadedResources) {
// Attempt to prefetch any necessary resources
mResourceManager.preloadResources(AndroidResourceType.STATIC,
StaticResourcePreloads.getSynchronousResources(getContext()),
StaticResourcePreloads.getAsynchronousResources(getContext()));
mPreloadedResources = true;
}
// IMPORTANT: Do not do anything that impacts the compositor layer tree before this line.
// If you do, you could inadvertently trigger follow up renders. For further information
// see dtrainor@, tedchoc@, or klobag@.
CompositorViewJni.get().setLayoutBounds(mNativeCompositorView, CompositorView.this);
SceneLayer sceneLayer =
provider.getUpdatedActiveSceneLayer(mLayerTitleCache, mTabContentManager,
mResourceManager, provider.getFullscreenManager());
CompositorViewJni.get().setSceneLayer(
mNativeCompositorView, CompositorView.this, sceneLayer);
TabModelImpl.flushActualTabSwitchLatencyMetric();
CompositorViewJni.get().finalizeLayers(mNativeCompositorView, CompositorView.this);
TraceEvent.end("CompositorView:finalizeLayers");
}
@Override
public void setWillNotDraw(boolean willNotDraw) {
mCompositorSurfaceManager.setWillNotDraw(willNotDraw);
}
@Override
public void setBackgroundDrawable(Drawable background) {
// We override setBackgroundDrawable since that's the common entry point from all the
// setBackground* calls in View. We still call to setBackground on the SurfaceView because
// SetBackgroundDrawable is deprecated, and the semantics are the same I think.
super.setBackgroundDrawable(background);
mCompositorSurfaceManager.setBackgroundDrawable(background);
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
// Also set the visibility on any child SurfaceViews, since that hides the surface as
// well. Otherwise, the surface is kept, which can interfere with VR.
mCompositorSurfaceManager.setVisibility(visibility);
// Clear out any outstanding callbacks that won't run if set to invisible.
if (visibility == View.INVISIBLE) {
runDrawFinishedCallbacks();
}
}
private void runDrawFinishedCallbacks() {
List<Runnable> runnables = mDrawingFinishedCallbacks;
mDrawingFinishedCallbacks = null;
if (runnables == null) return;
for (Runnable r : runnables) {
r.run();
}
}
/**
* Replaces the surface manager and swaps the window the compositor is attached to as tab
* reparenting doesn't handle replacing of the window the compositor uses.
*
* @param vrCompositorSurfaceManager The surface manager for VR.
* @param window The VR WindowAndroid to switch to.
*/
public void replaceSurfaceManagerForVr(
CompositorSurfaceManager vrCompositorSurfaceManager, WindowAndroid window) {
mIsInVr = true;
mCompositorSurfaceManager.shutDown();
CompositorViewJni.get().setCompositorWindow(
mNativeCompositorView, CompositorView.this, window);
mCompositorSurfaceManager = vrCompositorSurfaceManager;
mCompositorSurfaceManager.requestSurface(PixelFormat.OPAQUE);
CompositorViewJni.get().setNeedsComposite(mNativeCompositorView, CompositorView.this);
setWindowAndroid(window);
}
/**
* Restores the non-VR surface manager and passes back control over the surface(s) to it.
* Also restores the non-VR WindowAndroid.
*
* @param windowToRestore The non-VR WindowAndroid to restore.
*/
public void onExitVr(WindowAndroid windowToRestore) {
mIsInVr = false;
if (mNativeCompositorView == 0) return;
setWindowAndroid(windowToRestore);
mCompositorSurfaceManager.shutDown();
CompositorViewJni.get().setCompositorWindow(
mNativeCompositorView, CompositorView.this, mWindowAndroid);
createCompositorSurfaceManager();
}
private void createCompositorSurfaceManager() {
mCompositorSurfaceManager = new CompositorSurfaceManagerImpl(this, this);
mCompositorSurfaceManager.requestSurface(getSurfacePixelFormat());
CompositorViewJni.get().setNeedsComposite(mNativeCompositorView, CompositorView.this);
mCompositorSurfaceManager.setVisibility(getVisibility());
}
@NativeMethods
interface Natives {
long init(CompositorView caller, boolean lowMemDevice, WindowAndroid windowAndroid,
LayerTitleCache layerTitleCache, TabContentManager tabContentManager);
void destroy(long nativeCompositorView, CompositorView caller);
ResourceManager getResourceManager(long nativeCompositorView, CompositorView caller);
void surfaceCreated(long nativeCompositorView, CompositorView caller);
void surfaceDestroyed(long nativeCompositorView, CompositorView caller);
void surfaceChanged(long nativeCompositorView, CompositorView caller, int format, int width,
int height, boolean backedBySurfaceTexture, Surface surface);
void onPhysicalBackingSizeChanged(long nativeCompositorView, CompositorView caller,
WebContents webContents, int width, int height);
void finalizeLayers(long nativeCompositorView, CompositorView caller);
void setNeedsComposite(long nativeCompositorView, CompositorView caller);
void setLayoutBounds(long nativeCompositorView, CompositorView caller);
void setOverlayVideoMode(long nativeCompositorView, CompositorView caller, boolean enabled);
void setSceneLayer(long nativeCompositorView, CompositorView caller, SceneLayer sceneLayer);
void setCompositorWindow(
long nativeCompositorView, CompositorView caller, WindowAndroid window);
void cacheBackBufferForCurrentSurface(long nativeCompositorView, CompositorView caller);
void evictCachedBackBuffer(long nativeCompositorView, CompositorView caller);
}
}