chromium/chrome/android/java/src/org/chromium/chrome/browser/share/ShareDelegateImpl.java

// Copyright 2019 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.share;

import android.app.Activity;
import android.os.Build;

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

import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.device_lock.DeviceLockActivityLauncherImpl;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.history_clusters.HistoryClustersTabHelper;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
import org.chromium.chrome.browser.printing.TabPrinter;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.share.ShareContentTypeHelper.ContentType;
import org.chromium.chrome.browser.share.android_share_sheet.AndroidShareSheetController;
import org.chromium.chrome.browser.share.link_to_text.LinkToTextHelper;
import org.chromium.chrome.browser.share.share_sheet.ShareSheetCoordinator;
import org.chromium.chrome.browser.tab.SadTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.share.ShareParams;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.components.ui_metrics.CanonicalURLResult;
import org.chromium.content_public.browser.WebContents;
import org.chromium.printing.PrintManagerDelegateImpl;
import org.chromium.printing.PrintingController;
import org.chromium.printing.PrintingControllerImpl;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;

import java.util.Set;

/** Implementation of share interface. Mostly a wrapper around ShareSheetCoordinator. */
public class ShareDelegateImpl implements ShareDelegate {
    static final String CANONICAL_URL_RESULT_HISTOGRAM = "Mobile.CanonicalURLResult";

    private final BottomSheetController mBottomSheetController;
    private final ActivityLifecycleDispatcher mLifecycleDispatcher;
    private final Supplier<Tab> mTabProvider;
    private final Supplier<TabModelSelector> mTabModelSelectorProvider;
    private final Supplier<Profile> mProfileSupplier;
    private final ShareSheetDelegate mDelegate;
    private final boolean mIsCustomTab;
    private long mShareStartTime;

    /**
     * Constructs a new {@link ShareDelegateImpl}.
     *
     * @param controller The BottomSheetController for the current activity.
     * @param lifecycleDispatcher Dispatcher for activity lifecycle events, e.g. configuration
     * changes.
     * @param tabProvider Supplier for the current activity tab.
     * @param tabModelSelectorProvider Supplier for the {@link TabModelSelector}. Used to determine
     * whether incognito mode is selected or not.
     * @param profileSupplier Supplier for {@link Profile}.
     * @param delegate The ShareSheetDelegate for the current activity.
     * @param isCustomTab This share delegate is associated with a CCT.
     */
    public ShareDelegateImpl(
            BottomSheetController controller,
            ActivityLifecycleDispatcher lifecycleDispatcher,
            Supplier<Tab> tabProvider,
            Supplier<TabModelSelector> tabModelSelectorProvider,
            Supplier<Profile> profileSupplier,
            ShareSheetDelegate delegate,
            boolean isCustomTab) {
        mBottomSheetController = controller;
        mLifecycleDispatcher = lifecycleDispatcher;
        mTabProvider = tabProvider;
        mTabModelSelectorProvider = tabModelSelectorProvider;
        mProfileSupplier = profileSupplier;
        mDelegate = delegate;
        mIsCustomTab = isCustomTab;
    }

    // ShareDelegate implementation.
    @Override
    public void share(
            ShareParams params, ChromeShareExtras chromeShareExtras, @ShareOrigin int shareOrigin) {
        if (mShareStartTime == 0L) {
            mShareStartTime = System.currentTimeMillis();
        }
        mDelegate.share(
                params,
                chromeShareExtras,
                mBottomSheetController,
                mLifecycleDispatcher,
                mTabProvider,
                mTabModelSelectorProvider,
                mProfileSupplier,
                this::printTab,
                shareOrigin,
                mShareStartTime,
                isSharingHubEnabled());
        mShareStartTime = 0;
    }

    // ShareDelegate implementation.
    @Override
    public void share(Tab currentTab, boolean shareDirectly, @ShareOrigin int shareOrigin) {
        mShareStartTime = System.currentTimeMillis();
        onShareSelected(currentTab, shareOrigin, shareDirectly);
    }

