chromium/chrome/android/java/src/org/chromium/chrome/browser/compositor/CompositorView.java

// Copyright 2016 The Chromium Authors
// 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.Surface;
import android.view.View;
import android.widget.FrameLayout;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
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.resources.StaticResourcePreloads;
import org.chromium.chrome.browser.compositor.resources.SystemResourcePreloads;
import org.chromium.chrome.browser.externalnav.IntentWithRequestMetadataHandler;
import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.content_public.browser.SelectionPopupController;
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;

/**
 * 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 {
    // Cache objects that should not be created every frame
    private final Rect mCacheAppRect = new Rect();

    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 WindowAndroid mWindowAndroid;
    private TabContentManager mTabContentManager;

    private View mRootView;
    private boolean mPreloadedResources;
    private Runnable mDrawingFinishedCallback;

    // True while in a WebXR "immersive-ar" session with DOM Overlay enabled. This disables
    // SurfaceControl while active.
    private boolean mIsInXr;

    private boolean mIsSurfaceControlEnabled;
    private boolean mSelectionHandlesActive;

    private boolean mRenderHostNeedsDidSwapBuffersCallback;

    private boolean mHaveSwappedFramesSinceSurfaceCreated;

    // 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 {
        // True indicates we should destroy and recreate the surface manager.
        private boolean mNeedsReset;

        ScreenStateReceiverWorkaround() {
            IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
            ContextUtils.registerProtectedBroadcastReceiver(
                    getContext().getApplicationContext(), 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
                    && !mIsInXr
                    && mNativeCompositorView != 0) {
                mNeedsReset = true;
            }
        }

        public void maybeResetCompositorSurfaceManager() {
            if (!mNeedsReset) return;
            mNeedsReset = false;

            if (mCompositorSurfaceManager != null) {
                mCompositorSurfaceManager.shutDown();
                createCompositorSurfaceManager();
            }
        }

        public void clearNeedsReset() {
            mNeedsReset = false;
        }
    }

    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(ChromeColors.getPrimaryBackgroundColor(getContext(), 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().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
                || mIsInXr
                || !SelectionPopupController.needsSurfaceViewDuringSelection()) {
            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 tabContentManager    A {@link TabContentManager} instance.
     */
    public void initNativeCompositor(
            boolean lowMemDevice,
            WindowAndroid windowAndroid,
            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);

        mTabContentManager = tabContentManager;

        mNativeCompositorView =
                CompositorViewJni.get()
                        .init(CompositorView.this, lowMemDevice, windowAndroid, 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 |mDrawingFinishedCallback|.
        CompositorViewJni.get().setNeedsComposite(mNativeCompositorView, CompositorView.this);
    }

    private void setWindowAndroid(WindowAndroid windowAndroid) {
        assert mWindowAndroid != null;
        mWindowAndroid.removeSelectionHandlesObserver(this);

        mWindowAndroid = windowAndroid;
        mWindowAndroid.addSelectionHandlesObserver(this);
        onWindowVisibilityChangedInternal(getWindowVisibility());
    }

    /**
     * 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.
    }

    /**
     * Enables/disables immersive AR overlay mode, a variant of overlay video mode.
     * @param enabled Whether to enter or leave overlay immersive ar mode.
     */
    public void setOverlayImmersiveArMode(boolean enabled, boolean domSurfaceNeedsConfiguring) {
        // Disable SurfaceControl for the duration of the session. This works around a black
        // screen after activating the screen keyboard (IME), see https://crbug.com/1166248.
        mIsInXr = enabled;

        if (domSurfaceNeedsConfiguring) {
            setOverlayVideoMode(enabled);
        }

        CompositorViewJni.get()
                .setOverlayImmersiveArMode(mNativeCompositorView, CompositorView.this, enabled);
        // Entering or exiting AR mode can leave SurfaceControl in a confused state, especially if
        // the screen keyboard (IME) was activated, see https://crbug.com/1166248 and
        // https://crbug.com/1169822. Reset the surface manager at session start and exit to work
        // around this.
        mCompositorSurfaceManager.shutDown();
        createCompositorSurfaceManager();
    }

    /**
     * Enables/disables immersive VR overlay mode, a variant of overlay video mode.
     * @param enabled Whether to enter or leave overlay immersive vr mode.
     */
    public void setOverlayVrMode(boolean enabled) {
        mIsInXr = enabled;

        // We're essentially entering OverlayVideo mode because we're going to be rendering to an
        // overlay, but we don't actually need a new composite or to adjust the alpha blend.
        mCompositorSurfaceManager.requestSurface(getSurfacePixelFormat());
    }

    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 !mIsInXr && !mSelectionHandlesActive;
    }

    @Override
    public void surfaceRedrawNeededAsync(Runnable drawingFinished) {
        // Do not hold onto more than one draw callback, to prevent deadlock.
        // See https://crbug.com/1174273 and https://crbug.com/1223299 for more details.
        //
        // `drawingFinished` can, and often will, be run before this returns, since we cannot hold
        // onto more than one (android) callback without risking a deadlock in the framework.
        //
        // DO NOT ADD any more callbacks from inside chrome!  This is intended to implement
        // (indirectly) the android SurfaceHolder callback.  It is not intended as a general-purpose
        // mechanism for chromium to wait for a swap to occur.  In particular, we have workarounds
        // for android framework behavior here, that would be unexpected to other callers.  Also,
        // these behaviors can change without notice as new android versions show up.
        //
        // If you want to find out about a swap, please add a separate mechanism to this class to do
        // so, with more predictable semantics.
        runDrawFinishedCallback();
        mDrawingFinishedCallback = drawingFinished;
        if (mHaveSwappedFramesSinceSurfaceCreated) {
            // Don't hold onto the draw callback, since it can deadlock with ViewRootImpl performing
            // traversals in some cases.  Only wait if the surface is newly created.  Android allows
            // us to run the callback before returning; the default implementation of this method
            // does exactly that.  While there are a few calls into this method that are not from
            // the android framework, these are currently okay with this behavior.  Please do not
            // add any more, as described above.
            runDrawFinishedCallback();
        }
        updateNeedsDidSwapBuffersCallback();
        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;

        // if a requested surface is created successfully, CompositorSurfaceManager doesn't need to
        // be reset.
        if (mScreenStateReceiver != null) mScreenStateReceiver.clearNeedsReset();
        mFramesUntilHideBackground = 2;
        mHaveSwappedFramesSinceSurfaceCreated = false;
        updateNeedsDidSwapBuffersCallback();
        CompositorViewJni.get().surfaceCreated(mNativeCompositorView, CompositorView.this);
    }

    @Override
    public void surfaceDestroyed(Surface surface, boolean androidSurfaceDestroyed) {
        if (mNativeCompositorView == 0) return;

        // When we switch from Chrome to other app we can't detach child surface controls because it
        // leads to a visible hole: b/157439199. To avoid this we don't detach surfaces if the
        // surface is going to be destroyed, they will be detached and freed by OS.
        if (androidSurfaceDestroyed) {
            CompositorViewJni.get()
                    .preserveChildSurfaceControls(mNativeCompositorView, CompositorView.this);
        }

        CompositorViewJni.get().surfaceDestroyed(mNativeCompositorView, CompositorView.this);

        if (mScreenStateReceiver != null) {
            mScreenStateReceiver.maybeResetCompositorSurfaceManager();
        }
    }

    @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);
        }
        IntentWithRequestMetadataHandler.getInstance().clear();
    }

    void onPhysicalBackingSizeChanged(WebContents webContents, int width, int height) {
        CompositorViewJni.get()
                .onPhysicalBackingSizeChanged(
                        mNativeCompositorView, CompositorView.this, webContents, width, height);
    }

    void onControlsResizeViewChanged(WebContents webContents, boolean controlsResizeView) {
        CompositorViewJni.get()
                .onControlsResizeViewChanged(
                        mNativeCompositorView,
                        CompositorView.this,
                        webContents,
                        controlsResizeView);
    }

    /**
     * Notifies geometrychange event to JS.
     * @param webContents Active WebContent for which this event needs to be fired.
     * @param x When the keyboard is shown, it has the left position of the app's rect, else, 0.
     * @param y When the keyboard is shown, it has the top position of the app's rect, else, 0.
     * @param width  When the keyboard is shown, it has the width of the view, else, 0.
     * @param height When the keyboard is shown, it has the height of the keyboard, else, 0.
     */
    void notifyVirtualKeyboardOverlayRect(
            WebContents webContents, int x, int y, int width, int height) {
        CompositorViewJni.get()
                .notifyVirtualKeyboardOverlayRect(
                        mNativeCompositorView,
                        CompositorView.this,
                        webContents,
                        x,
                        y,
                        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);
        }
    }

    /**
     * Called by LayoutRenderHost to inform whether it needs `didSwapBuffers` calls.
     * Note the implementation is asynchronous so it may miss already pending calls when enabled
     * and can have a few trailing calls when disabled.
     */
    public void setRenderHostNeedsDidSwapBuffersCallback(boolean enable) {
        if (mRenderHostNeedsDidSwapBuffersCallback == enable) return;
        mRenderHostNeedsDidSwapBuffersCallback = enable;
        updateNeedsDidSwapBuffersCallback();
    }

    // Should be called any time the inputs used to compute `needsSwapCallback` change.
    private void updateNeedsDidSwapBuffersCallback() {
        if (mNativeCompositorView == 0) return;
        boolean needsSwapCallback =
                mRenderHostNeedsDidSwapBuffersCallback
                        || mFramesUntilHideBackground > 0
                        || mDrawingFinishedCallback != null;
        CompositorViewJni.get()
                .setDidSwapBuffersCallbackEnabled(mNativeCompositorView, needsSwapCallback);
    }

    @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();
        }

        // We must be careful about deferring draw callbacks, else Android can get into a bad state.
        // However, we can still reduce some types of visible jank by deferring these carefully.
        //
        // In particular, a callback that is sent to us as part of WindowManager's "first draw" will
        // defer putting buffers on the screen.  So, if we wait until we swap a correctly-sized
        // buffer, the user won't see the previous ones.  That's generally an improvement over the
        // clipping / guttering / stretching that would happen with the incorrectly-sized buffers.
        //
        // At other times, holding onto this draw callback doesn't change what's on the screen;
        // SurfaceFlinger will still show each buffer.  What happens instead is that only WM
        // transactions are deferred, like in the previous case, but it doesn't do us any good.
        //
        // Further, holding onto callbacks can prevent us from getting a surfaceCreated if the WM's
        // transaction is blocked.  This can lead to problems when chrome is re-launched.
        //
        // Our strategy is as follows:
        //
        //  - Defer at most one draw callback.  If we get a second, immediately call back the first.
        //  - If we are holding a draw callback when our surface is destroyed, then call it back.
        //  - Otherwise, defer the callback until we swap the right size buffer.
        //
        // See https://crbug.com/1174273 and https://crbug.com/1223299 for more details.
        if (swappedCurrentSize) {
            runDrawFinishedCallback();
        }
        mHaveSwappedFramesSinceSurfaceCreated = true;

        mRenderHost.didSwapBuffers(swappedCurrentSize, mFramesUntilHideBackground);

        updateNeedsDidSwapBuffersCallback();
    }

    @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.
     */
    public void finalizeLayers(final LayoutProvider provider) {
        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()));
            mResourceManager.preloadResources(
                    AndroidResourceType.SYSTEM,
                    SystemResourcePreloads.getSynchronousResources(),
                    SystemResourcePreloads.getAsynchronousResources());
            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(
                        mTabContentManager, mResourceManager, provider.getBrowserControlsManager());

        CompositorViewJni.get()
                .setSceneLayer(mNativeCompositorView, CompositorView.this, sceneLayer);

        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) {
            runDrawFinishedCallback();
        }
    }

    private void runDrawFinishedCallback() {
        Runnable runnable = mDrawingFinishedCallback;
        mDrawingFinishedCallback = null;
        if (runnable != null) {
            runnable.run();
        }
        updateNeedsDidSwapBuffersCallback();
    }

    private void createCompositorSurfaceManager() {
        mCompositorSurfaceManager = new CompositorSurfaceManagerImpl(this, this);
        mCompositorSurfaceManager.requestSurface(getSurfacePixelFormat());
        CompositorViewJni.get().setNeedsComposite(mNativeCompositorView, CompositorView.this);
        mCompositorSurfaceManager.setVisibility(getVisibility());
    }

    /**
     * Notifies the native compositor that a tab change has occurred. This
     * should be called when changing to a valid tab.
     */
    public void onTabChanged() {
        CompositorViewJni.get().onTabChanged(mNativeCompositorView, CompositorView.this);
    }

    @NativeMethods
    interface Natives {
        long init(
                CompositorView caller,
                boolean lowMemDevice,
                WindowAndroid windowAndroid,
                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 onControlsResizeViewChanged(
                long nativeCompositorView,
                CompositorView caller,
                WebContents webContents,
                boolean controlsResizeView);

        void notifyVirtualKeyboardOverlayRect(
                long nativeCompositorView,
                CompositorView caller,
                WebContents webContents,
                int x,
                int y,
                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 setOverlayImmersiveArMode(
                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);

        void onTabChanged(long nativeCompositorView, CompositorView caller);

        void preserveChildSurfaceControls(long nativeCompositorView, CompositorView caller);

        void setDidSwapBuffersCallbackEnabled(long nativeCompositorView, boolean enabled);
    }
}