chromium/components/browser_ui/photo_picker/android/java/src/org/chromium/components/browser_ui/photo_picker/PhotoPickerDialogTest.java

// Copyright 2017 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.components.browser_ui.photo_picker;

import android.content.ContentResolver;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.StrictMode;
import android.provider.MediaStore;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.widget.Button;

import androidx.recyclerview.widget.RecyclerView;
import androidx.test.filters.LargeTest;

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.Mockito;

import org.chromium.base.MathUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.DisableIf;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.base.test.util.TestAnimations.EnableAnimations;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.components.browser_ui.widget.RecyclerViewTestUtils;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate;
import org.chromium.components.browser_ui.widget.selectable_list.SelectionDelegate.SelectionObserver;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.ui.base.ActivityWindowAndroid;
import org.chromium.ui.base.IntentRequestTracker;
import org.chromium.ui.base.PhotoPickerListener;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.test.util.BlankUiTestActivityTestCase;
import org.chromium.ui.test.util.RenderTestRule;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/** Tests for the PhotoPickerDialog class. */
@RunWith(BaseJUnit4ClassRunner.class)
public class PhotoPickerDialogTest extends BlankUiTestActivityTestCase
        implements PhotoPickerListener,
                SelectionObserver<PickerBitmap>,
                DecoderServiceHost.DecoderStatusCallback,
                PickerVideoPlayer.VideoPlaybackStatusCallback,
                AnimationListener {
    // The timeout (in seconds) to wait for the decoder service to be ready.
    private static final long WAIT_TIMEOUT_SECONDS = 30L;
    private static final long VIDEO_TIMEOUT_SECONDS = 10L;

    @Rule
    public RenderTestRule mRenderTestRule =
            RenderTestRule.Builder.withPublicCorpus()
                    .setBugComponent(RenderTestRule.Component.UI_BROWSER_MEDIA_PICKER)
                    .build();

    private WindowAndroid mWindowAndroid;

    // The dialog we are testing.
    private PhotoPickerDialog mDialog;

    // The data to show in the dialog (A map of filepath to last-modified time).
    // Map<String, Long> mTestFiles;
    private List<PickerBitmap> mTestFiles;

    // The selection delegate for the dialog.
    private SelectionDelegate<PickerBitmap> mSelectionDelegate;

    // The last action recorded in the dialog (e.g. photo selected).
    private @PhotoPickerAction int mLastActionRecorded;

    // The final set of photos picked by the dialog. Can be an empty array, if
    // nothing was selected.
    private Uri[] mLastSelectedPhotos;

    // A list of view IDs we receive from an animating event in the order the events occurred.
    private List<Long> mLastViewAnimatingIds = new ArrayList();

    // A list of view alpha values we receive from an animating event in the order the events
    // occurred.
    private List<Float> mLastViewAnimatingAlphas = new ArrayList();

    // The list of currently selected photos (built piecemeal).
    private List<PickerBitmap> mCurrentPhotoSelection;

    // True when {@link onPhotoPickerDismissed} has been called.
    private boolean mDismissed;

    // A callback that fires when something is selected in the dialog.
    public final CallbackHelper mOnSelectionCallback = new CallbackHelper();

    // A callback that fires when an action is taken in the dialog (cancel/done etc).
    public final CallbackHelper mOnActionCallback = new CallbackHelper();

    // A callback that fires when the decoder is ready.
    public final CallbackHelper mOnDecoderReadyCallback = new CallbackHelper();

    // A callback that fires when the decoder is idle.
    public final CallbackHelper mOnDecoderIdleCallback = new CallbackHelper();

    // A callback that fires when a PickerBitmapView is animated in the dialog.
    public final CallbackHelper mOnAnimatedCallback = new CallbackHelper();

    // A callback that fires when playback starts for a video.
    public final CallbackHelper mOnVideoPlayingCallback = new CallbackHelper();

    // A callback that fires when playback ends for a video.
    public final CallbackHelper mOnVideoEndedCallback = new CallbackHelper();

    // A callback that fires when overlay controls finish animating.
    public final CallbackHelper mOnVideoAnimationEndCallback = new CallbackHelper();

    @Before
    public void setUp() throws Exception {
        NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();
        mWindowAndroid =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return new ActivityWindowAndroid(
                                    getActivity(),
                                    /* listenToActivityState= */ true,
                                    IntentRequestTracker.createFromActivity(getActivity()));
                        });
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    DecoderServiceHost.setIntentSupplier(
                            () -> {
                                return new Intent(getActivity(), TestImageDecoderService.class);
                            });
                });
        PickerVideoPlayer.setProgressCallback(this);
        PickerBitmapView.setAnimationListenerForTest(this);
        DecoderServiceHost.setStatusCallback(this);
    }

    @After
    public void tearDown() throws Exception {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mWindowAndroid.destroy();
                });
    }

    private void setupTestFiles() {
        mTestFiles = new ArrayList<>();
        mTestFiles.add(new PickerBitmap(Uri.parse("a"), 5L, PickerBitmap.TileTypes.PICTURE));
        mTestFiles.add(new PickerBitmap(Uri.parse("b"), 4L, PickerBitmap.TileTypes.PICTURE));
        mTestFiles.add(new PickerBitmap(Uri.parse("c"), 3L, PickerBitmap.TileTypes.PICTURE));
        mTestFiles.add(new PickerBitmap(Uri.parse("d"), 2L, PickerBitmap.TileTypes.PICTURE));
        mTestFiles.add(new PickerBitmap(Uri.parse("e"), 1L, PickerBitmap.TileTypes.PICTURE));
        mTestFiles.add(new PickerBitmap(Uri.parse("f"), 0L, PickerBitmap.TileTypes.PICTURE));
        PickerCategoryView.setTestFiles(mTestFiles);
    }

    private void setupTestFilesWith80ColoredSquares() {
        mTestFiles = new ArrayList<>();
        String green = "green100x100.jpg";
        String yellow = "yellow100x100.jpg";
        String red = "red100x100.jpg";
        String blue = "blue100x100.jpg";
        String filePath =
                UrlUtils.getIsolatedTestFilePath("chrome/test/data/android/photo_picker/");

        // The actual value of lastModified is not important, except that each entry must have a
        // unique lastModified stamp in order to ensure a stable order (tiles are ordered in
        // descending order by lastModified). Also, by decrementing this when adding entries (as
        // opposed to incrementing) the tiles will appear in same order as they are added.
        long lastModified = 1000;
        for (int i = 0; i < 50; ++i) {
            mTestFiles.add(
                    new PickerBitmap(
                            Uri.fromFile(new File(filePath + green)),
                            lastModified--,
                            PickerBitmap.TileTypes.PICTURE));
            mTestFiles.add(
                    new PickerBitmap(
                            Uri.fromFile(new File(filePath + yellow)),
                            lastModified--,
                            PickerBitmap.TileTypes.PICTURE));
            mTestFiles.add(
                    new PickerBitmap(
                            Uri.fromFile(new File(filePath + red)),
                            lastModified--,
                            PickerBitmap.TileTypes.PICTURE));
            mTestFiles.add(
                    new PickerBitmap(
                            Uri.fromFile(new File(filePath + blue)),
                            lastModified--,
                            PickerBitmap.TileTypes.PICTURE));
        }
        PickerCategoryView.setTestFiles(mTestFiles);
    }

    // PhotoPickerDialog.PhotoPickerListener:

    @Override
    public void onPhotoPickerUserAction(@PhotoPickerAction int action, Uri[] photos) {
        mLastActionRecorded = action;
        mLastSelectedPhotos = photos != null ? photos.clone() : null;
        if (mLastSelectedPhotos != null) Arrays.sort(mLastSelectedPhotos);
        mOnActionCallback.notifyCalled();
    }

    @Override
    public void onPhotoPickerDismissed() {
        Assert.assertFalse(mDismissed);
        mDismissed = true;
    }

    // DecoderServiceHost.DecoderStatusCallback:

    @Override
    public void serviceReady() {
        mOnDecoderReadyCallback.notifyCalled();
    }

    @Override
    public void decoderIdle() {
        mOnDecoderIdleCallback.notifyCalled();
    }

    // PickerCategoryView.VideoStatusCallback:

    @Override
    public void onVideoPlaying() {
        mOnVideoPlayingCallback.notifyCalled();
    }

    @Override
    public void onVideoEnded() {
        mOnVideoEndedCallback.notifyCalled();
    }

    @Override
    public void onAnimationStart(long viewId, float currentAlpha) {
        mLastViewAnimatingIds.add(viewId);
        mLastViewAnimatingAlphas.add(currentAlpha);
    }

    @Override
    public void onAnimationCancel(long viewId, float currentAlpha) {
        mLastViewAnimatingIds.add(viewId);
        mLastViewAnimatingAlphas.add(currentAlpha);
    }

    @Override
    public void onAnimationEnd(long viewId, float currentAlpha) {
        mLastViewAnimatingIds.add(viewId);
        mLastViewAnimatingAlphas.add(currentAlpha);

        mOnVideoAnimationEndCallback.notifyCalled();
    }

    // SelectionObserver:

    @Override
    public void onSelectionStateChange(List<PickerBitmap> photosSelected) {
        mCurrentPhotoSelection = new ArrayList<>(photosSelected);
        mOnSelectionCallback.notifyCalled();
    }

    // AnimationListener:
    @Override
    public void onAnimationStart(Animation animation) {}

    @Override
    public void onAnimationEnd(Animation animation) {
        mOnAnimatedCallback.notifyCalled();
    }

    @Override
    public void onAnimationRepeat(Animation animation) {}

    private RecyclerView getRecyclerView() {
        return (RecyclerView) mDialog.findViewById(R.id.selectable_list_recycler_view);
    }

    private PhotoPickerDialog createDialogWithContentResolver(
            final ContentResolver contentResolver,
            final boolean multiselect,
            final List<String> mimeTypes)
            throws Exception {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    final PhotoPickerDialog dialog =
                            new PhotoPickerDialog(
                                    mWindowAndroid,
                                    contentResolver,
                                    PhotoPickerDialogTest.this,
                                    multiselect,
                                    mimeTypes);
                    dialog.show();
                    mSelectionDelegate =
                            dialog.getCategoryViewForTesting().getSelectionDelegateForTesting();
                    if (!multiselect) mSelectionDelegate.setSingleSelectionMode();
                    mSelectionDelegate.addObserver(this);
                    mDialog = dialog;
                    return dialog;
                });
    }

    private PhotoPickerDialog createDialog(final boolean multiselect, final List<String> mimeTypes)
            throws Exception {
        return createDialogWithContentResolver(
                getActivity().getContentResolver(), multiselect, mimeTypes);
    }

    private void waitForDecoder() throws Exception {
        int callCount = mOnDecoderReadyCallback.getCallCount();
        mOnDecoderReadyCallback.waitForCallback(
                callCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
    }

    private void waitForDecoderIdle() throws Exception {
        int callCount = mOnDecoderIdleCallback.getCallCount();
        mOnDecoderIdleCallback.waitForCallback(
                callCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
    }

    private void clickView(final int position, final int expectedSelectionCount) throws Exception {
        RecyclerView recyclerView = getRecyclerView();
        RecyclerViewTestUtils.waitForView(recyclerView, position);

        int callCount = mOnSelectionCallback.getCallCount();
        TouchCommon.singleClickView(
                recyclerView.findViewHolderForAdapterPosition(position).itemView);
        mOnSelectionCallback.waitForCallback(callCount, 1);

        // Validate the correct selection took place.
        Assert.assertEquals(expectedSelectionCount, mCurrentPhotoSelection.size());
        Assert.assertTrue(mSelectionDelegate.isItemSelected(mTestFiles.get(position)));
    }

    private void clickDone() throws Exception {
        mLastActionRecorded = PhotoPickerAction.NUM_ENTRIES;

        PhotoPickerToolbar toolbar = (PhotoPickerToolbar) mDialog.findViewById(R.id.action_bar);
        Button done = (Button) toolbar.findViewById(R.id.done);
        int callCount = mOnActionCallback.getCallCount();
        TouchCommon.singleClickView(done);
        mOnActionCallback.waitForCallback(callCount, 1);
        Assert.assertEquals(PhotoPickerAction.PHOTOS_SELECTED, mLastActionRecorded);
        Assert.assertTrue(
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return mDismissed;
                        }));
    }

    private void clickCancel() throws Exception {
        mLastActionRecorded = PhotoPickerAction.NUM_ENTRIES;

        PickerCategoryView categoryView = mDialog.getCategoryViewForTesting();
        View cancel = new View(getActivity());
        int callCount = mOnActionCallback.getCallCount();
        categoryView.onClick(cancel);
        mOnActionCallback.waitForCallback(callCount, 1);
        Assert.assertEquals(PhotoPickerAction.CANCEL, mLastActionRecorded);
        Assert.assertTrue(
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return mDismissed;
                        }));
    }

    private void playVideo(Uri uri) throws Exception {
        int callCount = mOnVideoPlayingCallback.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mDialog.getCategoryViewForTesting().startVideoPlaybackAsync(uri);
                });
        mOnVideoPlayingCallback.waitForCallback(
                callCount, 1, VIDEO_TIMEOUT_SECONDS, TimeUnit.SECONDS);
    }

    private void dismissDialog() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mDialog.dismiss();
                    Assert.assertTrue(mDismissed);
                });
    }

    /**
     * Tests what happens when the ContentResolver returns a null cursor when query() is called (a
     * regression test for https://crbug.com/1072415). Note: This test does not call
     * setupTestFiles() so that the real FileEnumWorkerTask is used.
     */
    @Test
    @LargeTest
    public void testNoCrashWhenContentResolverQueryReturnsNull() throws Throwable {
        ContentResolver contentResolver = Mockito.mock(ContentResolver.class);
        Uri contentUri = MediaStore.Files.getContentUri("external");
        Mockito.doReturn(null)
                .when(contentResolver)
                .query(contentUri, new String[] {}, "", new String[] {}, "");

        createDialogWithContentResolver(
                contentResolver, false, Arrays.asList("image/*")); // Multi-select = false.
        Assert.assertTrue(mDialog.isShowing());
        waitForDecoder();

        // The test should not have crashed at this point, as per https://crbug.com/1072415,
        // so the loading should have aborted (gracefully) because the image cursor could not be
        // constructed.
        dismissDialog();
    }

    @Test
    @LargeTest
    public void testNoSelection() throws Throwable {
        setupTestFiles();
        createDialog(false, Arrays.asList("image/*")); // Multi-select = false.
        Assert.assertTrue(mDialog.isShowing());
        waitForDecoder();

        int expectedSelectionCount = 1;
        clickView(0, expectedSelectionCount);
        clickCancel();

        Assert.assertNull(mLastSelectedPhotos);
        Assert.assertEquals(PhotoPickerAction.CANCEL, mLastActionRecorded);
    }

    @Test
    @LargeTest
    public void testSingleSelectionPhoto() throws Throwable {
        setupTestFiles();
        createDialog(false, Arrays.asList("image/*")); // Multi-select = false.
        Assert.assertTrue(mDialog.isShowing());
        waitForDecoder();

        // Expected selection count is 1 because clicking on a new view unselects other.
        int expectedSelectionCount = 1;

        // Click the first view.
        int callCount = mOnAnimatedCallback.getCallCount();
        clickView(0, expectedSelectionCount);
        mOnAnimatedCallback.waitForCallback(callCount, 1);

        // Click the second view.
        callCount = mOnAnimatedCallback.getCallCount();
        clickView(1, expectedSelectionCount);
        mOnAnimatedCallback.waitForCallback(callCount, 1);

        clickDone();

        Assert.assertEquals(1, mLastSelectedPhotos.length);
        Assert.assertEquals(PhotoPickerAction.PHOTOS_SELECTED, mLastActionRecorded);
        Assert.assertEquals(mTestFiles.get(1).getUri().getPath(), mLastSelectedPhotos[0].getPath());
    }

    @Test
    @LargeTest
    public void testBackPressDismiss() throws Throwable {
        setupTestFiles();
        createDialog(false, Arrays.asList("image/*")); // Multi-select = false.
        Assert.assertTrue(mDialog.isShowing());
        waitForDecoder();

        // Expected selection count is 1 because clicking on a new view unselects other.
        int expectedSelectionCount = 1;

        // Click the first view.
        int callCount = mOnAnimatedCallback.getCallCount();
        clickView(0, expectedSelectionCount);
        mOnAnimatedCallback.waitForCallback(callCount, 1);

        // Click the second view.
        callCount = mOnAnimatedCallback.getCallCount();
        clickView(1, expectedSelectionCount);
        mOnAnimatedCallback.waitForCallback(callCount, 1);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mDialog.getOnBackPressedDispatcher().onBackPressed();
                });

        Assert.assertNull(mLastSelectedPhotos);
        Assert.assertEquals(PhotoPickerAction.CANCEL, mLastActionRecorded);
        Assert.assertFalse(mDialog.isShowing());
    }

    @Test
    @LargeTest
    public void testMultiSelectionPhoto() throws Throwable {
        setupTestFiles();
        createDialog(true, Arrays.asList("image/*")); // Multi-select = true.
        Assert.assertTrue(mDialog.isShowing());
        waitForDecoder();

        // Multi-selection is enabled, so each click is counted.
        int expectedSelectionCount = 1;

        // Click first view.
        int callCount = mOnAnimatedCallback.getCallCount();
        clickView(0, expectedSelectionCount++);
        mOnAnimatedCallback.waitForCallback(callCount, 1);

        // Click third view.
        callCount = mOnAnimatedCallback.getCallCount();
        clickView(2, expectedSelectionCount++);
        mOnAnimatedCallback.waitForCallback(callCount, 1);

        // Click fifth view.
        callCount = mOnAnimatedCallback.getCallCount();
        clickView(4, expectedSelectionCount++);
        mOnAnimatedCallback.waitForCallback(callCount, 1);

        clickDone();

        Assert.assertEquals(3, mLastSelectedPhotos.length);
        Assert.assertEquals(PhotoPickerAction.PHOTOS_SELECTED, mLastActionRecorded);
        Assert.assertEquals(mTestFiles.get(0).getUri().getPath(), mLastSelectedPhotos[0].getPath());
        Assert.assertEquals(mTestFiles.get(2).getUri().getPath(), mLastSelectedPhotos[1].getPath());
        Assert.assertEquals(mTestFiles.get(4).getUri().getPath(), mLastSelectedPhotos[2].getPath());
    }

    @Test
    @LargeTest
    @MinAndroidSdkLevel(Build.VERSION_CODES.O) // Video is only supported on O+.
    public void testVideoPlayerPlayAndRestart() throws Throwable {
        // Requesting to play a video is not a case of an accidental disk read on the UI thread.
        StrictMode.ThreadPolicy oldPolicy =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return StrictMode.allowThreadDiskReads();
                        });

        try {
            setupTestFiles();
            createDialog(true, Arrays.asList("image/*")); // Multi-select = true.
            Assert.assertTrue(mDialog.isShowing());
            waitForDecoder();

            PickerCategoryView categoryView = mDialog.getCategoryViewForTesting();

            View container = categoryView.findViewById(R.id.playback_container);
            Assert.assertTrue(container.getVisibility() == View.GONE);

            // This test video takes one second to play.
            String fileName = "chrome/test/data/android/photo_picker/noogler_1sec.mp4";
            File file = new File(UrlUtils.getIsolatedTestFilePath(fileName));

            int callCount = mOnVideoEndedCallback.getCallCount();

            playVideo(Uri.fromFile(file));
            Assert.assertTrue(container.getVisibility() == View.VISIBLE);

            mOnVideoEndedCallback.waitForCallback(callCount, 1);

            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        View mute = categoryView.findViewById(R.id.mute);
                        categoryView.getVideoPlayerForTesting().onClick(mute);
                    });

            // Clicking the play button should restart playback.
            callCount = mOnVideoEndedCallback.getCallCount();

            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        View playbutton = categoryView.findViewById(R.id.video_player_play_button);
                        categoryView.getVideoPlayerForTesting().onClick(playbutton);
                    });

            mOnVideoEndedCallback.waitForCallback(callCount, 1);

            dismissDialog();
        } finally {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        StrictMode.setThreadPolicy(oldPolicy);
                    });
        }
    }

    @Test
    @LargeTest
    @MinAndroidSdkLevel(Build.VERSION_CODES.O) // Video is only supported on O+.
    public void testVideoPlayerPlayAndBackPress() throws Throwable {
        // Requesting to play a video is not a case of an accidental disk read on the UI thread.
        StrictMode.ThreadPolicy oldPolicy =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return StrictMode.allowThreadDiskReads();
                        });

        try {
            setupTestFiles();
            createDialog(true, Arrays.asList("image/*")); // Multi-select = true.
            Assert.assertTrue(mDialog.isShowing());
            waitForDecoder();

            PickerCategoryView categoryView = mDialog.getCategoryViewForTesting();

            View container = categoryView.findViewById(R.id.playback_container);
            Assert.assertTrue(container.getVisibility() == View.GONE);

            // This test video takes one second to play.
            String fileName = "chrome/test/data/android/photo_picker/noogler_1sec.mp4";
            File file = new File(UrlUtils.getIsolatedTestFilePath(fileName));

            int callCount = mOnVideoEndedCallback.getCallCount();

            playVideo(Uri.fromFile(file));
            Assert.assertTrue(container.getVisibility() == View.VISIBLE);

            mOnVideoEndedCallback.waitForCallback(callCount, 1);

            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        mDialog.getOnBackPressedDispatcher().onBackPressed();
                    });

            // Clicking the play button should restart playback.
            callCount = mOnVideoEndedCallback.getCallCount();

            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        View playbutton = categoryView.findViewById(R.id.video_player_play_button);
                        categoryView.getVideoPlayerForTesting().onClick(playbutton);
                    });

            mOnVideoEndedCallback.waitForCallback(callCount, 1);

            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        mDialog.getOnBackPressedDispatcher().onBackPressed();
                    });
            Assert.assertTrue(mDismissed);
        } finally {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        StrictMode.setThreadPolicy(oldPolicy);
                    });
        }
    }

    private void verifyVisible(int viewId, int eventId) {
        Assert.assertEquals(
                "Unexpected view ID for event " + eventId,
                viewId,
                (long) mLastViewAnimatingIds.get(eventId));
        Assert.assertEquals(
                "Unexpected alpha value for event " + eventId,
                1.0f,
                (double) mLastViewAnimatingAlphas.get(eventId),
                MathUtils.EPSILON);
    }

    private void verifyHidden(int viewId, int eventId) {
        Assert.assertEquals(
                "Unexpected view ID for event " + eventId,
                viewId,
                (long) mLastViewAnimatingIds.get(eventId));
        Assert.assertEquals(
                "Unexpected alpha value for event " + eventId,
                0.0f,
                (double) mLastViewAnimatingAlphas.get(eventId),
                MathUtils.EPSILON);
    }

    @Test
    @LargeTest
    @EnableAnimations
    @MinAndroidSdkLevel(Build.VERSION_CODES.O) // Video is only supported on O+.
    @DisableIf.Build(supported_abis_includes = "x86", message = "https://crbug.com/1092104")
    @DisableIf.Build(supported_abis_includes = "x86_64", message = "https://crbug.com/1092104")
    @DisabledTest(message = "https://crbug.com/1311783")
    public void testVideoPlayerAnimations() throws Throwable {
        PickerVideoPlayer.setShortAnimationTimesForTesting(true);

        // Requesting to play a video is not a case of an accidental disk read on the UI thread.
        StrictMode.ThreadPolicy oldPolicy =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return StrictMode.allowThreadDiskReads();
                        });

        try {
            setupTestFiles();
            createDialog(true, Arrays.asList("image/*")); // Multi-select = true.
            Assert.assertTrue(mDialog.isShowing());
            waitForDecoder();

            PickerCategoryView categoryView = mDialog.getCategoryViewForTesting();

            View container = categoryView.findViewById(R.id.playback_container);
            Assert.assertTrue(container.getVisibility() == View.GONE);

            String fileName = "chrome/test/data/android/photo_picker/noogler_1sec.mp4";
            File file = new File(UrlUtils.getIsolatedTestFilePath(fileName));

            int callCount = mOnVideoAnimationEndCallback.getCallCount();

            playVideo(Uri.fromFile(file));
            Assert.assertTrue(container.getVisibility() == View.VISIBLE);

            // This keeps track of event ordering.
            int i = 0;

            // Wait for two animation sets (until the controls and play button have animated away).
            mOnVideoAnimationEndCallback.waitForCallback(callCount, 2);

            // All controls start off showing when the video starts playing, and animations will
            // start to fade them away: one animation for the video controls and a separate one for
            // the Play/Pause button. Play button is the first button to disappear (shortest start
            // time and duration) and shortly thereafter the video controls start disappearing.
            verifyVisible(R.id.video_player_play_button, i++);
            verifyHidden(R.id.video_player_play_button, i++);
            verifyVisible(R.id.video_controls, i++);
            verifyHidden(R.id.video_controls, i++);

            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        // Single-tapping should make the controls visible again and then fade away.
                        categoryView.getVideoPlayerForTesting().singleTapForTesting();
                    });

            // Animation-end has been called twice now, expect four more calls after single-tapping
            // because controls fade in and then fade out again.
            callCount += 2;
            mOnVideoAnimationEndCallback.waitForCallback(callCount, 4);

            // The controls and the Play button start animating into view at the same time but the
            // Play button is quicker to appear.
            verifyHidden(R.id.video_controls, i++);
            verifyHidden(R.id.video_player_play_button, i++);
            verifyVisible(R.id.video_player_play_button, i++);
            verifyVisible(R.id.video_controls, i++);

            // After a short while, the controls disappear again (with same delay and duration).
            verifyVisible(R.id.video_controls, i++);
            verifyVisible(R.id.video_player_play_button, i++);
            verifyHidden(R.id.video_controls, i++);
            verifyHidden(R.id.video_player_play_button, i++);

            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        // Double-tapping left of screen will cause the video to roll back to the
                        // beginning and controls to be shown immediately (no fade-in) and then
                        // gradually fade out.
                        categoryView.getVideoPlayerForTesting().doubleTapForTesting(/* x= */ 0f);
                    });

            callCount += 4;
            mOnVideoAnimationEndCallback.waitForCallback(callCount, 2);

            // Controls will show without animation, but should fade away (play fades out first).
            verifyVisible(R.id.video_player_play_button, i++);
            verifyHidden(R.id.video_player_play_button, i++);
            verifyVisible(R.id.video_controls, i++);
            verifyHidden(R.id.video_controls, i++);

            dismissDialog();
        } finally {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> {
                        StrictMode.setThreadPolicy(oldPolicy);
                    });
        }
    }

    @Test
    @LargeTest
    public void testOrientationChanges() throws Throwable {
        setupTestFiles();
        createDialog(true, Arrays.asList("image/*")); // Multi-select = true.
        Assert.assertTrue(mDialog.isShowing());

        int callCount = mOnDecoderReadyCallback.getCallCount();

        // Simulate an early configuration change for the photo grid.
        Configuration configuration = getActivity().getResources().getConfiguration();
        PickerCategoryView categoryView = mDialog.getCategoryViewForTesting();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    categoryView.onConfigurationChanged(configuration);
                });

        mOnDecoderReadyCallback.waitForCallback(
                callCount, 1, WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);

        // Simulate an early configuration change for the video player (before showing).
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PickerVideoPlayer videoPlayer = categoryView.getVideoPlayerForTesting();
                    videoPlayer.onConfigurationChanged(configuration);
                });

        dismissDialog();
    }

    @Test
    @LargeTest
    @Feature("RenderTest")
    public void testBorderPersistence() throws Exception {
        setupTestFilesWith80ColoredSquares();
        createDialog(false, Arrays.asList("image/*")); // Multi-select = false.
        waitForDecoderIdle();

        mRenderTestRule.render(mDialog.getCategoryViewForTesting(), "initial_load");

        // Click the first view.
        int expectedSelectionCount = 1;
        int callCount = mOnAnimatedCallback.getCallCount();
        clickView(0, expectedSelectionCount);
        mOnAnimatedCallback.waitForCallback(callCount, 1);

        mRenderTestRule.render(mDialog.getCategoryViewForTesting(), "first_view_clicked");

        // Now test that you can scroll the image out of view and back in again, and the selection
        // border should be maintained.
        RecyclerView recyclerView = getRecyclerView();
        RecyclerViewTestUtils.scrollToBottom(recyclerView);

        callCount = mOnAnimatedCallback.getCallCount();
        RecyclerViewTestUtils.scrollToView(recyclerView, 0);
        mOnAnimatedCallback.waitForCallback(callCount, 1);

        mRenderTestRule.render(mDialog.getCategoryViewForTesting(), "first_view_clicked");

        dismissDialog();
    }
}