chromium/chrome/android/junit/src/org/chromium/chrome/browser/share/ShareDelegateImplUnitTest.java

// Copyright 2023 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.net.Uri;

import androidx.annotation.NonNull;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;

import org.chromium.base.Callback;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.AppHooksImpl;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.share.ChromeShareExtras.DetailedContentType;
import org.chromium.chrome.browser.share.ShareDelegate.ShareOrigin;
import org.chromium.chrome.browser.share.ShareDelegateImpl.ShareContentType;
import org.chromium.chrome.browser.share.ShareDelegateImpl.ShareSheetDelegate;
import org.chromium.chrome.browser.share.ShareDelegateImplUnitTest.ShadowAndroidShareSheetController;
import org.chromium.chrome.browser.share.ShareDelegateImplUnitTest.ShadowShareHelper;
import org.chromium.chrome.browser.share.ShareDelegateImplUnitTest.ShadowShareSheetCoordinator;
import org.chromium.chrome.browser.share.android_share_sheet.AndroidShareSheetController;
import org.chromium.chrome.browser.share.share_sheet.ShareSheetCoordinator;
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.device_lock.DeviceLockActivityLauncher;
import org.chromium.components.browser_ui.share.ShareParams;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.components.favicon.LargeIconBridgeJni;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.JUnitTestGURLs;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

