chromium/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ContextMenuHelper.java

// Copyright 2013 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.contextmenu;

import android.util.Pair;
import android.view.View;

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

import org.chromium.base.Callback;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.components.embedder_support.contextmenu.ChipDelegate;
import org.chromium.components.embedder_support.contextmenu.ContextMenuNativeDelegate;
import org.chromium.components.embedder_support.contextmenu.ContextMenuParams;
import org.chromium.components.embedder_support.contextmenu.ContextMenuPopulator;
import org.chromium.components.embedder_support.contextmenu.ContextMenuPopulatorFactory;
import org.chromium.components.embedder_support.contextmenu.ContextMenuUi;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;

import java.util.List;

/** A helper class that handles generating and dismissing context menus for {@link WebContents}. */
public class ContextMenuHelper {
    private static Callback<ContextMenuCoordinator> sMenuShownCallbackForTesting;

    private final WebContents mWebContents;
    private long mNativeContextMenuHelper;

    private ContextMenuNativeDelegate mCurrentNativeDelegate;
    private ContextMenuPopulator mCurrentPopulator;
    private ContextMenuPopulatorFactory mPopulatorFactory;
    private ContextMenuParams mCurrentContextMenuParams;
    private ContextMenuUi mCurrentContextMenu;
    private WindowAndroid mWindow;
    private Callback<Integer> mCallback;
    private Runnable mOnMenuShown;
    private Runnable mOnMenuClosed;
    private ChipDelegate mChipDelegate;

    private ContextMenuHelper(long nativeContextMenuHelper, WebContents webContents) {
        mNativeContextMenuHelper = nativeContextMenuHelper;
        mWebContents = webContents;
    }

    @CalledByNative
    private static ContextMenuHelper create(long nativeContextMenuHelper, WebContents webContents) {
        return new ContextMenuHelper(nativeContextMenuHelper, webContents);
    }

    @CalledByNative
    private void destroy() {
        dismissContextMenu();
        if (mCurrentNativeDelegate != null) mCurrentNativeDelegate.destroy();
        if (mPopulatorFactory != null) mPopulatorFactory.onDestroy();
        mNativeContextMenuHelper = 0;
    }

    @CalledByNative
    private void setPopulatorFactory(ContextMenuPopulatorFactory populatorFactory) {
        dismissContextMenu();
        if (mCurrentNativeDelegate != null) mCurrentNativeDelegate.destroy();
        mCurrentPopulator = null;
        if (mPopulatorFactory != null) mPopulatorFactory.onDestroy();
        mPopulatorFactory = populatorFactory;
    }

    /**
     * Starts showing a context menu for {@code view} based on {@code params}.
     * @param params The {@link ContextMenuParams} that indicate what menu items to show.
     * @param renderFrameHost {@link RenderFrameHost} to get the encoded images from.
     * @param view container view for the menu.
     * @param topContentOffsetPx the offset of the content from the top.
     */
    @CalledByNative
    private void showContextMenu(
            final ContextMenuParams params,
            RenderFrameHost renderFrameHost,
            View view,
            float topContentOffsetPx) {
        if (params.isFile()) return;

        final WindowAndroid windowAndroid = mWebContents.getTopLevelNativeWindow();

        if (view == null
                || view.getVisibility() != View.VISIBLE
                || view.getParent() == null
                || windowAndroid == null
                || windowAndroid.getActivity().get() == null
                || mPopulatorFactory == null
                || !mPopulatorFactory.isEnabled()
                || mCurrentContextMenu != null) {
            return;
        }

        mCurrentNativeDelegate =
                new ContextMenuNativeDelegateImpl(mWebContents, renderFrameHost, params);
        mCurrentPopulator =
                mPopulatorFactory.createContextMenuPopulator(
                        windowAndroid.getActivity().get(), params, mCurrentNativeDelegate);
        mCurrentContextMenuParams = params;
        mWindow = windowAndroid;
        mCallback =
                (result) -> {
                    if (mCurrentPopulator == null) return;

                    mCurrentPopulator.onItemSelected(result);
                };
        mOnMenuShown =
                () -> {
                    RecordHistogram.recordBooleanHistogram(
                            "ContextMenu.Shown", mWebContents != null);
                    recordContextMenuShownType(params);
                    if (sMenuShownCallbackForTesting != null) {
                        sMenuShownCallbackForTesting.onResult(
                                (ContextMenuCoordinator) mCurrentContextMenu);
                    }
                };
        mOnMenuClosed =
                () -> {
                    mCurrentContextMenu = null;
                    if (mCurrentNativeDelegate != null) {
                        mCurrentNativeDelegate.destroy();
                        mCurrentNativeDelegate = null;
                    }
                    if (mCurrentPopulator != null) {
                        mCurrentPopulator.onMenuClosed();
                        mCurrentPopulator = null;
                    }
                    if (mChipDelegate != null) {
                        // If the image was being classified terminate the classification
                        // Has no effect if the classification already succeeded.
                        mChipDelegate.onMenuClosed();
                    }
                    if (mNativeContextMenuHelper == 0) return;
                    ContextMenuHelperJni.get()
                            .onContextMenuClosed(mNativeContextMenuHelper, ContextMenuHelper.this);
                };

        displayContextMenu(topContentOffsetPx);
    }

