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

// Copyright 2017 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.content.Context;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import org.chromium.base.Log;

/**
 * Manage multiple SurfaceViews for the compositor, so that transitions between
 * surfaces with and without an alpha channel can be visually smooth.
 *
 * This class allows a client to request a 'translucent' or 'opaque' surface, and we will signal via
 * SurfaceHolder.Callback when it's ready.  We guarantee that the client will receive surfaceCreated
 * / surfaceDestroyed only for a surface that represents the most recently requested PixelFormat.
 *
 * Internally, we maintain two SurfaceViews, since calling setFormat() to change the PixelFormat
 * results in a visual glitch as the surface is torn down.  crbug.com/679902
 *
 * The client has the responsibility to call doneWithUnownedSurface() at some point between when we
 * call back its surfaceCreated, when it is safe for us to hide the SurfaceView with the wrong
 * format.  It is okay if it requests multiple surfaces without calling doneWithUnownedSurface.
 *
 * If the client requests the same format more than once in a row, it will still receive destroyed /
 * created / changed messages for it, even though we won't tear it down.
 *
 * The full design doc is at https://goo.gl/aAmQzR .
 */
class CompositorSurfaceManagerImpl implements SurfaceHolder.Callback2, CompositorSurfaceManager {
    private static class SurfaceState {
        public SurfaceView surfaceView;

        // Do we expect a surfaceCreated?
        public boolean createPending;

        // Have we started destroying |surfaceView|, but haven't received surfaceDestroyed yet?
        public boolean destroyPending;

        // Last PixelFormat that we received, or UNKNOWN if we don't know / don't want to cache it.
        public int format;

        // Last known width, height for thsi surface.
        public int width;
        public int height;

        // Parent ViewGroup, or null.
        private ViewGroup mParent;

        public SurfaceState(Context context, int format, SurfaceHolder.Callback2 callback) {
            surfaceView = new SurfaceView(context);

            // Media overlays require a translucent surface for the compositor which should be
            // placed above them, so we mark it setZOrderMediaOverlay. But its not not needed for
            // the opaque one. In fact setting this for the opaque one causes glitches when
            // transitioning to the opaque SurfaceView. This is because if the opaque SurfaceView is
            // stacked on top of the translucent one, the framework doesn't draw any content
            // underneath it and shows its background instead when it has no content during the
            // transition.
            if (format == PixelFormat.TRANSLUCENT) surfaceView.setZOrderMediaOverlay(true);
            surfaceView.setVisibility(View.INVISIBLE);
            surfaceHolder().setFormat(format);
            surfaceHolder().addCallback(callback);

            // Set this to UNKNOWN until we get a format back.
            this.format = PixelFormat.UNKNOWN;
        }

        public SurfaceHolder surfaceHolder() {
            return surfaceView.getHolder();
        }

        public boolean isValid() {
            Surface surface = surfaceHolder().getSurface();
            if (surface == null) return false;
            return surface.isValid();
        }

        // Attach to |parent|, such that isAttached() will be correct immediately.  Otherwise,
        // attaching and detaching can cause surfaceCreated / surfaceDestroyed callbacks without
        // View.hasParent being up to date.
        public void attachTo(ViewGroup parent, FrameLayout.LayoutParams lp) {
            mParent = parent;
            mParent.addView(surfaceView, lp);
        }

        public void detachFromParent() {
            Log.i(TAG, "SurfaceState : detach from parent : %d", format);
            final ViewGroup parent = mParent;
            // Since removeView can call surfaceDestroyed before returning, be sure that isAttached
            // will return false.
            mParent = null;
            parent.removeView(surfaceView);
        }

        public boolean isAttached() {
            return mParent != null;
        }
    }

    private static final String TAG = "CompositorSurfaceMgr";

    // SurfaceView with a translucent PixelFormat.
    private final SurfaceState mTranslucent;

    // SurfaceView with an opaque PixelFormat.
    private final SurfaceState mOpaque;

    // Surface that we last gave to the client with surfaceCreated.  Cleared when we call
    // surfaceDestroyed on |mClient|.  Note that it's not necessary that Android has notified us
    // the surface has been destroyed; we deliberately keep it around until the client tells us that
    // it's okay to get rid of it.
    private SurfaceState mOwnedByClient;

    // Surface that was most recently requested by the client.
    private SurfaceState mRequestedByClient;

    // Client that we notify about surface change events.
    private SurfaceManagerCallbackTarget mClient;

    // View to which we'll attach the SurfaceView.
    private final ViewGroup mParentView;

