TranscodingAPI: Expose MediaTranscodeManager as System API.

Bug: 160260102
Test: Unit test
Change-Id: I38e9efc46e23fe1aafe65100bbc7f72eb200720f
diff --git a/Android.bp b/Android.bp
index e756b34..8475fbd 100644
--- a/Android.bp
+++ b/Android.bp
@@ -333,6 +333,7 @@
         ":installd_aidl",
         ":keystore_aidl",
         ":libaudioclient_aidl",
+        ":mediatranscoding_aidl_interface-java-source",
         ":libbinder_aidl",
         ":libbluetooth-binder-aidl",
         ":libcamera_client_aidl",
@@ -576,7 +577,6 @@
         // If MimeMap ever becomes its own APEX, then this dependency would need to be removed
         // in favor of an API stubs dependency in java_library "framework" below.
         "mimemap",
-        "mediatranscoding_aidl_interface-java",
     ],
     // For backwards compatibility.
     stem: "framework",
@@ -623,7 +623,6 @@
     static_libs: [
         "exoplayer2-extractor",
         "android.hardware.wifi-V1.0-java-constants",
-        "mediatranscoding_aidl_interface-java",
 
         // Additional dependencies needed to build the ike API classes.
         "ike-internals",
diff --git a/api/system-current.txt b/api/system-current.txt
index 00a59bf..dbe2e31 100755
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -1741,6 +1741,7 @@
     field public static final String ETHERNET_SERVICE = "ethernet";
     field public static final String EUICC_CARD_SERVICE = "euicc_card";
     field public static final String HDMI_CONTROL_SERVICE = "hdmi_control";
+    field public static final String MEDIA_TRANSCODING_SERVICE = "media_transcoding";
     field public static final String NETD_SERVICE = "netd";
     field public static final String NETWORK_SCORE_SERVICE = "network_score";
     field public static final String OEM_LOCK_SERVICE = "oem_lock";
@@ -4333,6 +4334,60 @@
     field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT) public static final int RADIO_TUNER = 1998; // 0x7ce
   }
 
