chromium/components/translate/content/android/java/src/org/chromium/components/translate/TranslateMessage.java

// Copyright 2022 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.translate;

import android.app.Activity;
import android.content.Context;
import android.database.DataSetObserver;
import android.text.TextUtils;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

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

import org.chromium.components.messages.DismissReason;
import org.chromium.components.messages.MessageBannerProperties;
import org.chromium.components.messages.MessageDispatcher;
import org.chromium.components.messages.MessageDispatcherProvider;
import org.chromium.components.messages.MessageIdentifier;
import org.chromium.components.messages.MessageScopeType;
import org.chromium.components.messages.PrimaryActionClickBehavior;
import org.chromium.components.messages.PrimaryWidgetAppearance;
import org.chromium.components.messages.SecondaryMenuMaxSize;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.listmenu.ListMenu;
import org.chromium.ui.listmenu.ListMenuButtonDelegate;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.widget.RectProvider;
import org.chromium.ui.widget.Toast;

import java.lang.ref.WeakReference;

/** Manages the translate message UI. */
@JNINamespace("translate")
class TranslateMessage implements TranslateMessageSecondaryMenu.Handler {
    public static class MenuItem {
        // If |title| is an empty string, then a divider will be shown.
        public final String title;
        // If |subtitle| is an empty string, then no subtitle will be shown.
        public final String subtitle;
        public final boolean hasCheckmark;
        public final int overflowMenuItemId;
        public final String languageCode;

        public MenuItem(
                String title,
                String subtitle,
                boolean hasCheckmark,
                int overflowMenuItemId,
                String languageCode) {
            this.title = title;
            this.subtitle = subtitle;
            this.hasCheckmark = hasCheckmark;
            this.overflowMenuItemId = overflowMenuItemId;
            this.languageCode = languageCode;
        }
    }

    private final Context mContext;
    private final MessageDispatcher mMessageDispatcher;
    private final WebContents mWebContents;
    private long mNativeTranslateMessage;
    private final int mDismissalDurationSeconds;

    // Will be null before the message is shown.
    private PropertyModel mMessageProperties;

    /** Shows a Toast with the general translate error message. */
    @CalledByNative
    public static void showTranslateError(WebContents webContents) {
        Context context = getContextFromWebContents(webContents);
        if (context == null) return;
        Toast toast = Toast.makeText(context, R.string.translate_infobar_error, Toast.LENGTH_SHORT);
        toast.show();
    }