    public CompositorSurfaceManagerImpl(ViewGroup parentView, SurfaceManagerCallbackTarget client) {
        mParentView = parentView;
        mClient = client;

        mTranslucent = new SurfaceState(parentView.getContext(), PixelFormat.TRANSLUCENT, this);
        mOpaque = new SurfaceState(mParentView.getContext(), PixelFormat.OPAQUE, this);
    }

    /** Turn off everything. */
    @Override
    public void shutDown() {
        mRequestedByClient = null;
        detachSurfaceNow(mOpaque);
        detachSurfaceNow(mTranslucent);

        mTranslucent.surfaceHolder().removeCallback(this);
        mOpaque.surfaceHolder().removeCallback(this);
    }

    @Override
    public int getFormatOfOwnedSurface() {
        if (mOwnedByClient == null) return PixelFormat.UNKNOWN;
        return mOwnedByClient.format;
    }

    @Override
    public void requestSurface(int format) {
        Log.i(TAG, "Transitioning to surface with format: %d", format);
        mRequestedByClient = (format == PixelFormat.TRANSLUCENT) ? mTranslucent : mOpaque;

        // If destruction is pending, then we must wait for it to complete.  When we're notified
        // that it is destroyed, we'll re-start construction if the client still wants this surface.
        // Note that we could send a surfaceDestroyed for the owned surface, if there is one, but we
        // defer it until later so that the client can still use it until the new one is ready.
        if (mRequestedByClient.destroyPending) return;

        // The requested surface isn't being torn down.

        // If the surface isn't attached yet, then attach it.  Otherwise, we're still waiting for
        // the surface to be created, or we've already received surfaceCreated for it.
        if (!mRequestedByClient.isAttached()) {
            attachSurfaceNow(mRequestedByClient);
            assert mRequestedByClient.isAttached();
            return;
        }

        // Surface is not pending destroy, and is attached.  See if we need to send any synthetic
        // callbacks to the client.  If we're expecting a callback from Android, then we'll handle
        // it when it arrives instead.
        if (mRequestedByClient.createPending) return;

        // Surface is attached and no create is pending.  Send a synthetic create.  Note that, if
        // Android destroyed the surface itself, then we'd have set |createPending| at that point.
        // We don't check |isValid| here, since, technically, there could be a destroy in flight
        // from Android.  It's okay; we'll just notify the client at that point.  Either way, we
        // must tell the client that it now owns the surface.

        // Send a notification about any owned surface.  Note that this can be |mRequestedByClient|
        // which is fine.  We'll send destroy / create for it.  Also note that we don't actually
        // start tear-down of the owned surface; the client notifies us via doneWithUnownedSurface
        // when it is safe to do that.
        disownClientSurface(mOwnedByClient, false);

        // `disownClientSurface` may recursively shutdown.
        if (mRequestedByClient == null) return;

        // The client now owns |mRequestedByClient|.  Notify it that it's ready.
        mOwnedByClient = mRequestedByClient;
        mClient.surfaceCreated(mOwnedByClient.surfaceHolder().getSurface());

        // See if we're expecting a surfaceChanged.  If not, then send a synthetic one.
        if (mOwnedByClient.format != PixelFormat.UNKNOWN) {
            mClient.surfaceChanged(
                    mOwnedByClient.surfaceHolder().getSurface(),
                    mOwnedByClient.format,
                    mOwnedByClient.width,
                    mOwnedByClient.height);
        }
    }

    @Override
    public void doneWithUnownedSurface() {
        if (mOwnedByClient == null) return;

        SurfaceState unowned = (mOwnedByClient == mTranslucent) ? mOpaque : mTranslucent;

        if (mRequestedByClient == unowned) {
            // Client is giving us back a surface that it's since requested but hasn't gotten yet.
            // Do nothing.  It will be notified when the new surface is ready, and it can call us
            // again for the other surface, if it wants.
            return;
        }

        // Start destruction of this surface.  To prevent recursive call-backs to the client, we
        // post this for later.
        detachSurfaceLater(unowned);
    }

