/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.media;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.system.Os;
import android.util.Log;

import androidx.annotation.RequiresApi;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.annotation.MinSdk;
import com.android.modules.utils.build.SdkLevel;

import java.io.FileNotFoundException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 Android 12 introduces Compatible media transcoding feature.  See
 <a href="https://developer.android.com/about/versions/12/features#compatible_media_transcoding">
 Compatible media transcoding</a>. MediaTranscodingManager provides an interface to the system's media
 transcoding service and can be used to transcode media files, e.g. transcoding a video from HEVC to
 AVC.

 <h3>Transcoding Types</h3>
 <h4>Video Transcoding</h4>
 When transcoding a video file, the video track will be transcoded based on the desired track format
 and the audio track will be pass through without any modification.
 <p class=note>
 Note that currently only support transcoding video file in mp4 format and with single video track.

 <h3>Transcoding Request</h3>
 <p>
 To transcode a media file, first create a {@link TranscodingRequest} through its builder class
 {@link VideoTranscodingRequest.Builder}. Transcode requests are then enqueue to the manager through
 {@link MediaTranscodingManager#enqueueRequest(
         TranscodingRequest, Executor, OnTranscodingFinishedListener)}
 TranscodeRequest are processed based on client process's priority and request priority. When a
 transcode operation is completed the caller is notified via its
 {@link OnTranscodingFinishedListener}.
 In the meantime the caller may use the returned TranscodingSession object to cancel or check the
 status of a specific transcode operation.
 <p>
 Here is an example where <code>Builder</code> is used to specify all parameters

 <pre class=prettyprint>
 VideoTranscodingRequest request =
    new VideoTranscodingRequest.Builder(srcUri, dstUri, videoFormat).build();
 }</pre>
 @hide
 */
@MinSdk(Build.VERSION_CODES.S)
@RequiresApi(Build.VERSION_CODES.S)
@SystemApi
public final class MediaTranscodingManager {
    private static final String TAG = "MediaTranscodingManager";

    /** Maximum number of retry to connect to the service. */
    private static final int CONNECT_SERVICE_RETRY_COUNT = 100;

    /** Interval between trying to reconnect to the service. */
    private static final int INTERVAL_CONNECT_SERVICE_RETRY_MS = 40;

    /** Default bpp(bits-per-pixel) to use for calculating default bitrate. */
    private static final float BPP = 0.25f;

    /**
     * Listener that gets notified when a transcoding operation has finished.
     * This listener gets notified regardless of how the operation finished. It is up to the
     * listener implementation to check the result and take appropriate action.
     */
    @FunctionalInterface
    public interface OnTranscodingFinishedListener {
        /**
         * Called when the transcoding operation has finished. The receiver may use the
         * TranscodingSession to check the result, i.e. whether the operation succeeded, was
         * canceled or if an error occurred.
         *
         * @param session The TranscodingSession instance for the finished transcoding operation.
         */
        void onTranscodingFinished(@NonNull TranscodingSession session);
    }

    private final Context mContext;
    private ContentResolver mContentResolver;
    private final String mPackageName;
    private final int mPid;
    private final int mUid;
    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
    private final HashMap<Integer, TranscodingSession> mPendingTranscodingSessions = new HashMap();
    private final Object mLock = new Object();
    @GuardedBy("mLock")
    @NonNull private ITranscodingClient mTranscodingClient = null;
    private static MediaTranscodingManager sMediaTranscodingManager;

