chromium/ui/android/java/src/org/chromium/ui/base/SelectFileDialog.java

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.ui.base;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.ContextCompat;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.ContentUriUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.FileProviderUtils;
import org.chromium.base.FileUtils;
import org.chromium.base.Log;
import org.chromium.base.PackageManagerUtils;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.ui.R;
import org.chromium.ui.UiUtils;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

/**
 * A dialog that is triggered from a file input field that allows a user to select a file based on
 * a set of accepted file types. The path of the selected file is passed to the native dialog.
 */
@JNINamespace("ui")
public class SelectFileDialog implements WindowAndroid.IntentCallback, PhotoPickerListener {
    private static final String TAG = "SelectFileDialog";
    private static final String IMAGE_TYPE = "image";
    private static final String VIDEO_TYPE = "video";
    private static final String AUDIO_TYPE = "audio";
    private static final String ALL_TYPES = "*/*";

    // Duration before temporary camera file is cleaned up, in milliseconds.
    private static final long DURATION_BEFORE_FILE_CLEAN_UP_IN_MILLIS = TimeUnit.HOURS.toMillis(1);

    // A list of some of the more popular image extensions. Not meant to be
    // exhaustive, but should cover the vast majority of image types.
    private static final String[] POPULAR_IMAGE_EXTENSIONS =
            new String[] {
                ".apng", ".bmp", ".gif", ".jpeg", ".jpg", ".png", ".tif", ".tiff", ".xcf", ".webp"
            };

    // A list of some of the more popular video extensions. Not meant to be
    // exhaustive, but should cover the vast majority of video types.
    private static final String[] POPULAR_VIDEO_EXTENSIONS =
            new String[] {
                ".asf", ".avhcd", ".avi", ".divx", ".flv", ".mov", ".mp4", ".mpeg", ".mpg", ".swf",
                ".wmv", ".webm", ".mkv"
            };

    /**
     * The SELECT_FILE_DIALOG_SCOPE_* enumerations are used to measure the sort of content that
     * developers are requesting to be shown in the select file dialog. Values must be kept in sync
     * with their definition in //tools/metrics/histograms/histograms.xml, and both the numbering
     * and meaning of the values must remain constant as they're recorded by UMA.
     *
     * Values are package visible because they're tested in the SelectFileDialogTest junit test.
     */
    static final int SELECT_FILE_DIALOG_SCOPE_GENERIC = 0;

    static final int SELECT_FILE_DIALOG_SCOPE_IMAGES = 1;
    static final int SELECT_FILE_DIALOG_SCOPE_VIDEOS = 2;
    static final int SELECT_FILE_DIALOG_SCOPE_IMAGES_AND_VIDEOS = 3;
    static final int SELECT_FILE_DIALOG_SCOPE_COUNT =
            SELECT_FILE_DIALOG_SCOPE_IMAGES_AND_VIDEOS + 1;

    /**
     * The Android Media Picker enumerations, used to measure which type of picker is shown to the
     * user. Values must be kept in sync with their definition in
     * //tools/metrics/histograms/histograms.xml, and both the numbering and meaning of the values
     * must remain constant as they're recorded by UMA.
     */
    static final int SHOWING_CHROME_PICKER = 0;

    static final int SHOWING_ANDROID_PICKER_DIRECT = 1;
    static final int SHOWING_SUPPRESSED = 2;
    static final int SHOWING_ANDROID_PICKER_INDIRECT = 3;
    static final int SHOWING_ENUM_COUNT = SHOWING_ANDROID_PICKER_INDIRECT + 1;

    /**
     * The FileSelectedUploadMethod tracks how media files are uploaded, split into the MediaPicker
     * and an external source (such as the Android Files app). These values are persisted to logs.
     * Entries should not be renumbered and numeric values should never be reused.
     */
    @IntDef({
        FileSelectedUploadMethod.MEDIA_PICKER_IMAGE,
        FileSelectedUploadMethod.MEDIA_PICKER_VIDEO,
        FileSelectedUploadMethod.MEDIA_PICKER_OTHER,
        FileSelectedUploadMethod.MEDIA_PICKER_UNKNOWN_TYPE,
        FileSelectedUploadMethod.EXTERNAL_PICKER_IMAGE,
        FileSelectedUploadMethod.EXTERNAL_PICKER_VIDEO,
        FileSelectedUploadMethod.EXTERNAL_PICKER_OTHER,
        FileSelectedUploadMethod.EXTERNAL_PICKER_UNKNOWN_TYPE,
        FileSelectedUploadMethod.COUNT,
    })
    protected @interface FileSelectedUploadMethod {
        // An image was picked using the Media Picker.
        int MEDIA_PICKER_IMAGE = 0;
        // A video was picked using the Media Picker.
        int MEDIA_PICKER_VIDEO = 1;
        // Something other than a video/photo was picked using the Media Picker.
        int MEDIA_PICKER_OTHER = 2;
        // Unable to determine type of file picked using the Media Picker.
        int MEDIA_PICKER_UNKNOWN_TYPE = 3;
        // An image was picked using an external source.
        int EXTERNAL_PICKER_IMAGE = 4;
        // A video was picked using an external source.
        int EXTERNAL_PICKER_VIDEO = 5;
        // Something other than a video/photo was picked using an external source.
        int EXTERNAL_PICKER_OTHER = 6;
        // Unable to determine type of file picked using an external source.
        int EXTERNAL_PICKER_UNKNOWN_TYPE = 7;

        // Keeps track of the number of options above. Must be the highest number.
        int COUNT = 8;
    }

    /**
     * The FileSelectAction tracks how many media files were uploaded, using either the MediaPicker
     * or an external source (such as the Android picker). These values are persisted to logs.
     * Entries should not be renumbered and numeric values should never be reused.
     */
    @IntDef({
        FileSelectedAction.MEDIA_PICKER_IMAGE_BY_MIME_TYPE,
        FileSelectedAction.MEDIA_PICKER_VIDEO_BY_MIME_TYPE,
        FileSelectedAction.MEDIA_PICKER_OTHER_BY_MIME_TYPE,
        FileSelectedAction.MEDIA_PICKER_IMAGE_BY_EXTENSION,
        FileSelectedAction.MEDIA_PICKER_VIDEO_BY_EXTENSION,
        FileSelectedAction.MEDIA_PICKER_OTHER_BY_EXTENSION,
        FileSelectedAction.MEDIA_PICKER_UNKNOWN_TYPE,
        FileSelectedAction.EXTERNAL_PICKER_IMAGE_BY_MIME_TYPE,
        FileSelectedAction.EXTERNAL_PICKER_VIDEO_BY_MIME_TYPE,
        FileSelectedAction.EXTERNAL_PICKER_OTHER_BY_MIME_TYPE,
        FileSelectedAction.EXTERNAL_PICKER_IMAGE_BY_EXTENSION,
        FileSelectedAction.EXTERNAL_PICKER_VIDEO_BY_EXTENSION,
        FileSelectedAction.EXTERNAL_PICKER_OTHER_BY_EXTENSION,
        FileSelectedAction.EXTERNAL_PICKER_UNKNOWN_TYPE,
        FileSelectedAction.COUNT,
    })
    protected @interface FileSelectedAction {
        // MediaPicker was used to pick a photo, as determined by its MIME type.
        int MEDIA_PICKER_IMAGE_BY_MIME_TYPE = 0;

        // MediaPicker was used to pick a video, as determined by its MIME type.
        int MEDIA_PICKER_VIDEO_BY_MIME_TYPE = 1;

        // MediaPicker was used to pick a file, but the ContentResolver returned a MIME type that
        // corresponds to neither an image, nor a video. This is not expected to happen, unless more
        // formats are added to the MediaPicker.
        int MEDIA_PICKER_OTHER_BY_MIME_TYPE = 2;

        // MediaPicker was used to pick a photo, as determined by its file extension. This is
        // primarily for images fresh off of the camera (where the ContentResolver doesn't know
        // know the MIME type). It may also catch corner cases where the user picked an existing
        // photo in the MediaPicker but the ContentResolver didn't know its MIME type. It is
        // unlikely, though, because those URIs don't normally contain the extension (except when
        // camera is the source), so these would more likely show up as MEDIA_PICKER_UNKNOWN_TYPE.
        int MEDIA_PICKER_IMAGE_BY_EXTENSION = 3;