+  public final class MediaTranscodeManager implements java.lang.AutoCloseable {
+    method public void close();
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingJob enqueueRequest(@NonNull android.media.MediaTranscodeManager.TranscodingRequest, @NonNull java.util.concurrent.Executor, @NonNull android.media.MediaTranscodeManager.OnTranscodingFinishedListener) throws java.io.FileNotFoundException;
+    method protected void finalize();
+    field public static final int PRIORITY_OFFLINE = 2; // 0x2
+    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.TranscodingJob);
+  }
+
+  public static final class MediaTranscodeManager.TranscodingJob {
+    method public void cancel();
+    method public int getJobId();
+    method @IntRange(from=0, to=100) public int getProgress();
+    method public int getResult();
+    method public int getStatus();
+    method public boolean retry();
+    method public void setOnProgressUpdateListener(@NonNull java.util.concurrent.Executor, @Nullable android.media.MediaTranscodeManager.TranscodingJob.OnProgressUpdateListener);
+    method public void setOnProgressUpdateListener(int, @NonNull java.util.concurrent.Executor, @Nullable android.media.MediaTranscodeManager.TranscodingJob.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.TranscodingJob.OnProgressUpdateListener {
+    method public void onProgressUpdate(@IntRange(from=0, to=100) int);
+  }
+
+  public static final class MediaTranscodeManager.TranscodingRequest {
+    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 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 class PlayerProxy {
     method public void pause();
     method public void setPan(float);
diff --git a/api/test-current.txt b/api/test-current.txt
index 5741fe7..2dd7409 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1819,6 +1819,60 @@
     method @NonNull public String getOriginalId();
   }
 
+  public final class MediaTranscodeManager implements java.lang.AutoCloseable {
+    method public void close();
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingJob enqueueRequest(@NonNull android.media.MediaTranscodeManager.TranscodingRequest, @NonNull java.util.concurrent.Executor, @NonNull android.media.MediaTranscodeManager.OnTranscodingFinishedListener) throws java.io.FileNotFoundException;
+    method protected void finalize();
+    field public static final int PRIORITY_OFFLINE = 2; // 0x2
+    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.TranscodingJob);
+  }
+
+  public static final class MediaTranscodeManager.TranscodingJob {
+    method public void cancel();
+    method public int getJobId();
+    method @IntRange(from=0, to=100) public int getProgress();
+    method public int getResult();
+    method public int getStatus();
+    method public boolean retry();
+    method public void setOnProgressUpdateListener(@NonNull java.util.concurrent.Executor, @Nullable android.media.MediaTranscodeManager.TranscodingJob.OnProgressUpdateListener);
+    method public void setOnProgressUpdateListener(int, @NonNull java.util.concurrent.Executor, @Nullable android.media.MediaTranscodeManager.TranscodingJob.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.TranscodingJob.OnProgressUpdateListener {
+    method public void onProgressUpdate(@IntRange(from=0, to=100) int);
+  }
+
+  public static final class MediaTranscodeManager.TranscodingRequest {
+    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 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 final class PlaybackParams implements android.os.Parcelable {
     method public int getAudioStretchMode();
     method public android.media.PlaybackParams setAudioStretchMode(int);
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 59997cc..3b11b0d 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -103,6 +103,7 @@
 import android.media.AudioManager;
 import android.media.MediaFrameworkInitializer;
 import android.media.MediaRouter;
+import android.media.MediaTranscodeManager;
 import android.media.midi.IMidiManager;
 import android.media.midi.MidiManager;
 import android.media.projection.MediaProjectionManager;
@@ -305,6 +306,15 @@
                 return new AudioManager(ctx);
             }});
 
+        registerService(Context.MEDIA_TRANSCODING_SERVICE, MediaTranscodeManager.class,
+                new CachedServiceFetcher<MediaTranscodeManager>() {
+                    @Override
+                    public MediaTranscodeManager createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        return new MediaTranscodeManager(ctx);
+                    }
+                });
+
         registerService(Context.MEDIA_ROUTER_SERVICE, MediaRouter.class,
                 new CachedServiceFetcher<MediaRouter>() {
             @Override
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 52b0467..98f7887 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4195,6 +4195,17 @@
     public static final String AUDIO_SERVICE = "audio";
 
     /**
+     * Use with {@link #getSystemService(String)} to retrieve a {@link
+     * android.media.MediaTranscodeManager} for transcoding media.
+     *
+     * @hide
+     * @see #getSystemService(String)
+     * @see android.media.MediaTranscodeManager
+     */
+    @SystemApi
+    public static final String MEDIA_TRANSCODING_SERVICE = "media_transcoding";
+
+    /**
      * AuthService orchestrates biometric and PIN/pattern/password authentication.
      *
      * BiometricService was split into two services, AuthService and BiometricService, where
diff --git a/media/java/android/media/MediaTranscodeManager.java b/media/java/android/media/MediaTranscodeManager.java
index 4e2ae5c..cf61152 100644
--- a/media/java/android/media/MediaTranscodeManager.java
+++ b/media/java/android/media/MediaTranscodeManager.java
@@ -18,8 +18,11 @@
 
 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.annotation.TestApi;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
@@ -97,6 +100,8 @@
  TODO(hkuang): Clarify whether supports framerate conversion.
  @hide
  */
+@TestApi
+@SystemApi
 public final class MediaTranscodeManager implements AutoCloseable {
     private static final String TAG = "MediaTranscodeManager";
 
@@ -127,20 +132,23 @@
     public static final int TRANSCODING_TYPE_IMAGE = 2;
 
     @Override
-    public void close() throws Exception {
+    public void close() {
         release();
     }
 
     /**
      * Releases the MediaTranscodeManager.
      */
-    //TODO(hkuang): add test for it.
-    private void release() throws Exception {
+    private void release() {
         synchronized (mLock) {
-            if (mTranscodingClient != null) {
-                mTranscodingClient.unregister();
-            } else {
-                throw new UnsupportedOperationException("Failed to release");
+            try {
+                if (mTranscodingClient != null) {
+                    mTranscodingClient.unregister();
+                }
+            } catch (Exception ex) {
+                Log.e(TAG, "Failed to unregister the client");
+            } finally {
+                mTranscodingClient = null;
             }
         }
     }
@@ -482,24 +490,22 @@
         throw new UnsupportedOperationException("Failed to register new client");
     }
 
-    /* Private constructor. */
-    private MediaTranscodeManager(@NonNull Context context,
-            IMediaTranscodingService transcodingService) {
+    /**
+     * @hide
+     */
+    public MediaTranscodeManager(@NonNull Context context) {
         mContext = context;
         mContentResolver = mContext.getContentResolver();
         mPackageName = mContext.getPackageName();
         mPid = Os.getuid();
         mUid = Os.getpid();
-        mTranscodingClient = registerClient(transcodingService);
+        IMediaTranscodingService service = getService(false /*retry*/);
+        mTranscodingClient = registerClient(service);
     }
 
     @Override
     protected void finalize() {
-        try {
-            release();
-        } catch (Exception ex) {
-            Log.e(TAG, "Failed to release");
-        }
+        release();
     }
 
     public static final class TranscodingRequest {
@@ -555,25 +561,25 @@
 
         /** Return the type of the transcoding. */
         @TranscodingType
-        int getType() {
+        public int getType() {
             return mType;
         }
 
         /** Return source uri of the transcoding. */
         @NonNull
-        Uri getSourceUri() {
+        public Uri getSourceUri() {
             return mSourceUri;
         }
 
         /** Return destination uri of the transcoding. */
         @NonNull
-        Uri getDestinationUri() {
+        public Uri getDestinationUri() {
             return mDestinationUri;
         }
 
         /** Return priority of the transcoding. */
         @TranscodingPriority
-        int getPriority() {
+        public int getPriority() {
             return mPriority;
         }
 
@@ -581,10 +587,20 @@
          * Return the video track format of the transcoding.
          * This will be null is the transcoding is not for video transcoding.
          */
-        MediaFormat getVideoTrackFormat() {
+        @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() {
             TranscodingRequestParcel parcel = new TranscodingRequestParcel();
@@ -665,7 +681,7 @@
          * Builder class for {@link TranscodingRequest} objects.
          * Use this class to configure and create a <code>TranscodingRequest</code> instance.
          */
-        public static class Builder {
+        public static final class Builder {
             private @NonNull Uri mSourceUri;
             private @NonNull Uri mDestinationUri;
             private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN;
@@ -684,7 +700,7 @@
              */
             // TODO(hkuang): Add documentation on how the app could generate the correct Uri.
             @NonNull
-            public Builder setSourceUri(@NonNull Uri sourceUri) throws IllegalArgumentException {
+            public Builder setSourceUri(@NonNull Uri sourceUri) {
                 if (sourceUri == null || Uri.EMPTY.equals(sourceUri)) {
                     throw new IllegalArgumentException(
                             "You must specify a non-empty source Uri.");
@@ -701,8 +717,7 @@
              * @throws IllegalArgumentException if Uri is null or empty.
              */
             @NonNull
-            public Builder setDestinationUri(@NonNull Uri destinationUri)
-                    throws IllegalArgumentException {
+            public Builder setDestinationUri(@NonNull Uri destinationUri) {
                 if (destinationUri == null || Uri.EMPTY.equals(destinationUri)) {
                     throw new IllegalArgumentException(
                             "You must specify a non-empty destination Uri.");
@@ -719,8 +734,7 @@
              * @throws IllegalArgumentException if flags is invalid.
              */
             @NonNull
-            public Builder setPriority(@TranscodingPriority int priority)
-                    throws IllegalArgumentException {
+            public Builder setPriority(@TranscodingPriority int priority) {
                 if (priority != PRIORITY_OFFLINE && priority != PRIORITY_REALTIME) {
                     throw new IllegalArgumentException("Invalid priority: " + priority);
                 }
@@ -738,8 +752,7 @@
              * @throws IllegalArgumentException if flags is invalid.
              */
             @NonNull
-            public Builder setType(@TranscodingType int type)
-                    throws IllegalArgumentException {
+            public Builder setType(@TranscodingType int type) {
                 if (type != TRANSCODING_TYPE_VIDEO && type != TRANSCODING_TYPE_IMAGE) {
                     throw new IllegalArgumentException("Invalid transcoding type");
                 }
@@ -763,8 +776,7 @@
              * @throws IllegalArgumentException if videoFormat is invalid.
              */
             @NonNull
-            public Builder setVideoTrackFormat(@NonNull MediaFormat videoFormat)
-                    throws IllegalArgumentException {
+            public Builder setVideoTrackFormat(@NonNull MediaFormat videoFormat) {
                 if (videoFormat == null) {
                     throw new IllegalArgumentException("videoFormat must not be null");
                 }
@@ -784,9 +796,11 @@
              * Sets the delay in processing this request.
              * @param config test config.
              * @return The same builder instance.
+             * @hide
              */
             @VisibleForTesting
-            public Builder setTestConfig(TranscodingTestConfig config) {
+            @NonNull
+            public Builder setTestConfig(@NonNull TranscodingTestConfig config) {
                 mTestConfig = config;
                 return this;
             }
@@ -799,7 +813,7 @@
              *         device.
              */
             @NonNull
-            public TranscodingRequest build() throws UnsupportedOperationException {
+            public TranscodingRequest build() {
                 if (mSourceUri == null) {
                     throw new UnsupportedOperationException("Source URI must not be null");
                 }
@@ -843,6 +857,7 @@
         /** The job is paused. */
         public static final int STATUS_PAUSED = 4;
 
+        /** @hide */
         @IntDef(prefix = { "STATUS_" }, value = {
                 STATUS_PENDING,
                 STATUS_RUNNING,
@@ -861,6 +876,7 @@
         /** The job was canceled by the caller. */
         public static final int RESULT_CANCELED = 4;
 
+        /** @hide */
         @IntDef(prefix = { "RESULT_" }, value = {
                 RESULT_NONE,
                 RESULT_SUCCESS,
@@ -878,24 +894,26 @@
              * where 0 means that the job has not yet started and 100 means that it has finished.
              * @param progress The new progress ranging from 0 ~ 100 inclusive.
              */
-            void onProgressUpdate(int progress);
+            void onProgressUpdate(@IntRange(from = 0, to = 100) int progress);
         }
 
         private final ITranscodingClient mJobOwner;
         private final Executor mListenerExecutor;
         private final OnTranscodingFinishedListener mListener;
         private int mJobId = -1;
-        @GuardedBy("this")
+        // Lock for internal state.
+        private final Object mLock = new Object();
+        @GuardedBy("mLock")
         private Executor mProgressUpdateExecutor = null;
-        @GuardedBy("this")
+        @GuardedBy("mLock")
         private OnProgressUpdateListener mProgressUpdateListener = null;
-        @GuardedBy("this")
+        @GuardedBy("mLock")
         private int mProgress = 0;
-        @GuardedBy("this")
+        @GuardedBy("mLock")
         private int mProgressUpdateInterval = 0;
-        @GuardedBy("this")
+        @GuardedBy("mLock")
         private @Status int mStatus = STATUS_PENDING;
-        @GuardedBy("this")
+        @GuardedBy("mLock")
         private @Result int mResult = RESULT_NONE;
 
         private TranscodingJob(
@@ -932,20 +950,24 @@
          * @param executor The executor on which listener will be invoked.
          * @param listener The progress listener.
          */
-        public synchronized void setOnProgressUpdateListener(
+        public void setOnProgressUpdateListener(
                 int minProgressUpdateInterval,
                 @NonNull @CallbackExecutor Executor executor,
                 @Nullable OnProgressUpdateListener listener) {
-            Objects.requireNonNull(executor, "listenerExecutor must not be null");
-            Objects.requireNonNull(listener, "listener must not be null");
-            mProgressUpdateExecutor = executor;
-            mProgressUpdateListener = listener;
+            synchronized (mLock) {
+                Objects.requireNonNull(executor, "listenerExecutor must not be null");
+                Objects.requireNonNull(listener, "listener must not be null");
+                mProgressUpdateExecutor = executor;
+                mProgressUpdateListener = listener;
+            }
         }
 
-        private synchronized void updateStatusAndResult(@Status int jobStatus,
+        private void updateStatusAndResult(@Status int jobStatus,
                 @Result int jobResult) {
-            mStatus = jobStatus;
-            mResult = jobResult;
+            synchronized (mLock) {
+                mStatus = jobStatus;
+                mResult = jobResult;
+            }
         }
 
         /**
@@ -953,8 +975,10 @@
          *
          * @return true if successfully resubmit the job to the service. False otherwise.
          */
-        public synchronized boolean retry() {
-            // TODO(hkuang): Implement this.
+        public boolean retry() {
+            synchronized (mLock) {
+                // TODO(hkuang): Implement this.
+            }
             return true;
         }
 
@@ -963,38 +987,46 @@
          * If the job happened to finish before being canceled this call is effectively a no-op and
          * will not update the result in that case.
          */
-        public synchronized void cancel() {
-            // Check if the job is finished already.
-            if (mStatus != STATUS_FINISHED) {
-                try {
-                    mJobOwner.cancelJob(mJobId);
-                } catch (RemoteException re) {
-                    //TODO(hkuang): Find out what to do if failing to cancel the job.
-                    Log.e(TAG, "Failed to cancel the job due to exception:  " + re);
-                }
-                mStatus = STATUS_FINISHED;
-                mResult = RESULT_CANCELED;
+        public void cancel() {
+            synchronized (mLock) {
+                // Check if the job is finished already.
+                if (mStatus != STATUS_FINISHED) {
+                    try {
+                        mJobOwner.cancelJob(mJobId);
+                    } catch (RemoteException re) {
+                        //TODO(hkuang): Find out what to do if failing to cancel the job.
+                        Log.e(TAG, "Failed to cancel the job due to exception:  " + re);
+                    }
+                    mStatus = STATUS_FINISHED;
+                    mResult = RESULT_CANCELED;
 
-                // Notifies client the job is canceled.
-                mListenerExecutor.execute(() -> mListener.onTranscodingFinished(this));
+                    // Notifies client the job is canceled.
+                    mListenerExecutor.execute(() -> mListener.onTranscodingFinished(this));
+                }
             }
         }
 
         /**
-         * Gets the progress of the transcoding job. The progress is between 0 and 1, where 0 means
-         * that the job has not yet started and 1 means that it is finished.
+         * Gets the progress of the transcoding job. The progress is between 0 and 100, where 0
+         * means that the job has not yet started and 100 means that it is finished. For the
+         * cancelled job, the progress will be the last updated progress before it is cancelled.
          * @return The progress.
          */
-        public synchronized int getProgress() {
-            return mProgress;
+        @IntRange(from = 0, to = 100)
+        public int getProgress() {
+            synchronized (mLock) {
+                return mProgress;
+            }
         }
 
         /**
          * Gets the status of the transcoding job.
          * @return The status.
          */
-        public synchronized @Status int getStatus() {
-            return mStatus;
+        public @Status int getStatus() {
+            synchronized (mLock) {
+                return mStatus;
+            }
         }
 
         /**
@@ -1009,53 +1041,22 @@
          * Gets the result of the transcoding job.
          * @return The result.
          */
-        public synchronized @Result int getResult() {
-            return mResult;
-        }
-
-        private synchronized void updateProgress(int newProgress) {
-            mProgress = newProgress;
-        }
-
-        private synchronized void updateStatus(int newStatus) {
-            mStatus = newStatus;
-        }
-    }
-
-    /**
-     * Gets the MediaTranscodeManager singleton instance.
-     *
-     * @param context The application context.
-     * @return the {@link MediaTranscodeManager} singleton instance.
-     * @throws UnsupportedOperationException if failing to acquire the MediaTranscodeManager.
-     */
-    public static MediaTranscodeManager getInstance(@NonNull Context context) {
-        // Acquires the MediaTranscoding service.
-        IMediaTranscodingService service = getService(false /*retry*/);
-        return getInstance(context, service);
-    }
-
-    /** Similar as above, but wait till the service is ready. */
-    @VisibleForTesting
-    public static MediaTranscodeManager getInstance(@NonNull Context context, boolean retry) {
-        // Acquires the MediaTranscoding service.
-        IMediaTranscodingService service = getService(retry);
-        return getInstance(context, service);
-    }
-
-    /** Similar as above, but allow injecting transcodingService for testing. */
-    @VisibleForTesting
-    public static MediaTranscodeManager getInstance(@NonNull Context context,
-            IMediaTranscodingService transcodingService) {
-        Objects.requireNonNull(context, "context must not be null");
-
-        synchronized (MediaTranscodeManager.class) {
-            if (sMediaTranscodeManager == null) {
-                sMediaTranscodeManager = new MediaTranscodeManager(context.getApplicationContext(),
-                        transcodingService);
+        public @Result int getResult() {
+            synchronized (mLock) {
+                return mResult;
             }
+        }
 
-            return sMediaTranscodeManager;
+        private void updateProgress(int newProgress) {
+            synchronized (mLock) {
+                mProgress = newProgress;
+            }
+        }
+
+        private void updateStatus(int newStatus) {
+            synchronized (mLock) {
+                mStatus = newStatus;
+            }
         }
     }
 
@@ -1077,7 +1078,7 @@
             @NonNull TranscodingRequest transcodingRequest,
             @NonNull @CallbackExecutor Executor listenerExecutor,
             @NonNull OnTranscodingFinishedListener listener)
-            throws UnsupportedOperationException, FileNotFoundException {
+            throws FileNotFoundException {
         Log.i(TAG, "enqueueRequest called.");
         Objects.requireNonNull(transcodingRequest, "transcodingRequest must not be null");
         Objects.requireNonNull(listenerExecutor, "listenerExecutor must not be null");
@@ -1092,8 +1093,14 @@
             // Synchronizes the access to mPendingTranscodingJobs to make sure the job Id is
             // inserted in the mPendingTranscodingJobs in the callback handler.
             synchronized (mPendingTranscodingJobs) {
-                if (!mTranscodingClient.submitRequest(requestParcel, jobParcel)) {
-                    throw new UnsupportedOperationException("Failed to enqueue request");
+                synchronized (mLock) {
+                    if (mTranscodingClient == null) {
+                        // TODO(hkuang): Handle the case if client is temporarily unavailable.
+                    }
+
+                    if (!mTranscodingClient.submitRequest(requestParcel, jobParcel)) {
+                        throw new UnsupportedOperationException("Failed to enqueue request");
+                    }
                 }
 
                 // Wraps the TranscodingJobParcel into a TranscodingJob and returns it to client for
diff --git a/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodeManagerTest.java b/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodeManagerTest.java
index 009a41e..a707c7a 100644
--- a/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodeManagerTest.java
+++ b/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodeManagerTest.java
@@ -69,6 +69,13 @@
     private static final String TAG = "MediaTranscodeManagerTest";
     /** The time to wait for the transcode operation to complete before failing the test. */
     private static final int TRANSCODE_TIMEOUT_SECONDS = 10;
+
+    /** 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;
+
     private Context mContext;
     private MediaTranscodeManager mMediaTranscodeManager = null;
     private Uri mSourceHEVCVideoUri = null;
@@ -129,13 +136,31 @@
         return format;
     }
 
+    private MediaTranscodeManager getManager() {
+        for (int count = 1;  count <= CONNECT_SERVICE_RETRY_COUNT; count++) {
+            Log.d(TAG, "Trying to connect to service. Try count: " + count);
+            MediaTranscodeManager manager = mContext.getSystemService(MediaTranscodeManager.class);
+            if (manager != null) {
+                return manager;
+            }
+            try {
+                // Sleep a bit before retry.
+                Thread.sleep(INTERVAL_CONNECT_SERVICE_RETRY_MS);
+            } catch (InterruptedException ie) {
+                /* ignore */
+            }
+        }
+
+        throw new UnsupportedOperationException("Failed to acquire MediaTranscodeManager");
+    }
+
     @Override
     public void setUp() throws Exception {
         Log.d(TAG, "setUp");
         super.setUp();
 
         mContext = getInstrumentation().getContext();
-        mMediaTranscodeManager = MediaTranscodeManager.getInstance(mContext, true /*retry*/);
+        mMediaTranscodeManager = getManager();
         assertNotNull(mMediaTranscodeManager);
         androidx.test.InstrumentationRegistry.registerInstance(getInstrumentation(), new Bundle());
 
diff --git a/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodeManagerWithMockServiceTest.java b/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodeManagerWithMockServiceTest.java
index 748c21a..167e474 100644
--- a/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodeManagerWithMockServiceTest.java
+++ b/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodeManagerWithMockServiceTest.java
@@ -259,7 +259,7 @@
         super.setUp();
         mTranscodingService = new MockTranscodingService();
         mContext = getInstrumentation().getContext();
-        mMediaTranscodeManager = MediaTranscodeManager.getInstance(mContext, mTranscodingService);
+        mMediaTranscodeManager = mContext.getSystemService(MediaTranscodeManager.class);
         assertNotNull(mMediaTranscodeManager);
 
         // Setup source HEVC file uri.
diff --git a/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodingBenchmark.java b/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodingBenchmark.java
index 5c87d30..04909ef 100644
--- a/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodingBenchmark.java
+++ b/media/tests/MediaTranscodingTest/src/com/android/mediatranscodingtest/MediaTranscodingBenchmark.java
@@ -89,7 +89,7 @@
         Log.d(TAG, "setUp");
         super.setUp();
         mContext = getInstrumentation().getContext();
-        mMediaTranscodeManager = MediaTranscodeManager.getInstance(mContext);
+        mMediaTranscodeManager = mContext.getSystemService(MediaTranscodeManager.class);
     }
 
     @Override
diff --git a/non-updatable-api/system-current.txt b/non-updatable-api/system-current.txt
index 7cce0f2..f64be2b 100644
--- a/non-updatable-api/system-current.txt
+++ b/non-updatable-api/system-current.txt
@@ -1681,6 +1681,7 @@
     field public static final String ETHERNET_SERVICE = "ethernet";
     field public static final String EUICC_CARD_SERVICE = "euicc_card";
     field public static final String HDMI_CONTROL_SERVICE = "hdmi_control";
+    field public static final String MEDIA_TRANSCODING_SERVICE = "media_transcoding";
     field public static final String NETD_SERVICE = "netd";
     field public static final String NETWORK_SCORE_SERVICE = "network_score";
     field public static final String OEM_LOCK_SERVICE = "oem_lock";
@@ -4273,6 +4274,60 @@
     field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT) public static final int RADIO_TUNER = 1998; // 0x7ce
   }
 
+  public final class MediaTranscodeManager implements java.lang.AutoCloseable {
+    method public void close();
+    method @NonNull public android.media.MediaTranscodeManager.TranscodingJob enqueueRequest(@NonNull android.media.MediaTranscodeManager.TranscodingRequest, @NonNull java.util.concurrent.Executor, @NonNull android.media.MediaTranscodeManager.OnTranscodingFinishedListener) throws java.io.FileNotFoundException;
+    method protected void finalize();
+    field public static final int PRIORITY_OFFLINE = 2; // 0x2
+    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.TranscodingJob);
+  }
+
+  public static final class MediaTranscodeManager.TranscodingJob {
+    method public void cancel();
+    method public int getJobId();
+    method @IntRange(from=0, to=100) public int getProgress();
+    method public int getResult();
+    method public int getStatus();
+    method public boolean retry();
+    method public void setOnProgressUpdateListener(@NonNull java.util.concurrent.Executor, @Nullable android.media.MediaTranscodeManager.TranscodingJob.OnProgressUpdateListener);
+    method public void setOnProgressUpdateListener(int, @NonNull java.util.concurrent.Executor, @Nullable android.media.MediaTranscodeManager.TranscodingJob.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.TranscodingJob.OnProgressUpdateListener {
+    method public void onProgressUpdate(@IntRange(from=0, to=100) int);
+  }
+
+  public static final class MediaTranscodeManager.TranscodingRequest {
+    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 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 class PlayerProxy {
     method public void pause();
     method public void setPan(float);