chromium/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/status/StatusCoordinator.java

// Copyright 2018 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.omnibox.status;

import android.animation.Animator;
import android.content.res.Resources;
import android.os.Handler;
import android.os.Looper;
import android.view.View;

import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ContextUtils;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.merchant_viewer.MerchantTrustSignalsCoordinator;
import org.chromium.chrome.browser.omnibox.LocationBarDataProvider;
import org.chromium.chrome.browser.omnibox.R;
import org.chromium.chrome.browser.omnibox.UrlBarEditingTextStateProvider;
import org.chromium.chrome.browser.page_info.ChromePageInfoHighlight;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.theme.BrandedColorScheme;
import org.chromium.chrome.browser.user_education.UserEducationHelper;
import org.chromium.components.permissions.PermissionDialogController;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelAnimatorFactory;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;

import java.util.List;

/**
 * A component for displaying a status icon (e.g. security icon or navigation icon) and optional
 * verbose status text.
 */
public class StatusCoordinator implements View.OnClickListener, LocationBarDataProvider.Observer {
    /** Interface for displaying page info popup on omnibox. */
    public interface PageInfoAction {
        /**
         * @param tab Tab containing the content to show page info for.
         * @param pageInfoHighlight Providing the highlight row info related to this dialog.
         */
        void show(Tab tab, ChromePageInfoHighlight pageInfoHighlight);
    }

    // TODO(crbug.com/40707964): Do not store the StatusView
    private final StatusView mStatusView;
    private final StatusMediator mMediator;
    private final PropertyModel mModel;
    private final boolean mIsTablet;
    private final PageInfoAction mPageInfoAction;
    private LocationBarDataProvider mLocationBarDataProvider;
    private boolean mUrlHasFocus;

    /**
     * Creates a new {@link StatusCoordinator}.
     *
     * @param isTablet Whether the UI is shown on a tablet.
     * @param statusView The status view, used to supply and manipulate child views.
     * @param urlBarEditingTextStateProvider The url coordinator.
     * @param templateUrlServiceSupplier A supplier for {@link TemplateUrlService} used to query the
     *     default search engine.
     * @param windowAndroid The {@link WindowAndroid} that is used by the owning {@link Activity}.
     * @param pageInfoAction Displays page info popup.
     * @param merchantTrustSignalsCoordinatorSupplier Supplier of {@link
     *     MerchantTrustSignalsCoordinator}. Can be null if a store icon shouldn't be shown, such as
     *     when called from a search activity.
     * @param browserControlsVisibilityDelegate Delegate interface allowing control of the
     *     visibility of the browser controls (i.e. toolbar).
     */
    public StatusCoordinator(
            boolean isTablet,
            StatusView statusView,
            UrlBarEditingTextStateProvider urlBarEditingTextStateProvider,
            LocationBarDataProvider locationBarDataProvider,
            OneshotSupplier<TemplateUrlService> templateUrlServiceSupplier,
            Supplier<Profile> profileSupplier,
            WindowAndroid windowAndroid,
            PageInfoAction pageInfoAction,
            @Nullable
                    Supplier<MerchantTrustSignalsCoordinator>
                            merchantTrustSignalsCoordinatorSupplier,
            BrowserStateBrowserControlsVisibilityDelegate browserControlsVisibilityDelegate) {
        mIsTablet = isTablet;
        mStatusView = statusView;
        mLocationBarDataProvider = locationBarDataProvider;
        mPageInfoAction = pageInfoAction;

        mModel = new PropertyModel(StatusProperties.ALL_KEYS);

        PropertyModelChangeProcessor.create(mModel, mStatusView, new StatusViewBinder());

        PageInfoIPHController pageInfoIPHController =
                new PageInfoIPHController(
                        new UserEducationHelper(
                                ContextUtils.activityFromContext(mStatusView.getContext()),
                                profileSupplier,
                                new Handler(Looper.getMainLooper())),
                        getSecurityIconView());

        mMediator =
                new StatusMediator(
                        mModel,
                        mStatusView.getResources(),
                        mStatusView.getContext(),
                        urlBarEditingTextStateProvider,
                        isTablet,
                        locationBarDataProvider,
                        PermissionDialogController.getInstance(),
                        templateUrlServiceSupplier,
                        profileSupplier,
                        pageInfoIPHController,
                        windowAndroid,
                        merchantTrustSignalsCoordinatorSupplier);

        Resources res = mStatusView.getResources();
        mMediator.setUrlMinWidth(
                res.getDimensionPixelSize(R.dimen.location_bar_min_url_width)
                        + res.getDimensionPixelSize(R.dimen.location_bar_status_icon_bg_size)
                        + res.getDimensionPixelSize(R.dimen.location_bar_start_padding)
                        + res.getDimensionPixelSize(R.dimen.location_bar_end_padding));

        mMediator.setSeparatorFieldMinWidth(
                res.getDimensionPixelSize(R.dimen.location_bar_status_separator_width)
                        + res.getDimensionPixelSize(R.dimen.location_bar_status_separator_spacer));

        mMediator.setVerboseStatusTextMinWidth(
                res.getDimensionPixelSize(R.dimen.location_bar_min_verbose_status_text_width));

        // Update status immediately after receiving the data provider to avoid initial presence
        // glitch on tablet devices. This glitch would be typically seen upon launch of app, right
        // before the landing page is presented to the user.
        updateSecurityIcon();
        updateVerboseStatusVisibility();
        mLocationBarDataProvider.addObserver(this);
        mStatusView.setBrowserControlsVisibilityDelegate(browserControlsVisibilityDelegate);

        if (isSearchEngineStatusIconVisible()) {
            setTooltipText(R.string.accessibility_menu_info);
            setHoverHighlight(R.drawable.status_view_ripple);
        } else {
            setTooltipText(Resources.ID_NULL);
            setHoverHighlight(Resources.ID_NULL);
        }
    }