    @CalledByNative
    private void dismissContextMenu() {
        if (mCurrentContextMenu != null) {
            mCurrentContextMenu.dismiss();
            mCurrentContextMenu = null;
        }
    }

    /** Record a histogram for a context menu shown even sliced by type. */
    private void recordContextMenuShownType(final ContextMenuParams params) {
        RecordHistogram.recordBooleanHistogram(
                String.format(
                        "ContextMenu.Shown.%s",
                        ContextMenuUtils.getContextMenuTypeForHistogram(params)),
                mWebContents != null);
    }

    private void displayContextMenu(float topContentOffsetPx) {
        List<Pair<Integer, ModelList>> items = mCurrentPopulator.buildContextMenu();
        if (items.isEmpty()) {
            PostTask.postTask(TaskTraits.UI_DEFAULT, mOnMenuClosed);
            // Only call if no items are populated. Otherwise call in mOnMenuShown callback.
            if (sMenuShownCallbackForTesting != null) {
                sMenuShownCallbackForTesting.onResult(null);
            }
            return;
        }

        final ContextMenuCoordinator menuCoordinator =
                new ContextMenuCoordinator(topContentOffsetPx, mCurrentNativeDelegate);
        mCurrentContextMenu = menuCoordinator;
        mChipDelegate = mCurrentPopulator.getChipDelegate();

        if (mChipDelegate != null) {
            menuCoordinator.displayMenuWithChip(
                    mWindow,
                    mWebContents,
                    mCurrentContextMenuParams,
                    items,
                    mCallback,
                    mOnMenuShown,
                    mOnMenuClosed,
                    mChipDelegate);
        } else {
            menuCoordinator.displayMenu(
                    mWindow,
                    mWebContents,
                    mCurrentContextMenuParams,
                    items,
                    mCallback,
                    mOnMenuShown,
                    mOnMenuClosed);
        }
    }

    public static void setMenuShownCallbackForTests(Callback<ContextMenuCoordinator> callback) {
        sMenuShownCallbackForTesting = callback;
        ResettersForTesting.register(() -> sMenuShownCallbackForTesting = null);
    }

    public static ContextMenuHelper createForTesting(
            long nativeContextMenuHelper, WebContents webContents) {
        return create(nativeContextMenuHelper, webContents);
    }

    void showContextMenuForTesting(
            ContextMenuPopulatorFactory populatorFactory,
            final ContextMenuParams params,
            RenderFrameHost renderFrameHost,
            View view,
            float topContentOffsetPx) {
        setPopulatorFactory(populatorFactory);
        showContextMenu(params, renderFrameHost, view, topContentOffsetPx);
    }

    @NativeMethods
    interface Natives {
        void onContextMenuClosed(long nativeContextMenuHelper, ContextMenuHelper caller);
    }
}