    @Override
    public void recreateSurface() {
        // If they don't have a surface, then they'll get a new one anyway.
        if (mOwnedByClient == null) return;

        // Notify the client that it no longer owns this surface, then destroy it.  When destruction
        // completes, we will recreate it automatically, since it will look like the client since
        // re-requested it.  That's why we send surfaceDestroyed here rather than letting our
        // surfaceDestroyed do it when destruction completes.  If we just started destruction while
        // the client still owns the surface, then our surfaceDestroyed would assume that Android
        // initiated the destruction, and wait for Android to recreate it.

        mParentView.post(
                new Runnable() {
                    @Override
                    public void run() {
                        if (mOwnedByClient == null) return;
                        SurfaceState owned = mOwnedByClient;
                        mClient.surfaceDestroyed(owned.surfaceHolder().getSurface(), true);
                        mOwnedByClient = null;
                        detachSurfaceNow(owned);
                    }
                });
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        SurfaceState state = getStateForHolder(holder);
        assert state != null;

        // If this is the surface that the client currently cares about, then notify the client.
        // Note that surfaceChanged is guaranteed to come only after surfaceCreated.  Also, if the
        // client has requested a different surface but hasn't gotten it yet, then skip this.
        if (state == mOwnedByClient && state == mRequestedByClient) {
            state.width = width;
            state.height = height;
            state.format = format;
            mClient.surfaceChanged(holder.getSurface(), format, width, height);
        }
    }

    @Override
    public void surfaceRedrawNeeded(SurfaceHolder holder) {
        // Intentionally not implemented.
    }

    @Override
    public void surfaceRedrawNeededAsync(SurfaceHolder holder, Runnable drawingFinished) {
        mClient.surfaceRedrawNeededAsync(drawingFinished);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        SurfaceState state = getStateForHolder(holder);
        assert state != null;
        // Note that |createPending| might not be set, if Android destroyed and recreated this
        // surface on its own.

        Log.i(TAG, "surfaceCreated format: %d", state.format);
        if (state != mRequestedByClient) {
            // Surface is created, but it's not the one that's been requested most recently.  Just
            // destroy it again.
            detachSurfaceLater(state);
            return;
        }

        // No create is pending.
        state.createPending = false;

        // A surfaceChanged should arrive.
        state.format = PixelFormat.UNKNOWN;

        // The client requested a surface, and it's now available.  If the client owns a surface,
        // then notify it that it doesn't.  Note that the client can't own |state| at this point,
        // since we would have removed ownership when we got surfaceDestroyed.  It's okay if the
        // client doesn't own either surface.
        assert mOwnedByClient != state;
        disownClientSurface(mOwnedByClient, false);

        // TODO(crbug.com/40195080): `disownClientSurface` may recursively shutdown which sets
        // `mRequestedByClient` to null. However testing shows throwing an NPE in this case
        // is caught by SurfaceView implementation and does not crash, and throwing actually
        // avoids an ANR due to some unexplained reason.

        // The client now owns this surface, so notify it.
        mOwnedByClient = mRequestedByClient;
        mClient.surfaceCreated(mOwnedByClient.surfaceHolder().getSurface());
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        SurfaceState state = getStateForHolder(holder);
        assert state != null;

        // If no destroy is pending, then Android chose to destroy this surface and will, hopefully,
        // re-create it at some point.  Otherwise, a destroy is either posted or has already
        // detached this SurfaceView.  If it's already detached, then the destruction is complete
        // and we can clear |destroyPending|.  Otherwise, Android has destroyed this surface while
        // our destroy was posted, and might even return it before it runs.  When the post runs, it
        // can sort that out based on whether the surface is valid or not.
        Log.e(TAG, "surfaceDestroyed format : " + state.format);
        if (!state.destroyPending) {
            state.createPending = true;
        } else if (!state.isAttached()) {
            state.destroyPending = false;
        }

        state.format = PixelFormat.UNKNOWN;

        // If the client owns this surface, then notify it synchronously that it no longer does.
        // This can happen if Android destroys the surface on its own.  It's also possible that
        // we've detached it, if a destroy was pending.  Either way, notify the client.
        if (state == mOwnedByClient) {
            disownClientSurface(mOwnedByClient, true);

            // Do not re-request the surface here.  If android gives the surface back, then we'll
            // re-signal the client about construction.
            return;
        }

        // Make sure the client has no remaining references to the destroyed surface.
        mClient.unownedSurfaceDestroyed();

        // The client doesn't own this surface, but might want it.
        // If the client has requested this surface, then start construction on it.  The client will
        // be notified when it completes.  This can happen if the client re-requests a surface after
        // we start destruction on it from a previous request, for example.  We post this for later,
        // since we might be called while removing |state| from the view tree. In general, posting
        // from here is good.
        if (state == mRequestedByClient && !state.isAttached()) {
            attachSurfaceLater(state);
        } else if (state != mRequestedByClient && state.isAttached()) {
            // This isn't the requested surface.  If android destroyed it, then also unhook it so
            // that it isn't recreated later.
            detachSurfaceLater(state);
        }
    }

