chromium/chrome/android/java/src/org/chromium/chrome/browser/page_info/ChromePageInfoControllerDelegate.java

// Copyright 2020 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.page_info;

import static org.chromium.components.browser_ui.site_settings.AllSiteSettings.EXTRA_SEARCH;

import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;

import org.chromium.base.Callback;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ephemeraltab.EphemeralTabCoordinator;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.feedback.HelpAndFeedbackLauncherImpl;
import org.chromium.chrome.browser.merchant_viewer.PageInfoStoreInfoController;
import org.chromium.chrome.browser.merchant_viewer.PageInfoStoreInfoController.StoreInfoActionHandler;
import org.chromium.chrome.browser.offlinepages.OfflinePageItem;
import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
import org.chromium.chrome.browser.offlinepages.OfflinePageUtils.OfflinePageLoadUrlDelegate;
import org.chromium.chrome.browser.omnibox.ChromeAutocompleteSchemeClassifier;
import org.chromium.chrome.browser.paint_preview.TabbedPaintPreview;
import org.chromium.chrome.browser.pdf.PdfUtils;
import org.chromium.chrome.browser.pdf.PdfUtils.PdfPageType;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.privacy_sandbox.PrivacySandboxReferrer;
import org.chromium.chrome.browser.privacy_sandbox.PrivacySandboxSettingsBaseFragment;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.settings.SettingsLauncherFactory;
import org.chromium.chrome.browser.site_settings.ChromeSiteSettingsDelegate;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabUtils;
import org.chromium.chrome.browser.tabmodel.TabCreator;
import org.chromium.chrome.browser.ui.favicon.FaviconHelper;
import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
import org.chromium.components.browser_ui.settings.SettingsLauncher;
import org.chromium.components.browser_ui.site_settings.AllSiteSettings;
import org.chromium.components.browser_ui.site_settings.SiteSettingsCategory;
import org.chromium.components.browser_ui.site_settings.SiteSettingsDelegate;
import org.chromium.components.browser_ui.widget.TintedDrawable;
import org.chromium.components.content_settings.CookieControlsBridge;
import org.chromium.components.content_settings.CookieControlsObserver;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.page_info.PageInfoAdPersonalizationController;
import org.chromium.components.page_info.PageInfoControllerDelegate;
import org.chromium.components.page_info.PageInfoMainController;
import org.chromium.components.page_info.PageInfoRowView;
import org.chromium.components.page_info.PageInfoSubpageController;
import org.chromium.components.page_info.PageInfoView;
import org.chromium.components.privacy_sandbox.TrackingProtectionSettings;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.content_public.browser.BrowserContextHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.url.GURL;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.function.Consumer;

/**
 * Chrome's customization of PageInfoControllerDelegate. This class provides Chrome-specific info to
 * PageInfoController. It also contains logic for Chrome-specific features, like {@link
 * TabbedPaintPreview}
 */
public class ChromePageInfoControllerDelegate extends PageInfoControllerDelegate {
    private final WebContents mWebContents;
    private final Supplier<ModalDialogManager> mModalDialogManagerSupplier;
    private final Supplier<EphemeralTabCoordinator> mEphemeralTabCoordinatorSupplier;
    private final Context mContext;
    private final Profile mProfile;
    private final Supplier<StoreInfoActionHandler> mStoreInfoActionHandlerSupplier;
    private final ChromePageInfoHighlight mPageInfoHighlight;
    private final OfflinePageLoadUrlDelegate mOfflinePageLoadUrlDelegate;
    private String mOfflinePageCreationDate;
    private final TabCreator mTabCreator;

    static final String FEEDBACK_REPORT_TYPE =
            "com.google.chrome.browser.page_info.USER_INITIATED_FEEDBACK_REPORT";