    /** Signals that native initialization has completed. */
    public void onNativeInitialized() {
        mMediator.updateLocationBarIcon(StatusView.IconTransitionType.CROSSFADE);
        mMediator.setStatusClickListener(this);
        mMediator.updateStatusVisibility();
        mMediator.setStoreIconController();
    }

    /**
     * @param urlHasFocus Whether the url currently has focus.
     */
    public void onUrlFocusChange(boolean urlHasFocus) {
        mMediator.setUrlHasFocus(urlHasFocus);
        mUrlHasFocus = urlHasFocus;
        updateVerboseStatusVisibility();
    }

    /**
     * @param show Whether the status icon should be VISIBLE, otherwise GONE.
     */
    public void setStatusIconShown(boolean show) {
        mMediator.setStatusIconShown(show);
    }

    /**
     * @param show Whether the status icon background should be VISIBLE, otherwise INVISIBLE.
     */
    public void setStatusIconBackgroundVisibility(boolean show) {
        mMediator.setStatusIconBackgroundVisibility(show);
    }

    /**
     * Set the url focus change percent.
     *
     * @param percent The current focus percent.
     */
    public void setUrlFocusChangePercent(float percent) {
        mMediator.setUrlFocusChangePercent(percent);
    }

    /** Set the x translation of the status view. */
    public void setTranslationX(float translationX) {
        mMediator.setTranslationX(translationX);
    }

    /** Set the tooltip text of the status view. */
    public void setTooltipText(@StringRes int tooltipTextResId) {
        mMediator.setTooltipText(tooltipTextResId);
    }

    /** Set the hover highlight of the status view. */
    public void setHoverHighlight(@DrawableRes int hoverHighlightResId) {
        mMediator.setHoverHighlight(hoverHighlightResId);
    }

    /**
     * @param brandedColorScheme The {@link BrandedColorScheme} to use for the status icon and text.
     */
    public void setBrandedColorScheme(@BrandedColorScheme int brandedColorScheme) {
        mMediator.setBrandedColorScheme(brandedColorScheme);

        // TODO(ender): remove this once icon selection has complete set of
        // corresponding properties (for tinting etc).
        updateSecurityIcon();
    }

    // LocationBarData.Observer implementation.
    // Using the default empty onPrimaryColorChanged.
    // Using the default empty onTitleChanged.
    // Using the default empty onUrlChanged.

    @Override
    public void onNtpStartedLoading() {
        mMediator.updateStatusVisibility();
    }

    @Override
    public void onIncognitoStateChanged() {
        mMediator.onIncognitoStateChanged();
    }

    @Override
    public void onSecurityStateChanged() {
        updateSecurityIcon();
        updateVerboseStatusVisibility();
    }

    @Override
    public void onUrlChanged() {
        mMediator.onUrlChanged();
    }

    @Override
    public void onPageLoadStopped() {
        mMediator.onPageLoadStopped();
    }

    @Override
    public void onTabCrashed() {
        mMediator.onTabCrashed();
    }

    /** Returns the resource identifier of the current security icon drawable. */
    public @DrawableRes int getSecurityIconResource() {
        return mMediator.getSecurityIconResource();
    }

    /** Updates the security icon displayed in the LocationBar. */
    private void updateSecurityIcon() {
        mMediator.updateSecurityIcon(
                mLocationBarDataProvider.getSecurityIconResource(mIsTablet),
                mLocationBarDataProvider.getSecurityIconColorStateList(),
                mLocationBarDataProvider.getSecurityIconContentDescriptionResourceId());
    }