    @Override
    public void setBackgroundDrawable(Drawable background) {
        mTranslucent.surfaceView.setBackgroundDrawable(background);
        mOpaque.surfaceView.setBackgroundDrawable(background);
    }

    @Override
    public void setWillNotDraw(boolean willNotDraw) {
        mTranslucent.surfaceView.setWillNotDraw(willNotDraw);
        mOpaque.surfaceView.setWillNotDraw(willNotDraw);
    }

    @Override
    public void setVisibility(int visibility) {
        mTranslucent.surfaceView.setVisibility(visibility);
        mOpaque.surfaceView.setVisibility(visibility);
    }

    @Override
    public View getActiveSurfaceView() {
        return mOwnedByClient == null ? null : mOwnedByClient.surfaceView;
    }

    /** Return the SurfaceState for |holder|, or null if it isn't either. */
    private SurfaceState getStateForHolder(SurfaceHolder holder) {
        if (mTranslucent.surfaceHolder() == holder) return mTranslucent;

        if (mOpaque.surfaceHolder() == holder) return mOpaque;

        return null;
    }

    /** Attach |state| to |mParentView| immedaitely. */
    private void attachSurfaceNow(SurfaceState state) {
        if (state.isAttached()) return;

        // If there is a destroy in-flight for this surface, then do nothing.
        if (state.destroyPending) return;

        state.createPending = true;
        FrameLayout.LayoutParams lp =
                new FrameLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        state.attachTo(mParentView, lp);
        mParentView.bringChildToFront(state.surfaceView);
        mParentView.postInvalidateOnAnimation();
    }

    /**
     * Post a Runnable to attach |state|.  This is helpful, since one cannot directly interact with
     * the View hierarchy during Surface callbacks.
     */
    private void attachSurfaceLater(final SurfaceState state) {
        // We shouldn't try to post construction if there's an in-flight destroy.
        assert !state.destroyPending;
        state.createPending = true;

        mParentView.post(
                new Runnable() {
                    @Override
                    public void run() {
                        attachSurfaceNow(state);
                    }
                });
    }

    /**
     * Cause the client to disown |state| if it currently owns it.  This involves notifying it that
     * the surface has been destroyed (recall that ownership involves getting created).  It's okay
     * if |state| is null or isn't owned by the client.
     */
    private void disownClientSurface(SurfaceState state, boolean surfaceDestroyed) {
        if (mOwnedByClient != state || state == null) return;

        mClient.surfaceDestroyed(mOwnedByClient.surfaceHolder().getSurface(), surfaceDestroyed);
        mOwnedByClient = null;
    }

    /** Detach |state| from |mParentView| immediately. */
    private void detachSurfaceNow(SurfaceState state) {
        // If we're called while we're not attached, then do nothing.  This makes it easier for the
        // client, since it doesn't have to keep track of whether the outgoing surface has been
        // destroyed or not.  The client will be notified (or has already) when the surface is
        // destroyed, if it currently owns it.
        if (state.isAttached()) {
            // We are attached.  If the surface is not valid, then Android has destroyed it for some
            // other reason, and we should clean up.  Otherwise, just wait for Android to finish.
            final boolean valid = state.isValid();

            // If the surface is valid, then we expect a callback to surfaceDestroyed eventually.
            state.destroyPending = valid;

            // Note that this might call back surfaceDestroyed before returning!
            state.detachFromParent();

            // If the surface was valid before, then we expect a surfaceDestroyed callback, which
            // might have arrived during removeView.  Either way, that callback will finish cleanup
            // of |state|.
            if (valid) return;
        }

        // The surface isn't attached, or was attached but wasn't currently valid.  Either way,
        // we're not going to get a destroy, so notify the client now if needed.
        disownClientSurface(state, false);

        // If the client has since re-requested the surface, then start construction.
        if (state == mRequestedByClient) attachSurfaceNow(mRequestedByClient);
    }

    /** Post detachment of |state|. This is safe during Surface callbacks. */
    private void detachSurfaceLater(final SurfaceState state) {
        // If |state| is not attached, then do nothing.  There might be a destroy pending from
        // Android, but in any case leave it be.
        if (!state.isAttached()) return;

        state.destroyPending = true;
        mParentView.post(
                new Runnable() {
                    @Override
                    public void run() {
                        detachSurfaceNow(state);
                    }
                });
    }
}