    /**
     * Triggered when the share menu item is selected.
     * This creates and shows a share intent picker dialog or starts a share intent directly.
     *
     * @param currentTab The current tab.
     * @param shareOrigin Where the share originated.
     * @param shareDirectly Whether it should share directly with the activity that was most
     * recently used to share.
     */
    private void onShareSelected(
            Tab currentTab, @ShareOrigin int shareOrigin, boolean shareDirectly) {
        if (currentTab == null) return;

        triggerShare(currentTab, shareOrigin, shareDirectly);
    }

    private void triggerShare(Tab currentTab, @ShareOrigin int shareOrigin, boolean shareDirectly) {
        OfflinePageUtils.maybeShareOfflinePage(
                currentTab,
                (ShareParams p) -> {
                    if (p != null) {
                        var extras =
                                new ChromeShareExtras.Builder().setIsUrlOfVisiblePage(true).build();
                        share(p, extras, shareOrigin);
                        return;
                    }
                    // Could not share as an offline page.
                    if (!shouldFetchCanonicalUrl(currentTab)) {
                        triggerShareWithCanonicalUrlResolved(
                                currentTab.getWindowAndroid(),
                                currentTab.getWebContents(),
                                currentTab.getTitle(),
                                currentTab.getUrl(),
                                GURL.emptyGURL(),
                                shareOrigin,
                                shareDirectly);
                    } else {
                        triggerShareWithUnresolvedUrl(currentTab, shareOrigin, shareDirectly);
                    }
                });
    }

    private void triggerShareWithUnresolvedUrl(
            Tab currentTab, @ShareOrigin int shareOrigin, boolean shareDirectly) {
        WindowAndroid window = currentTab.getWindowAndroid();
        WebContents webContents = currentTab.getWebContents();
        String title = currentTab.getTitle();
        GURL visibleUrl = currentTab.getUrl();
        webContents
                .getMainFrame()
                .getCanonicalUrlForSharing(
                        (GURL result) -> {
                            if (!LinkToTextHelper.hasTextFragment(visibleUrl)) {
                                logCanonicalUrlResult(visibleUrl, result);
                                triggerShareWithCanonicalUrlResolved(
                                        window,
                                        webContents,
                                        title,
                                        visibleUrl,
                                        result,
                                        shareOrigin,
                                        shareDirectly);
                                return;
                            }

                            LinkToTextHelper.getExistingSelectorsAllFrames(
                                    currentTab,
                                    (selectors) -> {
                                        GURL canonicalUrl =
                                                new GURL(
                                                        LinkToTextHelper.getUrlToShare(
                                                                result.getSpec(), selectors));
                                        logCanonicalUrlResult(visibleUrl, canonicalUrl);
                                        triggerShareWithCanonicalUrlResolved(
                                                window,
                                                webContents,
                                                title,
                                                visibleUrl,
                                                canonicalUrl,
                                                shareOrigin,
                                                shareDirectly);
                                    });
                        });
    }

    private void triggerShareWithCanonicalUrlResolved(
            final WindowAndroid window,
            final WebContents webContents,
            final String title,
            final @NonNull GURL visibleUrl,
            final GURL canonicalUrl,
            @ShareOrigin final int shareOrigin,
            final boolean shareDirectly) {
        share(
                new ShareParams.Builder(window, title, getUrlToShare(visibleUrl, canonicalUrl))
                        .build(),
                new ChromeShareExtras.Builder()
                        .setSaveLastUsed(!shareDirectly)
                        .setShareDirectly(shareDirectly)
                        .setIsUrlOfVisiblePage(true)
                        .build(),
                shareOrigin);

        HistoryClustersTabHelper.onCurrentTabUrlShared(webContents);
    }

    @VisibleForTesting
    static boolean shouldFetchCanonicalUrl(final Tab currentTab) {
        WebContents webContents = currentTab.getWebContents();
        if (webContents == null) return false;
        if (webContents.getMainFrame() == null) return false;
        if (currentTab.getUrl().isEmpty()) return false;
        if (currentTab.isShowingErrorPage() || SadTab.isShowing(currentTab)) {
            return false;
        }
        return true;
    }