    private void handleTranscodingFinished(int sessionId, TranscodingResultParcel result) {
        synchronized (mPendingTranscodingSessions) {
            // Gets the session associated with the sessionId and removes it from
            // mPendingTranscodingSessions.
            final TranscodingSession session = mPendingTranscodingSessions.remove(sessionId);

            if (session == null) {
                // This should not happen in reality.
                Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions");
                return;
            }

            // Updates the session status and result.
            session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED,
                    TranscodingSession.RESULT_SUCCESS,
                    TranscodingSession.ERROR_NONE);

            // Notifies client the session is done.
            if (session.mListener != null && session.mListenerExecutor != null) {
                session.mListenerExecutor.execute(
                        () -> session.mListener.onTranscodingFinished(session));
            }
        }
    }

    private void handleTranscodingFailed(int sessionId, int errorCode) {
        synchronized (mPendingTranscodingSessions) {
            // Gets the session associated with the sessionId and removes it from
            // mPendingTranscodingSessions.
            final TranscodingSession session = mPendingTranscodingSessions.remove(sessionId);

            if (session == null) {
                // This should not happen in reality.
                Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions");
                return;
            }

            // Updates the session status and result.
            session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED,
                    TranscodingSession.RESULT_ERROR, errorCode);

            // Notifies client the session failed.
            if (session.mListener != null && session.mListenerExecutor != null) {
                session.mListenerExecutor.execute(
                        () -> session.mListener.onTranscodingFinished(session));
            }
        }
    }

    private void handleTranscodingProgressUpdate(int sessionId, int newProgress) {
        synchronized (mPendingTranscodingSessions) {
            // Gets the session associated with the sessionId.
            final TranscodingSession session = mPendingTranscodingSessions.get(sessionId);

            if (session == null) {
                // This should not happen in reality.
                Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions");
                return;
            }

            // Update session progress and notify clients.
            session.updateProgress(newProgress);
        }
    }

    private IMediaTranscodingService getService(boolean retry) {
        // Do not try to get the service on pre-S. The service is lazy-start and getting the
        // service could block.
        if (!SdkLevel.isAtLeastS()) {
            return null;
        }

        int retryCount = !retry ? 1 :  CONNECT_SERVICE_RETRY_COUNT;
        Log.i(TAG, "get service with retry " + retryCount);
        for (int count = 1;  count <= retryCount; count++) {
            Log.d(TAG, "Trying to connect to service. Try count: " + count);
            IMediaTranscodingService service = IMediaTranscodingService.Stub.asInterface(
                    MediaFrameworkInitializer
                    .getMediaServiceManager()
                    .getMediaTranscodingServiceRegisterer()
                    .get());
            if (service != null) {
                return service;
            }
            try {
                // Sleep a bit before retry.
                Thread.sleep(INTERVAL_CONNECT_SERVICE_RETRY_MS);
            } catch (InterruptedException ie) {
                /* ignore */
            }
        }
        Log.w(TAG, "Failed to get service");
        return null;
    }

    /*
     * Handle client binder died event.
     * Upon receiving a binder died event of the client, we will do the following:
     * 1) For the session that is running, notify the client that the session is failed with
     *    error code,  so client could choose to retry the session or not.
     *    TODO(hkuang): Add a new error code to signal service died error.
     * 2) For the sessions that is still pending or paused, we will resubmit the session
     *    once we successfully reconnect to the service and register a new client.
     * 3) When trying to connect to the service and register a new client. The service may need time
     *    to reboot or never boot up again. So we will retry for a number of times. If we still
     *    could not connect, we will notify client session failure for the pending and paused
     *    sessions.
     */
    private void onClientDied() {
        synchronized (mLock) {
            mTranscodingClient = null;
        }

        // Delegates the session notification and retry to the executor as it may take some time.
        mExecutor.execute(() -> {
            // List to track the sessions that we want to retry.
            List<TranscodingSession> retrySessions = new ArrayList<TranscodingSession>();

            // First notify the client of session failure for all the running sessions.
            synchronized (mPendingTranscodingSessions) {
                for (Map.Entry<Integer, TranscodingSession> entry :
                        mPendingTranscodingSessions.entrySet()) {
                    TranscodingSession session = entry.getValue();

                    if (session.getStatus() == TranscodingSession.STATUS_RUNNING) {
                        session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED,
                                TranscodingSession.RESULT_ERROR,
                                TranscodingSession.ERROR_SERVICE_DIED);

                        // Remove the session from pending sessions.
                        mPendingTranscodingSessions.remove(entry.getKey());

                        if (session.mListener != null && session.mListenerExecutor != null) {
                            Log.i(TAG, "Notify client session failed");
                            session.mListenerExecutor.execute(
                                    () -> session.mListener.onTranscodingFinished(session));
                        }
                    } else if (session.getStatus() == TranscodingSession.STATUS_PENDING
                            || session.getStatus() == TranscodingSession.STATUS_PAUSED) {
                        // Add the session to retrySessions to handle them later.
                        retrySessions.add(session);
                    }
                }
            }

            // Try to register with the service once it boots up.
            IMediaTranscodingService service = getService(true /*retry*/);
            boolean haveTranscodingClient = false;
            if (service != null) {
                synchronized (mLock) {
                    mTranscodingClient = registerClient(service);
                    if (mTranscodingClient != null) {
                        haveTranscodingClient = true;
                    }
                }
            }

            for (TranscodingSession session : retrySessions) {
                // Notify the session failure if we fails to connect to the service or fail
                // to retry the session.
                if (!haveTranscodingClient) {
                    // TODO(hkuang): Return correct error code to the client.
                    handleTranscodingFailed(session.getSessionId(), 0 /*unused */);
                }

                try {
                    // Do not set hasRetried for retry initiated by MediaTranscodingManager.
                    session.retryInternal(false /*setHasRetried*/);
                } catch (Exception re) {
                    // TODO(hkuang): Return correct error code to the client.
                    handleTranscodingFailed(session.getSessionId(), 0 /*unused */);
                }
            }
        });
    }

    private void updateStatus(int sessionId, int status) {
        synchronized (mPendingTranscodingSessions) {
            final TranscodingSession session = mPendingTranscodingSessions.get(sessionId);

            if (session == null) {
                // This should not happen in reality.
                Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions");
                return;
            }

            // Updates the session status.
            session.updateStatus(status);
        }
    }

    // Just forwards all the events to the event handler.
    private ITranscodingClientCallback mTranscodingClientCallback =
            new ITranscodingClientCallback.Stub() {
                // TODO(hkuang): Add more unit test to test difference file open mode.
                @Override
                public ParcelFileDescriptor openFileDescriptor(String fileUri, String mode)
                        throws RemoteException {
                    if (!mode.equals("r") && !mode.equals("w") && !mode.equals("rw")) {
                        Log.e(TAG, "Unsupport mode: " + mode);
                        return null;
                    }

                    Uri uri = Uri.parse(fileUri);
                    try {
                        AssetFileDescriptor afd = mContentResolver.openAssetFileDescriptor(uri,
                                mode);
                        if (afd != null) {
                            return afd.getParcelFileDescriptor();
                        }
                    } catch (FileNotFoundException e) {
                        Log.w(TAG, "Cannot find content uri: " + uri, e);
                    } catch (SecurityException e) {
                        Log.w(TAG, "Cannot open content uri: " + uri, e);
                    } catch (Exception e) {
                        Log.w(TAG, "Unknown content uri: " + uri, e);
                    }
                    return null;
                }

                @Override
                public void onTranscodingStarted(int sessionId) throws RemoteException {
                    updateStatus(sessionId, TranscodingSession.STATUS_RUNNING);
                }

                @Override
                public void onTranscodingPaused(int sessionId) throws RemoteException {
                    updateStatus(sessionId, TranscodingSession.STATUS_PAUSED);
                }

                @Override
                public void onTranscodingResumed(int sessionId) throws RemoteException {
                    updateStatus(sessionId, TranscodingSession.STATUS_RUNNING);
                }

                @Override
                public void onTranscodingFinished(int sessionId, TranscodingResultParcel result)
                        throws RemoteException {
                    handleTranscodingFinished(sessionId, result);
                }

                @Override
                public void onTranscodingFailed(int sessionId, int errorCode)
                        throws RemoteException {
                    handleTranscodingFailed(sessionId, errorCode);
                }

                @Override
                public void onAwaitNumberOfSessionsChanged(int sessionId, int oldAwaitNumber,
                        int newAwaitNumber) throws RemoteException {
                    //TODO(hkuang): Implement this.
                }

                @Override
                public void onProgressUpdate(int sessionId, int newProgress)
                        throws RemoteException {
                    handleTranscodingProgressUpdate(sessionId, newProgress);
                }
            };

    private ITranscodingClient registerClient(IMediaTranscodingService service) {
        synchronized (mLock) {
            try {
                // Registers the client with MediaTranscoding service.
                mTranscodingClient = service.registerClient(
                        mTranscodingClientCallback,
                        mPackageName,
                        mPackageName);

                if (mTranscodingClient != null) {
                    mTranscodingClient.asBinder().linkToDeath(() -> onClientDied(), /* flags */ 0);
                }
            } catch (Exception ex) {
                Log.e(TAG, "Failed to register new client due to exception " + ex);
                mTranscodingClient = null;
            }
        }
        return mTranscodingClient;
    }

    /**
     * @hide
     */
    public MediaTranscodingManager(@NonNull Context context) {
        mContext = context;
        mContentResolver = mContext.getContentResolver();
        mPackageName = mContext.getPackageName();
        mUid = Os.getuid();
        mPid = Os.getpid();
    }

    /**
     * Abstract base class for all the TranscodingRequest.
     * <p> TranscodingRequest encapsulates the desired configuration for the transcoding.
     */
    public abstract static class TranscodingRequest {
        /**
         *
         * Default transcoding type.
         * @hide
         */
        public static final int TRANSCODING_TYPE_UNKNOWN = 0;

        /**
         * TRANSCODING_TYPE_VIDEO indicates that client wants to perform transcoding on a video.
         * <p>Note that currently only support transcoding video file in mp4 format.
         * @hide
         */
        public static final int TRANSCODING_TYPE_VIDEO = 1;

        /**
         * TRANSCODING_TYPE_IMAGE indicates that client wants to perform transcoding on an image.
         * @hide
         */
        public static final int TRANSCODING_TYPE_IMAGE = 2;

        /** @hide */
        @IntDef(prefix = {"TRANSCODING_TYPE_"}, value = {
                TRANSCODING_TYPE_UNKNOWN,
                TRANSCODING_TYPE_VIDEO,
                TRANSCODING_TYPE_IMAGE,
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface TranscodingType {}

        /**
         * Default value.
         *
         * @hide
         */
        public static final int PRIORITY_UNKNOWN = 0;
        /**
         * PRIORITY_REALTIME indicates that the transcoding request is time-critical and that the
         * client wants the transcoding result as soon as possible.
         * <p> Set PRIORITY_REALTIME only if the transcoding is time-critical as it will involve
         * performance penalty due to resource reallocation to prioritize the sessions with higher
         * priority.
         *
         * @hide
         */
        public static final int PRIORITY_REALTIME = 1;

        /**
         * PRIORITY_OFFLINE indicates the transcoding is not time-critical and the client does not
         * need the transcoding result as soon as possible.
         * <p>Sessions with PRIORITY_OFFLINE will be scheduled behind PRIORITY_REALTIME. Always set
         * to
         * PRIORITY_OFFLINE if client does not need the result as soon as possible and could accept
         * delay of the transcoding result.
         *
         * @hide
         *
         */
        public static final int PRIORITY_OFFLINE = 2;

        /** @hide */
        @IntDef(prefix = {"PRIORITY_"}, value = {
                PRIORITY_UNKNOWN,
                PRIORITY_REALTIME,
                PRIORITY_OFFLINE,
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface TranscodingPriority {}

        /** Uri of the source media file. */
        private @NonNull Uri mSourceUri;

        /** Uri of the destination media file. */
        private @NonNull Uri mDestinationUri;

        /** FileDescriptor of the source media file. */
        private @Nullable ParcelFileDescriptor mSourceFileDescriptor;

        /** FileDescriptor of the destination media file. */
        private @Nullable ParcelFileDescriptor mDestinationFileDescriptor;

        /**
         *  The UID of the client that the TranscodingRequest is for. Only privileged caller could
         *  set this Uid as only they could do the transcoding on behalf of the client.
         *  -1 means not available.
         */
        private int mClientUid = -1;

        /**
         *  The Pid of the client that the TranscodingRequest is for. Only privileged caller could
         *  set this Uid as only they could do the transcoding on behalf of the client.
         *  -1 means not available.
         */
        private int mClientPid = -1;

        /** Type of the transcoding. */
        private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN;

        /** Priority of the transcoding. */
        private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN;

        /**
         * Desired image format for the destination file.
         * <p> If this is null, source file's image track will be passed through and copied to the
         * destination file.
         * @hide
         */
        private @Nullable MediaFormat mImageFormat = null;

        @VisibleForTesting
        private TranscodingTestConfig mTestConfig = null;

        /**
         * Prevent public constructor access.
         */
        /* package private */ TranscodingRequest() {
        }

        private TranscodingRequest(Builder b) {
            mSourceUri = b.mSourceUri;
            mSourceFileDescriptor = b.mSourceFileDescriptor;
            mDestinationUri = b.mDestinationUri;
            mDestinationFileDescriptor = b.mDestinationFileDescriptor;
            mClientUid = b.mClientUid;
            mClientPid = b.mClientPid;
            mPriority = b.mPriority;
            mType = b.mType;
            mTestConfig = b.mTestConfig;
        }

        /**
         * Return the type of the transcoding.
         * @hide
         */
        @TranscodingType
        public int getType() {
            return mType;
        }

        /** Return source uri of the transcoding. */
        @NonNull
        public Uri getSourceUri() {
            return mSourceUri;
        }

        /**
         * Return source file descriptor of the transcoding.
         * This will be null if client has not provided it.
         */
        @Nullable
        public ParcelFileDescriptor getSourceFileDescriptor() {
            return mSourceFileDescriptor;
        }

        /** Return the UID of the client that this request is for. -1 means not available. */
        public int getClientUid() {
            return mClientUid;
        }

        /** Return the PID of the client that this request is for. -1 means not available. */
        public int getClientPid() {
            return mClientPid;
        }

        /** Return destination uri of the transcoding. */
        @NonNull
        public Uri getDestinationUri() {
            return mDestinationUri;
        }

        /**
         * Return destination file descriptor of the transcoding.
         * This will be null if client has not provided it.
         */
        @Nullable
        public ParcelFileDescriptor getDestinationFileDescriptor() {
            return mDestinationFileDescriptor;
        }

        /**
         * Return priority of the transcoding.
         * @hide
         */
        @TranscodingPriority
        public int getPriority() {
            return mPriority;
        }

        /**
         * Return TestConfig of the transcoding.
         * @hide
         */
        @Nullable
        public TranscodingTestConfig getTestConfig() {
            return mTestConfig;
        }

        abstract void writeFormatToParcel(TranscodingRequestParcel parcel);

        /* Writes the TranscodingRequest to a parcel. */
        private TranscodingRequestParcel writeToParcel(@NonNull Context context) {
            TranscodingRequestParcel parcel = new TranscodingRequestParcel();
            switch (mPriority) {
            case PRIORITY_OFFLINE:
                parcel.priority = TranscodingSessionPriority.kUnspecified;
                break;
            case PRIORITY_REALTIME:
            case PRIORITY_UNKNOWN:
            default:
                parcel.priority = TranscodingSessionPriority.kNormal;
                break;
            }
            parcel.transcodingType = mType;
            parcel.sourceFilePath = mSourceUri.toString();
            parcel.sourceFd = mSourceFileDescriptor;
            parcel.destinationFilePath = mDestinationUri.toString();
            parcel.destinationFd = mDestinationFileDescriptor;
            parcel.clientUid = mClientUid;
            parcel.clientPid = mClientPid;
            if (mClientUid < 0) {
                parcel.clientPackageName = context.getPackageName();
            } else {
                String packageName = context.getPackageManager().getNameForUid(mClientUid);
                // PackageName is optional as some uid does not have package name. Set to
                // "Unavailable" string in this case.
                if (packageName == null) {
                    Log.w(TAG, "Failed to find package for uid: " + mClientUid);
                    packageName = "Unavailable";
                }
                parcel.clientPackageName = packageName;
            }
            writeFormatToParcel(parcel);
            if (mTestConfig != null) {
                parcel.isForTesting = true;
                parcel.testConfig = mTestConfig;
            }
            return parcel;
        }

        /**
         * Builder to build a {@link TranscodingRequest} object.
         *
         * @param <T> The subclass to be built.
         */
        abstract static class Builder<T extends Builder<T>> {
            private @NonNull Uri mSourceUri;
            private @NonNull Uri mDestinationUri;
            private @Nullable ParcelFileDescriptor mSourceFileDescriptor = null;
            private @Nullable ParcelFileDescriptor mDestinationFileDescriptor = null;
            private int mClientUid = -1;
            private int mClientPid = -1;
            private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN;
            private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN;
            private TranscodingTestConfig mTestConfig;

            abstract T self();

            /**
             * Creates a builder for building {@link TranscodingRequest}s.
             *
             * Client must set the source Uri. If client also provides the source fileDescriptor
             * through is provided by {@link #setSourceFileDescriptor(ParcelFileDescriptor)},
             * TranscodingSession will use the fd instead of calling back to the client to open the
             * sourceUri.
             *
             *
             * @param type The transcoding type.
             * @param sourceUri Content uri for the source media file.
             * @param destinationUri Content uri for the destination media file.
             *
             */
            private Builder(@TranscodingType int type, @NonNull Uri sourceUri,
                    @NonNull Uri destinationUri) {
                mType = type;

                if (sourceUri == null || Uri.EMPTY.equals(sourceUri)) {
                    throw new IllegalArgumentException(
                            "You must specify a non-empty source Uri.");
                }
                mSourceUri = sourceUri;

                if (destinationUri == null || Uri.EMPTY.equals(destinationUri)) {
                    throw new IllegalArgumentException(
                            "You must specify a non-empty destination Uri.");
                }
                mDestinationUri = destinationUri;
            }

            /**
             * Specifies the fileDescriptor opened from the source media file.
             *
             * This call is optional. If the source fileDescriptor is provided, TranscodingSession
             * will use it directly instead of opening the uri from {@link #Builder(int, Uri, Uri)}.
             * It is client's responsibility to make sure the fileDescriptor is opened from the
             * source uri.
             * @param fileDescriptor a {@link ParcelFileDescriptor} opened from source media file.
             * @return The same builder instance.
             * @throws IllegalArgumentException if fileDescriptor is invalid.
             */
            @NonNull
            public T setSourceFileDescriptor(@NonNull ParcelFileDescriptor fileDescriptor) {
                if (fileDescriptor == null || fileDescriptor.getFd() < 0) {
                    throw new IllegalArgumentException(
                            "Invalid source descriptor.");
                }
                mSourceFileDescriptor = fileDescriptor;
                return self();
            }

            /**
             * Specifies the fileDescriptor opened from the destination media file.
             *
             * This call is optional. If the destination fileDescriptor is provided,
             * TranscodingSession will use it directly instead of opening the source uri from
             * {@link #Builder(int, Uri, Uri)} upon transcoding starts. It is client's
             * responsibility to make sure the fileDescriptor is opened from the destination uri.
             * @param fileDescriptor a {@link ParcelFileDescriptor} opened from destination media
             *                       file.
             * @return The same builder instance.
             * @throws IllegalArgumentException if fileDescriptor is invalid.
             */
            @NonNull
            public T setDestinationFileDescriptor(
                    @NonNull ParcelFileDescriptor fileDescriptor) {
                if (fileDescriptor == null || fileDescriptor.getFd() < 0) {
                    throw new IllegalArgumentException(
                            "Invalid destination descriptor.");
                }
                mDestinationFileDescriptor = fileDescriptor;
                return self();
            }

            /**
             * Specify the UID of the client that this request is for.
             * <p>
             * Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could forward the
             * pid. Note that the permission check happens on the service side upon starting the
             * transcoding. If the client does not have the permission, the transcoding will fail.
             *
             * @param uid client Uid.
             * @return The same builder instance.
             * @throws IllegalArgumentException if uid is invalid.
             */
            @NonNull
            public T setClientUid(int uid) {
                if (uid < 0) {
                    throw new IllegalArgumentException("Invalid Uid");
                }
                mClientUid = uid;
                return self();
            }

            /**
             * Specify the pid of the client that this request is for.
             * <p>
             * Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could forward the
             * pid. Note that the permission check happens on the service side upon starting the
             * transcoding. If the client does not have the permission, the transcoding will fail.
             *
             * @param pid client Pid.
             * @return The same builder instance.
             * @throws IllegalArgumentException if pid is invalid.
             */
            @NonNull
            public T setClientPid(int pid) {
                if (pid < 0) {
                    throw new IllegalArgumentException("Invalid pid");
                }
                mClientPid = pid;
                return self();
            }

            /**
             * Specifies the priority of the transcoding.
             *
             * @param priority Must be one of the {@code PRIORITY_*}
             * @return The same builder instance.
             * @throws IllegalArgumentException if flags is invalid.
             * @hide
             */
            @NonNull
            public T setPriority(@TranscodingPriority int priority) {
                if (priority != PRIORITY_OFFLINE && priority != PRIORITY_REALTIME) {
                    throw new IllegalArgumentException("Invalid priority: " + priority);
                }
                mPriority = priority;
                return self();
            }

            /**
             * Sets the delay in processing this request.
             * @param config test config.
             * @return The same builder instance.
             * @hide
             */
            @VisibleForTesting
            @NonNull
            public T setTestConfig(@NonNull TranscodingTestConfig config) {
                mTestConfig = config;
                return self();
            }
        }

        /**
         * Abstract base class for all the format resolvers.
         */
        abstract static class MediaFormatResolver {
            private @NonNull ApplicationMediaCapabilities mClientCaps;

            /**
             * Prevents public constructor access.
             */
            /* package private */ MediaFormatResolver() {
            }

            /**
             * Constructs MediaFormatResolver object.
             *
             * @param clientCaps An ApplicationMediaCapabilities object containing the client's
             *                   capabilities.
             */
            MediaFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps) {
                if (clientCaps == null) {
                    throw new IllegalArgumentException("Client capabilities must not be null");
                }
                mClientCaps = clientCaps;
            }

            /**
             * Returns the client capabilities.
             */
            @NonNull
            /* package */ ApplicationMediaCapabilities getClientCapabilities() {
                return mClientCaps;
            }

            abstract boolean shouldTranscode();
        }

        /**
         * VideoFormatResolver for deciding if video transcoding is needed, and if so, the track
         * formats to use.
         */
        public static class VideoFormatResolver extends MediaFormatResolver {
            private static final int BIT_RATE = 20000000;            // 20Mbps

            private MediaFormat mSrcVideoFormatHint;
            private MediaFormat mSrcAudioFormatHint;

            /**
             * Constructs a new VideoFormatResolver object.
             *
             * @param clientCaps An ApplicationMediaCapabilities object containing the client's
             *                   capabilities.
             * @param srcVideoFormatHint A MediaFormat object containing information about the
             *                           source's video track format that could affect the
             *                           transcoding decision. Such information could include video
             *                           codec types, color spaces, whether special format info (eg.
             *                           slow-motion markers) are present, etc.. If a particular
             *                           information is not present, it will not be used to make the
             *                           decision.
             */
            public VideoFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps,
                    @NonNull MediaFormat srcVideoFormatHint) {
                super(clientCaps);
                mSrcVideoFormatHint = srcVideoFormatHint;
            }

            /**
             * Constructs a new VideoFormatResolver object.
             *
             * @param clientCaps An ApplicationMediaCapabilities object containing the client's
             *                   capabilities.
             * @param srcVideoFormatHint A MediaFormat object containing information about the
             *                           source's video track format that could affect the
             *                           transcoding decision. Such information could include video
             *                           codec types, color spaces, whether special format info (eg.
             *                           slow-motion markers) are present, etc.. If a particular
             *                           information is not present, it will not be used to make the
             *                           decision.
             * @param srcAudioFormatHint A MediaFormat object containing information about the
             *                           source's audio track format that could affect the
             *                           transcoding decision.
             * @hide
             */
            VideoFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps,
                    @NonNull MediaFormat srcVideoFormatHint,
                    @NonNull MediaFormat srcAudioFormatHint) {
                super(clientCaps);
                mSrcVideoFormatHint = srcVideoFormatHint;
                mSrcAudioFormatHint = srcAudioFormatHint;
            }

            /**
             * Returns whether the source content should be transcoded.
             *
             * @return true if the source should be transcoded.
             */
            public boolean shouldTranscode() {
                boolean supportHevc = getClientCapabilities().isVideoMimeTypeSupported(
                        MediaFormat.MIMETYPE_VIDEO_HEVC);
                if (!supportHevc && MediaFormat.MIMETYPE_VIDEO_HEVC.equals(
                        mSrcVideoFormatHint.getString(MediaFormat.KEY_MIME))) {
                    return true;
                }
                // TODO: add more checks as needed below.
                return false;
            }

            /**
             * Retrieves the video track format to be used on
             * {@link VideoTranscodingRequest.Builder#setVideoTrackFormat(MediaFormat)} for this
             * configuration.
             *
             * @return the video track format to be used if transcoding should be performed,
             *         and null otherwise.
             * @throws IllegalArgumentException if the hinted source video format contains invalid
             *         parameters.
             */
            @Nullable
            public MediaFormat resolveVideoFormat() {
                if (!shouldTranscode()) {
                    return null;
                }

                MediaFormat videoTrackFormat = new MediaFormat(mSrcVideoFormatHint);
                videoTrackFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);

                int width = mSrcVideoFormatHint.getInteger(MediaFormat.KEY_WIDTH, -1);
                int height = mSrcVideoFormatHint.getInteger(MediaFormat.KEY_HEIGHT, -1);
                if (width <= 0 || height <= 0) {
                    throw new IllegalArgumentException(
                            "Source Width and height must be larger than 0");
                }

                float frameRate =
                        mSrcVideoFormatHint.getNumber(MediaFormat.KEY_FRAME_RATE, 30.0)
                        .floatValue();
                if (frameRate <= 0) {
                    throw new IllegalArgumentException(
                            "frameRate must be larger than 0");
                }

                int bitrate = getAVCBitrate(width, height, frameRate);
                videoTrackFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
                return videoTrackFormat;
            }

            /**
             * Generate a default bitrate with the fixed bpp(bits-per-pixel) 0.25.
             * This maps to:
             * 1080P@30fps -> 16Mbps
             * 1080P@60fps-> 32Mbps
             * 4K@30fps -> 62Mbps
             */
            private static int getDefaultBitrate(int width, int height, float frameRate) {
                return (int) (width * height * frameRate * BPP);
            }

            /**
             * Query the bitrate from CamcorderProfile. If there are two profiles that match the
             * width/height/framerate, we will use the higher one to get better quality.
             * Return default bitrate if could not find any match profile.
             */
            private static int getAVCBitrate(int width, int height, float frameRate) {
                int bitrate = -1;
                int[] cameraIds = {0, 1};

                // Profiles ordered in decreasing order of preference.
                int[] preferQualities = {
                        CamcorderProfile.QUALITY_2160P,
                        CamcorderProfile.QUALITY_1080P,
                        CamcorderProfile.QUALITY_720P,
                        CamcorderProfile.QUALITY_480P,
                        CamcorderProfile.QUALITY_LOW,
                };

                for (int cameraId : cameraIds) {
                    for (int quality : preferQualities) {
                        // Check if camera id has profile for the quality level.
                        if (!CamcorderProfile.hasProfile(cameraId, quality)) {
                            continue;
                        }
                        CamcorderProfile profile = CamcorderProfile.get(cameraId, quality);
                        // Check the width/height/framerate/codec, also consider portrait case.
                        if (((width == profile.videoFrameWidth
                                && height == profile.videoFrameHeight)
                                || (height == profile.videoFrameWidth
                                && width == profile.videoFrameHeight))
                                && (int) frameRate == profile.videoFrameRate
                                && profile.videoCodec == MediaRecorder.VideoEncoder.H264) {
                            if (bitrate < profile.videoBitRate) {
                                bitrate = profile.videoBitRate;
                            }
                            break;
                        }
                    }
                }

                if (bitrate == -1) {
                    Log.w(TAG, "Failed to find CamcorderProfile for w: " + width + "h: " + height
                            + " fps: "
                            + frameRate);
                    bitrate = getDefaultBitrate(width, height, frameRate);
                }
                Log.d(TAG, "Using bitrate " + bitrate + " for " + width + " " + height + " "
                        + frameRate);
                return bitrate;
            }

            /**
             * Retrieves the audio track format to be used for transcoding.
             *
             * @return the audio track format to be used if transcoding should be performed, and
             *         null otherwise.
             * @hide
             */
            @Nullable
            public MediaFormat resolveAudioFormat() {
                if (!shouldTranscode()) {
                    return null;
                }
                // Audio transcoding is not supported yet, always return null.
                return null;
            }
        }
    }

    /**
     * VideoTranscodingRequest encapsulates the configuration for transcoding a video.
     */
    public static final class VideoTranscodingRequest extends TranscodingRequest {
        /**
         * Desired output video format of the destination file.
         * <p> If this is null, source file's video track will be passed through and copied to the
         * destination file.
         */
        private @Nullable MediaFormat mVideoTrackFormat = null;

        /**
         * Desired output audio format of the destination file.
         * <p> If this is null, source file's audio track will be passed through and copied to the
         * destination file.
         */
        private @Nullable MediaFormat mAudioTrackFormat = null;

        private VideoTranscodingRequest(VideoTranscodingRequest.Builder builder) {
            super(builder);
            mVideoTrackFormat = builder.mVideoTrackFormat;
            mAudioTrackFormat = builder.mAudioTrackFormat;
        }

        /**
         * Return the video track format of the transcoding.
         * This will be null if client has not specified the video track format.
         */
        @NonNull
        public MediaFormat getVideoTrackFormat() {
            return mVideoTrackFormat;
        }

        @Override
        void writeFormatToParcel(TranscodingRequestParcel parcel) {
            parcel.requestedVideoTrackFormat = convertToVideoTrackFormat(mVideoTrackFormat);
        }

        /* Converts the MediaFormat to TranscodingVideoTrackFormat. */
        private static TranscodingVideoTrackFormat convertToVideoTrackFormat(MediaFormat format) {
            if (format == null) {
                throw new IllegalArgumentException("Invalid MediaFormat");
            }

            TranscodingVideoTrackFormat trackFormat = new TranscodingVideoTrackFormat();

            if (format.containsKey(MediaFormat.KEY_MIME)) {
                String mime = format.getString(MediaFormat.KEY_MIME);
                if (MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) {
                    trackFormat.codecType = TranscodingVideoCodecType.kAvc;
                } else if (MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mime)) {
                    trackFormat.codecType = TranscodingVideoCodecType.kHevc;
                } else {
                    throw new UnsupportedOperationException("Only support transcode to avc/hevc");
                }
            }

            if (format.containsKey(MediaFormat.KEY_BIT_RATE)) {
                int bitrateBps = format.getInteger(MediaFormat.KEY_BIT_RATE);
                if (bitrateBps <= 0) {
                    throw new IllegalArgumentException("Bitrate must be larger than 0");
                }
                trackFormat.bitrateBps = bitrateBps;
            }

            if (format.containsKey(MediaFormat.KEY_WIDTH) && format.containsKey(
                    MediaFormat.KEY_HEIGHT)) {
                int width = format.getInteger(MediaFormat.KEY_WIDTH);
                int height = format.getInteger(MediaFormat.KEY_HEIGHT);
                if (width <= 0 || height <= 0) {
                    throw new IllegalArgumentException("Width and height must be larger than 0");
                }
                // TODO: Validate the aspect ratio after adding scaling.
                trackFormat.width = width;
                trackFormat.height = height;
            }

            if (format.containsKey(MediaFormat.KEY_PROFILE)) {
                int profile = format.getInteger(MediaFormat.KEY_PROFILE);
                if (profile <= 0) {
                    throw new IllegalArgumentException("Invalid codec profile");
                }
                // TODO: Validate the profile according to codec type.
                trackFormat.profile = profile;
            }

            if (format.containsKey(MediaFormat.KEY_LEVEL)) {
                int level = format.getInteger(MediaFormat.KEY_LEVEL);
                if (level <= 0) {
                    throw new IllegalArgumentException("Invalid codec level");
                }
                // TODO: Validate the level according to codec type.
                trackFormat.level = level;
            }

            return trackFormat;
        }

        /**
         * Builder class for {@link VideoTranscodingRequest}.
         */
        public static final class Builder extends
                TranscodingRequest.Builder<VideoTranscodingRequest.Builder> {
            /**
             * Desired output video format of the destination file.
             * <p> If this is null, source file's video track will be passed through and
             * copied to the destination file.
             */
            private @Nullable MediaFormat mVideoTrackFormat = null;

            /**
             * Desired output audio format of the destination file.
             * <p> If this is null, source file's audio track will be passed through and copied
             * to the destination file.
             */
            private @Nullable MediaFormat mAudioTrackFormat = null;

            /**
             * Creates a builder for building {@link VideoTranscodingRequest}s.
             *
             * <p> Client could only specify the settings that matters to them, e.g. codec format or
             * bitrate. And by default, transcoding will preserve the original video's settings
             * (bitrate, framerate, resolution) if not provided.
             * <p>Note that some settings may silently fail to apply if the device does not support
             * them.
             * @param sourceUri Content uri for the source media file.
             * @param destinationUri Content uri for the destination media file.
             * @param videoFormat MediaFormat containing the settings that client wants override in
             *                    the original video's video track.
             * @throws IllegalArgumentException if videoFormat is invalid.
             */
            public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri,
                    @NonNull MediaFormat videoFormat) {
                super(TRANSCODING_TYPE_VIDEO, sourceUri, destinationUri);
                setVideoTrackFormat(videoFormat);
            }

            @Override
            @NonNull
            public Builder setClientUid(int uid) {
                super.setClientUid(uid);
                return self();
            }

            @Override
            @NonNull
            public Builder setClientPid(int pid) {
                super.setClientPid(pid);
                return self();
            }

            @Override
            @NonNull
            public Builder setSourceFileDescriptor(@NonNull ParcelFileDescriptor fd) {
                super.setSourceFileDescriptor(fd);
                return self();
            }

            @Override
            @NonNull
            public Builder setDestinationFileDescriptor(@NonNull ParcelFileDescriptor fd) {
                super.setDestinationFileDescriptor(fd);
                return self();
            }

            private void setVideoTrackFormat(@NonNull MediaFormat videoFormat) {
                if (videoFormat == null) {
                    throw new IllegalArgumentException("videoFormat must not be null");
                }

                // Check if the MediaFormat is for video by looking at the MIME type.
                String mime = videoFormat.containsKey(MediaFormat.KEY_MIME)
                        ? videoFormat.getString(MediaFormat.KEY_MIME) : null;
                if (mime == null || !mime.startsWith("video/")) {
                    throw new IllegalArgumentException("Invalid video format: wrong mime type");
                }

                mVideoTrackFormat = videoFormat;
            }

            /**
             * @return a new {@link TranscodingRequest} instance successfully initialized
             * with all the parameters set on this <code>Builder</code>.
             * @throws UnsupportedOperationException if the parameters set on the
             *                                       <code>Builder</code> were incompatible, or
             *                                       if they are not supported by the
             *                                       device.
             */
            @NonNull
            public VideoTranscodingRequest build() {
                return new VideoTranscodingRequest(this);
            }

            @Override
            VideoTranscodingRequest.Builder self() {
                return this;
            }
        }
    }

    /**
     * Handle to an enqueued transcoding operation. An instance of this class represents a single
     * enqueued transcoding operation. The caller can use that instance to query the status or
     * progress, and to get the result once the operation has completed.
     */
    public static final class TranscodingSession {
        /** The session is enqueued but not yet running. */
        public static final int STATUS_PENDING = 1;
        /** The session is currently running. */
        public static final int STATUS_RUNNING = 2;
        /** The session is finished. */
        public static final int STATUS_FINISHED = 3;
        /** The session is paused. */
        public static final int STATUS_PAUSED = 4;

        /** @hide */
        @IntDef(prefix = { "STATUS_" }, value = {
                STATUS_PENDING,
                STATUS_RUNNING,
                STATUS_FINISHED,
                STATUS_PAUSED,
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface Status {}

        /** The session does not have a result yet. */
        public static final int RESULT_NONE = 1;
        /** The session completed successfully. */
        public static final int RESULT_SUCCESS = 2;
        /** The session encountered an error while running. */
        public static final int RESULT_ERROR = 3;
        /** The session was canceled by the caller. */
        public static final int RESULT_CANCELED = 4;

        /** @hide */
        @IntDef(prefix = { "RESULT_" }, value = {
                RESULT_NONE,
                RESULT_SUCCESS,
                RESULT_ERROR,
                RESULT_CANCELED,
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface Result {}


        // The error code exposed here should be in sync with:
        // frameworks/av/media/libmediatranscoding/aidl/android/media/TranscodingErrorCode.aidl
        /** @hide */
        @IntDef(prefix = { "TRANSCODING_SESSION_ERROR_" }, value = {
                ERROR_NONE,
                ERROR_DROPPED_BY_SERVICE,
                ERROR_SERVICE_DIED})
        @Retention(RetentionPolicy.SOURCE)
        public @interface TranscodingSessionErrorCode{}
        /**
         * Constant indicating that no error occurred.
         */
        public static final int ERROR_NONE = 0;

        /**
         * Constant indicating that the session is dropped by Transcoding service due to hitting
         * the limit, e.g. too many back to back transcoding happen in a short time frame.
         */
        public static final int ERROR_DROPPED_BY_SERVICE = 1;

        /**
         * Constant indicating the backing transcoding service is died. Client should enqueue the
         * the request again.
         */
        public static final int ERROR_SERVICE_DIED = 2;

        /** Listener that gets notified when the progress changes. */
        @FunctionalInterface
        public interface OnProgressUpdateListener {
            /**
             * Called when the progress changes. The progress is in percentage between 0 and 1,
             * where 0 means the session has not yet started and 100 means that it has finished.
             *
             * @param session      The session associated with the progress.
             * @param progress The new progress ranging from 0 ~ 100 inclusive.
             */
            void onProgressUpdate(@NonNull TranscodingSession session,
                    @IntRange(from = 0, to = 100) int progress);
        }

        private final MediaTranscodingManager mManager;
        private Executor mListenerExecutor;
        private OnTranscodingFinishedListener mListener;
        private int mSessionId = -1;
        // Lock for internal state.
        private final Object mLock = new Object();
        @GuardedBy("mLock")
        private Executor mProgressUpdateExecutor = null;
        @GuardedBy("mLock")
        private OnProgressUpdateListener mProgressUpdateListener = null;
        @GuardedBy("mLock")
        private int mProgress = 0;
        @GuardedBy("mLock")
        private int mProgressUpdateInterval = 0;
        @GuardedBy("mLock")
        private @Status int mStatus = STATUS_PENDING;
        @GuardedBy("mLock")
        private @Result int mResult = RESULT_NONE;
        @GuardedBy("mLock")
        private @TranscodingSessionErrorCode int mErrorCode = ERROR_NONE;
        @GuardedBy("mLock")
        private boolean mHasRetried = false;
        // The original request that associated with this session.
        private final TranscodingRequest mRequest;

        private TranscodingSession(
                @NonNull MediaTranscodingManager manager,
                @NonNull TranscodingRequest request,
                @NonNull TranscodingSessionParcel parcel,
                @NonNull @CallbackExecutor Executor executor,
                @NonNull OnTranscodingFinishedListener listener) {
            Objects.requireNonNull(manager, "manager must not be null");
            Objects.requireNonNull(parcel, "parcel must not be null");
            Objects.requireNonNull(executor, "listenerExecutor must not be null");
            Objects.requireNonNull(listener, "listener must not be null");
            mManager = manager;
            mSessionId = parcel.sessionId;
            mListenerExecutor = executor;
            mListener = listener;
            mRequest = request;
        }

        /**
         * Set a progress listener.
         * @param executor The executor on which listener will be invoked.
         * @param listener The progress listener.
         */
        public void setOnProgressUpdateListener(
                @NonNull @CallbackExecutor Executor executor,
                @NonNull OnProgressUpdateListener listener) {
            synchronized (mLock) {
                Objects.requireNonNull(executor, "listenerExecutor must not be null");
                Objects.requireNonNull(listener, "listener must not be null");
                mProgressUpdateExecutor = executor;
                mProgressUpdateListener = listener;
            }
        }

        /** Removes the progress listener if any. */
        public void clearOnProgressUpdateListener() {
            synchronized (mLock) {
                mProgressUpdateExecutor = null;
                mProgressUpdateListener = null;
            }
        }

        private void updateStatusAndResult(@Status int sessionStatus,
                @Result int sessionResult, @TranscodingSessionErrorCode int errorCode) {
            synchronized (mLock) {
                mStatus = sessionStatus;
                mResult = sessionResult;
                mErrorCode = errorCode;
            }
        }

        /**
         * Retrieve the error code associated with the RESULT_ERROR.
         */
        public @TranscodingSessionErrorCode int getErrorCode() {
            synchronized (mLock) {
                return mErrorCode;
            }
        }

        /**
         * Resubmit the transcoding session to the service.
         * Note that only the session that fails or gets cancelled could be retried and each session
         * could be retried only once. After that, Client need to enqueue a new request if they want
         * to try again.
         *
         * @return true if successfully resubmit the job to service. False otherwise.
         * @throws UnsupportedOperationException if the retry could not be fulfilled.
         * @hide
         */
        public boolean retry() {
            return retryInternal(true /*setHasRetried*/);
        }

        // TODO(hkuang): Add more test for it.
        private boolean retryInternal(boolean setHasRetried) {
            synchronized (mLock) {
                if (mStatus == STATUS_PENDING || mStatus == STATUS_RUNNING) {
                    throw new UnsupportedOperationException(
                            "Failed to retry as session is in processing");
                }

                if (mHasRetried) {
                    throw new UnsupportedOperationException("Session has been retried already");
                }

                // Get the client interface.
                ITranscodingClient client = mManager.getTranscodingClient();
                if (client == null) {
                    Log.e(TAG, "Service rebooting. Try again later");
                    return false;
                }

                synchronized (mManager.mPendingTranscodingSessions) {
                    try {
                        // Submits the request to MediaTranscoding service.
                        TranscodingSessionParcel sessionParcel = new TranscodingSessionParcel();
                        if (!client.submitRequest(mRequest.writeToParcel(mManager.mContext),
                                                  sessionParcel)) {
                            mHasRetried = true;
                            throw new UnsupportedOperationException("Failed to enqueue request");
                        }

                        // Replace the old session id wit the new one.
                        mSessionId = sessionParcel.sessionId;
                        // Adds the new session back into pending sessions.
                        mManager.mPendingTranscodingSessions.put(mSessionId, this);
                    } catch (RemoteException re) {
                        return false;
                    }
                    mStatus = STATUS_PENDING;
                    mHasRetried = setHasRetried ? true : false;
                }
            }
            return true;
        }

        /**
         * Cancels the transcoding session and notify the listener.
         * If the session happened to finish before being canceled this call is effectively a no-op
         * and will not update the result in that case.
         */
        public void cancel() {
            synchronized (mLock) {
                // Check if the session is finished already.
                if (mStatus != STATUS_FINISHED) {
                    try {
                        ITranscodingClient client = mManager.getTranscodingClient();
                        // The client may be gone.
                        if (client != null) {
                            client.cancelSession(mSessionId);
                        }
                    } catch (RemoteException re) {
                        //TODO(hkuang): Find out what to do if failing to cancel the session.
                        Log.e(TAG, "Failed to cancel the session due to exception:  " + re);
                    }
                    mStatus = STATUS_FINISHED;
                    mResult = RESULT_CANCELED;

                    // Notifies client the session is canceled.
                    mListenerExecutor.execute(() -> mListener.onTranscodingFinished(this));
                }
            }
        }

        /**
         * Gets the progress of the transcoding session. The progress is between 0 and 100, where 0
         * means that the session has not yet started and 100 means that it is finished. For the
         * cancelled session, the progress will be the last updated progress before it is cancelled.
         * @return The progress.
         */
        @IntRange(from = 0, to = 100)
        public int getProgress() {
            synchronized (mLock) {
                return mProgress;
            }
        }

        /**
         * Gets the status of the transcoding session.
         * @return The status.
         */
        public @Status int getStatus() {
            synchronized (mLock) {
                return mStatus;
            }
        }

        /**
         * Adds a client uid that is also waiting for this transcoding session.
         * <p>
         * Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could add the
         * uid. Note that the permission check happens on the service side upon starting the
         * transcoding. If the client does not have the permission, the transcoding will fail.
         * @param uid  the additional client uid to be added.
         * @return true if successfully added, false otherwise.
         */
        public boolean addClientUid(int uid) {
            if (uid < 0) {
                throw new IllegalArgumentException("Invalid Uid");
            }

            // Get the client interface.
            ITranscodingClient client = mManager.getTranscodingClient();
            if (client == null) {
                Log.e(TAG, "Service is dead...");
                return false;
            }

            try {
                if (!client.addClientUid(mSessionId, uid)) {
                    Log.e(TAG, "Failed to add client uid");
                    return false;
                }
            } catch (Exception ex) {
                Log.e(TAG, "Failed to get client uids due to " + ex);
                return false;
            }
            return true;
        }

        /**
         * Query all the client that waiting for this transcoding session
         * @return a list containing all the client uids.
         */
        @NonNull
        public List<Integer> getClientUids() {
            List<Integer> uidList = new ArrayList<Integer>();

            // Get the client interface.
            ITranscodingClient client = mManager.getTranscodingClient();
            if (client == null) {
                Log.e(TAG, "Service is dead...");
                return uidList;
            }

            try {
                int[] clientUids  = client.getClientUids(mSessionId);
                for (int i : clientUids) {
                    uidList.add(i);
                }
            } catch (Exception ex) {
                Log.e(TAG, "Failed to get client uids due to " + ex);
            }

            return uidList;
        }

        /**
         * Gets sessionId of the transcoding session.
         * @return session id.
         */
        public int getSessionId() {
            return mSessionId;
        }

        /**
         * Gets the result of the transcoding session.
         * @return The result.
         */
        public @Result int getResult() {
            synchronized (mLock) {
                return mResult;
            }
        }

        @Override
        public String toString() {
            String result;
            String status;

            switch (mResult) {
                case RESULT_NONE:
                    result = "RESULT_NONE";
                    break;
                case RESULT_SUCCESS:
                    result = "RESULT_SUCCESS";
                    break;
                case RESULT_ERROR:
                    result = "RESULT_ERROR(" + mErrorCode + ")";
                    break;
                case RESULT_CANCELED:
                    result = "RESULT_CANCELED";
                    break;
                default:
                    result = String.valueOf(mResult);
                    break;
            }

            switch (mStatus) {
                case STATUS_PENDING:
                    status = "STATUS_PENDING";
                    break;
                case STATUS_PAUSED:
                    status = "STATUS_PAUSED";
                    break;
                case STATUS_RUNNING:
                    status = "STATUS_RUNNING";
                    break;
                case STATUS_FINISHED:
                    status = "STATUS_FINISHED";
                    break;
                default:
                    status = String.valueOf(mStatus);
                    break;
            }
            return String.format(" session: {id: %d, status: %s, result: %s, progress: %d}",
                    mSessionId, status, result, mProgress);
        }

        private void updateProgress(int newProgress) {
            synchronized (mLock) {
                mProgress = newProgress;
                if (mProgressUpdateExecutor != null && mProgressUpdateListener != null) {
                    final OnProgressUpdateListener listener = mProgressUpdateListener;
                    mProgressUpdateExecutor.execute(
                            () -> listener.onProgressUpdate(this, newProgress));
                }
            }
        }

        private void updateStatus(int newStatus) {
            synchronized (mLock) {
                mStatus = newStatus;
            }
        }
    }

    private ITranscodingClient getTranscodingClient() {
        synchronized (mLock) {
            return mTranscodingClient;
        }
    }

    /**
     * Enqueues a TranscodingRequest for execution.
     * <p> Upon successfully accepting the request, MediaTranscodingManager will return a
     * {@link TranscodingSession} to the client. Client should use {@link TranscodingSession} to
     * track the progress and get the result.
     * <p> MediaTranscodingManager will return null if fails to accept the request due to service
     * rebooting. Client could retry again after receiving null.
     *
     * @param transcodingRequest The TranscodingRequest to enqueue.
     * @param listenerExecutor   Executor on which the listener is notified.
     * @param listener           Listener to get notified when the transcoding session is finished.
     * @return A TranscodingSession for this operation.
     * @throws UnsupportedOperationException if the request could not be fulfilled.
     */
    @Nullable
    public TranscodingSession enqueueRequest(
            @NonNull TranscodingRequest transcodingRequest,
            @NonNull @CallbackExecutor Executor listenerExecutor,
            @NonNull OnTranscodingFinishedListener listener) {
        Log.i(TAG, "enqueueRequest called.");
        Objects.requireNonNull(transcodingRequest, "transcodingRequest must not be null");
        Objects.requireNonNull(listenerExecutor, "listenerExecutor must not be null");
        Objects.requireNonNull(listener, "listener must not be null");

        // Converts the request to TranscodingRequestParcel.
        TranscodingRequestParcel requestParcel = transcodingRequest.writeToParcel(mContext);

        Log.i(TAG, "Getting transcoding request " + transcodingRequest.getSourceUri());

        // Submits the request to MediaTranscoding service.
        try {
            TranscodingSessionParcel sessionParcel = new TranscodingSessionParcel();
            // Synchronizes the access to mPendingTranscodingSessions to make sure the session Id is
            // inserted in the mPendingTranscodingSessions in the callback handler.
            synchronized (mPendingTranscodingSessions) {
                synchronized (mLock) {
                    if (mTranscodingClient == null) {
                        // Try to register with the service again.
                        IMediaTranscodingService service = getService(false /*retry*/);
                        if (service == null) {
                            Log.w(TAG, "Service rebooting. Try again later");
                            return null;
                        }
                        mTranscodingClient = registerClient(service);
                        // If still fails, throws an exception to tell client to try later.
                        if (mTranscodingClient == null) {
                            Log.w(TAG, "Service rebooting. Try again later");
                            return null;
                        }
                    }

                    if (!mTranscodingClient.submitRequest(requestParcel, sessionParcel)) {
                        throw new UnsupportedOperationException("Failed to enqueue request");
                    }
                }

                // Wraps the TranscodingSessionParcel into a TranscodingSession and returns it to
                // client for tracking.
                TranscodingSession session = new TranscodingSession(this, transcodingRequest,
                        sessionParcel,
                        listenerExecutor,
                        listener);

                // Adds the new session into pending sessions.
                mPendingTranscodingSessions.put(session.getSessionId(), session);
                return session;
            }
        } catch (RemoteException ex) {
            Log.w(TAG, "Service rebooting. Try again later");
            return null;
        } catch (ServiceSpecificException ex) {
            throw new UnsupportedOperationException(
                    "Failed to submit request to Transcoding service. Error: " + ex);
        }
    }
}