        // Same comment applies as for MEDIA_PICKER_VIDEO_BY_EXTENSION, except in this case for a
        // video. Please note though that, in the emulator, the ContentResolver *is* able to lookup
        // the MIME types for videos (where it fails to do so for photos), so videos may be counted
        // as MEDIA_PICKER_VIDEO_BY_MIME_TYPE instead.
        int MEDIA_PICKER_VIDEO_BY_EXTENSION = 4;

        // MediaPicker was used to pick something other than a video/photo, as determined by the
        // file extension. This is not expected to happen, unless more formats are added to the
        // MediaPicker.
        int MEDIA_PICKER_OTHER_BY_EXTENSION = 5;

        // MediaPicker was used, but neither the MIME type nor the extension provided clues as to
        // what type of file it was (or the URI was null).
        int MEDIA_PICKER_UNKNOWN_TYPE = 6;

        // An external source (Android intent) was used to pick a photo, as determined by its MIME
        // type.
        int EXTERNAL_PICKER_IMAGE_BY_MIME_TYPE = 7;

        // An external source (Android intent) was used to pick a video, as determined by its MIME
        // type.
        int EXTERNAL_PICKER_VIDEO_BY_MIME_TYPE = 8;

        // An external source (Android intent) was used to pick something other than a video/photo,
        // as determined by the MIME type.
        int EXTERNAL_PICKER_OTHER_BY_MIME_TYPE = 9;

        // An external source (Android intent) was used to pick a photo, as determined by its file
        // extension.
        int EXTERNAL_PICKER_IMAGE_BY_EXTENSION = 10;

        // An external source (Android intent) was used to pick a video, as determined by its file
        // extension.
        int EXTERNAL_PICKER_VIDEO_BY_EXTENSION = 11;

        // An external source (Android intent) was used to pick something other than a video/photo,
        // as determined by its file extension.
        int EXTERNAL_PICKER_OTHER_BY_EXTENSION = 12;

        // An external source (Android intent) was used, but neither the MIME type nor the file
        // extension provided clues as to what type of file it was (or the URI was null).
        int EXTERNAL_PICKER_UNKNOWN_TYPE = 13;

        // Keeps track of the number of options above. Must be the highest number.
        int COUNT = 14;
    }

    /** If set, overrides the WindowAndroid passed in {@link selectFile()}. */
    @SuppressLint("StaticFieldLeak")
    private static WindowAndroid sWindowAndroidForTesting;

    private long mNativeSelectFileDialog;
    private String mIntentAction;
    private List<String> mFileTypes;
    private boolean mCapture;
    private boolean mAllowMultiple;
    private Uri mCameraOutputUri;
    private WindowAndroid mWindowAndroid;

    /** Whether an Activity is available on the system to support capturing images (i.e. Camera). */
    private boolean mSupportsImageCapture;

    /**
     * Whether an Activity is available to capture video (i.e. Camera with video recording
     * capabilities).
     */
    private boolean mSupportsVideoCapture;

    /** Whether an Activity is available to capture audio. */
    private boolean mSupportsAudioCapture;

    /**
     * Keeps track of whether the MediaPicker was used to upload files. The can be true while the
     * MediaPicker is showing, and flip to false if the user opts to use the 'Browse' escape hatch,
     * to use the stock Android picker.
     */
    private boolean mMediaPickerWasUsed;

    /** A delegate for the photo picker. */
    private static PhotoPickerDelegate sPhotoPickerDelegate;

    /** The active photo picker, or null if none is active. */
    private static PhotoPicker sPhotoPicker;

    /**
     * Allows setting a delegate to override the default Android stock photo picker.
     * @param delegate A {@link PhotoPickerDelegate} instance.
     */
    public static void setPhotoPickerDelegate(PhotoPickerDelegate delegate) {
        sPhotoPickerDelegate = delegate;
    }

    @VisibleForTesting
    SelectFileDialog(long nativeSelectFileDialog) {
        mNativeSelectFileDialog = nativeSelectFileDialog;
    }

    /** Overrides the WindowAndroid passed in {@link selectFile()}. */
    public static void setWindowAndroidForTests(WindowAndroid window) {
        sWindowAndroidForTesting = window;
        ResettersForTesting.register(() -> sWindowAndroidForTesting = null);
    }

    /** Overrides the list of accepted file types for testing purposes. */
    public void setFileTypesForTests(List<String> fileTypes) {
        List<String> oldValue = mFileTypes;
        mFileTypes = fileTypes;
        ResettersForTesting.register(() -> mFileTypes = oldValue);
    }

    /**
     * Creates and starts an intent based on the passed fileTypes and capture value.
     *
     * @param intentAction Intent action such as ACTION_GET_CONTENT.
     * @param fileTypes MIME types requested (i.e. "image/*")
     * @param capture The capture value as described in http://www.w3.org/TR/html-media-capture/
     * @param multiple Whether it should be possible to select multiple files.
     * @param window The WindowAndroid that can show intents
     */
    @CalledByNative
    protected void selectFile(
            String intentAction,
            String[] fileTypes,
            boolean capture,
            boolean multiple,
            WindowAndroid window) {
        mIntentAction =
                UiAndroidFeatureMap.isEnabled(UiAndroidFeatures.SELECT_FILE_OPEN_DOCUMENT)
                        ? intentAction
                        : Intent.ACTION_GET_CONTENT;
        mFileTypes = new ArrayList<String>(Arrays.asList(fileTypes));
        mCapture = capture;
        mAllowMultiple = multiple;
        mWindowAndroid = (sWindowAndroidForTesting == null) ? window : sWindowAndroidForTesting;

        mSupportsImageCapture =
                mWindowAndroid.canResolveActivity(new Intent(MediaStore.ACTION_IMAGE_CAPTURE));
        mSupportsVideoCapture =
                mWindowAndroid.canResolveActivity(new Intent(MediaStore.ACTION_VIDEO_CAPTURE));
        mSupportsAudioCapture =
                mWindowAndroid.canResolveActivity(
                        new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION));