    private static void logCanonicalUrlResult(GURL visibleUrl, GURL canonicalUrl) {
        @CanonicalURLResult int result = getCanonicalUrlResult(visibleUrl, canonicalUrl);
        RecordHistogram.recordEnumeratedHistogram(
                CANONICAL_URL_RESULT_HISTOGRAM,
                result,
                CanonicalURLResult.CANONICAL_URL_RESULT_COUNT);
    }

    @VisibleForTesting
    static String getUrlToShare(@NonNull GURL visibleUrl, GURL canonicalUrl) {
        if (canonicalUrl == null || canonicalUrl.isEmpty()) {
            return visibleUrl.getSpec();
        }
        if (!UrlConstants.HTTPS_SCHEME.equals(visibleUrl.getScheme())) {
            return visibleUrl.getSpec();
        }
        if (!UrlUtilities.isHttpOrHttps(canonicalUrl)) {
            return visibleUrl.getSpec();
        }
        return canonicalUrl.getSpec();
    }

    private static @CanonicalURLResult int getCanonicalUrlResult(
            GURL visibleUrl, GURL canonicalUrl) {
        if (!UrlConstants.HTTPS_SCHEME.equals(visibleUrl.getScheme())) {
            return CanonicalURLResult.FAILED_VISIBLE_URL_NOT_HTTPS;
        }
        if (canonicalUrl == null || canonicalUrl.isEmpty()) {
            return CanonicalURLResult.FAILED_NO_CANONICAL_URL_DEFINED;
        }
        String canonicalScheme = canonicalUrl.getScheme();
        if (!UrlConstants.HTTPS_SCHEME.equals(canonicalScheme)) {
            if (!UrlConstants.HTTP_SCHEME.equals(canonicalScheme)) {
                return CanonicalURLResult.FAILED_CANONICAL_URL_INVALID;
            } else {
                return CanonicalURLResult.SUCCESS_CANONICAL_URL_NOT_HTTPS;
            }
        }
        if (visibleUrl.equals(canonicalUrl)) {
            return CanonicalURLResult.SUCCESS_CANONICAL_URL_SAME_AS_VISIBLE;
        } else {
            return CanonicalURLResult.SUCCESS_CANONICAL_URL_DIFFERENT_FROM_VISIBLE;
        }
    }

    private void printTab(Tab tab) {
        Activity activity = mTabProvider.get().getWindowAndroid().getActivity().get();
        PrintingController printingController = PrintingControllerImpl.getInstance();
        if (printingController != null && !printingController.isBusy()) {
            printingController.startPrint(
                    new TabPrinter(mTabProvider.get()), new PrintManagerDelegateImpl(activity));
        }
    }

    @Override
    public boolean isSharingHubEnabled() {
        return !(mIsCustomTab || Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE);
    }

