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

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

package org.chromium.components.browser_ui.photo_picker;

import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.util.Pair;

import androidx.annotation.IntDef;

import org.chromium.base.ThreadUtils;
import org.chromium.base.task.AsyncTask;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Locale;

/** A worker task to decode video and extract information from it off of the UI thread. */
class DecodeVideoTask extends AsyncTask<List<Bitmap>> {
    /** The possible error states while decoding. */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
        DecodingResult.SUCCESS,
        DecodingResult.FILE_ERROR,
        DecodingResult.RUNTIME_ERROR,
        DecodingResult.IO_ERROR
    })
    public @interface DecodingResult {
        int SUCCESS = 0;
        int FILE_ERROR = 1;
        int RUNTIME_ERROR = 2;
        int IO_ERROR = 3;
    }

    /** An interface to use to communicate back the results to the client. */
    public interface VideoDecodingCallback {
        /**
         * A callback to define to receive the list of all images on disk.
         *
         * @param uri The uri of the video decoded.
         * @param bitmaps An array of thumbnails extracted from the video.
         * @param duration The duration of the video.
         * @param fullWidth Whether the image is using the full width of the screen.
         * @param decodingStatus Whether the decoding was successful.
         */
        void videoDecodedCallback(
                Uri uri,
                List<Bitmap> bitmaps,
                String duration,
                boolean fullWidth,
                @DecodingResult int decodingStatus,
                float ratio);
    }

    // The callback to use to communicate the results.
    private VideoDecodingCallback mCallback;

    // The URI of the video to decode.
    private Uri mUri;

    // The desired width and height (in pixels) of the returned thumbnail from the video.
    int mSize;

    // Whether the image is taking up the full width of the screen.
    boolean mFullWidth;

    // The number of frames to extract.
    int mFrames;

    // The interval between frames (in milliseconds).
    long mIntervalMs;

    // The ContentResolver to use to retrieve image metadata from disk.
    private ContentResolver mContentResolver;

    // Keeps track of errors during decoding.
    private @DecodingResult int mDecodingResult;

    // The duration of the video.
    private String mDuration;

    // The ratio of the first frame of the video.
    private float mRatio;

    /**
     * A DecodeVideoTask constructor.
     *
     * @param callback The callback to use to communicate back the results.
     * @param contentResolver The ContentResolver to use to retrieve image metadata from disk.
     * @param uri The URI of the video to decode.
     * @param size The desired width and height (in pixels) of the returned thumbnail from the
     *     video.
     * @param fullWidth Whether this is a video thumbnail that takes up the full screen width.
     * @param frames The number of frames to extract.
     * @param intervalMs The interval between frames (in milliseconds).
     */
    public DecodeVideoTask(
            VideoDecodingCallback callback,
            ContentResolver contentResolver,
            Uri uri,
            int size,
            boolean fullWidth,
            int frames,
            long intervalMs) {
        mCallback = callback;
        mContentResolver = contentResolver;
        mUri = uri;
        mSize = size;
        mFullWidth = fullWidth;
        mFrames = frames;
        mIntervalMs = intervalMs;
    }

    /**
     * Converts a duration string in ms to a human-readable form.
     *
     * @param durationMs The duration in milliseconds.
     * @return The duration in human-readable form.
     */
    public static String formatDuration(Long durationMs) {
        if (durationMs == null) return null;

        long duration = durationMs / 1000;
        long hours = duration / 3600;
        duration -= hours * 3600;
        long minutes = duration / 60;
        duration -= minutes * 60;
        long seconds = duration;
        if (hours > 0) {
            return String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds);
        } else {
            return String.format(Locale.US, "%d:%02d", minutes, seconds);
        }
    }

    /**
     * Decodes a video and extracts metadata and a thumbnail. Called on a non-UI thread
     *
     * @return A list of bitmaps (video thumbnails).
     */
    @Override
    protected List<Bitmap> doInBackground() {
        assert !ThreadUtils.runningOnUiThread();

        if (isCancelled()) return null;

        // TODO(finnur): Apply try-with-resources to MediaMetadataRetriever once Chrome no longer
        //               supports versions below Build.VERSION_CODES.Q. API 29 is when it started
        //               to implement AutoCloseable:
        //
        // https://developer.android.com/sdk/api_diff/29/changes/android.media.MediaMetadataRetriever
        MediaMetadataRetriever retriever = null;
        try (AssetFileDescriptor afd = mContentResolver.openAssetFileDescriptor(mUri, "r")) {
            retriever = new MediaMetadataRetriever();
            retriever.setDataSource(afd.getFileDescriptor());
            String duration =
                    retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
            if (duration != null) {
                // Adjust to a shorter video, if the frame requests exceed the length of the video.
                long durationMs = Long.parseLong(duration);
                if (mFrames > 1 && mFrames * mIntervalMs > durationMs) {
                    mIntervalMs = durationMs / mFrames;
                }
                duration = formatDuration(durationMs);
            }
            Pair<List<Bitmap>, Float> bitmaps =
                    BitmapUtils.decodeVideoFromFileDescriptor(
                            retriever,
                            afd.getFileDescriptor(),
                            mSize,
                            mFrames,
                            mFullWidth,
                            mIntervalMs);
            mDuration = duration;
            mRatio = bitmaps.second;
            mDecodingResult = DecodingResult.SUCCESS;
            return bitmaps.first;
        } catch (FileNotFoundException exception) {
            mDecodingResult = DecodingResult.FILE_ERROR;
            return null;
        } catch (RuntimeException exception) {
            mDecodingResult = DecodingResult.RUNTIME_ERROR;
            return null;
        } catch (IOException exception) {
            mDecodingResult = DecodingResult.IO_ERROR;
            return null;
        } finally {
            try {
                if (retriever != null) retriever.release();
            } catch (IOException exception) {
            }
        }
    }

    /**
     * Communicates the results back to the client. Called on the UI thread.
     *
     * @param results A pair of bitmap (video thumbnail) and the duration of the video.
     */
    @Override
    protected void onPostExecute(List<Bitmap> results) {
        if (isCancelled()) {
            return;
        }

        if (results == null) {
            mCallback.videoDecodedCallback(mUri, null, "", mFullWidth, mDecodingResult, 1.0f);
            return;
        }

        mCallback.videoDecodedCallback(
                mUri, results, mDuration, mFullWidth, mDecodingResult, mRatio);
    }
}