    /** Returns the view displaying the security icon. */
    public View getSecurityIconView() {
        return mStatusView.getSecurityView();
    }

    /** Returns {@code true} if the security button is currently being displayed. */
    @VisibleForTesting
    public boolean isSecurityViewShown() {
        return mMediator.isSecurityViewShown();
    }

    /** Returns {@code true} if the search engine status is currently being displayed. */
    public boolean isSearchEngineStatusIconVisible() {
        // TODO(crbug.com/40707964): try to hide this method
        return mStatusView.isSearchEngineStatusIconVisible();
    }

    /** Returns {@code true} if the search engine icon is currently being displayed. */
    public boolean shouldDisplaySearchEngineIcon() {
        return mMediator.shouldDisplaySearchEngineIcon();
    }

    /** Returns the ID of the drawable currently shown in the security icon. */
    public @DrawableRes int getSecurityIconResourceIdForTesting() {
        return mModel.get(StatusProperties.STATUS_ICON_RESOURCE) == null
                ? 0
                : mModel.get(StatusProperties.STATUS_ICON_RESOURCE).getIconResForTesting();
    }

    /** Returns the icon identifier used for custom resources. */
    public String getSecurityIconIdentifierForTesting() {
        return mModel.get(StatusProperties.STATUS_ICON_RESOURCE) == null
                ? null
                : mModel.get(StatusProperties.STATUS_ICON_RESOURCE).getIconIdentifierForTesting();
    }

    /**
     * Update visibility of the verbose status based on the button type and focus state of the
     * omnibox.
     */
    private void updateVerboseStatusVisibility() {
        mMediator.updateVerboseStatus(
                mLocationBarDataProvider.getSecurityLevel(),
                mLocationBarDataProvider.isOfflinePage(),
                mLocationBarDataProvider.isPaintPreview());
    }

    @Override
    public void onClick(View view) {
        if (mUrlHasFocus) return;

        if (!mLocationBarDataProvider.hasTab()
                || mLocationBarDataProvider.getTab().getWebContents() == null) {
            return;
        }

        mPageInfoAction.show(mLocationBarDataProvider.getTab(), mMediator.getPageInfoHighlight());
        mMediator.onPageInfoOpened();
    }

    /**
     * Called to set the width of the location bar when the url bar is not focused. This value is
     * used to determine whether the verbose status text should be visible.
     *
     * @param width The unfocused location bar width.
     */
    public void setUnfocusedLocationBarWidth(int width) {
        mMediator.setUnfocusedLocationBarWidth(width);
    }

    /** Toggle animation of icon changes. */
    public void setShouldAnimateIconChanges(boolean shouldAnimate) {
        mMediator.setAnimationsEnabled(shouldAnimate);
    }

    /** Returns width of the status icon including start/end margins. */
    public int getStatusIconWidth() {
        // TODO(crbug.com/40707964): try to hide this method
        return mStatusView.getStatusIconWidth();
    }

    /**
     * @see View#getMeasuredWidth()
     */
    public int getMeasuredWidth() {
        // TODO(crbug.com/40707964): try to hide this method
        return mStatusView.getMeasuredWidth();
    }

    /**
     * Notifies StatusCoordinator that the default match for the currently entered autocomplete text
     * has been classified, indicating whether the default match is a search.
     *
     * @param defaultMatchIsSearch Whether the default match is a search.
     */
    public void onDefaultMatchClassified(boolean defaultMatchIsSearch) {
        mMediator.updateLocationBarIconForDefaultMatchCategory(defaultMatchIsSearch);
    }

    public void destroy() {
        mMediator.destroy();
        mLocationBarDataProvider.removeObserver(this);
        mLocationBarDataProvider = null;
    }

    /** Returns whether the status view is currently in the process of animating a change. */
    public boolean isStatusIconAnimating() {
        return mStatusView.isStatusIconAnimating();
    }

    /**
     * Populates an animation that fades =/unfades the entire StatusView container with the given
     * start delay and duration, adding it to the given list of animators.
     */
    public void populateFadeAnimation(
            List<Animator> animators, long startDelayMs, long durationMs, float targetAlpha) {
        if (mLocationBarDataProvider.isIncognitoBranded()) {
            Animator animator =
                    PropertyModelAnimatorFactory.ofFloat(
                                    mModel, StatusProperties.ALPHA, targetAlpha)
                            .setDuration(durationMs);
            animator.setStartDelay(startDelayMs);
            animators.add(animator);
        }
    }
}