chromium/content/public/android/java/src/org/chromium/content/browser/androidoverlay/DialogOverlayImpl.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.content.browser.androidoverlay;

import android.content.Context;
import android.os.IBinder;
import android.view.Surface;
import android.view.View;
import android.view.ViewTreeObserver;

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.content.browser.webcontents.WebContentsImpl;
import org.chromium.gfx.mojom.Rect;
import org.chromium.media.mojom.AndroidOverlay;
import org.chromium.media.mojom.AndroidOverlayClient;
import org.chromium.media.mojom.AndroidOverlayConfig;
import org.chromium.mojo.system.MessagePipeHandle;
import org.chromium.mojo.system.MojoException;
import org.chromium.ui.base.WindowAndroid;

/**
 * Default AndroidOverlay impl.  Uses a separate (shared) overlay thread to own a Dialog instance,
 * probably via a separate object that operates only on that thread.  We will post messages to /
 * from that thread from the UI thread.
 */
@JNINamespace("content")
public class DialogOverlayImpl
        implements AndroidOverlay, DialogOverlayCore.Host, ViewTreeObserver.OnPreDrawListener {
    private static final String TAG = "DialogOverlayImpl";

    private AndroidOverlayClient mClient;
    // Runnable that we'll run when the overlay notifies us that it's been released.
    private Runnable mReleasedRunnable;

    private DialogOverlayCore mDialogCore;

    private long mNativeHandle;

    // If nonzero, then we have registered a surface with this ID.
    private int mSurfaceId;

    // Has close() been run yet?
    private boolean mClosed;

    // Temporary, so we don't need to keep allocating arrays.
    private final int[] mCompositorOffset = new int[2];

    // The last rect passed to scheduleLayout().
    private Rect mLastRect;

    // Observes the container view to update our location.
    private ViewTreeObserver mContainerViewViewTreeObserver;

    private final AndroidOverlayConfig mConfig;
    private final boolean mAsPanel;

    // The handler will be notified when the surface will be destroyed soon. We'll
    // notify the client to cleanup tasks on the surface, because the surface may be
    // destroyed before SurfaceHolder.Callback2.surfaceDestroyed returns.
    private final Runnable mTearDownDialogOverlaysHandler = this::onOverlayDestroyed;
    private WebContentsImpl mWebContents;

    /**
     * @param client Mojo client interface.
     * @param config initial overlay configuration.
     * @param provider the overlay provider that owns us.
     * @param asPanel the overlay should be a panel, above the compositor.  This is for testing.
     */
    public DialogOverlayImpl(
            AndroidOverlayClient client,
            final AndroidOverlayConfig config,
            Runnable releasedRunnable,
            final boolean asPanel) {
        ThreadUtils.assertOnUiThread();

        mClient = client;
        mReleasedRunnable = releasedRunnable;
        mLastRect = copyRect(config.rect);
        mConfig = config;
        mAsPanel = asPanel;

        // Register to get token updates.  Note that this may not call us back directly, since
        // |mDialogCore| hasn't been initialized yet.
        mNativeHandle =
                DialogOverlayImplJni.get()
                        .init(
                                DialogOverlayImpl.this,
                                config.routingToken.high,
                                config.routingToken.low,
                                config.powerEfficient);

        if (mNativeHandle == 0) {
            notifyDestroyed();
            cleanup();
            return;
        }

        DialogOverlayImplJni.get()
                .getCompositorOffset(mNativeHandle, DialogOverlayImpl.this, config.rect);
        DialogOverlayImplJni.get().completeInit(mNativeHandle, DialogOverlayImpl.this);
    }

    // AndroidOverlay impl.
    // Client is done with this overlay.
    @Override
    public void close() {
        ThreadUtils.assertOnUiThread();

        if (mClosed) return;

        mClosed = true;

        // TODO(liberato): verify that this actually works, else add an explicit shutdown and hope
        // that the client calls it.

        // Notify |mDialogCore| that it has been released.
        if (mDialogCore != null) {
            mDialogCore.release();

            // Note that we might get messagaes from |mDialogCore| after this, since they might be
            // dispatched before |r| arrives.  Clearing |mDialogCore| causes us to ignore them.
            cleanup();
        }

        // Notify the provider that we've been released by the client.  Note that the surface might
        // not have been destroyed yet, but that's okay.  We could wait for a callback from the
        // dialog core before proceeding, but this makes it easier for the client to destroy and
        // re-create an overlay without worrying about an intermittent failure due to having too
        // many overlays open at once.
        mReleasedRunnable.run();
    }

    // AndroidOverlay impl.
    @Override
    public void onConnectionError(MojoException e) {
        ThreadUtils.assertOnUiThread();

        close();
    }

    // AndroidOverlay impl.
    @Override
    public void scheduleLayout(final Rect rect) {
        ThreadUtils.assertOnUiThread();

        mLastRect = copyRect(rect);

        if (mDialogCore == null) return;

        // |rect| is relative to the compositor surface.  Convert it to be relative to the screen.
        DialogOverlayImplJni.get().getCompositorOffset(mNativeHandle, DialogOverlayImpl.this, rect);
        mDialogCore.layoutSurface(rect);
    }

    // Receive the compositor offset, as part of scheduleLayout.  Adjust the layout position.
    @CalledByNative
    private static void receiveCompositorOffset(Rect rect, int x, int y) {
        rect.x += x;
        rect.y += y;
    }

    // DialogOverlayCore.Host impl.
    // |surface| is now ready.  Register it with the surface tracker, and notify the client.
    @Override
    public void onSurfaceReady(Surface surface) {
        ThreadUtils.assertOnUiThread();

        if (mDialogCore == null || mClient == null) return;

        mSurfaceId = DialogOverlayImplJni.get().registerSurface(surface);
        mClient.onSurfaceReady(mSurfaceId);
    }

    // DialogOverlayCore.Host impl.
    @Override
    public void onOverlayDestroyed() {
        ThreadUtils.assertOnUiThread();

        if (mDialogCore == null) return;

        // Notify the client that the overlay is gone.
        notifyDestroyed();

        // Also clear out |mDialogCore| to prevent us from sending useless messages to it.
        cleanup();

        // Note that we don't notify |mReleasedRunnable| yet, though we could.  We wait for the
        // client to close their connection first.
    }

    // ViewTreeObserver.OnPreDrawListener implementation.
    @Override
    public boolean onPreDraw() {
        scheduleLayout(mLastRect);
        return true;
    }

    /** Callback from native that the window has changed. */
    @CalledByNative
    public void onWindowAndroid(final WindowAndroid window) {
        ThreadUtils.assertOnUiThread();

        if (mDialogCore == null) {
            initializeDialogCore(window);
            return;
        }

        // Forward this change.
        // Note that if we don't have a window token, then we could wait until we do, simply by
        // skipping sending null if we haven't sent any non-null token yet.  If we're transitioning
        // between windows, that might make the client's job easier. It wouldn't have to guess when
        // a new token is available.
        IBinder token = window != null ? window.getWindowToken() : null;
        mDialogCore.onWindowToken(token);
    }

    @CalledByNative
    private void observeContainerView(View containerView) {
        if (mContainerViewViewTreeObserver != null && mContainerViewViewTreeObserver.isAlive()) {
            mContainerViewViewTreeObserver.removeOnPreDrawListener(this);
        }
        mContainerViewViewTreeObserver = null;

        if (containerView != null) {
            mContainerViewViewTreeObserver = containerView.getViewTreeObserver();
            mContainerViewViewTreeObserver.addOnPreDrawListener(this);
        }
    }

    /** Callback from native that we will be getting no additional tokens. */
    @CalledByNative
    public void onDismissed() {
        ThreadUtils.assertOnUiThread();

        // Notify the client that the overlay is going away.
        notifyDestroyed();

        // Notify |mDialogCore| that it lost the token, if it had one.
        if (mDialogCore != null) mDialogCore.onWindowToken(null);

        cleanup();
    }

    /** Callback from native to tell us that the power-efficient state has changed. */
    @CalledByNative
    private void onPowerEfficientState(boolean isPowerEfficient) {
        ThreadUtils.assertOnUiThread();
        if (mDialogCore == null) return;
        if (mClient == null) return;
        mClient.onPowerEfficientState(isPowerEfficient);
    }

    /**
     * Callback from the native to provide the WebContents. It should be called inside completeInit.
     */
    @CalledByNative
    private void onWebContents(WebContentsImpl webContents) {
        assert mWebContents == null;
        assert webContents != null;

        mWebContents = webContents;
        mWebContents.addTearDownDialogOverlaysHandler(mTearDownDialogOverlaysHandler);
    }

    /** Initialize |mDialogCore| when the window is available. */
    private void initializeDialogCore(WindowAndroid window) {
        ThreadUtils.assertOnUiThread();

        if (window == null) return;

        Context context = window.getContext().get();
        if (ContextUtils.activityFromContext(context) == null) return;

        mDialogCore = new DialogOverlayCore();
        mDialogCore.initialize(context, mConfig, DialogOverlayImpl.this, mAsPanel);
        mDialogCore.onWindowToken(window.getWindowToken());
    }

    /**
     * Unregister for callbacks, unregister any surface that we have, and forget about
     * |mDialogCore|.  Multiple calls are okay.
     */
    private void cleanup() {
        ThreadUtils.assertOnUiThread();

        if (mSurfaceId != 0) {
            DialogOverlayImplJni.get().unregisterSurface(mSurfaceId);
            mSurfaceId = 0;
        }

        // Note that we might not be registered for a token.
        if (mNativeHandle != 0) {
            DialogOverlayImplJni.get().destroy(mNativeHandle, DialogOverlayImpl.this);
            mNativeHandle = 0;
        }

        // Also clear out |mDialogCore| to prevent us from sending useless messages to it.  Note
        // that we might have already sent useless messages to it, and it should be robust against
        // that sort of thing.
        mDialogCore = null;

        // If we wanted to send any message to |mClient|, we should have done so already.
        // We close |mClient| first to prevent leaking the mojo router object.
        if (mClient != null) mClient.close();
        mClient = null;

        // Native should have cleaned up the container view before we reach this.
        assert mContainerViewViewTreeObserver == null;

        if (mWebContents != null) {
            mWebContents.removeTearDownDialogOverlaysHandler(mTearDownDialogOverlaysHandler);
            mWebContents = null;
        }
    }

    private void notifyDestroyed() {
        if (mClient == null) return;

        // This is the last message to the client.
        final AndroidOverlayClient client = mClient;
        mClient = null;

        // If we've not provided a surface, then we don't need to wait for a reply.  This happens,
        // for example, if we fail immediately.
        if (mSurfaceId == 0) {
            client.onDestroyed();
            return;
        }

        // Notify the client that the overlay is gone, synchronously.  We have to do this once we
        // have a Surface, since we could get a surfaceDestroyed from Android at any time.  If we
        // signal async destruction, then get surfaceDestroyed, then we're stuck.  So, clean up
        // synchronously even if Android is not waiting for us right now.

        // Don't try this at home.  It's hacky.  All of DialogOverlay is deprecated.  It will be
        // removed once Android O is no longer supported.
        final AndroidOverlayClient.Proxy proxy = (AndroidOverlayClient.Proxy) client;
        final MessagePipeHandle handle = proxy.getProxyHandler().passHandle();
        final long nativeHandle = handle.releaseNativeHandle();
        DialogOverlayImplJni.get().notifyDestroyedSynchronously(nativeHandle);
    }

    /** Creates a copy of |rect| and returns it. */
    private static Rect copyRect(Rect rect) {
        Rect copy = new Rect();
        copy.x = rect.x;
        copy.y = rect.y;
        copy.width = rect.width;
        copy.height = rect.height;
        return copy;
    }

    @NativeMethods
    interface Natives {
        /**
         * Initializes native side.  Will register for onWindowToken callbacks on |this|.  Returns a
         * handle that should be provided to nativeDestroy.  This will not call back with a window
         * token immediately.  Call nativeCompleteInit() for the initial token.
         */
        long init(DialogOverlayImpl caller, long high, long low, boolean isPowerEfficient);

        void completeInit(long nativeDialogOverlayImpl, DialogOverlayImpl caller);

        /** Stops native side and deallocates |handle|. */
        void destroy(long nativeDialogOverlayImpl, DialogOverlayImpl caller);

        /**
         * Calls back ReceiveCompositorOffset with the screen location (in the
         * View.getLocationOnScreen sense) of the compositor for our WebContents.  Sends |rect|
         * along verbatim.
         */
        void getCompositorOffset(long nativeDialogOverlayImpl, DialogOverlayImpl caller, Rect rect);

        /**
         * Register a surface and return the surface id for it.
         * @param surface Surface that we should register.
         * @return surface id that we associated with |surface|.
         */
        int registerSurface(Surface surface);

        /**
         * Unregister a surface.
         * @param surfaceId Id that was returned by registerSurface.
         */
        void unregisterSurface(int surfaceId);

        /**
         * Look up and return a surface.
         * @param surfaceId Id that was returned by registerSurface.
         */
        Surface lookupSurfaceForTesting(int surfaceId);

        /**
         * Send a synchronous OnDestroyed message to the client.
         * @param messagePipe Mojo message pipe ID.
         * @param version Mojo interface version.
         * @return none, but the message pipe is closed.
         */
        void notifyDestroyedSynchronously(long messagePipeHandle);
    }
}