    /**
     * Create a new TranslateMessage, or return null if creation failed.
     *
     * Creation could fail in cases where the MessageDispatcher cannot be retrieved, such as when
     * the activity is being recreated or destroyed.
     */
    @CalledByNative
    public static TranslateMessage create(
            WebContents webContents, long nativeTranslateMessage, int dismissalDurationSeconds) {
        Context context = getContextFromWebContents(webContents);
        if (context == null) return null;
        MessageDispatcher messageDispatcher =
                MessageDispatcherProvider.from(webContents.getTopLevelNativeWindow());
        if (messageDispatcher == null) return null;

        return new TranslateMessage(
                context,
                messageDispatcher,
                webContents,
                nativeTranslateMessage,
                dismissalDurationSeconds);
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    TranslateMessage(
            @NonNull Context context,
            @NonNull MessageDispatcher messageDispatcher,
            @NonNull WebContents webContents,
            long nativeTranslateMessage,
            int dismissalDurationSeconds) {
        mContext = context;
        mMessageDispatcher = messageDispatcher;
        mWebContents = webContents;
        mNativeTranslateMessage = nativeTranslateMessage;
        mDismissalDurationSeconds = dismissalDurationSeconds;
    }

    @CalledByNative
    public void clearNativePointer() {
        mNativeTranslateMessage = 0;
    }

    @CalledByNative
    public void showMessage(
            String title, String description, String primaryButtonText, boolean hasOverflowMenu) {
        boolean needsDispatch = mMessageProperties == null;
        if (needsDispatch) {
            mMessageProperties =
                    new PropertyModel.Builder(MessageBannerProperties.ALL_KEYS)
                            .with(
                                    MessageBannerProperties.MESSAGE_IDENTIFIER,
                                    MessageIdentifier.TRANSLATE)
                            .with(
                                    MessageBannerProperties.ICON_RESOURCE_ID,
                                    R.drawable.infobar_translate_compact)
                            .with(
                                    MessageBannerProperties.ICON_TINT_COLOR,
                                    MessageBannerProperties.TINT_NONE)
                            .with(
                                    MessageBannerProperties.SECONDARY_MENU_BUTTON_DELEGATE,
                                    new SecondaryMenuButtonDelegate())
                            .with(
                                    MessageBannerProperties.SECONDARY_MENU_MAX_SIZE,
                                    SecondaryMenuMaxSize.LARGE)
                            .with(
                                    MessageBannerProperties.DISMISSAL_DURATION,
                                    mDismissalDurationSeconds)
                            .with(
                                    MessageBannerProperties.ON_PRIMARY_ACTION,
                                    this::handlePrimaryAction)
                            .with(MessageBannerProperties.ON_DISMISSED, this::handleDismiss)
                            .build();
        }

        mMessageProperties.set(MessageBannerProperties.TITLE, title);
        mMessageProperties.set(MessageBannerProperties.DESCRIPTION, description);

        if (TextUtils.isEmpty(primaryButtonText)) {
            mMessageProperties.set(
                    MessageBannerProperties.PRIMARY_WIDGET_APPEARANCE,
                    PrimaryWidgetAppearance.PROGRESS_SPINNER);
        } else {
            mMessageProperties.set(MessageBannerProperties.PRIMARY_BUTTON_TEXT, primaryButtonText);
            mMessageProperties.set(
                    MessageBannerProperties.PRIMARY_WIDGET_APPEARANCE,
                    PrimaryWidgetAppearance.BUTTON_IF_TEXT_IS_SET);
        }

        if (hasOverflowMenu) {
            mMessageProperties.set(
                    MessageBannerProperties.SECONDARY_ICON_RESOURCE_ID, R.drawable.settings_cog);
        }

        if (needsDispatch) {
            mMessageDispatcher.enqueueMessage(
                    mMessageProperties,
                    mWebContents,
                    MessageScopeType.NAVIGATION,
                    /* highPriority= */ false);
        }
    }

    private @PrimaryActionClickBehavior int handlePrimaryAction() {
        if (mNativeTranslateMessage != 0) {
            TranslateMessageJni.get().handlePrimaryAction(mNativeTranslateMessage);
        }
        return PrimaryActionClickBehavior.DO_NOT_DISMISS;
    }

    private void handleDismiss(int dismissReason) {
        mMessageProperties = null;
        if (mNativeTranslateMessage == 0) return;
        TranslateMessageJni.get().handleDismiss(mNativeTranslateMessage, dismissReason);
    }

    @CalledByNative
    public void dismiss() {
        mMessageDispatcher.dismissMessage(mMessageProperties, DismissReason.DISMISSED_BY_FEATURE);
    }

    private static Context getContextFromWebContents(WebContents webContents) {
        WindowAndroid windowAndroid = webContents.getTopLevelNativeWindow();
        if (windowAndroid == null) return null;
        WeakReference<Activity> ref = windowAndroid.getActivity();
        if (ref == null) return null;
        return ref.get();
    }

    @CalledByNative
    public static MenuItem[] constructMenuItemArray(
            String[] titles,
            String[] subtitles,
            boolean[] hasCheckmarks,
            int[] overflowMenuItemIds,
            String[] languageCodes) {
        assert titles.length == subtitles.length
                && titles.length == hasCheckmarks.length
                && titles.length == overflowMenuItemIds.length
                && titles.length == languageCodes.length;
        MenuItem[] menuItems = new MenuItem[titles.length];
        for (int i = 0; i < titles.length; ++i) {
            menuItems[i] =
                    new MenuItem(
                            titles[i],
                            subtitles[i],
                            hasCheckmarks[i],
                            overflowMenuItemIds[i],
                            languageCodes[i]);
        }
        return menuItems;
    }

    // TranslateMessageSecondaryMenu.Handler implementation:
    @Override
    public MenuItem[] handleSecondaryMenuItemClicked(MenuItem menuItem) {
        if (mNativeTranslateMessage == 0) return null;
        return TranslateMessageJni.get()
                .handleSecondaryMenuItemClicked(
                        mNativeTranslateMessage,
                        menuItem.overflowMenuItemId,
                        menuItem.languageCode,
                        menuItem.hasCheckmark);
    }

    private final class SecondaryMenuButtonDelegate extends DataSetObserver
            implements ListMenuButtonDelegate {
        /**
         * Keeps track of the RectProvider supplied to anchor the AnchoredPopupWindow to the
         * ListMenuButton. It's kept as a WeakReference so that this doesn't inadvertently extend
         * the lifetime of the RectProvider and all of its references past the time when the popup
         * window is dismissed.
         */
        private WeakReference<RectProvider> mRectProvider;

        // ListMenuButtonDelegate implementation:
        @Override
        public RectProvider getRectProvider(View listMenuButton) {
            RectProvider provider = ListMenuButtonDelegate.super.getRectProvider(listMenuButton);
            mRectProvider = new WeakReference<RectProvider>(provider);
            return provider;
        }

        @Override
        public ListMenu getListMenu() {
            return new TranslateMessageSecondaryMenu(
                    mContext,
                    /* handler= */ TranslateMessage.this,
                    /* dataSetObserver= */ this,
                    mNativeTranslateMessage == 0
                            ? null
                            : TranslateMessageJni.get().buildOverflowMenu(mNativeTranslateMessage));
        }

        // DataSetObserver implementation:
        @Override
        public void onChanged() {
            // If the mRectProvider is set, then call setRect() with the existing Rect in order to
            // force it to notify its observer, which will cause the AnchoredPopupWindow to update
            // its onscreen dimensions to fit the new menu items.
            RectProvider provider = mRectProvider.get();
            if (provider != null) provider.setRect(provider.getRect());
        }
    }

    @NativeMethods
    interface Natives {
        void handlePrimaryAction(long nativeTranslateMessage);

        void handleDismiss(long nativeTranslateMessage, int dismissReason);

        MenuItem[] buildOverflowMenu(long nativeTranslateMessage);

        MenuItem[] handleSecondaryMenuItemClicked(
                long nativeTranslateMessage,
                int overflowMenuItemId,
                String languageCode,
                boolean hadCheckmark);
    }
}