    public ChromePageInfoControllerDelegate(
            Context context,
            WebContents webContents,
            Supplier<ModalDialogManager> modalDialogManagerSupplier,
            OfflinePageLoadUrlDelegate offlinePageLoadUrlDelegate,
            @Nullable Supplier<StoreInfoActionHandler> storeInfoActionHandlerSupplier,
            Supplier<EphemeralTabCoordinator> ephemeralTabCoordinatorSupplier,
            ChromePageInfoHighlight pageInfoHighlight,
            TabCreator tabCreator) {
        super(
                new ChromeAutocompleteSchemeClassifier(Profile.fromWebContents(webContents)),
                /** isSiteSettingsAvailable= */
                SiteSettingsHelper.isSiteSettingsAvailable(webContents),
                /** cookieControlsShown= */
                CookieControlsBridge.isCookieControlsEnabled(Profile.fromWebContents(webContents)));
        mContext = context;
        mWebContents = webContents;
        mModalDialogManagerSupplier = modalDialogManagerSupplier;
        mEphemeralTabCoordinatorSupplier = ephemeralTabCoordinatorSupplier;
        mProfile = Profile.fromWebContents(mWebContents);
        mStoreInfoActionHandlerSupplier = storeInfoActionHandlerSupplier;
        mPageInfoHighlight = pageInfoHighlight;
        mTabCreator = tabCreator;

        initOfflinePageParams();
        mOfflinePageLoadUrlDelegate = offlinePageLoadUrlDelegate;

        TrackerFactory.getTrackerForProfile(mProfile).notifyEvent(EventConstants.PAGE_INFO_OPENED);
    }

