chromium/ui/android/javatests/src/org/chromium/ui/base/ClipboardAndroidTest.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.ui.base;

import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;

import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
import org.chromium.ui.test.util.BlankUiTestActivityTestCase;

import java.util.concurrent.TimeoutException;

/**
 * Clipboard tests for Android platform that depend on access to the ClipboardManager.
 *
 * This test suite can fail on Android 10+ if the activity does not maintain focus during testing.
 * For more information see: https://crbug.com/1297678 and
 * https://developer.android.com/about/versions/10/privacy/changes#clipboard-data
 */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.UNIT_TESTS)
public class ClipboardAndroidTest extends BlankUiTestActivityTestCase {
    private static final String TEXT_URL = "http://www.foo.com/";
    private static final String MIX_TEXT_URL = "test http://www.foo.com http://www.bar.com";
    private static final String MIX_TEXT_URL_NO_PROTOCOL = "test www.foo.com www.bar.com";

    @Override
    public void setUpTest() throws Exception {
        super.setUpTest();
        NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();
    }

    @Override
    public void tearDownTest() throws Exception {
        Clipboard.cleanupNativeForTesting();

        // Clear the clipboard to avoid leaving any state.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ClipboardManager clipboardManager =
                            (ClipboardManager)
                                    getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
                    ClipData clipData = ClipData.newPlainText("", "");
                    clipboardManager.setPrimaryClip(clipData);
                });
        super.tearDownTest();
    }

    /**
     * Test that if another application writes some text to the pasteboard the clipboard properly
     * invalidates other types.
     */
    @Test
    @SmallTest
    @DisabledTest(message = "https://crbug.com/1413839")
    public void internalClipboardInvalidation() throws TimeoutException {
        // Write to the clipboard in native and ensure that is propagated to the platform clipboard.
        final String originalText = "foo";
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(
                            "Original text was not written to the native clipboard.",
                            ClipboardAndroidTestSupport.writeHtml(originalText));
                });

        CallbackHelper helper = new CallbackHelper();
        ClipboardManager.OnPrimaryClipChangedListener clipboardChangedListener =
                new ClipboardManager.OnPrimaryClipChangedListener() {
                    @Override
                    public void onPrimaryClipChanged() {
                        helper.notifyCalled();
                    }
                };

        // Assert that the ClipboardManager contains the original text. Then simulate another
        // application writing to the clipboard.
        final String invalidatingText = "Hello, World!";
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ClipboardManager clipboardManager =
                            (ClipboardManager)
                                    getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
                    clipboardManager.addPrimaryClipChangedListener(clipboardChangedListener);

                    Assert.assertEquals(
                            "Original text not found in ClipboardManager.",
                            originalText,
                            Clipboard.getInstance()
                                    .clipDataToHtmlText(clipboardManager.getPrimaryClip()));

                    clipboardManager.setPrimaryClip(ClipData.newPlainText(null, invalidatingText));
                });

        helper.waitForCallback("ClipboardManager did not notify of PrimaryClip change.", 0);

        // Assert that the overwrite from another application is registered by the native clipboard.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(
                            "Invalidating text not found in the native clipboard.",
                            ClipboardAndroidTestSupport.clipboardContains(invalidatingText));

                    ClipboardManager clipboardManager =
                            (ClipboardManager)
                                    getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
                    clipboardManager.removePrimaryClipChangedListener(clipboardChangedListener);
                });
    }

    @Test
    @SmallTest
    public void hasHTMLOrStyledTextForNormalTextTest() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Clipboard.getInstance().setText("SampleTextToCopy");
                    Assert.assertFalse(Clipboard.getInstance().hasHTMLOrStyledText());
                });
    }

    @Test
    @SmallTest
    public void hasHTMLOrStyledTextForStyledTextTest() {
        SpannableString spanString = new SpannableString("SpannableString");
        spanString.setSpan(new BackgroundColorSpan(0), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        ClipData clipData =
                ClipData.newPlainText("text", spanString.subSequence(0, spanString.length() - 1));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ((ClipboardImpl) Clipboard.getInstance()).setPrimaryClipNoException(clipData);
                    Assert.assertTrue(Clipboard.getInstance().hasHTMLOrStyledText());
                });
    }

    @Test
    @SmallTest
    public void hasHTMLOrStyledTextForHtmlTextTest() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Clipboard.getInstance()
                            .setHTMLText(
                                    "<span style=\"color: red;\">HTMLTextToCopy</span>",
                                    "HTMLTextToCopy");
                    Assert.assertTrue(Clipboard.getInstance().hasHTMLOrStyledText());
                });
    }

    @Test
    @SmallTest
    public void hasUrlAndGetUrlTest() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Clipboard.getInstance().setText(TEXT_URL);
                });

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(Clipboard.getInstance().hasUrl(), Matchers.is(true));
                    Criteria.checkThat(Clipboard.getInstance().getUrl(), Matchers.is(TEXT_URL));
                });
    }

    // Only first URL is returned on S+ if clipboard contains multiple URLs.
    @Test
    @SmallTest
    @MinAndroidSdkLevel(Build.VERSION_CODES.S)
    public void hasUrlAndGetUrlMixTextAndLinkTest() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Clipboard.getInstance().setText(MIX_TEXT_URL);
                });

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(Clipboard.getInstance().hasUrl(), Matchers.is(true));
                    Criteria.checkThat(Clipboard.getInstance().getUrl(), Matchers.is(TEXT_URL));
                });
    }

    // Only first URL is returned on S+ if clipboard contains multiple URLs.
    @Test
    @SmallTest
    @MinAndroidSdkLevel(Build.VERSION_CODES.S)
    public void hasUrlAndGetUrlMixTextAndLinkWithoutProtocolTest() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Clipboard.getInstance().setText(MIX_TEXT_URL_NO_PROTOCOL);
                });

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(Clipboard.getInstance().hasUrl(), Matchers.is(true));
                    Criteria.checkThat(Clipboard.getInstance().getUrl(), Matchers.is(TEXT_URL));
                });
    }
}