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

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;

import android.Manifest;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Looper;
import android.provider.MediaStore;
import android.webkit.MimeTypeMap;

import androidx.core.content.ContextCompat;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.Shadows;
import org.robolectric.android.util.concurrent.PausedExecutorService;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowMimeTypeMap;

import org.chromium.base.ContextUtils;
import org.chromium.base.FileUtils;
import org.chromium.base.FileUtilsJni;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.ui.permissions.PermissionCallback;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/** Tests logic in the SelectFileDialog class. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@DisableFeatures({
    UiAndroidFeatures.DEPRECATED_EXTERNAL_PICKER_FUNCTION,
    UiAndroidFeatures.SELECT_FILE_OPEN_DOCUMENT
})
@LooperMode(LooperMode.Mode.PAUSED)
public class SelectFileDialogTest {
    // A callback that fires when the file selection pipeline shuts down as a result of an action.
    public final CallbackHelper mOnActionCallback = new CallbackHelper();

    // The Executor to run tasks on during the test.
    private final PausedExecutorService mExecutor = new PausedExecutorService();

    @Mock FileUtils.Natives mFileUtilsMocks;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        PostTask.setPrenativeThreadPoolExecutorForTesting(mExecutor);
    }

    private void runAllAsyncTasks() {
        // Run AsyncTasks
        mExecutor.runAll();

        // Wait for onPostExecute() of the AsyncTasks to run on the UI Thread.
        shadowOf(Looper.getMainLooper()).idle();
    }

    /** Argument matcher that matches Intents using |filterEquals| method. */
    private static class IntentArgumentMatcher implements ArgumentMatcher<Intent> {
        private final Intent mIntent;

        public IntentArgumentMatcher(Intent intent) {
            mIntent = intent;
        }

        @Override
        public boolean matches(Intent other) {
            return mIntent.filterEquals(other);
        }

        @Override
        public String toString() {
            return mIntent.toString();
        }
    }

    private class TestSelectFileDialog extends SelectFileDialog {
        // Counts how often the upload attempts are aborted.
        private int mFileSelectionAborted;

        // Counts how often the upload results in some files being uploaded.
        private int mFileSelectionSuccess;

        TestSelectFileDialog(long nativeDialog) {
            super(nativeDialog);
            mFileSelectionSuccess = 0;
        }

        @Override
        protected void onFileSelected(
                long nativeSelectFileDialogImpl, String filePath, String displayName) {
            mFileSelectionSuccess++;
            mOnActionCallback.notifyCalled();
        }

        @Override
        protected void onMultipleFilesSelected(
                long nativeSelectFileDialogImpl,
                String[] filePathArray,
                String[] displayNameArray) {
            mFileSelectionSuccess++;
            mOnActionCallback.notifyCalled();
        }

        @Override
        protected void onFileNotSelected(long nativeSelectFileDialogImpl) {
            mFileSelectionAborted++;
            mOnActionCallback.notifyCalled();
        }

        private void resetFileSelectionAttempts() {
            mFileSelectionAborted = 0;
            mFileSelectionSuccess = 0;
        }
    }

    public void testMimeTypesWithExternalPicker(String intentAction) throws Exception {
        TestSelectFileDialog selectFileDialog = new TestSelectFileDialog(0);
        WindowAndroid windowAndroid = Mockito.mock(WindowAndroid.class);

        // Select a simple (non-media) MIME type without setting up successful intent handling, to
        // simulate the pipeline aborting because showIntent fails.
        int callCount = mOnActionCallback.getCallCount();
        selectFileDialog.selectFile(
                intentAction,
                new String[] {"application/pdf"},
                /* capture= */ false,
                /* multiple= */ false,
                windowAndroid);
        mOnActionCallback.waitForCallback(callCount, 1);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(1, selectFileDialog.mFileSelectionAborted);
        selectFileDialog.resetFileSelectionAttempts();

        // Now setup WindowAndroid#showIntent to succeed for our next run.
        IntentArgumentMatcher chooserIntentArgumentMatcher =
                new IntentArgumentMatcher(new Intent(Intent.ACTION_CHOOSER));
        Mockito.doAnswer(
                        (invocation) -> {
                            // When showIntent is called, we use the opportunity to check on the
                            // values we expect to see within the Intent data.
                            Intent chooserIntent = (Intent) invocation.getArguments()[0];
                            Intent getContentIntent =
                                    (Intent) chooserIntent.getExtra(Intent.EXTRA_INTENT);
                            assertEquals(
                                    null, getContentIntent.getExtra(Intent.EXTRA_ALLOW_MULTIPLE));
                            assertEquals("*/*", getContentIntent.getType());
                            String[] mimeTypes =
                                    (String[]) getContentIntent.getExtra(Intent.EXTRA_MIME_TYPES);
                            assertArrayEquals(new String[] {"application/pdf"}, mimeTypes);
                            assertEquals(
                                    null, getContentIntent.getExtra(Intent.EXTRA_INITIAL_INTENTS));
                            assertTrue(getContentIntent.hasCategory(Intent.CATEGORY_OPENABLE));
                            return true;
                        })
                .when(windowAndroid)
                .showIntent(
                        ArgumentMatchers.argThat(chooserIntentArgumentMatcher),
                        (WindowAndroid.IntentCallback) any(),
                        anyInt());

        // Simulate showing the dialog, allowing a PDF to be uploaded and watch the pipeline
        // remain open.
        callCount = mOnActionCallback.getCallCount();
        selectFileDialog.selectFile(
                intentAction,
                new String[] {"application/pdf"},
                /* capture= */ false,
                /* multiple= */ false,
                windowAndroid);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(0, selectFileDialog.mFileSelectionAborted);
        selectFileDialog.resetFileSelectionAttempts();

        // Setup showIntent to check for slightly different values for our next run.
        Mockito.doAnswer(
                        (invocation) -> {
                            Intent chooserIntent = (Intent) invocation.getArguments()[0];
                            Intent getContentIntent =
                                    (Intent) chooserIntent.getExtra(Intent.EXTRA_INTENT);
                            assertEquals(
                                    true, getContentIntent.getExtra(Intent.EXTRA_ALLOW_MULTIPLE));
                            assertEquals("*/*", getContentIntent.getType());
                            String[] mimeTypes =
                                    (String[]) getContentIntent.getExtra(Intent.EXTRA_MIME_TYPES);
                            // Adding a media related MIME-type adds an extra MIME type to avoid
                            // ACTION_GET_CONTENT hijacking.
                            assertArrayEquals(
                                    new String[] {
                                        "application/pdf", "image/gif", "type/nonexistent"
                                    },
                                    mimeTypes);
                            assertEquals(
                                    null, getContentIntent.getExtra(Intent.EXTRA_INITIAL_INTENTS));
                            assertTrue(getContentIntent.hasCategory(Intent.CATEGORY_OPENABLE));
                            return true;
                        })
                .when(windowAndroid)
                .showIntent(
                        ArgumentMatchers.argThat(chooserIntentArgumentMatcher),
                        (WindowAndroid.IntentCallback) any(),
                        anyInt());

        // Add a media file to the mix and allow multiple files.
        callCount = mOnActionCallback.getCallCount();
        selectFileDialog.selectFile(
                intentAction,
                new String[] {"application/pdf", "image/gif"},
                /* capture= */ false,
                /* multiple= */ true,
                windowAndroid);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(0, selectFileDialog.mFileSelectionAborted);
        selectFileDialog.resetFileSelectionAttempts();
    }

    @Test
    public void testMimeTypesWithExternalPicker() throws Exception {
        testMimeTypesWithExternalPicker(Intent.ACTION_GET_CONTENT);
    }

    @Test
    @EnableFeatures({UiAndroidFeatures.SELECT_FILE_OPEN_DOCUMENT})
    public void testMimeTypesWithExternalPickerOpenDocument() throws Exception {
        testMimeTypesWithExternalPicker(Intent.ACTION_OPEN_DOCUMENT);
    }

    @Test
    public void testMimeTypesWithExternalPickerNoAcceptList() throws Exception {
        TestSelectFileDialog selectFileDialog = new TestSelectFileDialog(0);
        WindowAndroid windowAndroid = Mockito.mock(WindowAndroid.class);

        // Setup WindowAndroid#showIntent to succeed (and validate the call).
        IntentArgumentMatcher chooserIntentArgumentMatcher =
                new IntentArgumentMatcher(new Intent(Intent.ACTION_CHOOSER));
        Mockito.doAnswer(
                        (invocation) -> {
                            // When showIntent is called, we use the opportunity to check on the
                            // values we expect to see within the Intent data.
                            Intent chooserIntent = (Intent) invocation.getArguments()[0];
                            Intent getContentIntent =
                                    (Intent) chooserIntent.getExtra(Intent.EXTRA_INTENT);
                            assertEquals(
                                    null, getContentIntent.getExtra(Intent.EXTRA_ALLOW_MULTIPLE));
                            assertEquals("*/*", getContentIntent.getType());
                            assertEquals(null, getContentIntent.getExtra(Intent.EXTRA_MIME_TYPES));
                            assertEquals(
                                    null, getContentIntent.getExtra(Intent.EXTRA_INITIAL_INTENTS));
                            assertTrue(getContentIntent.hasCategory(Intent.CATEGORY_OPENABLE));
                            return true;
                        })
                .when(windowAndroid)
                .showIntent(
                        ArgumentMatchers.argThat(chooserIntentArgumentMatcher),
                        (WindowAndroid.IntentCallback) any(),
                        anyInt());

        // Select an empty MIME type.
        selectFileDialog.selectFile(
                Intent.ACTION_GET_CONTENT,
                new String[] {},
                /* capture= */ false,
                /* multiple= */ false,
                windowAndroid);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(0, selectFileDialog.mFileSelectionAborted);
        selectFileDialog.resetFileSelectionAttempts();
    }

    @Test
    public void testFileSelectionUserActions() throws Exception {
        TestSelectFileDialog selectFileDialog = new TestSelectFileDialog(0);

        WindowAndroid windowAndroid = Mockito.mock(WindowAndroid.class);
        when(windowAndroid.hasPermission(Manifest.permission.CAMERA)).thenReturn(false);

        // Start with a simple camera capture event (which should fail because the CAMERA permission
        // is denied).
        int callCount = mOnActionCallback.getCallCount();
        selectFileDialog.selectFile(
                Intent.ACTION_GET_CONTENT,
                new String[] {"image/jpeg"},
                /* capture= */ true,
                /* multiple= */ false,
                windowAndroid);
        mOnActionCallback.waitForCallback(callCount, 1);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(1, selectFileDialog.mFileSelectionAborted);
        selectFileDialog.resetFileSelectionAttempts();

        // The CANCEL event should also fail and not result in any files being selected.
        callCount = mOnActionCallback.getCallCount();
        selectFileDialog.onPhotoPickerUserAction(
                PhotoPickerListener.PhotoPickerAction.CANCEL, new Uri[0]);
        mOnActionCallback.waitForCallback(callCount, 1);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(1, selectFileDialog.mFileSelectionAborted);
        selectFileDialog.resetFileSelectionAttempts();

        // The PHOTOS_SELECTED event without images should have the same result.
        callCount = mOnActionCallback.getCallCount();
        selectFileDialog.onPhotoPickerUserAction(
                PhotoPickerListener.PhotoPickerAction.PHOTOS_SELECTED, new Uri[0]);
        mOnActionCallback.waitForCallback(callCount, 1);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(1, selectFileDialog.mFileSelectionAborted);
        selectFileDialog.resetFileSelectionAttempts();

        // Test LAUNCH_CAMERA, which requires a bit of mocking to make sure the permissions are
        // setup correctly (ensure that the requests for the CAMERA permission are denied).
        Mockito.doAnswer(
                        (invocation) -> {
                            PermissionCallback callback =
                                    (PermissionCallback) invocation.getArguments()[1];
                            callback.onRequestPermissionsResult(
                                    new String[] {Manifest.permission.CAMERA},
                                    new int[] {PackageManager.PERMISSION_DENIED});
                            return null;
                        })
                .when(windowAndroid)
                .requestPermissions(
                        aryEq(new String[] {Manifest.permission.CAMERA}),
                        (PermissionCallback) any());

        // Test LAUNCH_CAMERA when permission is denied. Note: this is different from the other
        // events because the MediaPicker dialog stays open and the pipeline should not shut down
        // (so onFileNotSelected should not be called). See https://crbug.com/1381455 for details.
        callCount = mOnActionCallback.getCallCount();
        selectFileDialog.onPhotoPickerUserAction(
                PhotoPickerListener.PhotoPickerAction.LAUNCH_CAMERA, new Uri[0]);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(0, selectFileDialog.mFileSelectionAborted);
        assertEquals(callCount, mOnActionCallback.getCallCount());
        selectFileDialog.resetFileSelectionAttempts();

        // Setup for another LAUNCH_CAMERA test, this time with the CAMERA permission enabled.
        Mockito.doAnswer(
                        (invocation) -> {
                            PermissionCallback callback =
                                    (PermissionCallback) invocation.getArguments()[1];
                            callback.onRequestPermissionsResult(
                                    new String[] {Manifest.permission.CAMERA},
                                    new int[] {PackageManager.PERMISSION_GRANTED});
                            return null;
                        })
                .when(windowAndroid)
                .requestPermissions(
                        aryEq(new String[] {Manifest.permission.CAMERA}),
                        (PermissionCallback) any());

        // Since the permission is now allowed, the LAUNCH_CAMERA event should keep the pipeline
        // open.
        callCount = mOnActionCallback.getCallCount();
        selectFileDialog.onPhotoPickerUserAction(
                PhotoPickerListener.PhotoPickerAction.LAUNCH_CAMERA, new Uri[0]);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(0, selectFileDialog.mFileSelectionAborted);
        assertEquals(callCount, mOnActionCallback.getCallCount());
        selectFileDialog.resetFileSelectionAttempts();

        // Test the LAUNCH_GALLERY event (which normally opens the Files app). However, by default
        // the showIntent will fail on the mock WindowAndroid object, so the file selection should
        // be aborted.
        selectFileDialog.setFileTypesForTests(new ArrayList<String>(Arrays.asList("image/jpeg")));

        callCount = mOnActionCallback.getCallCount();
        selectFileDialog.onPhotoPickerUserAction(
                PhotoPickerListener.PhotoPickerAction.LAUNCH_GALLERY, new Uri[0]);
        mOnActionCallback.waitForCallback(callCount, 1);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(1, selectFileDialog.mFileSelectionAborted);
        selectFileDialog.resetFileSelectionAttempts();

        // Force WindowAndroid#showIntent to succeed and make sure the pipeline remains open when
        // the test reruns.
        IntentArgumentMatcher chooserIntentArgumentMatcher =
                new IntentArgumentMatcher(new Intent(Intent.ACTION_CHOOSER));
        Mockito.doAnswer(
                        (invocation) -> {
                            Intent chooserIntent = (Intent) invocation.getArguments()[0];
                            Intent getContentIntent =
                                    (Intent) chooserIntent.getExtra(Intent.EXTRA_INTENT);
                            assertEquals(
                                    null, getContentIntent.getExtra(Intent.EXTRA_ALLOW_MULTIPLE));
                            assertEquals("*/*", getContentIntent.getType());
                            String[] mimeTypes =
                                    (String[]) getContentIntent.getExtra(Intent.EXTRA_MIME_TYPES);
                            assertArrayEquals(
                                    new String[] {"image/jpeg", "type/nonexistent"}, mimeTypes);
                            assertEquals(
                                    null, getContentIntent.getExtra(Intent.EXTRA_INITIAL_INTENTS));
                            return true;
                        })
                .when(windowAndroid)
                .showIntent(
                        ArgumentMatchers.argThat(chooserIntentArgumentMatcher),
                        (WindowAndroid.IntentCallback) any(),
                        anyInt());

        // Rerun the test. Because showIntent now reports success, the upload should still be in
        // progress.
        callCount = mOnActionCallback.getCallCount();
        selectFileDialog.onPhotoPickerUserAction(
                PhotoPickerListener.PhotoPickerAction.LAUNCH_GALLERY, new Uri[0]);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(0, selectFileDialog.mFileSelectionAborted);
        assertEquals(callCount, mOnActionCallback.getCallCount());
        selectFileDialog.resetFileSelectionAttempts();
    }

    @Test
    public void testFileSelectionPermissionInterrupted() throws Exception {
        TestSelectFileDialog selectFileDialog = new TestSelectFileDialog(0);

        WindowAndroid windowAndroid = Mockito.mock(WindowAndroid.class);
        when(windowAndroid.hasPermission(Manifest.permission.CAMERA)).thenReturn(false);

        IntentArgumentMatcher imageCaptureIntentArgumentMatcher =
                new IntentArgumentMatcher(new Intent(MediaStore.ACTION_IMAGE_CAPTURE));
        when(windowAndroid.canResolveActivity(
                        ArgumentMatchers.argThat(imageCaptureIntentArgumentMatcher)))
                .thenReturn(true);

        // Setup the request callback to simulate an interrupted permission flow.
        Mockito.doAnswer(
                        (invocation) -> {
                            PermissionCallback callback =
                                    (PermissionCallback) invocation.getArguments()[1];
                            callback.onRequestPermissionsResult(new String[] {}, new int[] {});
                            return null;
                        })
                .when(windowAndroid)
                .requestPermissions(
                        aryEq(new String[] {Manifest.permission.CAMERA}),
                        (PermissionCallback) any());

        // Ensure permission request in selectFile can handle interrupted permission flow.
        int callCount = mOnActionCallback.getCallCount();
        selectFileDialog.selectFile(
                Intent.ACTION_GET_CONTENT,
                new String[] {"image/jpeg"},
                /* capture= */ true,
                /* multiple= */ false,
                windowAndroid);
        mOnActionCallback.waitForCallback(callCount, 1);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(1, selectFileDialog.mFileSelectionAborted);
        selectFileDialog.resetFileSelectionAttempts();

        // Ensure permission request in onPhotoPickerUserAction can handle interrupted permission
        // flow.
        callCount = mOnActionCallback.getCallCount();
        selectFileDialog.onPhotoPickerUserAction(
                PhotoPickerListener.PhotoPickerAction.LAUNCH_CAMERA, new Uri[0]);
        assertEquals(0, selectFileDialog.mFileSelectionSuccess);
        assertEquals(0, selectFileDialog.mFileSelectionAborted);
        assertEquals(callCount, mOnActionCallback.getCallCount());
        selectFileDialog.resetFileSelectionAttempts();
    }

    /** Returns the determined scope for the accepted |fileTypes|. */
    private int scopeForFileTypes(String... fileTypes) {
        SelectFileDialog instance = SelectFileDialog.create((long) /* nativeSelectFileDialog= */ 0);
        instance.setFileTypesForTests(new ArrayList<String>(Arrays.asList(fileTypes)));

        return instance.determineSelectFileDialogScope();
    }

    @Test
    public void testDetermineSelectFileDialogScope() {
        assertEquals(SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_GENERIC, scopeForFileTypes());
        assertEquals(SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_GENERIC, scopeForFileTypes("*/*"));
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_GENERIC, scopeForFileTypes("text/plain"));

        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_IMAGES, scopeForFileTypes("image/*"));
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_IMAGES, scopeForFileTypes("image/png"));
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_GENERIC,
                scopeForFileTypes("image/*", "test/plain"));

        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_VIDEOS, scopeForFileTypes("video/*"));
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_VIDEOS, scopeForFileTypes("video/ogg"));
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_GENERIC,
                scopeForFileTypes("video/*", "test/plain"));
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_IMAGES,
                scopeForFileTypes("image/x-png", "image/gif", "image/jpeg"));
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_GENERIC,
                scopeForFileTypes("image/x-png", "image/gif", "image/jpeg", "text/plain"));

        // Test image extensions only.
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_IMAGES,
                scopeForFileTypes(
                        ".jpg", ".jpeg", ".png", ".gif", ".apng", ".tiff", ".tif", ".bmp", ".xcf",
                        ".webp"));
        // Test image extensions mixed with image MIME types.
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_IMAGES,
                scopeForFileTypes(".JPG", ".jpeg", "image/gif", "image/jpeg"));
        // Image extensions mixed with image MIME types and other.
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_GENERIC,
                scopeForFileTypes(".jpg", "image/gif", "text/plain"));
        // Video extensions only.
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_VIDEOS,
                scopeForFileTypes(
                        ".asf", ".avhcd", ".avi", ".flv", ".mov", ".mp4", ".mpeg", ".mpg", ".swf",
                        ".wmv", ".webm", ".mkv", ".divx"));
        // Video extensions and video MIME types.
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_VIDEOS,
                scopeForFileTypes(".avi", ".mp4", "video/ogg"));
        // Video extensions and video MIME types and other.
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_GENERIC,
                scopeForFileTypes(".avi", ".mp4", "video/ogg", "text/plain"));

        // Non-image, non-video extension only.
        assertEquals(SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_GENERIC, scopeForFileTypes(".doc"));

        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_IMAGES_AND_VIDEOS,
                scopeForFileTypes("video/*", "image/*"));
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_IMAGES_AND_VIDEOS,
                scopeForFileTypes("image/jpeg", "video/ogg"));
        assertEquals(
                SelectFileDialog.SELECT_FILE_DIALOG_SCOPE_GENERIC,
                scopeForFileTypes("video/*", "image/*", "text/plain"));
    }

    @Test
    public void testPhotoPickerLaunchAndMimeTypes() {
        ShadowMimeTypeMap shadowMimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton());
        shadowMimeTypeMap.addExtensionMimeTypeMapping("jpg", "image/jpeg");
        shadowMimeTypeMap.addExtensionMimeTypeMapping("gif", "image/gif");
        shadowMimeTypeMap.addExtensionMimeTypeMapping("txt", "text/plain");
        shadowMimeTypeMap.addExtensionMimeTypeMapping("mpg", "video/mpeg");

        assertEquals("", SelectFileDialog.ensureMimeType(""));
        assertEquals("image/jpeg", SelectFileDialog.ensureMimeType(".jpg"));
        assertEquals("image/jpeg", SelectFileDialog.ensureMimeType("image/jpeg"));
        // Unknown extension, expect default response:
        assertEquals("application/octet-stream", SelectFileDialog.ensureMimeType(".flv"));

        assertEquals(null, SelectFileDialog.convertToSupportedPhotoPickerTypes(new ArrayList<>()));
        assertEquals(null, SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList("")));
        assertEquals(
                null,
                SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList("foo/bar")));
        assertEquals(
                Arrays.asList("image/jpeg"),
                SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList(".jpg")));
        assertEquals(
                Arrays.asList("image/jpeg"),
                SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList("image/jpeg")));
        assertEquals(
                Arrays.asList("image/jpeg"),
                SelectFileDialog.convertToSupportedPhotoPickerTypes(
                        Arrays.asList(".jpg", "image/jpeg")));
        assertEquals(
                Arrays.asList("image/gif", "image/jpeg"),
                SelectFileDialog.convertToSupportedPhotoPickerTypes(
                        Arrays.asList(".gif", "image/jpeg")));

        // Video and mixed video/images support. This feature is supported, but off by default, so
        // expect failure until it is turned on by default.
        assertEquals(
                null, SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList(".mpg")));
        assertEquals(
                null,
                SelectFileDialog.convertToSupportedPhotoPickerTypes(Arrays.asList("video/mpeg")));
        assertEquals(
                null,
                SelectFileDialog.convertToSupportedPhotoPickerTypes(
                        Arrays.asList(".jpg", "image/jpeg", ".mpg")));

        // Returns null because generic picker is required (due to addition of .txt file).
        assertEquals(
                null,
                SelectFileDialog.convertToSupportedPhotoPickerTypes(
                        Arrays.asList(".txt", ".jpg", "image/jpeg")));
    }

    @Test
    public void testMultipleFileSelectorWithFileUris() {
        SelectFileDialog selectFileDialog = new SelectFileDialog(0);
        Uri[] filePathArray =
                new Uri[] {
                    Uri.parse("file:///storage/emulated/0/DCIM/Camera/IMG_0.jpg"),
                    Uri.parse("file:///storage/emulated/0/DCIM/Camera/IMG_1.jpg")
                };
        SelectFileDialog.GetDisplayNameTask task =
                selectFileDialog
                .new GetDisplayNameTask(ContextUtils.getApplicationContext(), true, filePathArray);
        task.doInBackground();
        assertEquals(task.mFilePaths[0].toString(), "///storage/emulated/0/DCIM/Camera/IMG_0.jpg");
        assertEquals(task.mFilePaths[1].toString(), "///storage/emulated/0/DCIM/Camera/IMG_1.jpg");
    }

    private void testFilePath(
            String path, SelectFileDialog selectFileDialog, boolean expectedPass) {
        testFilePath(path, selectFileDialog, expectedPass, expectedPass);
    }

    private void testFilePath(
            String path,
            SelectFileDialog selectFileDialog,
            boolean expectedFileSelectionResult,
            boolean expectedGetDisplayNameResult) {
        Uri[] uris = new Uri[1];
        uris[0] = Uri.fromFile(new File(path));

        SelectFileDialog.FilePathSelectedTask task =
                selectFileDialog
                .new FilePathSelectedTask(ContextUtils.getApplicationContext(), path, null);
        SelectFileDialog.GetDisplayNameTask task2 =
                selectFileDialog
                .new GetDisplayNameTask(
                        ContextUtils.getApplicationContext(), /* isMultiple= */ false, uris);
        assertEquals(expectedFileSelectionResult, task.doInBackground());
        assertEquals(expectedGetDisplayNameResult, null != task2.doInBackground());
    }

    @Test
    public void testFilePathTasks() throws IOException {
        FileUtilsJni.TEST_HOOKS.setInstanceForTesting(mFileUtilsMocks);
        doReturn("/tmp/xyz.jpn").when(mFileUtilsMocks).getAbsoluteFilePath(any());

        SelectFileDialog selectFileDialog = new SelectFileDialog(0);

        // Obtain the data directory for RoboElectric. It should look something like:
        //   /tmp/robolectric-Method_[testName][number]/org.chromium.test.ui-dataDir
        // ... where [testName] is the name of this test function and [number] is a unique id.
        String dataDir =
                ContextCompat.getDataDir(ContextUtils.getApplicationContext()).getCanonicalPath();

        // Passing in the data directory itself should fail.
        testFilePath(dataDir, selectFileDialog, /* expectedPass= */ false);
        // Passing in a subdirectory of the data directory should also fail.
        testFilePath(dataDir + "/tmp/xyz.jpg", selectFileDialog, /* expectedPass= */ false);
        // The parent directory of the data directory should, however, succeed.
        testFilePath(dataDir + "/../xyz.jpg", selectFileDialog, /* expectedPass= */ true);
        // Another way of specifying the data directory (should fail).
        testFilePath(dataDir + "/tmp/../xyz.jpg", selectFileDialog, /* expectedPass= */ false);
        // The directory outside the data directory should succeed.
        testFilePath("/data/local/tmp.jpg", selectFileDialog, /* expectedPass= */ true);

        Path path = new File(dataDir).toPath();
        String parent = path.getParent().toString();
        String lastComponent = path.getName(path.getNameCount() - 1).toString();

        // Make sure that base/./dataDir is treated the same as base/dataDir (and fail the request).
        testFilePath(
                parent + "/./" + lastComponent + "/xyz.jpg",
                selectFileDialog,
                /* expectedPass= */ false);
        // Make sure that dataDir/../dataDir is treated the same as dataDir (and fail the request).
        testFilePath(
                dataDir + "/../" + lastComponent + "/xyz.jpg",
                selectFileDialog,
                /* expectedPass= */ false);

        // Tests invalid file path should fail file selection.
        doReturn(new String()).when(mFileUtilsMocks).getAbsoluteFilePath(any());
        testFilePath(
                "\\/tmp/xyz.jpg",
                selectFileDialog,
                /* expectedFileSelectionResult= */ false,
                /* expectedGetDisplayNameResult= */ true);
    }

    @Test
    public void testShowTypes() {
        SelectFileDialog selectFileDialog = new SelectFileDialog(0);

        selectFileDialog.setFileTypesForTests(Arrays.asList("image/jpeg"));
        assertTrue(selectFileDialog.acceptsSingleType());
        assertTrue(selectFileDialog.shouldShowImageTypes());
        assertFalse(selectFileDialog.shouldShowVideoTypes());
        assertFalse(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("image/jpeg", "image/png"));
        assertFalse(selectFileDialog.acceptsSingleType());
        assertTrue(selectFileDialog.shouldShowImageTypes());
        assertFalse(selectFileDialog.shouldShowVideoTypes());
        assertFalse(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("image/*", "image/jpeg"));
        // Note: image/jpeg is part of image/* so this counts as a single type.
        assertTrue(selectFileDialog.acceptsSingleType());
        assertTrue(selectFileDialog.shouldShowImageTypes());
        assertFalse(selectFileDialog.shouldShowVideoTypes());
        assertFalse(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("image/*", "video/mp4"));
        assertFalse(selectFileDialog.acceptsSingleType());
        assertTrue(selectFileDialog.shouldShowImageTypes());
        assertTrue(selectFileDialog.shouldShowVideoTypes());
        assertFalse(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("image/jpeg", "video/mp4"));
        assertFalse(selectFileDialog.acceptsSingleType());
        assertTrue(selectFileDialog.shouldShowImageTypes());
        assertTrue(selectFileDialog.shouldShowVideoTypes());
        assertFalse(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("video/mp4"));
        assertTrue(selectFileDialog.acceptsSingleType());
        assertFalse(selectFileDialog.shouldShowImageTypes());
        assertTrue(selectFileDialog.shouldShowVideoTypes());
        assertFalse(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("video/mp4", "video/*"));
        // Note: video/mp4 is part of video/* so this counts as a single type.
        assertTrue(selectFileDialog.acceptsSingleType());
        assertFalse(selectFileDialog.shouldShowImageTypes());
        assertTrue(selectFileDialog.shouldShowVideoTypes());
        assertFalse(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("audio/wave", "audio/mpeg", "audio/*"));
        // Note: both audio/wave and audio/mpeg are part of audio/* so this counts as a single type.
        assertTrue(selectFileDialog.acceptsSingleType());
        assertFalse(selectFileDialog.shouldShowImageTypes());
        assertFalse(selectFileDialog.shouldShowVideoTypes());
        assertTrue(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("audio/wave", "audio/mpeg"));
        assertFalse(selectFileDialog.acceptsSingleType());
        assertFalse(selectFileDialog.shouldShowImageTypes());
        assertFalse(selectFileDialog.shouldShowVideoTypes());
        assertTrue(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("*/*"));
        assertFalse(selectFileDialog.acceptsSingleType());
        assertTrue(selectFileDialog.shouldShowImageTypes());
        assertTrue(selectFileDialog.shouldShowVideoTypes());
        assertTrue(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Collections.emptyList());
        assertFalse(selectFileDialog.acceptsSingleType());
        assertTrue(selectFileDialog.shouldShowImageTypes());
        assertTrue(selectFileDialog.shouldShowVideoTypes());
        assertTrue(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("image//png", "image/", "image"));
        assertFalse(selectFileDialog.acceptsSingleType());
        assertTrue(selectFileDialog.shouldShowImageTypes());
        assertFalse(selectFileDialog.shouldShowVideoTypes());
        assertFalse(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("/image", "/"));
        assertFalse(selectFileDialog.acceptsSingleType());
        assertFalse(selectFileDialog.shouldShowImageTypes());
        assertFalse(selectFileDialog.shouldShowVideoTypes());
        assertFalse(selectFileDialog.shouldShowAudioTypes());

        selectFileDialog.setFileTypesForTests(Arrays.asList("/", ""));
        assertFalse(selectFileDialog.acceptsSingleType());
        assertFalse(selectFileDialog.shouldShowImageTypes());
        assertFalse(selectFileDialog.shouldShowVideoTypes());
        assertFalse(selectFileDialog.shouldShowAudioTypes());
    }

    ContentResolver getMockContentResolver(String mimeType) {
        final ContentResolver contentResolver = Mockito.mock(ContentResolver.class);
        final Cursor cursor = Mockito.mock(Cursor.class);

        String[] filePathColumn = {
            MediaStore.Files.FileColumns.MIME_TYPE,
        };
        if ("THROW".equals(mimeType)) {
            Mockito.doThrow(new RuntimeException())
                    .when(contentResolver)
                    .query(any(), eq(filePathColumn), any(), any(), any());
        } else {
            Mockito.doReturn(cursor)
                    .when(contentResolver)
                    .query(any(), eq(filePathColumn), any(), any(), any());
            Mockito.doReturn(true).when(cursor).moveToFirst();
            Mockito.doReturn(true).when(cursor).moveToNext();
            Mockito.doReturn(0).when(cursor).getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE);
            Mockito.doReturn(mimeType).when(cursor).getString(0);
        }
        return contentResolver;
    }

    HistogramWatcher getHistogramWatcher(
            @SelectFileDialog.FileSelectedAction int expectAction,
            @SelectFileDialog.FileSelectedUploadMethod int expectMethod) {
        return HistogramWatcher.newBuilder()
                .expectIntRecord("Android.SelectFileDialogContentSelected", expectAction)
                .expectIntRecord("Android.SelectFileDialogUploadMethods", expectMethod)
                .build();
    }

    @Test
    public void testUploadMethodLogging() {
        SelectFileDialog selectFileDialog = new SelectFileDialog(0);

        List<Object[]> testCases =
                Arrays.asList(
                        new Object[][] {
                            // Test cases for MIME-type lookup:
                            {
                                "foo.jpg",
                                "image/jpg",
                                /* useMediaPicker= */ true,
                                SelectFileDialog.FileSelectedAction.MEDIA_PICKER_IMAGE_BY_MIME_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod.MEDIA_PICKER_IMAGE
                            },
                            {
                                "foo.jpg",
                                "image/jpg",
                                /* useMediaPicker= */ false,
                                SelectFileDialog.FileSelectedAction
                                        .EXTERNAL_PICKER_IMAGE_BY_MIME_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod.EXTERNAL_PICKER_IMAGE
                            },
                            {
                                "foo.mp4",
                                "video/mp4",
                                /* useMediaPicker= */ true,
                                SelectFileDialog.FileSelectedAction.MEDIA_PICKER_VIDEO_BY_MIME_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod.MEDIA_PICKER_VIDEO
                            },
                            {
                                "foo.mp4",
                                "video/mp4",
                                /* useMediaPicker= */ false,
                                SelectFileDialog.FileSelectedAction
                                        .EXTERNAL_PICKER_VIDEO_BY_MIME_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod.EXTERNAL_PICKER_VIDEO
                            },
                            {
                                "foo.txt",
                                "text/plain",
                                /* useMediaPicker= */ true,
                                SelectFileDialog.FileSelectedAction.MEDIA_PICKER_OTHER_BY_MIME_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod.MEDIA_PICKER_OTHER
                            },
                            {
                                "foo.txt",
                                "text/plain",
                                /* useMediaPicker= */ false,
                                SelectFileDialog.FileSelectedAction
                                        .EXTERNAL_PICKER_OTHER_BY_MIME_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod.EXTERNAL_PICKER_OTHER
                            },
                            // Test cases for lookup by extension:
                            {
                                "foo.jpg",
                                null,
                                /* useMediaPicker= */ true,
                                SelectFileDialog.FileSelectedAction.MEDIA_PICKER_IMAGE_BY_EXTENSION,
                                SelectFileDialog.FileSelectedUploadMethod.MEDIA_PICKER_IMAGE
                            },
                            {
                                "foo.jpg",
                                null,
                                /* useMediaPicker= */ false,
                                SelectFileDialog.FileSelectedAction
                                        .EXTERNAL_PICKER_IMAGE_BY_EXTENSION,
                                SelectFileDialog.FileSelectedUploadMethod.EXTERNAL_PICKER_IMAGE
                            },
                            {
                                "foo.mp4",
                                null,
                                /* useMediaPicker= */ true,
                                SelectFileDialog.FileSelectedAction.MEDIA_PICKER_VIDEO_BY_EXTENSION,
                                SelectFileDialog.FileSelectedUploadMethod.MEDIA_PICKER_VIDEO
                            },
                            {
                                "foo.mp4",
                                null,
                                /* useMediaPicker= */ false,
                                SelectFileDialog.FileSelectedAction
                                        .EXTERNAL_PICKER_VIDEO_BY_EXTENSION,
                                SelectFileDialog.FileSelectedUploadMethod.EXTERNAL_PICKER_VIDEO
                            },
                            {
                                "foo.txt",
                                null,
                                /* useMediaPicker= */ true,
                                SelectFileDialog.FileSelectedAction.MEDIA_PICKER_OTHER_BY_EXTENSION,
                                SelectFileDialog.FileSelectedUploadMethod.MEDIA_PICKER_OTHER
                            },
                            {
                                "foo.txt",
                                null,
                                /* useMediaPicker= */ false,
                                SelectFileDialog.FileSelectedAction
                                        .EXTERNAL_PICKER_OTHER_BY_EXTENSION,
                                SelectFileDialog.FileSelectedUploadMethod.EXTERNAL_PICKER_OTHER
                            },

                            // Pathological (no filename -- results in URI parsing failing):
                            {
                                "",
                                null,
                                /* useMediaPicker= */ true,
                                SelectFileDialog.FileSelectedAction.MEDIA_PICKER_UNKNOWN_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod.MEDIA_PICKER_UNKNOWN_TYPE
                            },
                            {
                                "",
                                null,
                                /* useMediaPicker= */ false,
                                SelectFileDialog.FileSelectedAction.EXTERNAL_PICKER_UNKNOWN_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod
                                        .EXTERNAL_PICKER_UNKNOWN_TYPE
                            },
                            // Pathological (no MIME type and no extension):
                            {
                                "foo",
                                null,
                                /* useMediaPicker= */ true,
                                SelectFileDialog.FileSelectedAction.MEDIA_PICKER_UNKNOWN_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod.MEDIA_PICKER_UNKNOWN_TYPE
                            },
                            {
                                "foo",
                                null,
                                /* useMediaPicker= */ false,
                                SelectFileDialog.FileSelectedAction.EXTERNAL_PICKER_UNKNOWN_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod
                                        .EXTERNAL_PICKER_UNKNOWN_TYPE
                            },
                            // Pathological (ContentResolver throwing exception):
                            {
                                "foo",
                                "THROW",
                                /* useMediaPicker= */ true,
                                SelectFileDialog.FileSelectedAction.MEDIA_PICKER_UNKNOWN_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod.MEDIA_PICKER_UNKNOWN_TYPE
                            },
                            {
                                "foo",
                                "THROW",
                                /* useMediaPicker= */ false,
                                SelectFileDialog.FileSelectedAction.EXTERNAL_PICKER_UNKNOWN_TYPE,
                                SelectFileDialog.FileSelectedUploadMethod
                                        .EXTERNAL_PICKER_UNKNOWN_TYPE
                            },
                        });

        for (Object[] testCase : testCases) {
            String filename = (String) testCase[0];
            String mimeType = (String) testCase[1];
            boolean useMediaPicker = (boolean) testCase[2];
            int action = (int) testCase[3];
            int method = (int) testCase[4];

            var histogramWatcher = getHistogramWatcher(action, method);
            String[] filesSelected = new String[] {filename};
            AsyncTask<Boolean> task =
                    selectFileDialog.getUploadMetricTaskForTesting(
                            mimeType != null
                                    ? getMockContentResolver(mimeType)
                                    : ContextUtils.getApplicationContext().getContentResolver(),
                            filesSelected,
                            useMediaPicker);
            task.executeOnExecutor(mExecutor);
            runAllAsyncTasks();
            histogramWatcher.assertExpected(
                    "File: "
                            + filename
                            + " MimeType: "
                            + mimeType
                            + " Action: "
                            + action
                            + " Method: "
                            + method);
        }
    }
}