transcoding: move MediaTranscodeManager to apex

bug: 159172726
test: builds; presubmit; manual testing transcoding locally.
Change-Id: I727912a04cf4f7f03fdf5fa0a74e522f69dca66f
diff --git a/apex/media/framework/Android.bp b/apex/media/framework/Android.bp
index b3c9a9a..ea30b7a 100644
--- a/apex/media/framework/Android.bp
+++ b/apex/media/framework/Android.bp
@@ -36,7 +36,8 @@
         "framework_media_annotation",
     ],
     static_libs: [
-        "exoplayer2-extractor"
+        "exoplayer2-extractor",
+        "mediatranscoding_aidl_interface-java",
     ],
     jarjar_rules: "jarjar_rules.txt",
 
@@ -53,10 +54,12 @@
 filegroup {
     name: "updatable-media-srcs",
     srcs: [
+        "java/android/media/MediaFrameworkInitializer.java",
         ":media-aidl-srcs",
         ":mediaparceledlistslice-java-srcs",
         ":mediaparser-srcs",
         ":mediasession2-java-srcs",
+        ":mediatranscoding-srcs",
     ],
 }
 
@@ -93,6 +96,17 @@
     path: "java",
 }
 
+filegroup {
+    name: "mediatranscoding-srcs",
+    srcs: [
+        "java/android/media/ApplicationMediaCapabilities.java",
+        "java/android/media/MediaFeature.java",
+        "java/android/media/MediaTranscodeManager.java",
+        "java/android/media/MediaTranscodingException.java",
+    ],
+    path: "java",
+}
+
 java_sdk_library {
     name: "framework-media",
     defaults: ["framework-module-defaults"],
diff --git a/apex/media/framework/api/current.txt b/apex/media/framework/api/current.txt
index 0cc8e52..ce3bcbe 100644
--- a/apex/media/framework/api/current.txt
+++ b/apex/media/framework/api/current.txt
@@ -1,6 +1,26 @@
 // Signature format: 2.0
 package android.media {
 
+  public final class ApplicationMediaCapabilities implements android.os.Parcelable {
+    method @NonNull public static android.media.ApplicationMediaCapabilities createFromXml(@NonNull org.xmlpull.v1.XmlPullParser);
+    method public int describeContents();
+    method @NonNull public java.util.List<java.lang.String> getSupportedHdrTypes();
+    method @NonNull public java.util.List<java.lang.String> getSupportedVideoMimeTypes();
+    method public boolean isHdrTypeSupported(@NonNull String);
+    method public boolean isSlowMotionSupported();
+    method public boolean isVideoMimeTypeSupported(@NonNull String);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.media.ApplicationMediaCapabilities> CREATOR;
+  }
+
+  public static final class ApplicationMediaCapabilities.Builder {
+    ctor public ApplicationMediaCapabilities.Builder();
+    method @NonNull public android.media.ApplicationMediaCapabilities.Builder addSupportedHdrType(@NonNull String);
+    method @NonNull public android.media.ApplicationMediaCapabilities.Builder addSupportedVideoMimeType(@NonNull String);
+    method @NonNull public android.media.ApplicationMediaCapabilities build();
+    method @NonNull public android.media.ApplicationMediaCapabilities.Builder setSlowMotionSupported(boolean);
+  }
+
   public class MediaController2 implements java.lang.AutoCloseable {
     method public void cancelSessionCommand(@NonNull Object);
     method public void close();
@@ -25,6 +45,17 @@
     method @Nullable public android.media.Session2Command.Result onSessionCommand(@NonNull android.media.MediaController2, @NonNull android.media.Session2Command, @Nullable android.os.Bundle);
   }
 
+  public final class MediaFeature {
+    ctor public MediaFeature();
+  }
+
+  public static final class MediaFeature.HdrType {
+    field public static final String DOLBY_VISION = "android.media.feature.hdr.dolby_vision";
+    field public static final String HDR10 = "android.media.feature.hdr.hdr10";
+    field public static final String HDR10_PLUS = "android.media.feature.hdr.hdr10_plus";
+    field public static final String HLG = "android.media.feature.hdr.hlg";
+  }
+
   public final class MediaParser {
     method public boolean advance(@NonNull android.media.MediaParser.SeekableInputReader) throws java.io.IOException;
     method @NonNull public static android.media.MediaParser create(@NonNull android.media.MediaParser.OutputConsumer, @NonNull java.lang.String...);
@@ -170,6 +201,12 @@
     method public int getNotificationId();
   }
 
+  public class MediaTranscodingException extends java.lang.Exception {
+  }
+
+  public static final class MediaTranscodingException.ServiceNotAvailableException extends android.media.MediaTranscodingException {
+  }
+
   public final class Session2Command implements android.os.Parcelable {
     ctor public Session2Command(int);
     ctor public Session2Command(@NonNull String, @Nullable android.os.Bundle);
diff --git a/apex/media/framework/api/module-lib-current.txt b/apex/media/framework/api/module-lib-current.txt
index 2b69863..ad9114f 100644
--- a/apex/media/framework/api/module-lib-current.txt
+++ b/apex/media/framework/api/module-lib-current.txt
@@ -1,6 +1,11 @@
 // Signature format: 2.0
 package android.media {
 
+  public class MediaFrameworkInitializer {
+    method public static void registerServiceWrappers();
+    method public static void setMediaServiceManager(@NonNull android.media.MediaServiceManager);
+  }
+
   @Deprecated public final class MediaParceledListSlice<T extends android.os.Parcelable> implements android.os.Parcelable {
     ctor @Deprecated public MediaParceledListSlice(@NonNull java.util.List<T>);
     method @Deprecated public int describeContents();
diff --git a/apex/media/framework/api/system-current.txt b/apex/media/framework/api/system-current.txt
index d802177..89cf77c 100644
--- a/apex/media/framework/api/system-current.txt
+++ b/apex/media/framework/api/system-current.txt
@@ -1 +1,67 @@
 // Signature format: 2.0
+package android.media {
+
+  public final class MediaTranscodeManager {
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingSession enqueueRequest(@NonNull android.media.MediaTranscodeManager.TranscodingRequest, @NonNull java.util.concurrent.Executor, @NonNull android.media.MediaTranscodeManager.OnTranscodingFinishedListener) throws java.io.FileNotFoundException, android.media.MediaTranscodingException.ServiceNotAvailableException;
+    field public static final int PRIORITY_REALTIME = 1; // 0x1
+    field public static final int TRANSCODING_TYPE_VIDEO = 1; // 0x1
+  }
+
+  @java.lang.FunctionalInterface public static interface MediaTranscodeManager.OnTranscodingFinishedListener {
+    method public void onTranscodingFinished(@NonNull android.media.MediaTranscodeManager.TranscodingSession);
+  }
+
+  public static final class MediaTranscodeManager.TranscodingRequest {
+    method public int getClientPid();
+    method public int getClientUid();
+    method @NonNull public android.net.Uri getDestinationUri();
+    method public int getPriority();
+    method @NonNull public android.net.Uri getSourceUri();
+    method public int getType();
+    method @Nullable public android.media.MediaFormat getVideoTrackFormat();
+  }
+
+  public static final class MediaTranscodeManager.TranscodingRequest.Builder {
+    ctor public MediaTranscodeManager.TranscodingRequest.Builder();
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingRequest build();
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingRequest.Builder setClientPid(int);
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingRequest.Builder setClientUid(int);
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingRequest.Builder setDestinationUri(@NonNull android.net.Uri);
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingRequest.Builder setPriority(int);
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingRequest.Builder setSourceUri(@NonNull android.net.Uri);
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingRequest.Builder setType(int);
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingRequest.Builder setVideoTrackFormat(@NonNull android.media.MediaFormat);
+  }
+
+  public static class MediaTranscodeManager.TranscodingRequest.MediaFormatResolver {
+    ctor public MediaTranscodeManager.TranscodingRequest.MediaFormatResolver();
+    method @Nullable public android.media.MediaFormat resolveVideoFormat();
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingRequest.MediaFormatResolver setClientCapabilities(@NonNull android.media.ApplicationMediaCapabilities);
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingRequest.MediaFormatResolver setSourceVideoFormatHint(@NonNull android.media.MediaFormat);
+    method public boolean shouldTranscode();
+  }
+
+  public static final class MediaTranscodeManager.TranscodingSession {
+    method public void cancel();
+    method @IntRange(from=0, to=100) public int getProgress();
+    method public int getResult();
+    method public int getSessionId();
+    method public int getStatus();
+    method public void setOnProgressUpdateListener(@NonNull java.util.concurrent.Executor, @Nullable android.media.MediaTranscodeManager.TranscodingSession.OnProgressUpdateListener);
+    method public void setOnProgressUpdateListener(int, @NonNull java.util.concurrent.Executor, @Nullable android.media.MediaTranscodeManager.TranscodingSession.OnProgressUpdateListener);
+    field public static final int RESULT_CANCELED = 4; // 0x4
+    field public static final int RESULT_ERROR = 3; // 0x3
+    field public static final int RESULT_NONE = 1; // 0x1
+    field public static final int RESULT_SUCCESS = 2; // 0x2
+    field public static final int STATUS_FINISHED = 3; // 0x3
+    field public static final int STATUS_PAUSED = 4; // 0x4
+    field public static final int STATUS_PENDING = 1; // 0x1
+    field public static final int STATUS_RUNNING = 2; // 0x2
+  }
+
+  @java.lang.FunctionalInterface public static interface MediaTranscodeManager.TranscodingSession.OnProgressUpdateListener {
+    method public void onProgressUpdate(@NonNull android.media.MediaTranscodeManager.TranscodingSession, @IntRange(from=0, to=100) int);
+  }
+
+}
+
diff --git a/apex/media/framework/java/android/media/ApplicationMediaCapabilities.java b/apex/media/framework/java/android/media/ApplicationMediaCapabilities.java
new file mode 100644
index 0000000..36f6b94
--- /dev/null
+++ b/apex/media/framework/java/android/media/ApplicationMediaCapabilities.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2020 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.NonNull;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * ApplicationMediaCapabilities is an immutable class that encapsulates an application's
+ * capabilities for handling newer video codec format and media features.
+ *
+ * The ApplicationMediaCapabilities class is used by the platform to represent an application's
+ * media capabilities as defined in their manifest(TODO: Add link) in order to determine
+ * whether modern media files need to be transcoded for that application (TODO: Add link).
+ *
+ * ApplicationMediaCapabilities objects can also be built by applications at runtime for use with
+ * {@link ContentResolver#openTypedAssetFileDescriptor(Uri, String, Bundle)} to provide more
+ * control over the transcoding that is built into the platform. ApplicationMediaCapabilities
+ * provided by applications at runtime like this override the default manifest capabilities for that
+ * media access.
+ *
+ * <h3> Video Codec Support</h3>
+ * Newer video codes include HEVC, VP9 and AV1. Application only needs to indicate their support
+ * for newer format with this class as they are assumed to support older format like h.264.
+ *
+ * <h4>Capability of handling HDR(high dynamic range) video</h4>
+ * There are four types of HDR video(Dolby-Vision, HDR10, HDR10+, HLG) supported by the platform,
+ * application will only need to specify individual types they supported.
+ *
+ * <h4>Capability of handling Slow Motion video</h4>
+ * There is no standard format for slow motion yet. If an application indicates support for slow
+ * motion, it is application's responsibility to parse the slow motion videos using their own parser
+ * or using support library.
+ */
+// TODO(huang): Correct openTypedAssetFileDescriptor with the new API after it is added.
+// TODO(hkuang): Add a link to seamless transcoding detail when it is published
+// TODO(hkuang): Add code sample on how to build a capability object with MediaCodecList
+// TODO(hkuang): Add the support library page on parsing slow motion video.
+public final class ApplicationMediaCapabilities implements Parcelable {
+    private static final String TAG = "ApplicationMediaCapabilities";
+
+    /** List of supported video codec mime types. */
+    // TODO: init it with avc and mpeg4 as application is assuming to support them.
+    private Set<String> mSupportedVideoMimeTypes = new HashSet<>();
+
+    /** List of supported hdr types. */
+    private Set<String> mSupportedHdrTypes = new HashSet<>();
+
+    private boolean mIsSlowMotionSupported = false;
+
+    private ApplicationMediaCapabilities(Builder b) {
+        mSupportedVideoMimeTypes.addAll(b.getSupportedVideoMimeTypes());
+        mSupportedHdrTypes.addAll(b.getSupportedHdrTypes());
+        mIsSlowMotionSupported = b.mIsSlowMotionSupported;
+    }
+
+    /**
+     * Query if an video codec is supported by the application.
+     */
+    public boolean isVideoMimeTypeSupported(
+            @NonNull String videoMime) {
+        return mSupportedVideoMimeTypes.contains(videoMime);
+    }
+
+    /**
+     * Query if a hdr type is supported by the application.
+     */
+    public boolean isHdrTypeSupported(
+            @NonNull @MediaFeature.MediaHdrType String hdrType) {
+        return mSupportedHdrTypes.contains(hdrType);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        // Write out the supported video mime types.
+        dest.writeInt(mSupportedVideoMimeTypes.size());
+        for (String cap : mSupportedVideoMimeTypes) {
+            dest.writeString(cap);
+        }
+        // Write out the supported hdr types.
+        dest.writeInt(mSupportedHdrTypes.size());
+        for (String cap : mSupportedHdrTypes) {
+            dest.writeString(cap);
+        }
+        // Write out the supported slow motion.
+        dest.writeBoolean(mIsSlowMotionSupported);
+    }
+
+    @Override
+    public String toString() {
+        String caps = new String(
+                "Supported Video MimeTypes: " + mSupportedVideoMimeTypes.toString());
+        caps += "Supported HDR types: " + mSupportedHdrTypes.toString();
+        caps += "Supported slow motion: " + mIsSlowMotionSupported;
+        return caps;
+    }
+
+    @NonNull
+    public static final Creator<ApplicationMediaCapabilities> CREATOR =
+            new Creator<ApplicationMediaCapabilities>() {
+                public ApplicationMediaCapabilities createFromParcel(Parcel in) {
+                    ApplicationMediaCapabilities.Builder builder =
+                            new ApplicationMediaCapabilities.Builder();
+
+                    // Parse supported video codec mime types.
+                    int count = in.readInt();
+                    for (int readCount = 0; readCount < count; ++readCount) {
+                        builder.addSupportedVideoMimeType(in.readString());
+                    }
+                    // Parse supported hdr types.
+                    count = in.readInt();
+                    for (int readCount = 0; readCount < count; ++readCount) {
+                        builder.addSupportedHdrType(in.readString());
+                    }
+
+                    boolean supported = in.readBoolean();
+                    builder.setSlowMotionSupported(supported);
+
+                    return builder.build();
+                }
+
+                public ApplicationMediaCapabilities[] newArray(int size) {
+                    return new ApplicationMediaCapabilities[size];
+                }
+            };
+
+    /*
+     * Returns a list that contains all the video codec mime types supported by the application.
+     * The list will be empty if no codecs are supported by the application.
+     * @return List of supported video codec mime types.
+     */
+    @NonNull
+    public List<String> getSupportedVideoMimeTypes() {
+        return new ArrayList<>(mSupportedVideoMimeTypes);
+    }
+
+    /*
+     * Returns a list that contains all hdr types supported by the application.
+     * The list will be empty if no hdr types are supported by the application.
+     * @return List of supported hdr types.
+     */
+    @NonNull
+    public List<String> getSupportedHdrTypes() {
+        return new ArrayList<>(mSupportedHdrTypes);
+    }
+
+    /*
+     * Whether handling of slow-motion video is supported
+     */
+    public boolean isSlowMotionSupported() {
+        return mIsSlowMotionSupported;
+    }
+
+    /**
+     * Creates {@link ApplicationMediaCapabilities} from an xml.
+     * @param xmlParser The underlying {@link XmlPullParser} that will read the xml.
+     * @return An ApplicationMediaCapabilities object.
+     * @throws UnsupportedOperationException if the capabilities in xml config are invalid or
+     * incompatible.
+     */
+    @NonNull
+    public static ApplicationMediaCapabilities createFromXml(@NonNull XmlPullParser xmlParser) {
+        ApplicationMediaCapabilities.Builder builder = new ApplicationMediaCapabilities.Builder();
+        builder.parseXml(xmlParser);
+        return builder.build();
+    }
+
+    /**
+     * Builder class for {@link ApplicationMediaCapabilities} objects.
+     * Use this class to configure and create an ApplicationMediaCapabilities instance. Builder
+     * could be created from an existing ApplicationMediaCapabilities object, from a xml file or
+     * MediaCodecList.
+     * //TODO(hkuang): Add xml parsing support to the builder.
+     */
+    public final static class Builder {
+        /** List of supported video codec mime types. */
+        private Set<String> mSupportedVideoMimeTypes = new HashSet<>();
+
+        /** List of supported hdr types. */
+        private Set<String> mSupportedHdrTypes = new HashSet<>();
+
+        private boolean mIsSlowMotionSupported = false;
+
+        /* Map to save the format read from the xml. */
+        private Map<String, Boolean> mFormatSupportedMap =  new HashMap<String, Boolean>();
+
+        /**
+         * Constructs a new Builder with all the supports default to false.
+         */
+        public Builder() {
+        }
+
+        private void parseXml(@NonNull XmlPullParser xmlParser)
+                throws UnsupportedOperationException {
+            if (xmlParser == null) {
+                throw new IllegalArgumentException("XmlParser must not be null");
+            }
+
+            try {
+                while (xmlParser.next() != XmlPullParser.START_TAG) {
+                    continue;
+                }
+
+                // Validates the tag is "media-capabilities".
+                if (!xmlParser.getName().equals("media-capabilities")) {
+                    throw new UnsupportedOperationException("Invalid tag");
+                }
+
+                xmlParser.next();
+                while (xmlParser.getEventType() != XmlPullParser.END_TAG) {
+                    while (xmlParser.getEventType() != XmlPullParser.START_TAG) {
+                        if (xmlParser.getEventType() == XmlPullParser.END_DOCUMENT) {
+                            return;
+                        }
+                        xmlParser.next();
+                    }
+
+                    // Validates the tag is "format".
+                    if (xmlParser.getName().equals("format")) {
+                        parseFormatTag(xmlParser);
+                    } else {
+                        throw new UnsupportedOperationException("Invalid tag");
+                    }
+                    while (xmlParser.getEventType() != XmlPullParser.END_TAG) {
+                        xmlParser.next();
+                    }
+                    xmlParser.next();
+                }
+            } catch (XmlPullParserException xppe) {
+                throw new UnsupportedOperationException("Ill-formatted xml file");
+            } catch (java.io.IOException ioe) {
+                throw new UnsupportedOperationException("Unable to read xml file");
+            }
+        }
+
+        private void parseFormatTag(XmlPullParser xmlParser) {
+            String name = null;
+            String supported = null;
+            for (int i = 0; i < xmlParser.getAttributeCount(); i++) {
+                String attrName = xmlParser.getAttributeName(i);
+                if (attrName.equals("name")) {
+                    name = xmlParser.getAttributeValue(i);
+                } else if (attrName.equals("supported")) {
+                    supported = xmlParser.getAttributeValue(i);
+                } else {
+                    throw new UnsupportedOperationException("Invalid attribute name " + attrName);
+                }
+            }
+
+            if (name != null && supported != null) {
+                if (!supported.equals("true") && !supported.equals("false")) {
+                    throw new UnsupportedOperationException(
+                            ("Supported value must be either true or false"));
+                }
+                boolean isSupported = Boolean.parseBoolean(supported);
+
+                // Check if the format is already found before.
+                if (mFormatSupportedMap.get(name) != null && mFormatSupportedMap.get(name)
+                        != isSupported) {
+                    throw new UnsupportedOperationException(
+                            "Format: " + name + " has conflict supported value");
+                }
+
+                switch (name) {
+                    case "HEVC":
+                        if (isSupported) {
+                            mSupportedVideoMimeTypes.add(MediaFormat.MIMETYPE_VIDEO_HEVC);
+                        }
+                        break;
+                    case "HDR10":
+                        if (isSupported) {
+                            mSupportedHdrTypes.add(MediaFeature.HdrType.HDR10);
+                        }
+                        break;
+                    case "HDR10Plus":
+                        if (isSupported) {
+                            mSupportedHdrTypes.add(MediaFeature.HdrType.HDR10_PLUS);
+                        }
+                        break;
+                    case "Dolby-Vision":
+                        if (isSupported) {
+                            mSupportedHdrTypes.add(MediaFeature.HdrType.DOLBY_VISION);
+                        }
+                        break;
+                    case "HLG":
+                        if (isSupported) {
+                            mSupportedHdrTypes.add(MediaFeature.HdrType.HLG);
+                        }
+                        break;
+                    case "SlowMotion":
+                        mIsSlowMotionSupported = isSupported;
+                        break;
+                    default:
+                        throw new UnsupportedOperationException("Invalid format name " + name);
+                }
+                // Save the name and isSupported into the map for validate later.
+                mFormatSupportedMap.put(name, isSupported);
+            } else {
+                throw new UnsupportedOperationException(
+                        "Format name and supported must both be specified");
+            }
+        }
+
+        /**
+         * Builds a {@link ApplicationMediaCapabilities} object.
+         *
+         * @return a new {@link ApplicationMediaCapabilities} 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 ApplicationMediaCapabilities build() {
+            Log.d(TAG,
+                    "Building ApplicationMediaCapabilities with: " + mSupportedHdrTypes.toString()
+                            + " " + mSupportedVideoMimeTypes.toString() + " "
+                            + mIsSlowMotionSupported);
+
+            // If hdr is supported, application must also support hevc.
+            if (!mSupportedHdrTypes.isEmpty() && !mSupportedVideoMimeTypes.contains(
+                    MediaFormat.MIMETYPE_VIDEO_HEVC)) {
+                throw new UnsupportedOperationException("Only support HEVC mime type");
+            }
+            return new ApplicationMediaCapabilities(this);
+        }
+
+        /**
+         * Adds a supported video codec mime type.
+         *
+         * @param codecMime Supported codec mime types. Must be one of the mime type defined
+         *                  in {@link MediaFormat}.
+         * @throws UnsupportedOperationException if the codec mime type is not supported.
+         * @throws IllegalArgumentException      if mime type is not valid.
+         */
+        @NonNull
+        public Builder addSupportedVideoMimeType(
+                @NonNull String codecMime) {
+            mSupportedVideoMimeTypes.add(codecMime);
+            return this;
+        }
+
+        private List<String> getSupportedVideoMimeTypes() {
+            return new ArrayList<>(mSupportedVideoMimeTypes);
+        }
+
+        /**
+         * Adds a supported hdr type.
+         *
+         * @param hdrType Supported hdr types. Must be one of the String defined in
+         *                {@link MediaFeature.HdrType}.
+         * @throws IllegalArgumentException if hdrType is not valid.
+         */
+        @NonNull
+        public Builder addSupportedHdrType(
+                @NonNull @MediaFeature.MediaHdrType String hdrType) {
+            mSupportedHdrTypes.add(hdrType);
+            return this;
+        }
+
+        private List<String> getSupportedHdrTypes() {
+            return new ArrayList<>(mSupportedHdrTypes);
+        }
+
+        /**
+         * Sets whether slow-motion video is supported.
+         * If an application indicates support for slow-motion, it is application's responsibility
+         * to parse the slow-motion videos using their own parser or using support library.
+         * @see android.media.MediaFormat#KEY_SLOW_MOTION_MARKERS
+         */
+        @NonNull
+        public Builder setSlowMotionSupported(boolean slowMotionSupported) {
+            mIsSlowMotionSupported = slowMotionSupported;
+            return this;
+        }
+    }
+}
diff --git a/apex/media/framework/java/android/media/MediaFeature.java b/apex/media/framework/java/android/media/MediaFeature.java
new file mode 100644
index 0000000..0e461888
--- /dev/null
+++ b/apex/media/framework/java/android/media/MediaFeature.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 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.StringDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * MediaFeature defines various media features, e.g. hdr type.
+ */
+public final class MediaFeature {
+     /**
+     * Defines tye type of HDR(high dynamic range) video.
+     */
+    public static final class HdrType {
+        private HdrType() {
+        }
+
+        /**
+         * HDR type for dolby-vision.
+         */
+        public static final String DOLBY_VISION = "android.media.feature.hdr.dolby_vision";
+        /**
+         * HDR type for hdr10.
+         */
+        public static final String HDR10 = "android.media.feature.hdr.hdr10";
+        /**
+         * HDR type for hdr10+.
+         */
+        public static final String HDR10_PLUS = "android.media.feature.hdr.hdr10_plus";
+        /**
+         * HDR type for hlg.
+         */
+        public static final String HLG = "android.media.feature.hdr.hlg";
+    }
+
+    /** @hide */
+    @StringDef({
+            HdrType.DOLBY_VISION,
+            HdrType.HDR10,
+            HdrType.HDR10_PLUS,
+            HdrType.HLG,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MediaHdrType {
+    }
+}
diff --git a/apex/media/framework/java/android/media/MediaFrameworkInitializer.java b/apex/media/framework/java/android/media/MediaFrameworkInitializer.java
new file mode 100644
index 0000000..813ad7b
--- /dev/null
+++ b/apex/media/framework/java/android/media/MediaFrameworkInitializer.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2020 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.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.SystemApi.Client;
+import android.media.MediaTranscodeManager;
+import android.app.SystemServiceRegistry;
+import android.content.Context;
+
+/**
+ * Class for performing registration for all media services on com.android.media apex.
+ *
+ * @hide
+ */
+@SystemApi(client = Client.MODULE_LIBRARIES)
+public class MediaFrameworkInitializer {
+    private MediaFrameworkInitializer() {
+    }
+
+    private static volatile MediaServiceManager sMediaServiceManager;
+
+    /**
+     * Sets an instance of {@link MediaServiceManager} that allows
+     * the media mainline module to register/obtain media binder services. This is called
+     * by the platform during the system initialization.
+     *
+     * @param mediaServiceManager instance of {@link MediaServiceManager} that allows
+     * the media mainline module to register/obtain media binder services.
+     */
+    public static void setMediaServiceManager(
+            @NonNull MediaServiceManager mediaServiceManager) {
+        if (sMediaServiceManager != null) {
+            throw new IllegalStateException("setMediaServiceManager called twice!");
+        }
+
+        if (mediaServiceManager == null) {
+            throw new NullPointerException("mediaServiceManager is null!");
+        }
+
+        sMediaServiceManager = mediaServiceManager;
+    }
+
+    /** @hide */
+    public static MediaServiceManager getMediaServiceManager() {
+        return sMediaServiceManager;
+    }
+
+    /**
+     * Called by {@link SystemServiceRegistry}'s static initializer and registers all media
+     * services to {@link Context}, so that {@link Context#getSystemService} can return them.
+     *
+     * @throws IllegalStateException if this is called from anywhere besides
+     * {@link SystemServiceRegistry}
+     */
+    public static void registerServiceWrappers() {
+        SystemServiceRegistry.registerContextAwareService(
+                Context.MEDIA_TRANSCODING_SERVICE,
+                MediaTranscodeManager.class,
+                context -> new MediaTranscodeManager(context)
+        );
+    }
+}
diff --git a/apex/media/framework/java/android/media/MediaTranscodeManager.java b/apex/media/framework/java/android/media/MediaTranscodeManager.java
new file mode 100644
index 0000000..d449289
--- /dev/null
+++ b/apex/media/framework/java/android/media/MediaTranscodeManager.java
@@ -0,0 +1,1435 @@
+/*
+ * 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.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+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;
+
+/**
+ MediaTranscodeManager 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 file could be of any of the following types:
+ <ul>
+ <li> Video file with single video track. </li>
+ <li> Video file with multiple video track. </li>
+ <li> Video file with multiple video tracks and audio tracks. </li>
+ <li> Video file with video/audio tracks and metadata track. Note that metadata track will be passed
+ through only if it could be recognized by {@link MediaExtractor}.
+ TODO(hkuang): Finalize the metadata track behavior. </li>
+ </ul>
+ <p class=note>
+ Note that currently only support transcoding video file in mp4 format.
+
+ <h3>Transcoding Request</h3>
+ <p>
+ To transcode a media file, first create a {@link TranscodingRequest} through its builder class
+ {@link TranscodingRequest.Builder}. Transcode requests are then enqueue to the manager through
+ {@link MediaTranscodeManager#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>
+ TranscodingRequest request =
+    new TranscodingRequest.Builder()
+        .setSourceUri(srcUri)
+        .setDestinationUri(dstUri)
+        .setType(MediaTranscodeManager.TRANSCODING_TYPE_VIDEO)
+        .setPriority(REALTIME)
+        .setVideoTrackFormat(videoFormat)
+        .build();
+ }</pre>
+
+ TODO(hkuang): Add architecture diagram showing the transcoding service and api.
+ TODO(hkuang): Add sample code when API is settled.
+ TODO(hkuang): Clarify whether multiple video tracks is supported or not.
+ TODO(hkuang): Clarify whether image/audio transcoding is supported or not.
+ TODO(hkuang): Clarify what will happen if there is unrecognized track in the source.
+ TODO(hkuang): Clarify whether supports scaling.
+ TODO(hkuang): Clarify whether supports framerate conversion.
+ @hide
+ */
+@SystemApi
+public final class MediaTranscodeManager {
+    private static final String TAG = "MediaTranscodeManager";
+
+    /** 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 transcoding type.
+     * @hide
+     */
+    public static final int TRANSCODING_TYPE_UNKNOWN = 0;
+
+    /**
+     * TRANSCODING_TYPE_VIDEO indicates that client wants to perform transcoding on a video file.
+     * <p>Note that currently only support transcoding video file in mp4 format.
+     */
+    public static final int TRANSCODING_TYPE_VIDEO = 1;
+
+    /**
+     * TRANSCODING_TYPE_IMAGE indicates that client wants to perform transcoding on an image file.
+     * @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.
+     * TODO(hkuang): Add more description of this when priority is finalized.
+     */
+    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
+     * TODO(hkuang): Add more description of this when priority is finalized.
+     */
+    public static final int PRIORITY_OFFLINE = 2;
+
+    /** @hide */
+    @IntDef(prefix = {"PRIORITY_"}, value = {
+            PRIORITY_UNKNOWN,
+            PRIORITY_REALTIME,
+            PRIORITY_OFFLINE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TranscodingPriority {}
+
+    /**
+     * 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 MediaTranscodeManager sMediaTranscodeManager;
+
+    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);
+
+            // 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);
+
+            // 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;
+            }
+
+            // Updates the session progress.
+            session.updateProgress(newProgress);
+
+            // Notifies client the progress update.
+            if (session.mProgressUpdateExecutor != null
+                    && session.mProgressUpdateListener != null) {
+                session.mProgressUpdateExecutor.execute(
+                        () -> session.mProgressUpdateListener.onProgressUpdate(session,
+                                newProgress));
+            }
+        }
+    }
+
+    private static IMediaTranscodingService getService(boolean retry) {
+        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 */
+            }
+        }
+
+        throw new UnsupportedOperationException("Failed to connect to MediaTranscoding service");
+    }
+
+    /*
+     * 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);
+
+                        // 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 MediaTranscodeManager.
+                    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)
+            throws UnsupportedOperationException {
+        synchronized (mLock) {
+            try {
+                // Registers the client with MediaTranscoding service.
+                mTranscodingClient = service.registerClient(
+                        mTranscodingClientCallback,
+                        mPackageName,
+                        mPackageName);
+
+                if (mTranscodingClient != null) {
+                    mTranscodingClient.asBinder().linkToDeath(() -> onClientDied(), /* flags */ 0);
+                }
+                return mTranscodingClient;
+            } catch (RemoteException re) {
+                Log.e(TAG, "Failed to register new client due to exception " + re);
+                mTranscodingClient = null;
+            }
+        }
+        throw new UnsupportedOperationException("Failed to register new client");
+    }
+
+    /**
+     * @hide
+     */
+    public MediaTranscodeManager(@NonNull Context context) {
+        mContext = context;
+        mContentResolver = mContext.getContentResolver();
+        mPackageName = mContext.getPackageName();
+        mUid = Os.getuid();
+        mPid = Os.getpid();
+        IMediaTranscodingService service = getService(false /*retry*/);
+        mTranscodingClient = registerClient(service);
+    }
+
+    public static final class TranscodingRequest {
+        /** Uri of the source media file. */
+        private @NonNull Uri mSourceUri;
+
+        /** Uri of the destination media file. */
+        private @NonNull Uri mDestinationUri;
+
+        /**
+         *  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 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.
+         * <p>
+         */
+        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.
+         * @hide
+         */
+        private @Nullable MediaFormat mAudioTrackFormat = null;
+
+        /**
+         * 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;
+
+        private TranscodingRequest(Builder b) {
+            mSourceUri = b.mSourceUri;
+            mDestinationUri = b.mDestinationUri;
+            mClientUid = b.mClientUid;
+            mClientPid = b.mClientPid;
+            mPriority = b.mPriority;
+            mType = b.mType;
+            mVideoTrackFormat = b.mVideoTrackFormat;
+            mAudioTrackFormat = b.mAudioTrackFormat;
+            mImageFormat = b.mImageFormat;
+            mTestConfig = b.mTestConfig;
+        }
+
+        /** Return the type of the transcoding. */
+        @TranscodingType
+        public int getType() {
+            return mType;
+        }
+
+        /** Return source uri of the transcoding. */
+        @NonNull
+        public Uri getSourceUri() {
+            return mSourceUri;
+        }
+
+        /** 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 priority of the transcoding. */
+        @TranscodingPriority
+        public int getPriority() {
+            return mPriority;
+        }
+
+        /**
+         * Return the video track format of the transcoding.
+         * This will be null is the transcoding is not for video transcoding.
+         */
+        @Nullable
+        public MediaFormat getVideoTrackFormat() {
+            return mVideoTrackFormat;
+        }
+
+        /**
+         * Return TestConfig of the transcoding.
+         * @hide
+         */
+        @Nullable
+        public TranscodingTestConfig getTestConfig() {
+            return mTestConfig;
+        }
+
+        /* Writes the TranscodingRequest to a parcel. */
+        private TranscodingRequestParcel writeToParcel(@NonNull Context context) {
+            TranscodingRequestParcel parcel = new TranscodingRequestParcel();
+            parcel.priority = mPriority;
+            parcel.transcodingType = mType;
+            parcel.sourceFilePath = mSourceUri.toString();
+            parcel.destinationFilePath = mDestinationUri.toString();
+            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;
+            }
+            parcel.requestedVideoTrackFormat = convertToVideoTrackFormat(mVideoTrackFormat);
+            if (mTestConfig != null) {
+                parcel.isForTesting = true;
+                parcel.testConfig = mTestConfig;
+            }
+            return parcel;
+        }
+
+        /* 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(hkuang): 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(hkuang): 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(hkuang): Validate the level according to codec type.
+                trackFormat.level = level;
+            }
+
+            return trackFormat;
+        }
+
+        /**
+         * Builder class for {@link TranscodingRequest} objects.
+         * Use this class to configure and create a <code>TranscodingRequest</code> instance.
+         */
+        public static final class Builder {
+            private @NonNull Uri mSourceUri;
+            private @NonNull Uri mDestinationUri;
+            private int mClientUid = -1;
+            private int mClientPid = -1;
+            private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN;
+            private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN;
+            private @Nullable MediaFormat mVideoTrackFormat;
+            private @Nullable MediaFormat mAudioTrackFormat;
+            private @Nullable MediaFormat mImageFormat;
+            private TranscodingTestConfig mTestConfig;
+
+            /**
+             * Specifies the uri of source media file.
+             *
+             * @param sourceUri Content uri for the source media file.
+             * @return The same builder instance.
+             * @throws IllegalArgumentException if Uri is null or empty.
+             */
+            // TODO(hkuang): Add documentation on how the app could generate the correct Uri.
+            @NonNull
+            public Builder setSourceUri(@NonNull Uri sourceUri) {
+                if (sourceUri == null || Uri.EMPTY.equals(sourceUri)) {
+                    throw new IllegalArgumentException(
+                            "You must specify a non-empty source Uri.");
+                }
+                mSourceUri = sourceUri;
+                return this;
+            }
+
+            /**
+             * Specifies the uri of the destination media file.
+             *
+             * @param destinationUri Content uri for the destination media file.
+             * @return The same builder instance.
+             * @throws IllegalArgumentException if Uri is null or empty.
+             */
+            @NonNull
+            public Builder setDestinationUri(@NonNull Uri destinationUri) {
+                if (destinationUri == null || Uri.EMPTY.equals(destinationUri)) {
+                    throw new IllegalArgumentException(
+                            "You must specify a non-empty destination Uri.");
+                }
+                mDestinationUri = destinationUri;
+                return this;
+            }
+
+            /**
+             * Specify the UID of the client that this request is for.
+             * @param uid client Uid.
+             * @return The same builder instance.
+             * @throws IllegalArgumentException if uid is invalid.
+             * TODO(hkuang): Check the permission if it is allowed.
+             */
+            @NonNull
+            public Builder setClientUid(int uid) {
+                if (uid < 0) {
+                    throw new IllegalArgumentException("Invalid Uid");
+                }
+                mClientUid = uid;
+                return this;
+            }
+
+            /**
+             * Specify the PID of the client that this request is for.
+             * @param pid client Pid.
+             * @return The same builder instance.
+             * @throws IllegalArgumentException if pid is invalid.
+             * TODO(hkuang): Check the permission if it is allowed.
+             */
+            @NonNull
+            public Builder setClientPid(int pid) {
+                if (pid < 0) {
+                    throw new IllegalArgumentException("Invalid pid");
+                }
+                mClientPid = pid;
+                return this;
+            }
+
+            /**
+             * 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.
+             */
+            @NonNull
+            public Builder setPriority(@TranscodingPriority int priority) {
+                if (priority != PRIORITY_OFFLINE && priority != PRIORITY_REALTIME) {
+                    throw new IllegalArgumentException("Invalid priority: " + priority);
+                }
+                mPriority = priority;
+                return this;
+            }
+
+            /**
+             * Specifies the type of transcoding.
+             * <p> Clients must provide the source and destination that corresponds to the
+             * transcoding type.
+             *
+             * @param type Must be one of the {@code TRANSCODING_TYPE_*}
+             * @return The same builder instance.
+             * @throws IllegalArgumentException if flags is invalid.
+             */
+            @NonNull
+            public Builder setType(@TranscodingType int type) {
+                if (type != TRANSCODING_TYPE_VIDEO && type != TRANSCODING_TYPE_IMAGE) {
+                    throw new IllegalArgumentException("Invalid transcoding type");
+                }
+                mType = type;
+                return this;
+            }
+
+            /**
+             * Specifies the desired video track format in the destination media file.
+             * <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.
+             * TODO(hkuang): Add MediaTranscodeUtil to help client generate transcoding setting.
+             * TODO(hkuang): Add MediaTranscodeUtil to check if the setting is valid.
+             *
+             * @param videoFormat MediaFormat containing the settings that client wants override in
+             *                    the original video's video track.
+             * @return The same builder instance.
+             * @throws IllegalArgumentException if videoFormat is invalid.
+             */
+            @NonNull
+            public Builder 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 this;
+            }
+
+            /**
+             * Sets the delay in processing this request.
+             * @param config test config.
+             * @return The same builder instance.
+             * @hide
+             */
+            @VisibleForTesting
+            @NonNull
+            public Builder setTestConfig(@NonNull TranscodingTestConfig config) {
+                mTestConfig = config;
+                return this;
+            }
+
+            /**
+             * @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 TranscodingRequest build() {
+                if (mSourceUri == null) {
+                    throw new UnsupportedOperationException("Source URI must not be null");
+                }
+
+                if (mDestinationUri == null) {
+                    throw new UnsupportedOperationException("Destination URI must not be null");
+                }
+
+                if (mPriority == PRIORITY_UNKNOWN) {
+                    throw new UnsupportedOperationException("Must specify transcoding priority");
+                }
+
+                // Only support video transcoding now.
+                if (mType != TRANSCODING_TYPE_VIDEO) {
+                    throw new UnsupportedOperationException("Only supports video transcoding now");
+                }
+
+                // Must provide video track format for video transcoding.
+                if (mType == TRANSCODING_TYPE_VIDEO && mVideoTrackFormat == null) {
+                    throw new UnsupportedOperationException(
+                            "Must provide video track format for video transcoding");
+                }
+
+                return new TranscodingRequest(this);
+            }
+        }
+
+        /**
+         * Helper class for deciding if transcoding is needed, and if so, the track
+         * formats to use.
+         */
+        public static class MediaFormatResolver {
+            private static final int BIT_RATE = 20000000;            // 20Mbps
+
+            private MediaFormat mSrcVideoFormatHint;
+            private MediaFormat mSrcAudioFormatHint;
+            private ApplicationMediaCapabilities mClientCaps;
+
+            /**
+             * Sets the abilities of the client consuming the media. Must be called
+             * before {@link #shouldTranscode()} or {@link #resolveVideoFormat()}.
+             *
+             * @param clientCaps An ApplicationMediaCapabilities object containing the client's
+             *                   capabilities.
+             * @return the same VideoFormatResolver instance.
+             */
+            @NonNull
+            public MediaFormatResolver setClientCapabilities(
+                    @NonNull ApplicationMediaCapabilities clientCaps) {
+                mClientCaps = clientCaps;
+                return this;
+            }
+
+            /**
+             * Sets the video format hint about the source. Must be called before
+             * {@link #shouldTranscode()} or {@link #resolveVideoFormat()}.
+             *
+             * @param format 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.
+             * @return the same MediaFormatResolver instance.
+             */
+            @NonNull
+            public MediaFormatResolver setSourceVideoFormatHint(@NonNull MediaFormat format) {
+                mSrcVideoFormatHint = format;
+                return this;
+            }
+
+            /**
+             * Sets the audio format hint about the source.
+             *
+             * @param format A MediaFormat object containing information about the source's
+             *               audio track format that could affect the transcoding decision.
+             * @return the same MediaFormatResolver instance.
+             * @hide
+             */
+            @NonNull
+            public MediaFormatResolver setSourceAudioFormatHint(@NonNull MediaFormat format) {
+                mSrcAudioFormatHint = format;
+                return this;
+            }
+
+            /**
+             * Returns whether the source content should be transcoded.
+             *
+             * @return true if the source should be transcoded.
+             * @throws UnsupportedOperationException
+             *         if {@link #setClientCapabilities(ApplicationMediaCapabilities)}
+             *         or {@link #setSourceVideoFormatHint(MediaFormat)} was not called.
+             */
+            public boolean shouldTranscode() {
+                if (mClientCaps == null) {
+                    throw new UnsupportedOperationException(
+                            "Client caps must be set!");
+                }
+                // Video src hint must be provided, audio src hint is not used right now.
+                if (mSrcVideoFormatHint == null) {
+                    throw new UnsupportedOperationException(
+                            "Source video format hint must be set!");
+                }
+                boolean supportHevc = mClientCaps.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 Builder#setVideoTrackFormat(MediaFormat)} for this configuration.
+             *
+             * @return the video track format to be used if transcoding should be performed,
+             *         and null otherwise.
+             * @throws UnsupportedOperationException
+             *         if {@link #setClientCapabilities(ApplicationMediaCapabilities)}
+             *         or {@link #setSourceVideoFormatHint(MediaFormat)} was not called.
+             */
+            @Nullable
+            public MediaFormat resolveVideoFormat() {
+                if (!shouldTranscode()) {
+                    return null;
+                }
+                // TODO(hkuang): Only modified the video codec type, and use fixed bitrate for now.
+                // May switch to transcoding profile when it's available.
+                MediaFormat videoTrackFormat = new MediaFormat(mSrcVideoFormatHint);
+                videoTrackFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
+                videoTrackFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
+                return videoTrackFormat;
+            }
+
+            /**
+             * 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.
+             * @throws UnsupportedOperationException
+             *         if {@link #setClientCapabilities(ApplicationMediaCapabilities)}
+             *         or {@link #setSourceVideoFormatHint(MediaFormat)} was not called.
+             * @hide
+             */
+            @Nullable
+            public MediaFormat resolveAudioFormat() {
+                if (!shouldTranscode()) {
+                    return null;
+                }
+                // Audio transcoding is not supported yet, always return null.
+                return null;
+            }
+        }
+    }
+
+    /**
+     * 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 {}
+
+        /** 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 MediaTranscodeManager 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 boolean mHasRetried = false;
+        // The original request that associated with this session.
+        private final TranscodingRequest mRequest;
+
+        private TranscodingSession(
+                @NonNull MediaTranscodeManager 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,
+                @Nullable OnProgressUpdateListener listener) {
+            setOnProgressUpdateListener(
+                    0 /* minProgressUpdateInterval */,
+                    executor, listener);
+        }
+
+        /**
+         * Set a progress listener with specified progress update interval.
+         * @param minProgressUpdateInterval The minimum interval between each progress update.
+         * @param executor The executor on which listener will be invoked.
+         * @param listener The progress listener.
+         */
+        public void setOnProgressUpdateListener(
+                int minProgressUpdateInterval,
+                @NonNull @CallbackExecutor Executor executor,
+                @Nullable OnProgressUpdateListener listener) {
+            synchronized (mLock) {
+                Objects.requireNonNull(executor, "listenerExecutor must not be null");
+                Objects.requireNonNull(listener, "listener must not be null");
+                mProgressUpdateExecutor = executor;
+                mProgressUpdateListener = listener;
+            }
+        }
+
+        private void updateStatusAndResult(@Status int sessionStatus,
+                @Result int sessionResult) {
+            synchronized (mLock) {
+                mStatus = sessionStatus;
+                mResult = sessionResult;
+            }
+        }
+
+        /**
+         * 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.
+         *
+         * @throws MediaTranscodingException.ServiceNotAvailableException if the service
+         *         is temporarily unavailable due to internal service rebooting. Client could retry
+         *         again after receiving this exception.
+         * @throws UnsupportedOperationException if the retry could not be fulfilled.
+         * @hide
+         */
+        public void retry() throws MediaTranscodingException.ServiceNotAvailableException {
+            retryInternal(true /*setHasRetried*/);
+        }
+
+        // TODO(hkuang): Add more test for it.
+        private void retryInternal(boolean setHasRetried)
+                throws MediaTranscodingException.ServiceNotAvailableException {
+            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) {
+                    throw new MediaTranscodingException.ServiceNotAvailableException(
+                            "Service rebooting. Try again later");
+                }
+
+                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) {
+                        throw new MediaTranscodingException.ServiceNotAvailableException(
+                                "Failed to resubmit request to Transcoding service");
+                    }
+                    mStatus = STATUS_PENDING;
+                    mHasRetried = setHasRetried ? true : false;
+                }
+            }
+        }
+
+        /**
+         * 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;
+            }
+        }
+
+        /**
+         * 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";
+                    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;
+            }
+        }
+
+        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, MediaTranscodeManager will return a
+     * {@link TranscodingSession} to the client. Client should use {@link TranscodingSession} to
+     * track the progress and get the result.
+     *
+     * @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 FileNotFoundException if the source Uri or destination Uri could not be opened.
+     * @throws UnsupportedOperationException if the request could not be fulfilled.
+     * @throws MediaTranscodingException.ServiceNotAvailableException if the service
+     *         is temporarily unavailable due to internal service rebooting. Client could retry
+     *         again after receiving this exception.
+     */
+    @NonNull
+    public TranscodingSession enqueueRequest(
+            @NonNull TranscodingRequest transcodingRequest,
+            @NonNull @CallbackExecutor Executor listenerExecutor,
+            @NonNull OnTranscodingFinishedListener listener)
+            throws FileNotFoundException,
+            MediaTranscodingException.ServiceNotAvailableException {
+        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*/);
+                        mTranscodingClient = registerClient(service);
+                        // If still fails, throws an exception to tell client to try later.
+                        if (mTranscodingClient == null) {
+                            throw new MediaTranscodingException.ServiceNotAvailableException(
+                                    "Service rebooting. Try again later");
+                        }
+                    }
+
+                    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 | ServiceSpecificException ex) {
+            throw new UnsupportedOperationException(
+                    "Failed to submit request to Transcoding service");
+        }
+    }
+}
diff --git a/apex/media/framework/java/android/media/MediaTranscodingException.java b/apex/media/framework/java/android/media/MediaTranscodingException.java
new file mode 100644
index 0000000..50cc9c4
--- /dev/null
+++ b/apex/media/framework/java/android/media/MediaTranscodingException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 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;
+
+/**
+ * Base class for MediaTranscoding exceptions
+ */
+public class MediaTranscodingException extends Exception {
+    private MediaTranscodingException(String detailMessage) {
+        super(detailMessage);
+    }
+
+    /**
+     * Exception thrown when the service is rebooting and MediaTranscodeManager is temporarily
+     * unavailable for accepting new request. It's likely that retrying will be successful.
+     */
+    public static final class ServiceNotAvailableException extends
+            MediaTranscodingException {
+        /** @hide */
+        public ServiceNotAvailableException(String detailMessage) {
+            super(detailMessage);
+        }
+    }
+}