chromium/chrome/browser/paint_preview/android/javatests/src/org/chromium/chrome/browser/paint_preview/TabbedPaintPreviewTest.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.paint_preview;

import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Parcel;

import androidx.annotation.NonNull;
import androidx.test.filters.MediumTest;

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.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.UnguessableToken;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.paint_preview.services.PaintPreviewTabService;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabClosureParams;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.paintpreview.browser.NativePaintPreviewServiceProvider;
import org.chromium.components.paintpreview.player.PlayerCompositorDelegate;
import org.chromium.components.paintpreview.player.PlayerManager;
import org.chromium.ui.util.TokenHolder;
import org.chromium.url.GURL;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/** Tests for the {@link TabbedPaintPreview} class. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class TabbedPaintPreviewTest {
    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    // Tell R8 not to break the ability to mock the class.
    @Mock private PaintPreviewTabService mUnused;

    private static final String TEST_URL = "/chrome/test/data/android/about.html";

    /** Implementation of {@link PlayerCompositorDelegate.Factory} for tests. */
    public static class TestCompositorDelegateFactory implements PlayerCompositorDelegate.Factory {
        @Override
        public PlayerCompositorDelegate create(
                NativePaintPreviewServiceProvider service,
                GURL url,
                String directoryKey,
                boolean mainFrameMode,
                @NonNull PlayerCompositorDelegate.CompositorListener compositorListener,
                Callback<Integer> compositorErrorCallback) {
            return new TestCompositorDelegate(
                    service,
                    0,
                    url,
                    directoryKey,
                    mainFrameMode,
                    compositorListener,
                    compositorErrorCallback);
        }

        @Override
        public PlayerCompositorDelegate createForCaptureResult(
                NativePaintPreviewServiceProvider service,
                long nativeCaptureResultPtr,
                GURL url,
                String directoryKey,
                boolean mainFrameMode,
                @NonNull PlayerCompositorDelegate.CompositorListener compositorListener,
                Callback<Integer> compositorErrorCallback) {
            Assert.fail("createForProto shouldn't be called");
            return null;
        }
    }

    @Before
    public void setUp() {
        PaintPreviewTabService mockService = Mockito.mock(PaintPreviewTabService.class);
        Mockito.doReturn(true).when(mockService).hasCaptureForTab(Mockito.anyInt());
        TabbedPaintPreview.overridePaintPreviewTabServiceForTesting(mockService);
        PlayerManager.overrideCompositorDelegateFactoryForTesting(
                new TestCompositorDelegateFactory());
        mActivityTestRule.startMainActivityWithURL(
                mActivityTestRule.getTestServer().getURL(TEST_URL));
    }

    @After
    public void tearDown() {
        PlayerManager.overrideCompositorDelegateFactoryForTesting(null);
        TabbedPaintPreview.overridePaintPreviewTabServiceForTesting(null);
    }

    /**
     * Tests that TabbedPaintPreview is displayed correctly if a paint preview for the current tab
     * has been captured before.
     */
    @Test
    @MediumTest
    public void testDisplayedCorrectly() throws ExecutionException, TimeoutException {
        Tab tab = mActivityTestRule.getActivity().getActivityTab();
        TabbedPaintPreview tabbedPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(() -> TabbedPaintPreview.get(tab));
        CallbackHelper viewReadyCallback = new CallbackHelper();
        CallbackHelper firstPaintCallback = new CallbackHelper();
        PlayerManager.Listener listener =
                new EmptyPlayerListener() {
                    @Override
                    public void onCompositorError(int status) {
                        Assert.fail(
                                "Paint Preview should have been displayed successfully"
                                        + "with no errors.");
                    }

                    @Override
                    public void onViewReady() {
                        viewReadyCallback.notifyCalled();
                    }

                    @Override
                    public void onFirstPaint() {
                        firstPaintCallback.notifyCalled();
                    }
                };

        boolean showed =
                ThreadUtils.runOnUiThreadBlocking(() -> tabbedPaintPreview.maybeShow(listener));
        Assert.assertTrue("Paint Preview failed to display.", showed);
        Assert.assertTrue("Paint Preview was not displayed.", tabbedPaintPreview.isShowing());
        Assert.assertTrue(
                "Paint Preview was not attached to tab.", tabbedPaintPreview.isAttached());
        viewReadyCallback.waitForOnly("Paint preview view ready never happened.");
        firstPaintCallback.waitForOnly("Paint preview first paint never happened.");
        ThreadUtils.runOnUiThreadBlocking(() -> tabbedPaintPreview.remove(false, false));
    }

    /**
     * Tests that we correctly make the browser controls persistent and non-persistent on showing
     * and hiding the paint preview, or the tab.
     */
    @Test
    @MediumTest
    public void testBrowserControlsPersistent() throws ExecutionException {
        TestControlsVisibilityDelegate visibilityDelegate =
                ThreadUtils.runOnUiThreadBlocking(TestControlsVisibilityDelegate::new);
        Tab tab = mActivityTestRule.getActivity().getActivityTab();
        TabbedPaintPreview tabbedPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(() -> TabbedPaintPreview.get(tab));
        tabbedPaintPreview.setBrowserVisibilityDelegate(visibilityDelegate);
        PlayerManager.Listener emptyListener = new EmptyPlayerListener();

        // Assert toolbar persistence is changed based on paint preview visibility.
        assertToolbarPersistence(false, visibilityDelegate);
        boolean showed =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> tabbedPaintPreview.maybeShow(emptyListener));
        Assert.assertTrue("Paint Preview failed to display.", showed);
        assertToolbarPersistence(true, visibilityDelegate);
        ThreadUtils.runOnUiThreadBlocking(() -> tabbedPaintPreview.remove(false, false));
        assertToolbarPersistence(false, visibilityDelegate);

        // Assert toolbar persistence is changed based visibility of the tab that is showing the
        // paint preview.
        showed =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> tabbedPaintPreview.maybeShow(emptyListener));
        Assert.assertTrue("Paint Preview failed to display.", showed);
        assertToolbarPersistence(true, visibilityDelegate);
        Tab newTab =
                mActivityTestRule.loadUrlInNewTab(
                        mActivityTestRule.getTestServer().getURL(TEST_URL));
        assertToolbarPersistence(false, visibilityDelegate);
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mActivityTestRule
                                .getActivity()
                                .getTabModelSelector()
                                .getCurrentModel()
                                .closeTabs(
                                        TabClosureParams.closeTab(newTab)
                                                .allowUndo(false)
                                                .build()));
        assertToolbarPersistence(true, visibilityDelegate);
    }

    /**
     * Tests that the progressbar behaves as expected when TabbedPaintPreview is showing.
     * Progressbar updates should be prevented when the current tab is showing a paint preview. A
     * progressbar fill simulation should be requested when paint preview is removed.
     */
    @Test
    @MediumTest
    public void testProgressbar() throws ExecutionException {
        Tab tab = mActivityTestRule.getActivity().getActivityTab();
        TabbedPaintPreview tabbedPaintPreview =
                ThreadUtils.runOnUiThreadBlocking(() -> TabbedPaintPreview.get(tab));

        CallbackHelper simulateCallback = new CallbackHelper();
        BooleanCallbackHelper preventionCallback = new BooleanCallbackHelper();
        tabbedPaintPreview.setProgressSimulatorNeededCallback(simulateCallback::notifyCalled);
        tabbedPaintPreview.setProgressbarUpdatePreventionCallback(preventionCallback::set);
        PlayerManager.Listener emptyListener = new EmptyPlayerListener();

        // Paint Preview not showing.
        Assert.assertEquals(
                "Progressbar simulate callback shouldn't have been called.",
                0,
                simulateCallback.getCallCount());
        assertProgressbarUpdatePreventionCallback(false, preventionCallback);

        // Paint Preview showing in the current tab.
        ThreadUtils.runOnUiThreadBlocking(() -> tabbedPaintPreview.maybeShow(emptyListener));
        assertProgressbarUpdatePreventionCallback(true, preventionCallback);

        // Switch to a new tab that doesn't show paint preview.
        Tab newTab =
                mActivityTestRule.loadUrlInNewTab(
                        mActivityTestRule.getTestServer().getURL(TEST_URL));
        assertProgressbarUpdatePreventionCallback(false, preventionCallback);

        // Close the new tab, we should be back at the old tab with the paint preview showing.
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        mActivityTestRule
                                .getActivity()
                                .getTabModelSelector()
                                .getCurrentModel()
                                .closeTabs(
                                        TabClosureParams.closeTab(newTab)
                                                .allowUndo(false)
                                                .build()));
        assertProgressbarUpdatePreventionCallback(true, preventionCallback);

        // Remove paint preview.
        Assert.assertEquals(
                "Should have not requested progressbar fill simulation.",
                0,
                simulateCallback.getCallCount());
        ThreadUtils.runOnUiThreadBlocking(() -> tabbedPaintPreview.remove(false, false));
        Assert.assertEquals(
                "Should have requested progressbar fill simulation.",
                1,
                simulateCallback.getCallCount());
        assertProgressbarUpdatePreventionCallback(false, preventionCallback);
    }

    private static class BooleanCallbackHelper {
        private boolean mLastResult;

        public boolean get() {
            return mLastResult;
        }

        public void set(boolean lastResult) {
            this.mLastResult = lastResult;
        }
    }

    private void assertProgressbarUpdatePreventionCallback(
            boolean expected, BooleanCallbackHelper callbackHelper) {
        String message =
                expected
                        ? "Progressbar updates should be prevented."
                        : "Progressbar updates should not be prevented.";
        CriteriaHelper.pollInstrumentationThread(() -> expected == callbackHelper.get(), message);
    }

    private void assertToolbarPersistence(
            boolean expected, TestControlsVisibilityDelegate visibilityDelegate) {
        String message =
                expected ? "Toolbar should be persistent." : "Toolbar should not be persistent.";
        CriteriaHelper.pollInstrumentationThread(
                () -> expected == visibilityDelegate.isPersistent(), message);
    }

    public static void assertAttachedAndShown(
            TabbedPaintPreview tabbedPaintPreview, boolean attached, boolean shown) {
        String attachedMessage =
                attached
                        ? "Paint Preview should be attached."
                        : "Paint Preview should not be attached.";
        String shownMessage =
                shown ? "Paint Preview should be shown." : "Paint Preview should not be shown.";
        CriteriaHelper.pollUiThread(
                () -> tabbedPaintPreview.isAttached() == attached, attachedMessage);
        CriteriaHelper.pollUiThread(() -> tabbedPaintPreview.isShowing() == shown, shownMessage);
    }

    public static void assertWasEverShown(
            TabbedPaintPreview tabbedPaintPreview, boolean expectedShown) {
        String message =
                expectedShown
                        ? "Paint Preview should have been shown, but never was."
                        : "Paint Preview should not have been shown, but was.";
        CriteriaHelper.pollUiThread(
                () -> tabbedPaintPreview.wasEverShown() == expectedShown, message);
    }

    private static class TestControlsVisibilityDelegate
            extends BrowserStateBrowserControlsVisibilityDelegate {
        private int mLastToken = TokenHolder.INVALID_TOKEN;

        public TestControlsVisibilityDelegate() {
            super(new ObservableSupplierImpl<>(false));
        }

        public boolean isPersistent() {
            return mLastToken != TokenHolder.INVALID_TOKEN;
        }

        @Override
        public int showControlsPersistent() {
            Assert.assertEquals(
                    "Lock toolbar persistence is called before releasing a " + "previous token.",
                    mLastToken,
                    TokenHolder.INVALID_TOKEN);
            mLastToken = super.showControlsPersistent();
            return mLastToken;
        }

        @Override
        public void releasePersistentShowingToken(int token) {
            Assert.assertEquals(
                    "Release toolbar persistence is called with the wrong" + "token.",
                    mLastToken,
                    token);
            super.releasePersistentShowingToken(token);
            mLastToken = TokenHolder.INVALID_TOKEN;
        }
    }

    /** Dummy implementation of {@link PlayerCompositorDelegate}. */
    public static class TestCompositorDelegate implements PlayerCompositorDelegate {
        private int mNextRequestId;

        TestCompositorDelegate(
                NativePaintPreviewServiceProvider service,
                long nativeCaptureResultPtr,
                GURL url,
                String directoryKey,
                boolean mainFrameMode,
                @NonNull CompositorListener compositorListener,
                Callback<Integer> compositorErrorCallback) {
            Assert.assertEquals(nativeCaptureResultPtr, 0);
            Assert.assertFalse(mainFrameMode);
            new Handler()
                    .postDelayed(
                            () -> {
                                Parcel parcel = Parcel.obtain();
                                parcel.writeLong(4577L);
                                parcel.writeLong(23L);
                                parcel.setDataPosition(0);
                                UnguessableToken token =
                                        UnguessableToken.CREATOR.createFromParcel(parcel);
                                compositorListener.onCompositorReady(
                                        token,
                                        new UnguessableToken[] {token},
                                        new int[] {500, 500},
                                        new int[] {0, 0},
                                        new int[] {0},
                                        null,
                                        null,
                                        0f,
                                        0);
                            },
                            250);
        }

        @Override
        public void addMemoryPressureListener(Runnable runnable) {}

        @Override
        public int requestBitmap(
                UnguessableToken frameGuid,
                Rect clipRect,
                float scaleFactor,
                Callback<Bitmap> bitmapCallback,
                Runnable errorCallback) {
            new Handler()
                    .postDelayed(
                            () -> {
                                Bitmap emptyBitmap =
                                        Bitmap.createBitmap(
                                                clipRect.width(),
                                                clipRect.height(),
                                                Bitmap.Config.ARGB_4444);
                                bitmapCallback.onResult(emptyBitmap);
                            },
                            100);
            int requestId = mNextRequestId;
            mNextRequestId++;
            return requestId;
        }

        @Override
        public int requestBitmap(
                Rect clipRect,
                float scaleFactor,
                Callback<Bitmap> bitmapCallback,
                Runnable errorCallback) {
            Assert.fail(
                    "The GUIDless version of TestCompositorDelegate#requestBitmap() shouldn't"
                            + " be called.");
            return 0;
        }

        @Override
        public boolean cancelBitmapRequest(int requestId) {
            return false;
        }

        @Override
        public void cancelAllBitmapRequests() {}

        @Override
        public GURL onClick(UnguessableToken frameGuid, int x, int y) {
            return null;
        }

        @Override
        public Point getRootFrameOffsets() {
            return new Point();
        }

        @Override
        public void setCompressOnClose(boolean compressOnClose) {}

        @Override
        public void destroy() {}
    }

    /** Blank implementation of {@link PlayerManager.Listener}. */
    public static class EmptyPlayerListener implements PlayerManager.Listener {
        @Override
        public void onCompositorError(int status) {}

        @Override
        public void onViewReady() {}

        @Override
        public void onFirstPaint() {}

        @Override
        public void onUserInteraction() {}

        @Override
        public void onUserFrustration() {}

        @Override
        public void onPullToRefresh() {}

        @Override
        public void onLinkClick(GURL url) {}

        @Override
        public boolean isAccessibilityEnabled() {
            return false;
        }

        @Override
        public void onAccessibilityNotSupported() {}
    }
}