chromium/components/embedder_support/android/java/src/org/chromium/components/embedder_support/view/ContentViewRenderView.java

// Copyright 2012 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.components.embedder_support.view;

import android.content.Context;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
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.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;

/***
 * This view is used by a ContentView to render its content.
 * Call {@link #setCurrentWebContents(WebContents)} with the webContents that should be
 * managing the content.
 * Note that only one WebContents can be shown at a time.
 */
@JNINamespace("embedder_support")
public class ContentViewRenderView extends FrameLayout {
    // The native side of this object.
    private long mNativeContentViewRenderView;
    private WindowAndroid mWindowAndroid;

    protected SurfaceBridge mSurfaceBridge;
    protected WebContents mWebContents;

    private int mWidth;
    private int mHeight;

    /**
     * Constructs a new ContentViewRenderView.
     * This should be called and the {@link ContentViewRenderView} should be added to the view
     * hierarchy before the first draw to avoid a black flash that is seen every time a
     * {@link SurfaceView} is added.
     * @param context The context used to create this.
     */
    public ContentViewRenderView(Context context) {
        super(context);

        mSurfaceBridge = createSurfaceBridge();
        mSurfaceBridge.initialize(this);
    }

    protected SurfaceBridge createSurfaceBridge() {
        return new SurfaceBridge();
    }

    /**
     * Initialization that requires native libraries should be done here.
     * Native code should add/remove the layers to be rendered through the ContentViewLayerRenderer.
     * @param rootWindow The {@link WindowAndroid} this render view should be linked to.
     */
    public void onNativeLibraryLoaded(WindowAndroid rootWindow) {
        assert !getSurfaceView().getHolder().getSurface().isValid()
                : "Surface created before native library loaded.";
        assert rootWindow != null;
        mNativeContentViewRenderView =
                ContentViewRenderViewJni.get().init(ContentViewRenderView.this, rootWindow);
        assert mNativeContentViewRenderView != 0;
        mWindowAndroid = rootWindow;
        SurfaceHolder.Callback surfaceCallback =
                new SurfaceHolder.Callback() {
                    @Override
                    public void surfaceChanged(
                            SurfaceHolder holder, int format, int width, int height) {
                        assert mNativeContentViewRenderView != 0;
                        ContentViewRenderViewJni.get()
                                .surfaceChanged(
                                        mNativeContentViewRenderView,
                                        ContentViewRenderView.this,
                                        format,
                                        width,
                                        height,
                                        holder.getSurface());
                        if (mWebContents != null) {
                            ContentViewRenderViewJni.get()
                                    .onPhysicalBackingSizeChanged(
                                            mNativeContentViewRenderView,
                                            ContentViewRenderView.this,
                                            mWebContents,
                                            width,
                                            height);
                        }
                    }

                    @Override
                    public void surfaceCreated(SurfaceHolder holder) {
                        assert mNativeContentViewRenderView != 0;
                        ContentViewRenderViewJni.get()
                                .surfaceCreated(
                                        mNativeContentViewRenderView, ContentViewRenderView.this);

                        // On pre-M Android, layers start in the hidden state until a relayout
                        // happens.
                        // There is a bug that manifests itself when entering overlay mode on pre-M
                        // devices, where a relayout never happens. This bug is out of Chromium's
                        // control, but can be worked around by forcibly re-setting the visibility
                        // of the surface view. Otherwise, the screen stays black, and some tests
                        // fail.
                        getSurfaceView().setVisibility(getSurfaceView().getVisibility());

                        onReadyToRender();
                    }

                    @Override
                    public void surfaceDestroyed(SurfaceHolder holder) {
                        assert mNativeContentViewRenderView != 0;
                        ContentViewRenderViewJni.get()
                                .surfaceDestroyed(
                                        mNativeContentViewRenderView, ContentViewRenderView.this);
                    }
                };
        mSurfaceBridge.connect(surfaceCallback);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        mWidth = w;
        mHeight = h;
        if (mWebContents != null) mWebContents.setSize(w, h);
    }

    /** View's method override to notify WindowAndroid about changes in its visibility. */
    @Override
    protected void onWindowVisibilityChanged(int visibility) {
        super.onWindowVisibilityChanged(visibility);

        if (mWindowAndroid == null) return;

        if (visibility == View.GONE) {
            mWindowAndroid.onVisibilityChanged(false);
        } else if (visibility == View.VISIBLE) {
            mWindowAndroid.onVisibilityChanged(true);
        }
    }

    /**
     * Sets the background color of the surface view.  This method is necessary because the
     * background color of ContentViewRenderView itself is covered by the background of
     * SurfaceView.
     * @param color The color of the background.
     */
    public void setSurfaceViewBackgroundColor(int color) {
        if (getSurfaceView() != null) {
            getSurfaceView().setBackgroundColor(color);
        }
    }