    /** Delegate for share handling. */
    public static class ShareSheetDelegate {
        /** Trigger the share action for the specified params. */
        void share(
                ShareParams params,
                ChromeShareExtras chromeShareExtras,
                BottomSheetController controller,
                ActivityLifecycleDispatcher lifecycleDispatcher,
                Supplier<Tab> tabProvider,
                Supplier<TabModelSelector> tabModelSelectorSupplier,
                Supplier<Profile> profileSupplier,
                Callback<Tab> printCallback,
                @ShareOrigin int shareOrigin,
                long shareStartTime,
                boolean sharingHubEnabled) {
            Profile profile = profileSupplier.get();
            if (profile == null) {
                assert false : "Unexpected null profile";
                return;
            }

            if (chromeShareExtras.shareDirectly()) {
                ShareHelper.shareWithLastUsedComponent(params);
            } else if (sharingHubEnabled) {
                // TODO(crbug.com/40132040): Sharing hub is suppressed for tab group sharing.
                // Re-enable it when tab group sharing is supported by sharing hub.
                RecordHistogram.recordEnumeratedHistogram(
                        "Sharing.SharingHubAndroid.Opened", shareOrigin, ShareOrigin.COUNT);
                ShareHelper.recordShareSource(ShareHelper.ShareSourceAndroid.CHROME_SHARE_SHEET);
                boolean isIncognito =
                        tabModelSelectorSupplier.hasValue()
                                && tabModelSelectorSupplier.get().isIncognitoSelected();
                ShareSheetCoordinator coordinator =
                        new ShareSheetCoordinator(
                                controller,
                                lifecycleDispatcher,
                                tabProvider,
                                printCallback,
                                new LargeIconBridge(profile),
                                isIncognito,
                                TrackerFactory.getTrackerForProfile(profile),
                                profile,
                                DeviceLockActivityLauncherImpl.get());
                coordinator.showInitialShareSheet(params, chromeShareExtras, shareStartTime);
                RecordHistogram.recordEnumeratedHistogram(
                        "Sharing.SharingHubAndroid.ShareContentType",
                        getShareContentType(params, chromeShareExtras),
                        ShareContentType.COUNT);
            } else {
                RecordHistogram.recordEnumeratedHistogram(
                        "Sharing.DefaultSharesheetAndroid.Opened", shareOrigin, ShareOrigin.COUNT);
                RecordHistogram.recordEnumeratedHistogram(
                        "Sharing.DefaultSharesheetAndroid.ShareContentType",
                        getShareContentType(params, chromeShareExtras),
                        ShareContentType.COUNT);
                AndroidShareSheetController.showShareSheet(
                        params,
                        chromeShareExtras,
                        controller,
                        tabProvider,
                        tabModelSelectorSupplier,
                        profileSupplier,
                        printCallback,
                        DeviceLockActivityLauncherImpl.get());
                RecordHistogram.recordEnumeratedHistogram(
                        "Sharing.SharingHubAndroid.ShareContentType",
                        getShareContentType(params, chromeShareExtras),
                        ShareContentType.COUNT);
            }
        }
    }

    // These values are recorded as histogram values. Entries should not be
    // renumbered and numeric values should never be reused.
    @IntDef({
        ShareContentType.UNKNOWN,
        ShareContentType.TEXT,
        ShareContentType.TEXT_WITH_LINK,
        ShareContentType.LINK,
        ShareContentType.IMAGE,
        ShareContentType.IMAGE_WITH_LINK,
        ShareContentType.FILES,
        ShareContentType.COUNT
    })
    @interface ShareContentType {
        int UNKNOWN = 0;
        int TEXT = 1;
        int TEXT_WITH_LINK = 2;
        int LINK = 3;
        int IMAGE = 4;
        int IMAGE_WITH_LINK = 5;
        int FILES = 6;

        int COUNT = 7;
    }

    static @ShareContentType int getShareContentType(
            ShareParams params, ChromeShareExtras chromeShareExtras) {
        @ContentType
        Set<Integer> types = ShareContentTypeHelper.getContentTypes(params, chromeShareExtras);
        if (types.contains(ContentType.OTHER_FILE_TYPE)) {
            return ShareContentType.FILES;
        }
        if (types.contains(ContentType.IMAGE_AND_LINK)) {
            return ShareContentType.IMAGE_WITH_LINK;
        }
        if (types.contains(ContentType.IMAGE)) {
            return ShareContentType.IMAGE;
        }
        if (types.contains(ContentType.HIGHLIGHTED_TEXT)
                || types.contains(ContentType.LINK_AND_TEXT)) {
            return ShareContentType.TEXT_WITH_LINK;
        }
        if (types.contains(ContentType.LINK_PAGE_NOT_VISIBLE)
                || types.contains(ContentType.LINK_PAGE_VISIBLE)) {
            return ShareContentType.LINK;
        }
        if (types.contains(ContentType.TEXT)) {
            return ShareContentType.TEXT;
        }
        return ShareContentType.UNKNOWN;
    }
}