chromium/content/public/android/java/src/org/chromium/content/browser/androidoverlay/DialogOverlayCore.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.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.os.IBinder;
import android.view.Gravity;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.Window;
import android.view.WindowManager;

import org.chromium.base.Log;
import org.chromium.gfx.mojom.Rect;
import org.chromium.media.mojom.AndroidOverlayConfig;

/**
 * Core class for control of a single Dialog-based AndroidOverlay instance.  Everything runs on the
 * Browser UI thread.
 *
 * Note that this does not implement AndroidOverlay; we just manage the android side of it.  The
 * mojo interface is implemented by DialogOverlayImpl.
 */
class DialogOverlayCore {
    private static final String TAG = "DSCore";

    // Host interface, since we're on the wrong thread to talk to mojo, or anything else, really.
    public interface Host {
        // Notify the host that we have a surface.
        void onSurfaceReady(Surface surface);

        // Notify the host that we have failed to get a surface or the surface was destroyed.  This
        // must synchronously stop using the surface we've provided, if any.
        void onOverlayDestroyed();
    }

    private Host mHost;

    // When initialized via Init, we'll create mDialog.  We'll clear it when we send
    // onOverlayDestroyed to the host.  In general, when this is null, either we haven't been
    // initialized yet, or we've been torn down.  It shouldn't be the case that anything calls
    // methods after construction but before |initialize()|, though.
    private Dialog mDialog;

    private Callbacks mDialogCallbacks;

    // Most recent layout parameters.
    private WindowManager.LayoutParams mLayoutParams;

    // If true, then we'll be a panel rather than media overlay.  This is for testing.
    private boolean mAsPanel;

    /**
     * Construction may be called from a random thread, for simplicity.  Call initialize from the
     * proper thread before doing anything else.
     */
    public DialogOverlayCore() {}

    /**
     * Finish init on the proper thread.  We'll use this thread for the Dialog Looper thread.
     * @param dialog the dialog, which uses our current thread as the UI thread.
     * @param config initial config.
     * @param host host interface, for sending messages that (probably) need to thread hop.
     * @param asPanel if true, then we'll be a panel.  This is intended for tests only.
     */
    public void initialize(
            Context context, AndroidOverlayConfig config, Host host, boolean asPanel) {
        mHost = host;
        mAsPanel = asPanel;

        mDialog = new Dialog(context, android.R.style.Theme_NoDisplay);
        mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        mDialog.setCancelable(false);

        mLayoutParams = createLayoutParams(config.secure);
        copyRectToLayoutParams(config.rect);
    }

    /**
     * Release the underlying surface, and generally clean up, in response to
     * the client releasing the AndroidOverlay.  This may be called more than once.
     */
    public void release() {
        // If we've not released the dialog yet, then do so.
        dismissDialogQuietly();

        mLayoutParams.token = null;

        // We don't bother to notify |mHost| that we've been destroyed; it told us.
        mHost = null;
    }

    /**
     * Updates the most recent position/size for the dialog window. Returns false if |rect| already
     * matches the current params.
     */
    private boolean copyRectToLayoutParams(final Rect rect) {
        if (mLayoutParams.x == rect.x
                && mLayoutParams.y == rect.y
                && mLayoutParams.width == rect.width
                && mLayoutParams.height == rect.height) {
            return false;
        }

        // TODO(liberato): adjust for CompositorView screen location here if we want to support
        // non-full screen use cases.
        mLayoutParams.x = rect.x;
        mLayoutParams.y = rect.y;
        mLayoutParams.width = rect.width;
        mLayoutParams.height = rect.height;
        return true;
    }

    /**
     * Layout the AndroidOverlay.  If we don't have a token, then we ignore it, since a well-behaved
     * client shouldn't call us before getting the surface anyway.
     */
    public void layoutSurface(final Rect rect) {
        if (mDialog == null || mLayoutParams.token == null) return;

        // Note that it is important to not update the attributes if updating the layout params was
        // a no-op because it results in unnecessary re-layouts for the window.
        if (!copyRectToLayoutParams(rect)) return;

        mDialog.getWindow().setAttributes(mLayoutParams);
    }

    /**
     * Callbacks for finding out about the Dialog's Surface.
     * These happen on the looper thread.
     */
    private class Callbacks implements SurfaceHolder.Callback2 {
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            // Make sure that we haven't torn down the dialog yet.
            if (mDialog == null) return;