    private void initOfflinePageParams() {
        mOfflinePageState = OfflinePageState.NOT_OFFLINE_PAGE;
        OfflinePageItem offlinePage = OfflinePageUtils.getOfflinePage(mWebContents);
        if (offlinePage != null) {
            mOfflinePageUrl = offlinePage.getUrl();
            if (OfflinePageUtils.isShowingTrustedOfflinePage(mWebContents)) {
                mOfflinePageState = OfflinePageState.TRUSTED_OFFLINE_PAGE;
            } else {
                mOfflinePageState = OfflinePageState.UNTRUSTED_OFFLINE_PAGE;
            }
            // Get formatted creation date of the offline page. If the page was shared (so the
            // creation date cannot be acquired), make date an empty string and there will be
            // specific processing for showing different string in UI.
            long pageCreationTimeMs = offlinePage.getCreationTimeMs();
            if (pageCreationTimeMs != 0) {
                Date creationDate = new Date(offlinePage.getCreationTimeMs());
                DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM);
                mOfflinePageCreationDate = df.format(creationDate);
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public ModalDialogManager getModalDialogManager() {
        return mModalDialogManagerSupplier.get();
    }

    /** {@inheritDoc} */
    @Override
    public void initOfflinePageUiParams(
            PageInfoView.Params viewParams, Consumer<Runnable> runAfterDismiss) {
        if (isShowingOfflinePage() && OfflinePageUtils.isConnected()) {
            viewParams.openOnlineButtonClickCallback =
                    () -> {
                        runAfterDismiss.accept(
                                () -> {
                                    // Attempt to reload to an online version of the viewed offline
                                    // web page.
                                    // This attempt might fail if the user is offline, in which case
                                    // an offline copy will be reloaded.
                                    OfflinePageUtils.reload(
                                            mWebContents, mOfflinePageLoadUrlDelegate);
                                });
                    };
        } else {
            viewParams.openOnlineButtonShown = false;
        }
    }

    /** {@inheritDoc} */
    @Override
    public @Nullable String getOfflinePageConnectionMessage() {
        if (mOfflinePageState == OfflinePageState.TRUSTED_OFFLINE_PAGE) {
            return String.format(
                    mContext.getString(R.string.page_info_connection_offline),
                    mOfflinePageCreationDate);
        } else if (mOfflinePageState == OfflinePageState.UNTRUSTED_OFFLINE_PAGE) {
            // For untrusted pages, if there's a creation date, show it in the message.
            if (TextUtils.isEmpty(mOfflinePageCreationDate)) {
                return mContext.getString(R.string.page_info_offline_page_not_trusted_without_date);
            } else {
                return String.format(
                        mContext.getString(R.string.page_info_offline_page_not_trusted_with_date),
                        mOfflinePageCreationDate);
            }
        }
        return null;
    }

    /** {@inheritDoc} */
    @Override
    public boolean isShowingPaintPreviewPage() {
        Tab tab = TabUtils.fromWebContents(mWebContents);
        return tab != null && TabbedPaintPreview.get(tab).isShowing();
    }

    /** {@inheritDoc} */
    @Override
    public @Nullable String getPaintPreviewPageConnectionMessage() {
        if (!isShowingPaintPreviewPage()) return null;

        return mContext.getString(R.string.page_info_connection_paint_preview);
    }

    /** {@inheritDoc} */
    @Override
    public @PdfPageType int getPdfPageType() {
        Tab tab = TabUtils.fromWebContents(mWebContents);
        // TODO(shuyng): move this check to PdfUtils. Currently PdfUtils cannot depends on Tab.
        if (tab == null) {
            return PdfPageType.NONE;
        }
        return PdfUtils.getPdfPageType(tab.getNativePage());
    }

    /** {@inheritDoc} */
    @Override
    public @Nullable String getPdfPageConnectionMessage() {
        switch (getPdfPageType()) {
            case PdfPageType.TRANSIENT_SECURE:
                return mContext.getString(R.string.page_info_connection_transient_pdf);
            case PdfPageType.TRANSIENT_INSECURE:
                return mContext.getString(R.string.page_info_connection_transient_pdf_insecure);
            case PdfPageType.LOCAL:
                return mContext.getString(R.string.page_info_connection_local_pdf);
            default:
                return null;
        }
    }

    /** {@inheritDoc} */
    @Override
    public void showCookieSettings() {
        SiteSettingsHelper.showCategorySettings(
                mContext, SiteSettingsCategory.Type.THIRD_PARTY_COOKIES);
    }

    /** {@inheritDoc} */
    @Override
    public void showTrackingProtectionSettings() {
        SettingsLauncher settingsLauncher = SettingsLauncherFactory.createSettingsLauncher();
        settingsLauncher.launchSettingsActivity(mContext, TrackingProtectionSettings.class);
    }

    /** {@inheritDoc} */
    @Override
    public void showAllSettingsForRws(String rwsOwner) {
        Bundle extras = new Bundle();
        extras.putString(EXTRA_SEARCH, rwsOwner);

        SettingsLauncher settingsLauncher = SettingsLauncherFactory.createSettingsLauncher();
        settingsLauncher.launchSettingsActivity(mContext, AllSiteSettings.class, extras);
    }

    @Override
    public void showCookieFeedback(Activity activity) {
        Tab tab = TabUtils.fromWebContents(mWebContents);

        // FEEDBACK_REPORT_TYPE: Reports for Chrome mobile must have a contextTag of the form
        // com.chrome.feed.USER_INITIATED_FEEDBACK_REPORT, or they will be discarded for not
        // matching an allow list rule.
        HelpAndFeedbackLauncherImpl.getForProfile(mProfile)
                .showFeedback(activity, tab.getOriginalUrl().getHost(), FEEDBACK_REPORT_TYPE);
    }

    @Override
    public void showAdPersonalizationSettings() {
        PrivacySandboxSettingsBaseFragment.launchPrivacySandboxSettings(
                mContext, PrivacySandboxReferrer.PAGE_INFO_AD_PRIVACY_SECTION);
    }

    @NonNull
    @Override
    public Collection<PageInfoSubpageController> createAdditionalRowViews(
            PageInfoMainController mainController, ViewGroup rowWrapper) {
        Collection<PageInfoSubpageController> controllers = new ArrayList<>();
        var adPersonalizationRow = new PageInfoRowView(rowWrapper.getContext(), null);
        adPersonalizationRow.setId(PageInfoAdPersonalizationController.ROW_ID);
        rowWrapper.addView(adPersonalizationRow);
        controllers.add(
                new PageInfoAdPersonalizationController(
                        mainController, adPersonalizationRow, this));

        // Add history row.
        final Tab tab = TabUtils.fromWebContents(mWebContents);
        var historyRow = new PageInfoRowView(rowWrapper.getContext(), null);
        historyRow.setId(PageInfoHistoryController.HISTORY_ROW_ID);
        rowWrapper.addView(historyRow);
        controllers.add(
                new PageInfoHistoryController(
                        mainController,
                        historyRow,
                        this,
                        () -> {
                            return tab;
                        }));

        if (PageInfoAboutThisSiteController.isFeatureEnabled()) {
            var aboutThisSiteRow = new PageInfoRowView(rowWrapper.getContext(), null);
            aboutThisSiteRow.setId(PageInfoAboutThisSiteController.ROW_ID);
            rowWrapper.addView(aboutThisSiteRow);
            new PageInfoAboutThisSiteController(
                    mainController,
                    mEphemeralTabCoordinatorSupplier,
                    aboutThisSiteRow,
                    this,
                    mWebContents,
                    mTabCreator);
        }
        if (!isIncognito()) {
            var storeInfoRow = new PageInfoRowView(rowWrapper.getContext(), null);
            storeInfoRow.setId(PageInfoStoreInfoController.STORE_INFO_ROW_ID);
            rowWrapper.addView(storeInfoRow);
            controllers.add(
                    new PageInfoStoreInfoController(
                            mainController,
                            storeInfoRow,
                            mStoreInfoActionHandlerSupplier,
                            mPageInfoHighlight.shouldHighlightStoreInfo(),
                            mWebContents,
                            mProfile));
        }
        return controllers;
    }

    /** {@inheritDoc} */
    @Override
    public @NonNull CookieControlsBridge createCookieControlsBridge(
            CookieControlsObserver observer) {
        return new CookieControlsBridge(
                observer,
                mWebContents,
                mProfile.isOffTheRecord() ? mProfile.getOriginalProfile() : null);
    }

    /** {@inheritDoc} */
    @Override
    public @NonNull BrowserContextHandle getBrowserContext() {
        return mProfile;
    }

    /** {@inheritDoc} */
    @Override
    public @NonNull SiteSettingsDelegate getSiteSettingsDelegate() {
        return new ChromeSiteSettingsDelegate(mContext, mProfile);
    }

    @Override
    public void getFavicon(GURL url, Callback<Drawable> callback) {
        Resources resources = mContext.getResources();
        int size = resources.getDimensionPixelSize(R.dimen.page_info_favicon_size);
        new FaviconHelper()
                .getLocalFaviconImageForURL(
                        mProfile,
                        url,
                        size,
                        (image, iconUrl) -> {
                            if (image != null) {
                                callback.onResult(new BitmapDrawable(resources, image));
                            } else if (UrlUtilities.isInternalScheme(url)) {
                                callback.onResult(
                                        TintedDrawable.constructTintedDrawable(
                                                mContext, R.drawable.chromelogo16));
                            } else {
                                callback.onResult(null);
                            }
                        });
    }

    @Override
    public boolean isAccessibilityEnabled() {
        return ChromeAccessibilityUtil.get().isAccessibilityEnabled();
    }

    @Override
    public FragmentManager getFragmentManager() {
        FragmentActivity activity = ((FragmentActivity) mContext);
        if (activity.isFinishing()) return null;
        return activity.getSupportFragmentManager();
    }

    /** {@inheritDoc} */
    @Override
    public boolean isIncognito() {
        return mProfile.isOffTheRecord();
    }

    @Override
    public boolean showTrackingProtectionUI() {
        return getSiteSettingsDelegate().shouldShowTrackingProtectionUI();
    }

    @Override
    public boolean showTrackingProtectionACTFeaturesUI() {
        return getSiteSettingsDelegate().shouldShowTrackingProtectionACTFeaturesUI();
    }

    @Override
    public boolean allThirdPartyCookiesBlockedTrackingProtection() {
        return UserPrefs.get(mProfile).getBoolean(Pref.BLOCK_ALL3PC_TOGGLE_ENABLED);
    }
}