        List<String> missingPermissions = new ArrayList<>();
        String storagePermission = Manifest.permission.READ_EXTERNAL_STORAGE;
        boolean shouldUsePhotoPicker = shouldUsePhotoPicker();
        if (shouldUsePhotoPicker) {
            // The permission scenario for accessing media has evolved a bit over the years:
            // Early on, READ_EXTERNAL_STORAGE was required to access media, but that permission was
            // later deprecated. In its place (starting with Android T) READ_MEDIA_IMAGES and
            // READ_MEDIA_VIDEO were required. To make matters more interesting, a native Android
            // Media Picker was also introduced at the same time, but it functions without requiring
            // Chrome to request any permission.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                if (!preferAndroidMediaPicker()) {
                    if (!window.hasPermission(Manifest.permission.READ_MEDIA_IMAGES)
                            && shouldShowImageTypes()) {
                        missingPermissions.add(Manifest.permission.READ_MEDIA_IMAGES);
                    }
                    if (!window.hasPermission(Manifest.permission.READ_MEDIA_VIDEO)
                            && shouldShowVideoTypes()) {
                        missingPermissions.add(Manifest.permission.READ_MEDIA_VIDEO);
                    }
                }
            } else {
                if (!window.hasPermission(storagePermission)) {
                    missingPermissions.add(storagePermission);
                }
            }
        } else {
            if (((mSupportsImageCapture && shouldShowImageTypes())
                            || (mSupportsVideoCapture && shouldShowVideoTypes()))
                    && !window.hasPermission(Manifest.permission.CAMERA)) {
                missingPermissions.add(Manifest.permission.CAMERA);
            }
            if (mSupportsAudioCapture
                    && shouldShowAudioTypes()
                    && !window.hasPermission(Manifest.permission.RECORD_AUDIO)) {
                missingPermissions.add(Manifest.permission.RECORD_AUDIO);
            }
        }

        if (missingPermissions.isEmpty()) {
            launchSelectFileIntent();
        } else {
            String[] requestPermissions =
                    missingPermissions.toArray(new String[missingPermissions.size()]);
            window.requestPermissions(
                    requestPermissions,
                    (permissions, grantResults) -> {
                        for (int i = 0; i < grantResults.length; i++) {
                            if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
                                if (mCapture) {
                                    onFileNotSelected();
                                    return;
                                }

                                // TODO(finnur): Remove once we figure out the cause of
                                // crbug.com/950024.
                                if (shouldUsePhotoPicker) {
                                    if (permissions.length != requestPermissions.length) {
                                        throw new RuntimeException(
                                                String.format(
                                                        "Permissions arrays misaligned: %d != %d",
                                                        permissions.length,
                                                        requestPermissions.length));
                                    }

                                    if (!permissions[i].equals(requestPermissions[i])) {
                                        throw new RuntimeException(
                                                String.format(
                                                        "Permissions arrays don't match: %s != %s",
                                                        permissions[i], requestPermissions[i]));
                                    }
                                }

                                if (shouldUsePhotoPicker) {
                                    if (permissions[i].equals(storagePermission)
                                            || permissions[i].equals(
                                                    Manifest.permission.READ_MEDIA_IMAGES)
                                            || permissions[i].equals(
                                                    Manifest.permission.READ_MEDIA_VIDEO)) {
                                        WindowAndroid.showError(R.string.permission_denied_error);
                                        onFileNotSelected();
                                        return;
                                    }
                                }
                            }
                        }
                        launchSelectFileIntent();
                    });
        }
    }

    /** Called to launch an intent to allow user to select files. */
    private void launchSelectFileIntent() {
        boolean hasCameraPermission = mWindowAndroid.hasPermission(Manifest.permission.CAMERA);
        if (mSupportsImageCapture && hasCameraPermission) {
            // GetCameraIntentTask will call LaunchSelectFileWithCameraIntent later.
            new GetCameraIntentTask(false, mWindowAndroid, this)
                    .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        } else {
            launchSelectFileWithCameraIntent(null);
        }
    }

    /** Returns an Image capture Intent with the right flags and extra data. */
    private Intent getImageCaptureIntent() {
        Intent camera = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        camera.setFlags(
                Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        camera.putExtra(MediaStore.EXTRA_OUTPUT, mCameraOutputUri);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            // ClipData.newUri may access the disk (for reading mime types).
            try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
                camera.setClipData(
                        ClipData.newUri(
                                ContextUtils.getApplicationContext().getContentResolver(),
                                UiUtils.IMAGE_FILE_PATH,
                                mCameraOutputUri));
            }
        }
        return camera;
    }

    /**
     * Returns a Video capture Intent. Can return null if video capture is not supported or the
     * camera permission has not been granted.
     */
    @Nullable
    private Intent getVideoCaptureIntent() {
        boolean hasCameraPermission = mWindowAndroid.hasPermission(Manifest.permission.CAMERA);
        if (mSupportsVideoCapture && hasCameraPermission) {
            return new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
        }
        return null;
    }

    /**
     * Returns a SoundRecorder Intent. Can return null if sound capture is not supported or the
     * sound permission has not been granted.
     */
    @Nullable
    private Intent getSoundRecorderIntent() {
        boolean hasAudioPermission = mWindowAndroid.hasPermission(Manifest.permission.RECORD_AUDIO);
        if (mSupportsAudioCapture && hasAudioPermission) {
            return new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
        }
        return null;
    }

    /**
     * Called to launch an intent to allow user to select files. If |camera| is null,
     * the select file dialog shouldn't include any files from the camera. Otherwise, user
     * is allowed to choose files from the camera.
     * @param camera Intent for selecting files from camera.
     */
    private void launchSelectFileWithCameraIntent(Intent camera) {
        RecordHistogram.recordEnumeratedHistogram(
                "Android.SelectFileDialogScope",
                determineSelectFileDialogScope(),
                SELECT_FILE_DIALOG_SCOPE_COUNT);

        Intent videoCapture = getVideoCaptureIntent();
        Intent soundRecorder = getSoundRecorderIntent();

        // Quick check - if the |capture| parameter is set and |fileTypes| has the appropriate MIME
        // type, we should just launch the appropriate intent. Otherwise build up a chooser based
        // on the accept type and then display that to the user.
        if (captureImage() && camera != null) {
            if (mWindowAndroid.showIntent(camera, this, R.string.low_memory_error)) return;
        } else if (captureVideo() && videoCapture != null) {
            if (mWindowAndroid.showIntent(videoCapture, this, R.string.low_memory_error)) return;
        } else if (captureAudio() && soundRecorder != null) {
            if (mWindowAndroid.showIntent(soundRecorder, this, R.string.low_memory_error)) return;
        }

        // Use the new photo picker, if available.
        List<String> imageMimeTypes = convertToSupportedPhotoPickerTypes(mFileTypes);
        if (shouldUsePhotoPicker()
                && showPhotoPicker(
                        mWindowAndroid,
                        /* intentCallback= */ this,
                        /* listener= */ this,
                        mAllowMultiple,
                        imageMimeTypes)) {
            mMediaPickerWasUsed = true;
            return;
        } else {
            mMediaPickerWasUsed = false;
            if (!shouldUsePhotoPicker()) {
                logMediaPickerShown(SHOWING_SUPPRESSED);
            }
        }

        showExternalPicker(camera, videoCapture, soundRecorder);
    }

    /**
     * Launches a chooser intent to get files from an external source. If launching the Intent is
     * not successful, the onFileNotSelected is called to end file upload.
     * @param camera A camera capture intent to supply as extra Intent data.
     * @param camcorder A camcorder intent to supply as extra Intent data.
     * @param soundRecorder A soundRecorder intent to supply as extra Intent data.
     */
    private void showExternalPicker(Intent camera, Intent camcorder, Intent soundRecorder) {
        if (UiAndroidFeatureMap.isEnabled(UiAndroidFeatures.DEPRECATED_EXTERNAL_PICKER_FUNCTION)) {
            showExternalPickerDeprecated(camera, camcorder, soundRecorder);
            return;
        }

        Intent getContentIntent = new Intent(mIntentAction);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && mAllowMultiple) {
            getContentIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
        }

        // Set to all types if not a dir, and restrict further by MIME-type below.
        if (!Intent.ACTION_OPEN_DOCUMENT_TREE.equals(getContentIntent.getAction())) {
            getContentIntent.setType(ALL_TYPES);
        }

        List<String> types = new ArrayList<>(mFileTypes);
        if (types.size() > 0) {
            // Calls to ACTION_GET_CONTENT can result in the MediaPicker hijacking the call and
            // showing itself instead of the Files app, when only images or videos are provided.
            // This flow is not only confusing for the user (a MediaPicker on top of a MediaPicker?)
            // but also breaks our cloud media integration, which is currently provided via the
            // Files app. We therefore add a non-existent MIME-type to the mix, which the Files app
            // will ignore, but ensures the MediaPicker wont hijack the call.
            if (shouldShowImageTypes() || shouldShowVideoTypes()) {
                types.add("type/nonexistent");
            }
            getContentIntent.putExtra(Intent.EXTRA_MIME_TYPES, types.toArray(new String[0]));
        }

        ArrayList<Intent> extraIntents = new ArrayList<Intent>();
        if (shouldShowImageTypes() && camera != null) extraIntents.add(camera);
        if (shouldShowVideoTypes() && camcorder != null) extraIntents.add(camcorder);
        if (shouldShowAudioTypes() && soundRecorder != null) extraIntents.add(soundRecorder);

        // Only accept openable files, as coercing virtual files may yield to a MIME type different
        // than expected.
        getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);

        Intent chooser = new Intent(Intent.ACTION_CHOOSER);
        if (!extraIntents.isEmpty()) {
            chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[] {}));
        }
        chooser.putExtra(Intent.EXTRA_INTENT, getContentIntent);

        if (!mWindowAndroid.showIntent(chooser, this, R.string.low_memory_error)) {
            onFileNotSelected();
        }
    }

    /**
     * The deprecated way of launching a chooser intent to get files from an external source (use
     * showExternalPicker instead). If launching the Intent is not successful, the onFileNotSelected
     * is called to end file upload.
     *
     * @param camera A camera capture intent to supply as extra Intent data.
     * @param camcorder A camcorder intent to supply as extra Intent data.
     * @param soundRecorder A soundRecorder intent to supply as extra Intent data.
     */
    private void showExternalPickerDeprecated(
            Intent camera, Intent camcorder, Intent soundRecorder) {
        Intent getContentIntent = new Intent(mIntentAction);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && mAllowMultiple) {
            getContentIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
        }

        // Set to all types if not a dir, but potentially restricted further by MIME-type below.
        if (!Intent.ACTION_OPEN_DOCUMENT_TREE.equals(getContentIntent.getAction())) {
            getContentIntent.setType(ALL_TYPES);
        }

        ArrayList<Intent> extraIntents = new ArrayList<Intent>();
        if (acceptsSingleType()) {
            List<String> types = mFileTypes;
            // Calls to ACTION_GET_CONTENT can result in the MediaPicker hijacking the call and
            // showing itself instead of the Files app, when only images or videos are provided.
            // This flow is not only confusing for the user (a MediaPicker on top of a MediaPicker?)
            // but also breaks our cloud media integration, which is currently provided via the
            // Files app. We therefore add a non-existant MIME-type to the mix, which the Files app
            // will ignore, but ensures the MediaPicker wont hijack the call.
            String noOpMimeType = "type/nonexistent";

            // If one and only one category of accept type was specified (image, video, etc..),
            // then update the intent to specifically target that request.
            if (shouldShowImageTypes()) {
                if (camera != null) extraIntents.add(camera);
                types.add(noOpMimeType);
                getContentIntent.putExtra(Intent.EXTRA_MIME_TYPES, types.toArray(new String[0]));
            } else if (shouldShowVideoTypes()) {
                if (camcorder != null) extraIntents.add(camcorder);
                types.add(noOpMimeType);
                getContentIntent.putExtra(Intent.EXTRA_MIME_TYPES, types.toArray(new String[0]));
            } else if (shouldShowAudioTypes()) {
                if (soundRecorder != null) extraIntents.add(soundRecorder);
                getContentIntent.putExtra(Intent.EXTRA_MIME_TYPES, types.toArray(new String[0]));
            }

            // If any types are specified, then only accept openable files, as coercing
            // virtual files may yield to a MIME type different than expected.
            getContentIntent.addCategory(Intent.CATEGORY_OPENABLE);
        }

        Bundle extras = getContentIntent.getExtras();
        if (extras == null || extras.get(Intent.EXTRA_MIME_TYPES) == null) {
            // We couldn't resolve a single accept type, so fallback to a generic chooser.
            if (camera != null) extraIntents.add(camera);
            if (camcorder != null) extraIntents.add(camcorder);
            if (soundRecorder != null) extraIntents.add(soundRecorder);
        }

        Intent chooser = new Intent(Intent.ACTION_CHOOSER);
        if (!extraIntents.isEmpty()) {
            chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[] {}));
        }
        chooser.putExtra(Intent.EXTRA_INTENT, getContentIntent);

        if (!mWindowAndroid.showIntent(chooser, this, R.string.low_memory_error)) {
            onFileNotSelected();
        }
    }

    /**
     * Determines whether the photo picker should be used for this select file request.  To be
     * applicable for the photo picker, the following must be true:
     *   1.) Only media types were requested in the file request
     *   2.) The file request did not explicitly ask to capture camera directly.
     *   3.) The photo picker is supported by the embedder (i.e. Chrome).
     *   4.) There is a valid Android Activity associated with the file request.
     */
    private boolean shouldUsePhotoPicker() {
        List<String> mediaMimeTypes = convertToSupportedPhotoPickerTypes(mFileTypes);
        return !captureImage()
                && mediaMimeTypes != null
                && shouldShowPhotoPicker()
                && mWindowAndroid.getActivity().get() != null;
    }

    /**
     * Converts a list of extensions and Mime types to a list of de-duped Mime types supported by
     * the photo picker only. If the input list contains a unsupported type, then null is returned.
     * @param fileTypes the list of filetypes (extensions and Mime types) to convert.
     * @return A de-duped list of supported types only, or null if one or more unsupported types
     *         were given as input.
     */
    @VisibleForTesting
    public static List<String> convertToSupportedPhotoPickerTypes(List<String> fileTypes) {
        if (fileTypes.size() == 0) return null;
        List<String> mimeTypes = new ArrayList<>();
        for (String type : fileTypes) {
            String mimeType = ensureMimeType(type);
            if (!mimeType.startsWith("image/")) {
                if (!photoPickerSupportsVideo() || !mimeType.startsWith("video/")) {
                    return null;
                }
            }
            if (!mimeTypes.contains(mimeType)) mimeTypes.add(mimeType);
        }
        return mimeTypes;
    }

    /**
     * Convert |type| to MIME type (known types only).
     * @param type The type to convert. Can be either a MIME type or an extension (should include
     *             the leading dot). If an extension is passed in, it is converted to the
     *             corresponding MIME type (via {@link MimeTypeMap}), or "application/octet-stream"
     *             if the MIME type is not known.
     * @return The MIME type, if known, or "application/octet-stream" otherwise (or blank if input
     *         is blank).
     */
    @VisibleForTesting
    public static String ensureMimeType(String type) {
        if (type.length() == 0) return "";

        String extension = MimeTypeMap.getFileExtensionFromUrl(type);
        if (extension.length() > 0) {
            String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
            if (mimeType != null) return mimeType;
            return "application/octet-stream";
        }
        return type;
    }

    @Override
    public void onPhotoPickerUserAction(@PhotoPickerAction int action, Uri[] photos) {
        switch (action) {
            case PhotoPickerAction.CANCEL:
                onFileNotSelected();
                break;

            case PhotoPickerAction.PHOTOS_SELECTED:
                if (photos.length == 0) {
                    onFileNotSelected();
                    return;
                }

                GetDisplayNameTask task =
                        new GetDisplayNameTask(
                                ContextUtils.getApplicationContext(), photos.length > 1, photos);
                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                break;

            case PhotoPickerAction.LAUNCH_GALLERY:
                mMediaPickerWasUsed = false;
                showExternalPicker(
                        /* camera= */ null, /* camcorder= */ null, /* soundRecorder= */ null);
                break;

            case PhotoPickerAction.LAUNCH_CAMERA:
                if (!mWindowAndroid.hasPermission(Manifest.permission.CAMERA)) {
                    mWindowAndroid.requestPermissions(
                            new String[] {Manifest.permission.CAMERA},
                            (permissions, grantResults) -> {
                                if (grantResults.length == 0
                                        || grantResults[0] == PackageManager.PERMISSION_DENIED) {
                                    return;
                                }
                                assert grantResults.length == 1;
                                new GetCameraIntentTask(true, mWindowAndroid, this)
                                        .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                            });
                } else {
                    new GetCameraIntentTask(true, mWindowAndroid, this)
                            .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                }
                break;
        }
    }

    @Override
    public void onPhotoPickerDismissed() {
        assert sPhotoPicker != null;
        sPhotoPicker = null;
    }

    private class GetCameraIntentTask extends AsyncTask<Uri> {
        private Boolean mDirectToCamera;
        private WindowAndroid mWindow;
        private WindowAndroid.IntentCallback mCallback;

        public GetCameraIntentTask(
                Boolean directToCamera,
                WindowAndroid window,
                WindowAndroid.IntentCallback callback) {
            mDirectToCamera = directToCamera;
            mWindow = window;
            mCallback = callback;
        }

        @Override
        public Uri doInBackground() {
            try {
                Context context = ContextUtils.getApplicationContext();
                return FileProviderUtils.getContentUriFromFile(getFileForImageCapture(context));
            } catch (IOException e) {
                Log.e(TAG, "Cannot retrieve content uri from file", e);
                return null;
            }
        }

        @Override
        protected void onPostExecute(Uri result) {
            mCameraOutputUri = result;
            if (mCameraOutputUri == null) {
                if (captureImage() || mDirectToCamera) {
                    onFileNotSelected();
                } else {
                    launchSelectFileWithCameraIntent(null);
                }
                return;
            }

            if (mDirectToCamera) {
                // Android doesn't support launching an intent flexible enough to let the user
                // decide whether to record photos _or_ videos so, when both types are requested,
                // we choose one over the other. We currently default to photos, to maintain past
                // behavior, but should perhaps consider showing the user a chooser instead.
                Intent intent =
                        acceptsOnlyType(VIDEO_TYPE)
                                ? getVideoCaptureIntent()
                                : getImageCaptureIntent();
                mWindow.showIntent(intent, mCallback, R.string.low_memory_error);
            } else {
                launchSelectFileWithCameraIntent(getImageCaptureIntent());
            }
        }
    }

    /**
     * Get a file for the image capture operation. For devices with JB MR2 or
     * latter android versions, the file is put under IMAGE_FILE_PATH directory.
     * For ICS devices, the file is put under CAPTURE_IMAGE_DIRECTORY.
     *
     * @param context The application context.
     * @return file path for the captured image to be stored.
     */
    private File getFileForImageCapture(Context context) throws IOException {
        assert !ThreadUtils.runningOnUiThread();
        File photoFile =
                File.createTempFile(
                        String.valueOf(System.currentTimeMillis()),
                        ".jpg",
                        UiUtils.getDirectoryForImageCapture(context));
        return photoFile;
    }

    // TODO(crbug.com/41484704): Merge the Chrome and WebView implementations
    // of isPathUnderAppDir into one.
    private static boolean isPathUnderAppDir(String path, Context context) {
        File file = new File(path);
        File dataDir = ContextCompat.getDataDir(context);
        try {
            String pathCanonical = file.getCanonicalPath();
            String dataDirCanonical = dataDir.getCanonicalPath();
            return pathCanonical.startsWith(dataDirCanonical);
        } catch (Exception e) {
            return false;
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    public static boolean isContentUriUnderAppDir(Uri uri, Context context) {
        assert !ThreadUtils.runningOnUiThread();
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return false;
        }
        try {
            ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
            int fd = pfd.getFd();
            // Use the file descriptor to find out the read file path thru symbolic link.
            Path fdPath = Paths.get("/proc/self/fd/" + fd);
            Path filePath = Files.readSymbolicLink(fdPath);
            return isPathUnderAppDir(filePath.toString(), context);
        } catch (Exception e) {
            return false;
        }
    }

    // WindowAndroid.IntentCallback:

    /**
     * Callback method to handle the intent results and pass on the path to the native
     * SelectFileDialog.
     * @param resultCode The result code whether the intent returned successfully.
     * @param results The results of the requested intent.
     */
    @Override
    public void onIntentCompleted(int resultCode, Intent results) {
        if (sPhotoPicker != null) {
            sPhotoPicker.onExternalIntentCompleted();
        }

        if (resultCode != Activity.RESULT_OK) {
            onFileNotSelected();
            return;
        }

        if (results == null
                || (results.getData() == null
                        && (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
                                || results.getClipData() == null))) {
            // If we have a successful return but no data, then assume this is the camera returning
            // the photo that we requested.
            // If the uri is a file, we need to convert it to the absolute path or otherwise
            // android cannot handle it correctly on some earlier versions.
            // http://crbug.com/423338.
            String path =
                    ContentResolver.SCHEME_FILE.equals(mCameraOutputUri.getScheme())
                            ? mCameraOutputUri.getPath()
                            : mCameraOutputUri.toString();

            if (!isPathUnderAppDir(
                    mCameraOutputUri.getSchemeSpecificPart(),
                    mWindowAndroid.getApplicationContext())) {
                onFileSelected(
                        mNativeSelectFileDialog, path, mCameraOutputUri.getLastPathSegment());
                // Broadcast to the media scanner that there's a new photo on the device so it will
                // show up right away in the gallery (rather than waiting until the next time the
                // media scanner runs).
                mWindowAndroid.sendBroadcast(
                        new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mCameraOutputUri));
            } else {
                onFileNotSelected();
            }
            return;
        }

        // Path for when EXTRA_ALLOW_MULTIPLE Intent extra has been defined. Each of the selected
        // files will be shared as an entry on the Intent's ClipData. This functionality is only
        // available in Android JellyBean MR2 and higher.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2
                && results.getData() == null
                && results.getClipData() != null) {
            ClipData clipData = results.getClipData();

            int itemCount = clipData.getItemCount();
            if (itemCount == 0) {
                onFileNotSelected();
                return;
            }

            Uri[] filePathArray = new Uri[itemCount];
            for (int i = 0; i < itemCount; ++i) {
                filePathArray[i] = clipData.getItemAt(i).getUri();
            }
            GetDisplayNameTask task =
                    new GetDisplayNameTask(
                            ContextUtils.getApplicationContext(), true, filePathArray);
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            return;
        }

        if (ContentResolver.SCHEME_FILE.equals(results.getData().getScheme())) {
            String filePath = results.getData().getPath();
            if (!TextUtils.isEmpty(filePath)) {
                FilePathSelectedTask task =
                        new FilePathSelectedTask(
                                ContextUtils.getApplicationContext(), filePath, mWindowAndroid);
                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                return;
            }
        }

        if (ContentResolver.SCHEME_CONTENT.equals(results.getScheme())) {
            Uri uri = results.getData();
            if (UiAndroidFeatureMap.isEnabled(UiAndroidFeatures.SELECT_FILE_OPEN_DOCUMENT)) {
                ContentResolver cr = ContextUtils.getApplicationContext().getContentResolver();
                try {
                    cr.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
                } catch (SecurityException e) {
                    Log.w(TAG, "No persisted read permission for " + uri);
                }
                try {
                    cr.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                } catch (SecurityException e) {
                    Log.w(TAG, "No persisted write permission for " + uri);
                }
            }
            GetDisplayNameTask task =
                    new GetDisplayNameTask(
                            ContextUtils.getApplicationContext(), false, new Uri[] {uri});
            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            return;
        }

        onFileNotSelected();
        WindowAndroid.showError(R.string.opening_file_error);
    }

    private void onFileNotSelected() {
        onFileNotSelected(mNativeSelectFileDialog);
    }

    // Determines the scope of the requested select file dialog for use in a UMA histogram. Right
    // now we want to distinguish between generic, photo and visual media pickers.
    @VisibleForTesting
    int determineSelectFileDialogScope() {
        if (mFileTypes.size() == 0) return SELECT_FILE_DIALOG_SCOPE_GENERIC;

        // Capture the MIME types:
        int acceptsImages = countAcceptTypesFor(IMAGE_TYPE);
        int acceptsVideos = countAcceptTypesFor(VIDEO_TYPE);

        // Capture the most common image and video extensions:
        if (mFileTypes.size() > acceptsImages + acceptsVideos) {
            for (String left : mFileTypes) {
                boolean found = false;
                for (String right : POPULAR_IMAGE_EXTENSIONS) {
                    if (left.equalsIgnoreCase(right)) {
                        found = true;
                        acceptsImages++;
                        break;
                    }
                }

                if (found) continue;

                for (String right : POPULAR_VIDEO_EXTENSIONS) {
                    if (left.equalsIgnoreCase(right)) {
                        acceptsVideos++;
                        break;
                    }
                }
            }
        }

        int acceptsOthers = mFileTypes.size() - acceptsImages - acceptsVideos;

        if (acceptsOthers > 0) return SELECT_FILE_DIALOG_SCOPE_GENERIC;
        if (acceptsVideos > 0) {
            return (acceptsImages == 0)
                    ? SELECT_FILE_DIALOG_SCOPE_VIDEOS
                    : SELECT_FILE_DIALOG_SCOPE_IMAGES_AND_VIDEOS;
        }
        return SELECT_FILE_DIALOG_SCOPE_IMAGES;
    }

    /**
     * Whether any of the mime-types in mFileTypes accepts the given type.
     * If mFileTypes contains ALL_TYPES or is empty every type is accepted so always return true.
     * @param superType The superType to look for, such as 'image' or 'video'.
     *                  Note: This is string-matched on the prefix, so using generics as
     *                  'image/*' or '*' will not work.
     */
    private boolean acceptsType(String superType) {
        if (mFileTypes.isEmpty() || mFileTypes.contains(ALL_TYPES)) return true;
        return countAcceptTypesFor(superType) > 0;
    }

    /**
     * Whether all mime-types in mFileTypes accepts the given type.
     * @param superType The superType to look for, such as 'image' or 'video'.
     *                  Note: This is string-matched on the prefix, so using generics as
     *                  'image/*' or '*' will not work.
     */
    private boolean acceptsOnlyType(String superType) {
        return countAcceptTypesFor(superType) == mFileTypes.size();
    }

    /**
     * Checks whether the list of accepted types effectively describes only a single
     * type, which might be wildcard. For example:
     *
     * [image/jpeg]            -> true: Only one type is specified.
     * [image/jpeg, image/gif] -> false: Contains two distinct types.
     * [image/*, image/gif]    -> true: image/gif already part of image/*.
     */
    @VisibleForTesting
    boolean acceptsSingleType() {
        // We use a single Intent to decide the type of the file chooser we display to the user,
        // which means we can only give it a single type. If there are multiple accept types
        // specified, we will fallback to a generic chooser (unless a capture parameter has been
        // specified, in which case we'll try to satisfy that first.
        if (mFileTypes.size() == 1) return !mFileTypes.contains(ALL_TYPES);
        // Also return true when a generic subtype "type/*" and one or more specific subtypes
        // "type/subtype" are listed but all still have the same supertype.
        // Ie. treat ["image/png", "image/*"] as if it said just ["image/*"].
        String superTypeFound = null;
        boolean foundGenericSubtype = false;
        for (String fileType : mFileTypes) {
            int slash = fileType.indexOf('/');
            if (slash == -1) return false;
            String superType = fileType.substring(0, slash);
            boolean genericSubtype = fileType.substring(slash + 1).equals("*");
            if (superTypeFound == null) {
                superTypeFound = superType;
            } else if (!superTypeFound.equals(superType)) {
                // More than one type.
                return false;
            }
            if (genericSubtype) foundGenericSubtype = true;
        }
        return foundGenericSubtype;
    }

    @VisibleForTesting
    boolean shouldShowImageTypes() {
        return acceptsType(IMAGE_TYPE);
    }

    @VisibleForTesting
    boolean shouldShowVideoTypes() {
        return acceptsType(VIDEO_TYPE);
    }

    @VisibleForTesting
    boolean shouldShowAudioTypes() {
        return acceptsType(AUDIO_TYPE);
    }

    /**
     * Whether the HTML input field specified the 'capture' attribute and specifically requested
     * image capture.
     *
     * See https://www.w3.org/TR/html-media-capture/ for further description.
     */
    private boolean captureImage() {
        return mCapture && acceptsOnlyType(IMAGE_TYPE);
    }

    /**
     * Whether the HTML input field specified the 'capture' attribute and specifically requested
     * video capture.
     */
    private boolean captureVideo() {
        return mCapture && acceptsOnlyType(VIDEO_TYPE);
    }

    /**
     * Whether the HTML input field specified the 'capture' attribute and specifically requested
     * audio capture.
     */
    private boolean captureAudio() {
        return mCapture && acceptsOnlyType(AUDIO_TYPE);
    }

    private int countAcceptTypesFor(String superType) {
        assert superType.indexOf('/') == -1;
        int count = 0;
        for (String type : mFileTypes) {
            if (type.startsWith(superType)) {
                count++;
            }
        }
        return count;
    }

    final class FilePathSelectedTask extends AsyncTask<Boolean> {
        final Context mContext;
        final String mFilePath;
        final WindowAndroid mWindow;

        public FilePathSelectedTask(Context context, String filePath, WindowAndroid window) {
            mContext = context;
            mFilePath = filePath;
            mWindow = window;
        }

        @Override
        public Boolean doInBackground() {
            // Don't allow invalid file path or files under app dir to be uploaded.
            return !isPathUnderAppDir(mFilePath, mContext)
                    && !FileUtils.getAbsoluteFilePath(mFilePath).isEmpty();
        }

        @Override
        protected void onPostExecute(Boolean result) {
            if (result) {
                onFileSelected(mNativeSelectFileDialog, mFilePath, "");
                WindowAndroid.showError(R.string.opening_file_error);
            } else {
                onFileNotSelected();
            }
        }
    }

    class GetDisplayNameTask extends AsyncTask<String[]> {
        String[] mFilePaths;
        final Context mContext;
        final boolean mIsMultiple;
        final Uri[] mUris;

        public GetDisplayNameTask(Context context, boolean isMultiple, Uri[] uris) {
            mContext = context;
            mIsMultiple = isMultiple;
            mUris = uris;
        }

        @Override
        @SuppressLint("NewApi")
        public String[] doInBackground() {
            mFilePaths = new String[mUris.length];
            String[] displayNames = new String[mUris.length];
            try {
                for (int i = 0; i < mUris.length; i++) {
                    // The selected files must be returned as a list of absolute paths. A MIUI 8.5
                    // device was observed to return a file:// URI instead, so convert if necessary.
                    // See https://crbug.com/752834 for context.
                    if (ContentResolver.SCHEME_FILE.equals(mUris[i].getScheme())) {
                        if (isPathUnderAppDir(mUris[i].getSchemeSpecificPart(), mContext)) {
                            return null;
                        }
                        mFilePaths[i] = mUris[i].getSchemeSpecificPart();
                    } else {
                        if (ContentResolver.SCHEME_CONTENT.equals(mUris[i].getScheme())
                                && isContentUriUnderAppDir(mUris[i], mContext)) {
                            return null;
                        }
                        mFilePaths[i] = mUris[i].toString();
                    }

                    displayNames[i] =
                            ContentUriUtils.getDisplayName(
                                    mUris[i], mContext, MediaStore.MediaColumns.DISPLAY_NAME);
                }
            } catch (SecurityException e) {
                // Some third party apps will present themselves as being able
                // to handle the ACTION_GET_CONTENT intent but then declare themselves
                // as exported=false (or more often omit the exported keyword in
                // the manifest which defaults to false after JB).
                // In those cases trying to access the contents raises a security exception
                // which we should not crash on. See crbug.com/382367 for details.
                Log.w(TAG, "Unable to extract results from the content provider");
                return null;
            }

            return displayNames;
        }

        @Override
        protected void onPostExecute(String[] result) {
            if (result == null) {
                onFileNotSelected();
                return;
            }
            if (mIsMultiple) {
                onMultipleFilesSelected(mNativeSelectFileDialog, mFilePaths, result);
            } else {
                onFileSelected(mNativeSelectFileDialog, mFilePaths[0], result[0]);
            }
        }
    }

    final class RecordUploadMetricsTask extends AsyncTask<Boolean> {
        final String[] mFilesSelected;
        final boolean mMediaPickerWasUsed;
        final ContentResolver mContentResolver;

        public RecordUploadMetricsTask(
                ContentResolver contentResolver,
                String[] filesSelected,
                boolean mediaPickerWasUsed) {
            mContentResolver = contentResolver;
            mFilesSelected = filesSelected;
            mMediaPickerWasUsed = mediaPickerWasUsed;
        }

        @Override
        public Boolean doInBackground() {
            for (String path : mFilesSelected) {
                // The |path| variable will now contain a content URI such as:
                //   content://media/external/file/1234
                //   content://com.android.providers.media.documents/document/image%3A1234
                //   content://org.chromium.chrome.FileProvider/images/1234.jpg
                // The first is an example URI from Chrome's MediaPicker, the second from the stock
                // Android picker and the third is from the camera (when taking a photo). All
                // obtained using the Emulator.
                logFileSelectedAction(
                        getMediaType(Uri.parse(path), mMediaPickerWasUsed, mContentResolver));
            }
            return true;
        }

        @Override
        protected void onPostExecute(Boolean result) {}
    }

    protected RecordUploadMetricsTask getUploadMetricTaskForTesting(
            ContentResolver contentResolver, String[] filesSelected, boolean mediaPickerWasUsed) {
        return new RecordUploadMetricsTask(contentResolver, filesSelected, mediaPickerWasUsed);
    }

    /** Clears all captured camera files. */
    public static void clearCapturedCameraFiles() {
        PostTask.postTask(
                TaskTraits.BEST_EFFORT_MAY_BLOCK,
                () -> {
                    try {
                        File path =
                                UiUtils.getDirectoryForImageCapture(
                                        ContextUtils.getApplicationContext());
                        if (!path.isDirectory()) return;
                        File[] files = path.listFiles();
                        if (files == null) return;
                        long now = System.currentTimeMillis();
                        for (File file : files) {
                            if (now - file.lastModified()
                                    > DURATION_BEFORE_FILE_CLEAN_UP_IN_MILLIS) {
                                if (!file.delete()) Log.e(TAG, "Failed to delete: " + file);
                            }
                        }
                    } catch (IOException e) {
                        Log.w(TAG, "Failed to delete captured camera files.", e);
                    }
                });
    }

    private boolean eligibleForPhotoPicker() {
        return convertToSupportedPhotoPickerTypes(mFileTypes) != null;
    }

    protected void onFileSelected(
            long nativeSelectFileDialogImpl, String filePath, String displayName) {
        recordImageCountHistograms(new String[] {filePath});
        if (nativeSelectFileDialogImpl != 0) {
            SelectFileDialogJni.get()
                    .onFileSelected(
                            nativeSelectFileDialogImpl,
                            SelectFileDialog.this,
                            filePath,
                            displayName);
        }
    }

    protected void onMultipleFilesSelected(
            long nativeSelectFileDialogImpl, String[] filePathArray, String[] displayNameArray) {
        recordImageCountHistograms(filePathArray);
        if (nativeSelectFileDialogImpl != 0) {
            SelectFileDialogJni.get()
                    .onMultipleFilesSelected(
                            nativeSelectFileDialogImpl,
                            SelectFileDialog.this,
                            filePathArray,
                            displayNameArray);
        }
    }

    protected void onFileNotSelected(long nativeSelectFileDialogImpl) {
        recordImageCountHistograms(new String[] {});
        if (nativeSelectFileDialogImpl != 0) {
            SelectFileDialogJni.get()
                    .onFileNotSelected(nativeSelectFileDialogImpl, SelectFileDialog.this);
        }
    }

    private void recordImageCountHistograms(String[] filesSelected) {
        if (eligibleForPhotoPicker()) {
            // Record the total number of images selected via the Chrome Media Picker.
            RecordHistogram.recordCount100Histogram(
                    "Android.SelectFileDialogImgCount", filesSelected.length);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            new RecordUploadMetricsTask(
                            ContextUtils.getApplicationContext().getContentResolver(),
                            filesSelected,
                            mMediaPickerWasUsed)
                    .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        }
    }

    // This function returns the method used to upload, by looking it up from
    // mediaType details.
    private static int getUploadMethod(@FileSelectedAction int mediaType) {
        switch (mediaType) {
            case FileSelectedAction.MEDIA_PICKER_IMAGE_BY_MIME_TYPE:
            case FileSelectedAction.MEDIA_PICKER_IMAGE_BY_EXTENSION:
                return FileSelectedUploadMethod.MEDIA_PICKER_IMAGE;
            case FileSelectedAction.MEDIA_PICKER_VIDEO_BY_MIME_TYPE:
            case FileSelectedAction.MEDIA_PICKER_VIDEO_BY_EXTENSION:
                return FileSelectedUploadMethod.MEDIA_PICKER_VIDEO;
            case FileSelectedAction.MEDIA_PICKER_OTHER_BY_MIME_TYPE:
            case FileSelectedAction.MEDIA_PICKER_OTHER_BY_EXTENSION:
                return FileSelectedUploadMethod.MEDIA_PICKER_OTHER;
            case FileSelectedAction.MEDIA_PICKER_UNKNOWN_TYPE:
                return FileSelectedUploadMethod.MEDIA_PICKER_UNKNOWN_TYPE;
            case FileSelectedAction.EXTERNAL_PICKER_IMAGE_BY_MIME_TYPE:
            case FileSelectedAction.EXTERNAL_PICKER_IMAGE_BY_EXTENSION:
                return FileSelectedUploadMethod.EXTERNAL_PICKER_IMAGE;
            case FileSelectedAction.EXTERNAL_PICKER_VIDEO_BY_MIME_TYPE:
            case FileSelectedAction.EXTERNAL_PICKER_VIDEO_BY_EXTENSION:
                return FileSelectedUploadMethod.EXTERNAL_PICKER_VIDEO;
            case FileSelectedAction.EXTERNAL_PICKER_OTHER_BY_MIME_TYPE:
            case FileSelectedAction.EXTERNAL_PICKER_OTHER_BY_EXTENSION:
                return FileSelectedUploadMethod.EXTERNAL_PICKER_OTHER;
            case FileSelectedAction.EXTERNAL_PICKER_UNKNOWN_TYPE:
                return FileSelectedUploadMethod.EXTERNAL_PICKER_UNKNOWN_TYPE;
            default:
                assert false;
                return FileSelectedUploadMethod.MEDIA_PICKER_UNKNOWN_TYPE;
        }
    }

    private int getMediaType(
            Uri mediaUri, boolean mediaPickerWasUsed, ContentResolver contentResolver) {
        if (mediaUri == null) {
            return mediaPickerWasUsed
                    ? FileSelectedAction.MEDIA_PICKER_UNKNOWN_TYPE
                    : FileSelectedAction.EXTERNAL_PICKER_UNKNOWN_TYPE;
        }

        // Note: ContentResolver also allows MEDIA_TYPE to be queried instead, but that is less
        // reliable than the MIME type (frequently returns MEDIA_TYPE_IMAGE when selecting videos in
        // Android picker in the emulator).
        String[] filePathColumn = {
            MediaStore.Files.FileColumns.MIME_TYPE,
        };

        Cursor cursor = null;
        try {
            cursor = contentResolver.query(mediaUri, filePathColumn, null, null, null);
        } catch (Exception e) {
            // The OS may fail at some point during this, as seen in crbug.com/1395702.
            Log.w(TAG, "Failed to use ContentResolver", e);
            return mediaPickerWasUsed
                    ? FileSelectedAction.MEDIA_PICKER_UNKNOWN_TYPE
                    : FileSelectedAction.EXTERNAL_PICKER_UNKNOWN_TYPE;
        }

        if (cursor != null) {
            Integer mediaType = null;
            if (cursor.moveToFirst()) {
                int column = cursor.getColumnIndex(filePathColumn[0]);
                if (column != -1) {
                    String mimeType = cursor.getString(column);
                    if (mimeType != null) {
                        if (mimeType.startsWith("image/")) {
                            mediaType =
                                    mediaPickerWasUsed
                                            ? FileSelectedAction.MEDIA_PICKER_IMAGE_BY_MIME_TYPE
                                            : FileSelectedAction.EXTERNAL_PICKER_IMAGE_BY_MIME_TYPE;
                        } else if (mimeType.startsWith("video/")) {
                            mediaType =
                                    mediaPickerWasUsed
                                            ? FileSelectedAction.MEDIA_PICKER_VIDEO_BY_MIME_TYPE
                                            : FileSelectedAction.EXTERNAL_PICKER_VIDEO_BY_MIME_TYPE;
                        } else {
                            mediaType =
                                    mediaPickerWasUsed
                                            ? FileSelectedAction.MEDIA_PICKER_OTHER_BY_MIME_TYPE
                                            : FileSelectedAction.EXTERNAL_PICKER_OTHER_BY_MIME_TYPE;
                        }
                    }
                }
            }
            cursor.close();
            if (mediaType != null) {
                return mediaType;
            }
        }

        // Unable to look up the MIME type, most likely because this URI is for media captured by
        // the camera. Use the extension if provided.
        int index = mediaUri.getPath().lastIndexOf(".");
        if (index > -1) {
            String extension = mediaUri.getPath().substring(index);
            for (String right : POPULAR_IMAGE_EXTENSIONS) {
                if (extension.equalsIgnoreCase(right)) {
                    return mediaPickerWasUsed
                            ? FileSelectedAction.MEDIA_PICKER_IMAGE_BY_EXTENSION
                            : FileSelectedAction.EXTERNAL_PICKER_IMAGE_BY_EXTENSION;
                }
            }
            for (String right : POPULAR_VIDEO_EXTENSIONS) {
                if (extension.equalsIgnoreCase(right)) {
                    return mediaPickerWasUsed
                            ? FileSelectedAction.MEDIA_PICKER_VIDEO_BY_EXTENSION
                            : FileSelectedAction.EXTERNAL_PICKER_VIDEO_BY_EXTENSION;
                }
            }

            return mediaPickerWasUsed
                    ? FileSelectedAction.MEDIA_PICKER_OTHER_BY_EXTENSION
                    : FileSelectedAction.EXTERNAL_PICKER_OTHER_BY_EXTENSION;
        }

        return mediaPickerWasUsed
                ? FileSelectedAction.MEDIA_PICKER_UNKNOWN_TYPE
                : FileSelectedAction.EXTERNAL_PICKER_UNKNOWN_TYPE;
    }

    private static void logFileSelectedAction(@FileSelectedAction int action) {
        RecordHistogram.recordEnumeratedHistogram(
                "Android.SelectFileDialogContentSelected", action, FileSelectedAction.COUNT);
        RecordHistogram.recordEnumeratedHistogram(
                "Android.SelectFileDialogUploadMethods",
                getUploadMethod(action),
                FileSelectedUploadMethod.COUNT);
    }

    private static boolean shouldShowPhotoPicker() {
        return sPhotoPickerDelegate != null;
    }

    private static boolean photoPickerSupportsVideo() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false;
        return shouldShowPhotoPicker();
    }

    private static boolean preferAndroidMediaPickerViaGetContent() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
                && sPhotoPickerDelegate != null
                && sPhotoPickerDelegate.launchViaActionGetContent();
    }

    private static boolean preferAndroidMediaPickerViaPickImage() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
                && sPhotoPickerDelegate != null
                && sPhotoPickerDelegate.launchViaActionPickImages();
    }

    private static boolean preferAndroidMediaPickerViaPickImagePlus() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
                && sPhotoPickerDelegate != null
                && sPhotoPickerDelegate.launchViaActionPickImagesPlus();
    }

    private static boolean preferAndroidMediaPicker() {
        return preferAndroidMediaPickerViaGetContent()
                || preferAndroidMediaPickerViaPickImage()
                || preferAndroidMediaPickerViaPickImagePlus();
    }

    /**
     * The Android media picker currently doesn't fully support multiple mime types. It can only do
     * a single specific mime type, all images, all videos, or all media (images/videos).
     */
    private static String singleMimeTypeForAndroidPicker(List<String> mimeTypes) {
        if (mimeTypes.size() == 1) {
            return mimeTypes.get(0);
        }

        boolean showImages = false;
        boolean showVideos = false;
        for (String mimeType : mimeTypes) {
            String type = mimeType.toLowerCase(Locale.ROOT);
            if (type.startsWith(IMAGE_TYPE)) {
                showImages = true;
            } else if (type.startsWith(VIDEO_TYPE)) {
                showVideos = true;
            }

            if (showImages && showVideos) break;
        }

        if (showImages && showVideos) {
            return ALL_TYPES;
        } else if (showVideos) {
            return VIDEO_TYPE + "/*";
        } else if (showImages) {
            return IMAGE_TYPE + "/*";
        } else {
            return "";
        }
    }

    private static void logMediaPickerShown(int value) {
        RecordHistogram.recordEnumeratedHistogram(
                "Android.MediaPickerShown", value, SHOWING_ENUM_COUNT);
    }

    private static String resolvePackageNameFromIntent(Intent intent) {
        String packageName = "";
        ResolveInfo resolveInfo = PackageManagerUtils.resolveActivity(intent, 0);
        if (resolveInfo != null
                && resolveInfo.activityInfo != null
                && resolveInfo.activityInfo.applicationInfo != null
                && resolveInfo.activityInfo.applicationInfo.packageName != null) {
            packageName = resolveInfo.activityInfo.applicationInfo.packageName;
        }
        return packageName;
    }

    private static boolean showPhotoPicker(
            WindowAndroid windowAndroid,
            WindowAndroid.IntentCallback intentCallback,
            PhotoPickerListener listener,
            boolean allowMultiple,
            List<String> mimeTypes) {
        if (preferAndroidMediaPickerViaGetContent()) {
            return showAndroidMediaPickerIndirect(
                    windowAndroid, intentCallback, allowMultiple, mimeTypes);
        } else if (preferAndroidMediaPickerViaPickImage()
                || preferAndroidMediaPickerViaPickImagePlus()) {
            return showAndroidMediaPickerDirect(
                    windowAndroid, intentCallback, allowMultiple, mimeTypes);
        } else {
            return showChromeMediaPicker(windowAndroid, listener, allowMultiple, mimeTypes);
        }
    }

    private static boolean showAndroidMediaPickerDirect(
            WindowAndroid windowAndroid,
            WindowAndroid.IntentCallback intentCallback,
            boolean allowMultiple,
            List<String> mimeTypes) {
        // This default value is kept for backwards compatibility, but it is effectively never used,
        // because the Android Media Picker is limited to T and newer (in preferAndroidMediaPicker).
        int maxImagesForUpload = 50;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            maxImagesForUpload = MediaStore.getPickImagesMaxLimit();
        }

        Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
        if (allowMultiple) {
            intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxImagesForUpload);
        }

        // This flag is currently a no-op for the Android Media Picker, but we are hoping it is
        // something the team will consider adding in the future (we have to add it now for
        // scheduling purposes).
        intent.putExtra("forceShowBrowse", true);

        // Note: The showAndroidMediaPickerDirect is not only used for the Direct and DirectPlus
        // flavors, but also as a fallback for Indirect. Only the Direct flavor should use the
        // deprecated MIME-type code-path.
        if (!preferAndroidMediaPickerViaPickImage()) {
            intent.setType("*/*");
            intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toArray(new String[0]));
        } else {
            String mimeType = singleMimeTypeForAndroidPicker(mimeTypes);
            if (mimeType.isEmpty()) {
                return false;
            }
            intent.setType(mimeType);
        }

        if (!windowAndroid.showIntent(
                intent, intentCallback, /* errorId= */ R.string.opening_android_media_picker)) {
            return false;
        }

        logMediaPickerShown(SHOWING_ANDROID_PICKER_DIRECT);
        return true;
    }

    private static boolean showAndroidMediaPickerIndirect(
            WindowAndroid windowAndroid,
            WindowAndroid.IntentCallback intentCallback,
            boolean allowMultiple,
            List<String> mimeTypes) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        if (allowMultiple) {
            // Note that the ACTION_GET_CONTENT intent does not support a parameter to set a max
            // limit of photos (ACTION_PICK_IMAGES support is via MediaStore.EXTRA_PICK_IMAGES_MAX).
            // There is therefore no enforced max limit and all we need to do is set the 'allow
            // multiple' flag.
            intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
        }
        intent.setType("*/*");
        intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toArray(new String[0]));

        // When relying on the indirect way of launching the Android Media Picker, we want to be
        // sure that the Android Media Picker is the one handling the request and not something
        // random.
        String packageNameForGetContent = resolvePackageNameFromIntent(intent);
        if (!"com.google.android.providers.media.module".equals(packageNameForGetContent)) {
            return showAndroidMediaPickerDirect(
                    windowAndroid, intentCallback, allowMultiple, mimeTypes);
        }

        if (!windowAndroid.showIntent(
                intent, intentCallback, /* errorId= */ R.string.opening_android_media_picker)) {
            return false;
        }

        logMediaPickerShown(SHOWING_ANDROID_PICKER_INDIRECT);
        return true;
    }

    private static boolean showChromeMediaPicker(
            WindowAndroid windowAndroid,
            PhotoPickerListener listener,
            boolean allowMultiple,
            List<String> mimeTypes) {
        if (sPhotoPickerDelegate == null) return false;
        assert sPhotoPicker == null;
        sPhotoPicker =
                sPhotoPickerDelegate.showPhotoPicker(
                        windowAndroid, listener, allowMultiple, mimeTypes);
        logMediaPickerShown(SHOWING_CHROME_PICKER);
        return true;
    }

    @CalledByNative
    private void nativeDestroyed() {
        mNativeSelectFileDialog = 0;
    }

    @VisibleForTesting
    @CalledByNative
    static SelectFileDialog create(long nativeSelectFileDialog) {
        return new SelectFileDialog(nativeSelectFileDialog);
    }

    @NativeMethods
    interface Natives {
        void onFileSelected(
                long nativeSelectFileDialogImpl,
                SelectFileDialog caller,
                String filePath,
                String displayName);

        void onMultipleFilesSelected(
                long nativeSelectFileDialogImpl,
                SelectFileDialog caller,
                String[] filePathArray,
                String[] displayNameArray);

        void onFileNotSelected(long nativeSelectFileDialogImpl, SelectFileDialog caller);
    }
}