            if (mHost != null) mHost.onSurfaceReady(holder.getSurface());
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            if (mDialog == null || mHost == null) return;

            // Notify the host that we've been destroyed, and wait for it to clean up or time out.
            mHost.onOverlayDestroyed();
            mHost = null;
        }

        @Override
        public void surfaceRedrawNeeded(SurfaceHolder holder) {}
    }

    public void onWindowToken(IBinder token) {
        if (mDialog == null || mHost == null) return;

        if (token == null || (mLayoutParams.token != null && token != mLayoutParams.token)) {
            // We've lost the token, if we had one, or we got a new one.
            // Notify the client.
            mHost.onOverlayDestroyed();
            mHost = null;
            dismissDialogQuietly();
            return;
        }

        if (mLayoutParams.token == token) {
            // Same token, do nothing.
            return;
        }

        // We have a token, so layout the dialog.
        mLayoutParams.token = token;
        mDialog.getWindow().setAttributes(mLayoutParams);
        mDialogCallbacks = new Callbacks();
        mDialog.getWindow().takeSurface(mDialogCallbacks);
        mDialog.show();

        // We don't notify the client here.  We'll wait until the Android Surface is created.
    }

    @SuppressLint("RtlHardcoded")
    private WindowManager.LayoutParams createLayoutParams(boolean secure) {
        // Rather than using getAttributes, we just create them from scratch.
        // The default dialog attributes aren't what we want.
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();

        // NOTE: we really do want LEFT here, since we're dealing in compositor
        // coordinates.  Those are always from the left.
        layoutParams.gravity = Gravity.TOP | Gravity.LEFT;

        // Use a media surface, which is what SurfaceView uses by default.  For
        // debugging overlay drawing, consider using TYPE_APPLICATION_PANEL to
        // move the dialog over the CompositorView.
        layoutParams.type =
                mAsPanel
                        ? WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
                        : WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;

        layoutParams.flags =
                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;

        if (secure) {
            layoutParams.flags |= WindowManager.LayoutParams.FLAG_SECURE;
        }

        // Don't set FLAG_SCALED.  in addition to not being sure what it does
        // (SV uses it), it also causes a crash in WindowManager when we hide
        // (not dismiss), navigate, and/or exit the app without hide/dismiss.
        // There's a missing null check in WindowManagerService.java@3170
        // on M MR2.  To repro, change dimiss() to hide(), bring up a SV, and
        // navigate away or press home.

        // Turn off the position animation, so that it doesn't animate from one
        // position to the next.  Ignore errors.
        // 0x40 is PRIVATE_FLAG_NO_MOVE_ANIMATION.
        try {
            int currentFlags =
                    (Integer) layoutParams.getClass().getField("privateFlags").get(layoutParams);
            layoutParams
                    .getClass()
                    .getField("privateFlags")
                    .set(layoutParams, currentFlags | 0x00000040);
            // It would be nice to just catch Exception, but findbugs doesn't
            // allow it.  If we cannot set the flag, then that's okay too.
        } catch (NoSuchFieldException e) {
        } catch (NullPointerException e) {
        } catch (SecurityException e) {
        } catch (IllegalAccessException e) {
        } catch (IllegalArgumentException e) {
        } catch (ExceptionInInitializerError e) {
        }

        return layoutParams;
    }

    /** Package-private to retrieve our current dialog for tests. */
    Dialog getDialog() {
        return mDialog;
    }

    // Dismiss |mDialog| if needed, and clear it and the callbacks.  This hides any exception, since
    // there's a race during app shutdown between this running on the overlay-ui thread, and losing
    // the window token on the browser UI thread.  Now that we run on the browser UI thread, it's
    // likely that this race no longer exists.  Since it's hard to repro locally, and because this
    // doesn't hurt, the try / catch is still here.
    // See crbug.com/784224 .
    private void dismissDialogQuietly() {
        if (mDialog != null && mDialog.isShowing()) {
            try {
                mDialog.dismiss();
            } catch (Exception e) {
                Log.w(TAG, "Failed to dismiss overlay dialog.  \"WindowLeaked\" is ignorable.");
            }
        }

        mDialog = null;
        mDialogCallbacks = null;
    }
}