/** Unit test for {@link ShareDelegateImpl} that mocked out most native class calls. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
        shadows = {
            ShadowShareSheetCoordinator.class,
            ShadowShareHelper.class,
            ShadowAndroidShareSheetController.class,
        })
public class ShareDelegateImplUnitTest {
    @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
    @Rule public JniMocker mJniMocker = new JniMocker();

    @Mock private BottomSheetController mBottomSheetController;
    @Mock private Profile mProfile;
    @Mock private Tab mTab;
    @Mock private ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
    @Mock private TabModelSelector mTabModelSelector;
    @Mock private WindowAndroid mWindowAndroid;
    @Mock private Activity mActivity;
    @Mock private LargeIconBridgeJni mLargeIconBridgeJni;
    @Mock private AppHooksImpl mAppHooks;
    @Mock private Tracker mTracker;

    private ShareDelegateImpl mShareDelegate;

    private void createShareDelegate(boolean isCustomTab) {
        mShareDelegate =
                new ShareDelegateImpl(
                        mBottomSheetController,
                        mActivityLifecycleDispatcher,
                        (() -> mTab),
                        (() -> mTabModelSelector),
                        (() -> mProfile),
                        new ShareSheetDelegate(),
                        isCustomTab);
    }

    @Before
    public void setup() {
        mJniMocker.mock(LargeIconBridgeJni.TEST_HOOKS, mLargeIconBridgeJni);
        AppHooks.setInstanceForTesting(mAppHooks);
        TrackerFactory.setTrackerForTests(mTracker);
        Mockito.doReturn(new WeakReference<>(mActivity)).when(mWindowAndroid).getActivity();
        createShareDelegate(false);
    }

    @After
    public void tearDown() {
        ShadowShareSheetCoordinator.reset();
        ShadowShareHelper.reset();
        ShadowAndroidShareSheetController.reset();
    }

    @Test
    public void shareWithSharingHub() {
        Assert.assertTrue("ShareHub not enabled.", mShareDelegate.isSharingHubEnabled());

        HistogramWatcher histogramWatcher =
                HistogramWatcher.newBuilder()
                        .expectAnyRecord("Sharing.SharingHubAndroid.ShareContentType")
                        .expectAnyRecord("Sharing.SharingHubAndroid.Opened")
                        .build();
        ShareParams shareParams = new ShareParams.Builder(mWindowAndroid, "", "").build();
        ChromeShareExtras chromeShareExtras = new ChromeShareExtras.Builder().build();
        mShareDelegate.share(shareParams, chromeShareExtras, ShareOrigin.OVERFLOW_MENU);

        Assert.assertTrue(
                "ShareSheetCoordinator not used.",
                ShadowShareSheetCoordinator.sChromeShareSheetShowed);
        histogramWatcher.assertExpected();
    }

    @Test
    public void shareLastUsedComponent() {
        Assert.assertTrue("ShareHub not enabled.", mShareDelegate.isSharingHubEnabled());

        HistogramWatcher histogramWatcher =
                HistogramWatcher.newBuilder()
                        .expectNoRecords("Sharing.SharingHubAndroid.ShareContentType")
                        .expectNoRecords("Sharing.SharingHubAndroid.Opened")
                        .build();
        ShareParams shareParams = new ShareParams.Builder(mWindowAndroid, "", "").build();
        ChromeShareExtras chromeShareExtras =
                new ChromeShareExtras.Builder().setShareDirectly(true).build();
        mShareDelegate.share(shareParams, chromeShareExtras, ShareOrigin.OVERFLOW_MENU);

        Assert.assertFalse(
                "ShareSheetCoordinator should not be used.",
                ShadowShareSheetCoordinator.sChromeShareSheetShowed);
        Assert.assertTrue(
                "ShareWithLastUsedComponentCalled not called.",
                ShadowShareHelper.sShareWithLastUsedComponentCalled);
        histogramWatcher.assertExpected();
    }

    @Test
    @Config(sdk = 34)
    public void shareWithAndroidShareSheetForU() {
        Assert.assertFalse("ShareHub enabled.", mShareDelegate.isSharingHubEnabled());

        HistogramWatcher histogramWatcher =
                HistogramWatcher.newBuilder()
                        .expectAnyRecord("Sharing.DefaultSharesheetAndroid.ShareContentType")
                        .expectAnyRecord("Sharing.DefaultSharesheetAndroid.Opened")
                        .build();

        ShareParams shareParams = new ShareParams.Builder(mWindowAndroid, "", "").build();
        ChromeShareExtras chromeShareExtras = new ChromeShareExtras.Builder().build();
        mShareDelegate.share(shareParams, chromeShareExtras, ShareOrigin.OVERFLOW_MENU);

        Assert.assertFalse(
                "ShareSheetCoordinator should not be used.",
                ShadowShareSheetCoordinator.sChromeShareSheetShowed);
        Assert.assertTrue(
                "shareWithSystemShareSheetUi not called.",
                ShadowAndroidShareSheetController.sShareWithSystemShareSheetUiCalled);
        histogramWatcher.assertExpected();
    }

    @Test
    public void androidShareSheetDisableNonU() {
        Assert.assertTrue("ShareHub should be enabled T-.", mShareDelegate.isSharingHubEnabled());
    }

    @Test
    public void testGetShareContentType_link() {
        ShareParams params =
                new ShareParams.Builder(mWindowAndroid, "", JUnitTestGURLs.EXAMPLE_URL.getSpec())
                        .setBypassFixingDomDistillerUrl(true)
                        .build();
        ChromeShareExtras extras = new ChromeShareExtras.Builder().build();
        Assert.assertEquals(
                "Expected ShareContentType.LINK.",
                ShareContentType.LINK,
                ShareDelegateImpl.getShareContentType(params, extras));

        params =
                new ShareParams.Builder(
                                mWindowAndroid, "title", JUnitTestGURLs.EXAMPLE_URL.getSpec())
                        .setPreviewImageUri(Uri.parse("content://path/to/preview"))
                        .setBypassFixingDomDistillerUrl(true)
                        .build();
        extras = new ChromeShareExtras.Builder().build();
        Assert.assertEquals(
                "Title and preview does not impact types. Expected ShareContentType.LINK.",
                ShareContentType.LINK,
                ShareDelegateImpl.getShareContentType(params, extras));
    }

    @Test
    public void testGetShareContentType_linkWithText() {
        ShareParams params =
                new ShareParams.Builder(mWindowAndroid, "", JUnitTestGURLs.EXAMPLE_URL.getSpec())
                        .setBypassFixingDomDistillerUrl(true)
                        .setText("text")
                        .build();
        ChromeShareExtras extras = new ChromeShareExtras.Builder().build();
        Assert.assertEquals(
                "Expected ShareContentType.TEXT_WITH_LINK.",
                ShareContentType.TEXT_WITH_LINK,
                ShareDelegateImpl.getShareContentType(params, extras));

        params =
                new ShareParams.Builder(
                                mWindowAndroid, "", JUnitTestGURLs.TEXT_FRAGMENT_URL.getSpec())
                        .setBypassFixingDomDistillerUrl(true)
                        .setText("text")
                        .setLinkToTextSuccessful(true)
                        .build();
        extras =
                new ChromeShareExtras.Builder()
                        .setDetailedContentType(DetailedContentType.HIGHLIGHTED_TEXT)
                        .build();
        Assert.assertEquals(
                "Expected ShareContentType.TEXT_WITH_LINK.",
                ShareContentType.TEXT_WITH_LINK,
                ShareDelegateImpl.getShareContentType(params, extras));
    }

    @Test
    public void testGetShareContentType_Image() {
        ShareParams params =
                new ShareParams.Builder(mWindowAndroid, "", "")
                        .setBypassFixingDomDistillerUrl(true)
                        .setSingleImageUri(Uri.parse("content://path/to/image1"))
                        .setFileContentType("image/png")
                        .build();
        ChromeShareExtras extras = new ChromeShareExtras.Builder().build();
        Assert.assertEquals(
                "Expected ShareContentType.IMAGE.",
                ShareContentType.IMAGE,
                ShareDelegateImpl.getShareContentType(params, extras));

        // Multiple image should be the same.
        params =
                new ShareParams.Builder(mWindowAndroid, "", "")
                        .setBypassFixingDomDistillerUrl(true)
                        .setFileUris(
                                new ArrayList<>(
                                        List.of(
                                                Uri.parse("content://path/to/image1"),
                                                Uri.parse("content://path/to/image2"))))
                        .setLinkToTextSuccessful(true)
                        .setFileContentType("image/png")
                        .build();
        extras =
                new ChromeShareExtras.Builder()
                        .setDetailedContentType(DetailedContentType.HIGHLIGHTED_TEXT)
                        .build();
        Assert.assertEquals(
                "Expected ShareContentType.IMAGE.",
                ShareContentType.IMAGE,
                ShareDelegateImpl.getShareContentType(params, extras));
    }

    @Test
    public void testGetShareContentType_imageWithLink() {
        ShareParams params =
                new ShareParams.Builder(mWindowAndroid, "", JUnitTestGURLs.EXAMPLE_URL.getSpec())
                        .setBypassFixingDomDistillerUrl(true)
                        .setSingleImageUri(Uri.parse("content://path/to/image1"))
                        .setFileContentType("image/png")
                        .setText("text") // text is ignored.
                        .build();
        ChromeShareExtras extras = new ChromeShareExtras.Builder().build();
        Assert.assertEquals(
                "Expected ShareContentType.IMAGE_WITH_LINK.",
                ShareContentType.IMAGE_WITH_LINK,
                ShareDelegateImpl.getShareContentType(params, extras));
    }

    @Test
    public void testGetShareContentType_files() {
        ShareParams params =
                new ShareParams.Builder(mWindowAndroid, "", JUnitTestGURLs.EXAMPLE_URL.getSpec())
                        .setBypassFixingDomDistillerUrl(true)
                        .setSingleImageUri(Uri.parse("content://path/to/video1"))
                        .setFileContentType("video/mp4")
                        .setText("text") // text is ignored.
                        .build();
        ChromeShareExtras extras = new ChromeShareExtras.Builder().build();
        Assert.assertEquals(
                "Expected ShareContentType.FILES.",
                ShareContentType.FILES,
                ShareDelegateImpl.getShareContentType(params, extras));
    }

    @Test
    public void testGetShareContentType_text() {
        ShareParams params =
                new ShareParams.Builder(mWindowAndroid, "", "")
                        .setBypassFixingDomDistillerUrl(true)
                        .setPreviewImageUri(
                                Uri.parse("content://path/to/preview")) // preview is ignored.
                        .setText("text")
                        .build();
        ChromeShareExtras extras = new ChromeShareExtras.Builder().build();
        Assert.assertEquals(
                "Expected ShareContentType.TEXT.",
                ShareContentType.TEXT,
                ShareDelegateImpl.getShareContentType(params, extras));
    }

    @Test
    public void testGetShareContentType_unknown() {
        ShareParams params =
                new ShareParams.Builder(mWindowAndroid, "", "")
                        .setBypassFixingDomDistillerUrl(true)
                        .build();
        ChromeShareExtras extras = new ChromeShareExtras.Builder().build();
        Assert.assertEquals(
                "Expected ShareContentType.UNKNOWN.",
                ShareContentType.UNKNOWN,
                ShareDelegateImpl.getShareContentType(params, extras));
    }

    @Implements(ShareHelper.class)
    static class ShadowShareHelper {
        static boolean sShareWithLastUsedComponentCalled;

        @Implementation
        protected static void shareWithLastUsedComponent(@NonNull ShareParams params) {
            sShareWithLastUsedComponentCalled = true;
        }

        public static void reset() {
            sShareWithLastUsedComponentCalled = false;
        }
    }

    /** Convenient class to avoid creating the real ShareSheetDelegate. */
    @Implements(ShareSheetCoordinator.class)
    public static class ShadowShareSheetCoordinator {
        static boolean sChromeShareSheetShowed;

        public ShadowShareSheetCoordinator() {}

        @Implementation
        protected void __constructor__(
                BottomSheetController controller,
                ActivityLifecycleDispatcher lifecycleDispatcher,
                Supplier<Tab> tabProvider,
                Callback<Tab> printTab,
                LargeIconBridge iconBridge,
                boolean isIncognito,
                Tracker featureEngagementTracker,
                Profile profile) {
            // Leave blank to avoid creating unnecessary objects.
        }

        @Implementation
        protected void showInitialShareSheet(
                ShareParams params, ChromeShareExtras chromeShareExtras, long shareStartTime) {
            sChromeShareSheetShowed = true;
        }

        public static void reset() {
            sChromeShareSheetShowed = false;
        }
    }

    @Implements(AndroidShareSheetController.class)
    static class ShadowAndroidShareSheetController {
        static boolean sShareWithSystemShareSheetUiCalled;

        // Directly call share helper, as we don't care about whether the right params are used in
        // this test.
        @Implementation
        public static void showShareSheet(
                ShareParams params,
                ChromeShareExtras chromeShareExtras,
                BottomSheetController controller,
                Supplier<Tab> tabProvider,
                Supplier<TabModelSelector> tabModelSelectorSupplier,
                Supplier<Profile> profileSupplier,
                Callback<Tab> printCallback,
                DeviceLockActivityLauncher deviceLockActivityLauncher) {
            sShareWithSystemShareSheetUiCalled = true;
        }

        public static void reset() {
            sShareWithSystemShareSheetUiCalled = false;
        }
    }
}