    /** Gets the SurfaceView for this ContentViewRenderView */
    public SurfaceView getSurfaceView() {
        return mSurfaceBridge.getSurfaceView();
    }

    /**
     * Should be called when the ContentViewRenderView is not needed anymore so its associated
     * native resource can be freed.
     */
    public void destroy() {
        mSurfaceBridge.disconnect();
        mWindowAndroid = null;
        ContentViewRenderViewJni.get()
                .destroy(mNativeContentViewRenderView, ContentViewRenderView.this);
        mNativeContentViewRenderView = 0;
    }

    public void setCurrentWebContents(WebContents webContents) {
        assert mNativeContentViewRenderView != 0;
        mWebContents = webContents;

        if (webContents != null) {
            webContents.setSize(mWidth, mHeight);
            ContentViewRenderViewJni.get()
                    .onPhysicalBackingSizeChanged(
                            mNativeContentViewRenderView,
                            ContentViewRenderView.this,
                            webContents,
                            mWidth,
                            mHeight);
        }
        ContentViewRenderViewJni.get()
                .setCurrentWebContents(
                        mNativeContentViewRenderView, ContentViewRenderView.this, webContents);
    }

    /**
     * This method should be subclassed to provide actions to be performed once the view is ready to
     * render.
     */
    protected void onReadyToRender() {}

    /**
     * This method could be subclassed optionally to provide a custom SurfaceView object to
     * this ContentViewRenderView.
     * @param context The context used to create the SurfaceView object.
     * @return The created SurfaceView object.
     */
    protected SurfaceView createSurfaceView(Context context) {
        return new SurfaceView(context);
    }

    /** @return whether the surface view is initialized and ready to render. */
    public boolean isInitialized() {
        return getSurfaceView().getHolder().getSurface() != null;
    }

    /**
     * Enter or leave overlay video mode.
     * @param enabled Whether overlay mode is enabled.
     */
    public void setOverlayVideoMode(boolean enabled) {
        int format = enabled ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
        getSurfaceView().getHolder().setFormat(format);
        ContentViewRenderViewJni.get()
                .setOverlayVideoMode(
                        mNativeContentViewRenderView, ContentViewRenderView.this, enabled);
    }

    @CalledByNative
    private void didSwapFrame() {
        if (getSurfaceView().getBackground() != null) {
            post(
                    new Runnable() {
                        @Override
                        public void run() {
                            getSurfaceView().setBackgroundResource(0);
                        }
                    });
        }
    }

    protected static class SurfaceBridge {
        private SurfaceView mSurfaceView;
        private SurfaceHolder.Callback mSurfaceCallback;

        protected SurfaceView getSurfaceView() {
            return mSurfaceView;
        }

        protected void initialize(ContentViewRenderView renderView) {
            mSurfaceView = renderView.createSurfaceView(renderView.getContext());
            mSurfaceView.setZOrderMediaOverlay(true);

            renderView.setSurfaceViewBackgroundColor(Color.WHITE);
            renderView.addView(
                    mSurfaceView,
                    new FrameLayout.LayoutParams(
                            FrameLayout.LayoutParams.MATCH_PARENT,
                            FrameLayout.LayoutParams.MATCH_PARENT));
            mSurfaceView.setVisibility(GONE);
        }

        protected void connect(SurfaceHolder.Callback surfaceCallback) {
            mSurfaceCallback = surfaceCallback;
            mSurfaceView.getHolder().addCallback(mSurfaceCallback);
            mSurfaceView.setVisibility(VISIBLE);
        }

        protected void disconnect() {
            mSurfaceView.getHolder().removeCallback(mSurfaceCallback);
        }
    }

    @NativeMethods
    interface Natives {
        long init(ContentViewRenderView caller, WindowAndroid rootWindow);

        void destroy(long nativeContentViewRenderView, ContentViewRenderView caller);

        void setCurrentWebContents(
                long nativeContentViewRenderView,
                ContentViewRenderView caller,
                WebContents webContents);

        void onPhysicalBackingSizeChanged(
                long nativeContentViewRenderView,
                ContentViewRenderView caller,
                WebContents webContents,
                int width,
                int height);

        void surfaceCreated(long nativeContentViewRenderView, ContentViewRenderView caller);

        void surfaceDestroyed(long nativeContentViewRenderView, ContentViewRenderView caller);

        void surfaceChanged(
                long nativeContentViewRenderView,
                ContentViewRenderView caller,
                int format,
                int width,
                int height,
                Surface surface);

        void setOverlayVideoMode(
                long nativeContentViewRenderView, ContentViewRenderView caller, boolean enabled);
    }
}