Transcoding API: Hook up with media.transcoding service.

Note the test is tested with a mock transcoding service.
Another test will be added when the service's transcoding
is up and running.

Also remove the jni call and talk with service directly.

Bug: 145628554
Test: atest MediaTranscodeManagerTest
Change-Id: Ifd7babb235fcd034ba7ee1758985a9e4d031170c
diff --git a/Android.bp b/Android.bp
index 7a7464f..18ae76c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -572,6 +572,7 @@
         // 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",
@@ -618,6 +619,7 @@
     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/media/java/android/media/MediaTranscodeManager.java b/media/java/android/media/MediaTranscodeManager.java
index 510da81..2c5268b 100644
--- a/media/java/android/media/MediaTranscodeManager.java
+++ b/media/java/android/media/MediaTranscodeManager.java
@@ -20,17 +20,26 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.res.AssetFileDescriptor;
 import android.net.Uri;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+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.HashMap;
 import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.Executor;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 /**
  MediaTranscodeManager provides an interface to the system's media transcoding service and can be
@@ -54,7 +63,7 @@
  <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#enqueueTranscodingRequest(
+ {@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
@@ -87,6 +96,8 @@
 public final class MediaTranscodeManager {
     private static final String TAG = "MediaTranscodeManager";
 
+    private static final String MEDIA_TRANSCODING_SERVICE = "media.transcoding";
+
     /**
      * Default transcoding type.
      * @hide
@@ -147,34 +158,6 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface TranscodingPriority {}
 
-    // Invalid ID passed from native means the request was never enqueued.
-    private static final long ID_INVALID = -1;
-
-    // Events passed from native.
-    private static final int EVENT_JOB_STARTED = 1;
-    private static final int EVENT_JOB_PROGRESSED = 2;
-    private static final int EVENT_JOB_FINISHED = 3;
-
-    /** @hide */
-    @IntDef(prefix = {"EVENT_"}, value = {
-            EVENT_JOB_STARTED,
-            EVENT_JOB_PROGRESSED,
-            EVENT_JOB_FINISHED,
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface Event {}
-
-    private static MediaTranscodeManager sMediaTranscodeManager;
-
-    private final ConcurrentMap<Long, TranscodingJob> mPendingTranscodingJobs =
-            new ConcurrentHashMap<>();
-    private final Context mContext;
-
-    /* Private constructor. */
-    private MediaTranscodeManager(@NonNull Context context) {
-        mContext = context;
-    }
-
     /**
      * 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
@@ -192,6 +175,111 @@
         void onTranscodingFinished(@NonNull TranscodingJob transcodingJob);
     }
 
+    private final Context mContext;
+    private ContentResolver mContentResolver;
+    private final String mPackageName;
+    private final int mPid;
+    private final int mUid;
+    private final ExecutorService mCallbackExecutor = Executors.newSingleThreadExecutor();
+    private static MediaTranscodeManager sMediaTranscodeManager;
+    private final HashMap<Integer, TranscodingJob> mPendingTranscodingJobs = new HashMap();
+    @NonNull private ITranscodingClient mTranscodingClient;
+
+    private void handleTranscodingFinished(int jobId, TranscodingResultParcel result) {
+        synchronized (mPendingTranscodingJobs) {
+            // Gets the job associated with the jobId and removes it from
+            // mPendingTranscodingJobs.
+            final TranscodingJob job = mPendingTranscodingJobs.remove(jobId);
+
+            if (job == null) {
+                // This should not happen in reality.
+                Log.e(TAG, "Job " + jobId + " is not in PendingJobs");
+                return;
+            }
+
+            // Updates the job status and result.
+            job.updateStatusAndResult(TranscodingJob.STATUS_FINISHED,
+                    TranscodingJob.RESULT_SUCCESS);
+
+            // Notifies client the job is done.
+            if (job.mListener != null && job.mListenerExecutor != null) {
+                job.mListenerExecutor.execute(() -> job.mListener.onTranscodingFinished(job));
+            }
+        }
+    }
+
+    private void handleTranscodingFailed(int jobId, int errorCodec) {
+        synchronized (mPendingTranscodingJobs) {
+            // Gets the job associated with the jobId and removes it from
+            // mPendingTranscodingJobs.
+            final TranscodingJob job = mPendingTranscodingJobs.remove(jobId);
+
+            if (job == null) {
+                // This should not happen in reality.
+                Log.e(TAG, "Job " + jobId + " is not in PendingJobs");
+                return;
+            }
+
+            // Updates the job status and result.
+            job.updateStatusAndResult(TranscodingJob.STATUS_FINISHED,
+                    TranscodingJob.RESULT_ERROR);
+
+            // Notifies client the job is done.
+            if (job.mListener != null && job.mListenerExecutor != null) {
+                job.mListenerExecutor.execute(() -> job.mListener.onTranscodingFinished(job));
+            }
+        }
+    }
+
+    // Just forwards all the events to the event handler.
+    private ITranscodingClientCallback mTranscodingClientCallback =
+            new ITranscodingClientCallback.Stub() {
+                @Override
+                public void onTranscodingFinished(int jobId, TranscodingResultParcel result)
+                        throws RemoteException {
+                    handleTranscodingFinished(jobId, result);
+                }
+
+                @Override
+                public void onTranscodingFailed(int jobId, int errorCode) throws RemoteException {
+                    handleTranscodingFailed(jobId, errorCode);
+                }
+
+                @Override
+                public void onAwaitNumberOfJobsChanged(int jobId, int oldAwaitNumber,
+                        int newAwaitNumber) throws RemoteException {
+                    //TODO(hkuang): Implement this.
+                }
+
+                @Override
+                public void onProgressUpdate(int jobId, int progress) throws RemoteException {
+                    //TODO(hkuang): Implement this.
+                }
+            };
+
+    /* Private constructor. */
+    private MediaTranscodeManager(@NonNull Context context,
+            IMediaTranscodingService transcodingService) {
+        mContext = context;
+        mContentResolver = mContext.getContentResolver();
+        mPackageName = mContext.getPackageName();
+        mPid = Os.getuid();
+        mUid = Os.getpid();
+
+        try {
+            // Registers the client with MediaTranscoding service.
+            mTranscodingClient = transcodingService.registerClient(
+                    mTranscodingClientCallback,
+                    mPackageName,
+                    mPackageName,
+                    IMediaTranscodingService.USE_CALLING_UID,
+                    IMediaTranscodingService.USE_CALLING_PID);
+        } catch (RemoteException re) {
+            Log.e(TAG, "Failed to register new client due to exception " + re);
+            throw new UnsupportedOperationException("Failed to register new client");
+        }
+    }
+
     public static final class TranscodingRequest {
         /** Uri of the source media file. */
         private @NonNull Uri mSourceUri;
@@ -229,6 +317,9 @@
          */
         private @Nullable MediaFormat mImageFormat = null;
 
+        @VisibleForTesting
+        private int mProcessingDelayMs = 0;
+
         private TranscodingRequest(Builder b) {
             mSourceUri = b.mSourceUri;
             mDestinationUri = b.mDestinationUri;
@@ -237,6 +328,7 @@
             mVideoTrackFormat = b.mVideoTrackFormat;
             mAudioTrackFormat = b.mAudioTrackFormat;
             mImageFormat = b.mImageFormat;
+            mProcessingDelayMs = b.mProcessingDelayMs;
         }
 
         /** Return the type of the transcoding. */
@@ -271,6 +363,20 @@
             return mVideoTrackFormat;
         }
 
+        /* Writes the TranscodingRequest to a parcel. */
+        private TranscodingRequestParcel writeToParcel() {
+            TranscodingRequestParcel parcel = new TranscodingRequestParcel();
+            // TODO(hkuang): Implement all the fields here to pass to service.
+            parcel.priority = mPriority;
+            parcel.transcodingType = mType;
+            if (mProcessingDelayMs != 0) {
+                parcel.isForTesting = true;
+                parcel.testConfig = new TranscodingTestConfig();
+                parcel.testConfig.processingDelayMs = mProcessingDelayMs;
+            }
+            return parcel;
+        }
+
         /**
          * Builder class for {@link TranscodingRequest} objects.
          * Use this class to configure and create a <code>TranscodingRequest</code> instance.
@@ -280,10 +386,10 @@
             private @NonNull Uri mDestinationUri;
             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 int mProcessingDelayMs = 0;
 
             /**
              * Specifies the uri of source media file.
@@ -390,6 +496,17 @@
             }
 
             /**
+             * Sets the delay in processing this request.
+             * @param processingDelayMs delay in milliseconds.
+             * @return The same builder instance.
+             */
+            @VisibleForTesting
+            public Builder setProcessingDelayMs(int processingDelayMs) {
+                mProcessingDelayMs = processingDelayMs;
+                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
@@ -467,51 +584,102 @@
 
         /** Listener that gets notified when the progress changes. */
         @FunctionalInterface
-        public interface OnProgressChangedListener {
-
+        public interface OnProgressUpdateListener {
             /**
-             * Called when the progress changes. The progress is between 0 and 1, where 0 means
-             * that the job has not yet started and 1 means that it has finished.
-             * @param progress The new progress.
+             * Called when the progress changes. The progress is in percentage between 0 and 1,
+             * 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 onProgressChanged(float progress);
+            void onProgressUpdate(int progress);
         }
 
-        private final Executor mExecutor;
+        private final ITranscodingClient mJobOwner;
+        private final Executor mListenerExecutor;
         private final OnTranscodingFinishedListener mListener;
-        private final ReentrantLock mStatusChangeLock = new ReentrantLock();
-        private Executor mProgressChangedExecutor;
-        private OnProgressChangedListener mProgressChangedListener;
-        private long mID;
-        private float mProgress = 0.0f;
+        private int mJobId = -1;
+        @GuardedBy("this")
+        private Executor mProgressUpdateExecutor = null;
+        @GuardedBy("this")
+        private OnProgressUpdateListener mProgressUpdateListener = null;
+        @GuardedBy("this")
+        private int mProgress = 0;
+        @GuardedBy("this")
+        private int mProgressUpdateInterval = 0;
+        @GuardedBy("this")
         private @Status int mStatus = STATUS_PENDING;
+        @GuardedBy("this")
         private @Result int mResult = RESULT_NONE;
 
-        private TranscodingJob(long id, @NonNull @CallbackExecutor Executor executor,
+        private TranscodingJob(
+                @NonNull ITranscodingClient jobOwner,
+                @NonNull TranscodingJobParcel parcel,
+                @NonNull @CallbackExecutor Executor executor,
                 @NonNull OnTranscodingFinishedListener listener) {
-            mID = id;
-            mExecutor = executor;
+            Objects.requireNonNull(jobOwner, "JobOwner must not be null");
+            Objects.requireNonNull(parcel, "TranscodingJobParcel must not be null");
+            Objects.requireNonNull(executor, "listenerExecutor must not be null");
+            Objects.requireNonNull(listener, "listener must not be null");
+            mJobOwner = jobOwner;
+            mJobId = parcel.jobId;
+            mListenerExecutor = executor;
             mListener = listener;
         }
 
         /**
          * Set a progress listener.
+         * @param executor The executor on which listener will be invoked.
          * @param listener The progress listener.
          */
-        public void setOnProgressChangedListener(@NonNull @CallbackExecutor Executor executor,
-                @Nullable OnProgressChangedListener listener) {
-            mProgressChangedExecutor = executor;
-            mProgressChangedListener = listener;
+        public void setOnProgressUpdateListener(
+                @NonNull @CallbackExecutor Executor executor,
+                @Nullable OnProgressUpdateListener listener) {
+            setOnProgressUpdateListener(
+                    0 /* minProgressUpdateInterval */,
+                    executor, listener);
         }
 
         /**
-         * Cancels the transcoding job and notify the listener. 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.
+         * 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 cancel() {
-            setJobFinished(RESULT_CANCELED);
-            sMediaTranscodeManager.native_cancelTranscodingRequest(mID);
+        public synchronized 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;
+        }
+
+        private synchronized void updateStatusAndResult(@Status int jobStatus,
+                @Result int jobResult) {
+            mStatus = jobStatus;
+            mResult = jobResult;
+        }
+
+        /**
+         * Cancels the transcoding job and notify the listener.
+         * 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;
+
+                // Notifies client the job is canceled.
+                mListenerExecutor.execute(() -> mListener.onTranscodingFinished(this));
+            }
         }
 
         /**
@@ -519,7 +687,7 @@
          * that the job has not yet started and 1 means that it is finished.
          * @return The progress.
          */
-        public float getProgress() {
+        public synchronized int getProgress() {
             return mProgress;
         }
 
@@ -527,100 +695,38 @@
          * Gets the status of the transcoding job.
          * @return The status.
          */
-        public @Status int getStatus() {
+        public synchronized @Status int getStatus() {
             return mStatus;
         }
 
         /**
+         * Gets jobId of the transcoding job.
+         * @return job id.
+         */
+        public int getJobId() {
+            return mJobId;
+        }
+
+        /**
          * Gets the result of the transcoding job.
          * @return The result.
          */
-        public @Result int getResult() {
+        public synchronized @Result int getResult() {
             return mResult;
         }
 
-        private void setJobStarted() {
-            mStatus = STATUS_RUNNING;
-        }
-
-        private void setJobProgress(float newProgress) {
-            mProgress = newProgress;
+        private void setJobProgress(int newProgress) {
+            synchronized (this) {
+                mProgress = newProgress;
+            }
 
             // Notify listener.
-            OnProgressChangedListener onProgressChangedListener = mProgressChangedListener;
-            if (onProgressChangedListener != null) {
-                mProgressChangedExecutor.execute(
-                        () -> onProgressChangedListener.onProgressChanged(mProgress));
+            OnProgressUpdateListener onProgressUpdateListener = mProgressUpdateListener;
+            if (mProgressUpdateListener != null) {
+                mProgressUpdateExecutor.execute(
+                        () -> onProgressUpdateListener.onProgressUpdate(mProgress));
             }
         }
-
-        private void setJobFinished(int result) {
-            boolean doNotifyListener = false;
-
-            // Prevent conflicting simultaneous status updates from native (finished) and from the
-            // caller (cancel).
-            try {
-                mStatusChangeLock.lock();
-                if (mStatus != STATUS_FINISHED) {
-                    mStatus = STATUS_FINISHED;
-                    mResult = result;
-                    doNotifyListener = true;
-                }
-            } finally {
-                mStatusChangeLock.unlock();
-            }
-
-            if (doNotifyListener) {
-                mExecutor.execute(() -> mListener.onTranscodingFinished(this));
-            }
-        }
-
-        private void processJobEvent(@Event int event, int arg) {
-            switch (event) {
-                case EVENT_JOB_STARTED:
-                    setJobStarted();
-                    break;
-                case EVENT_JOB_PROGRESSED:
-                    setJobProgress((float) arg / 100);
-                    break;
-                case EVENT_JOB_FINISHED:
-                    setJobFinished(arg);
-                    break;
-                default:
-                    Log.e(TAG, "Unsupported event: " + event);
-                    break;
-            }
-        }
-    }
-
-    /** Initializes the native library. */
-    private static native void native_init();
-
-    /** Requests a new job ID from the native service. */
-    private native long native_requestUniqueJobID();
-
-    /** Enqueues a transcoding request to the native service. */
-    private native boolean native_enqueueTranscodingRequest(
-            long id, @NonNull TranscodingRequest transcodingRequest, @NonNull Context context);
-
-    /** Cancels an enqueued transcoding request. */
-    private native void native_cancelTranscodingRequest(long id);
-
-    /** Events posted from the native service. */
-    @SuppressWarnings("unused")
-    private void postEventFromNative(@Event int event, long id, int arg) {
-        Log.d(TAG, String.format("postEventFromNative. Event %d, ID %d, arg %d", event, id, arg));
-
-        TranscodingJob transcodingJob = mPendingTranscodingJobs.get(id);
-
-        // Job IDs are added to the tracking set before the job is enqueued so it should never
-        // be null unless the service misbehaves.
-        if (transcodingJob == null) {
-            Log.e(TAG, "No matching transcode job found for id " + id);
-            return;
-        }
-
-        transcodingJob.processJobEvent(event, arg);
     }
 
     /**
@@ -628,12 +734,26 @@
      *
      * @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 = IMediaTranscodingService.Stub.asInterface(
+                ServiceManager.getService(MEDIA_TRANSCODING_SERVICE));
+
+        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());
+                sMediaTranscodeManager = new MediaTranscodeManager(context.getApplicationContext(),
+                        transcodingService);
             }
             return sMediaTranscodeManager;
         }
@@ -649,41 +769,54 @@
      * @param listenerExecutor   Executor on which the listener is notified.
      * @param listener           Listener to get notified when the transcoding job is finished.
      * @return A TranscodingJob for this operation.
-     * @throws UnsupportedOperationException if the result could not be fulfilled.
+     * @throws FileNotFoundException if the source Uri or destination Uri could not be opened.
+     * @throws UnsupportedOperationException if the request could not be fulfilled.
      */
     @NonNull
-    public TranscodingJob enqueueTranscodingRequest(
+    public TranscodingJob enqueueRequest(
             @NonNull TranscodingRequest transcodingRequest,
             @NonNull @CallbackExecutor Executor listenerExecutor,
-            @NonNull OnTranscodingFinishedListener listener) throws UnsupportedOperationException {
-        Log.i(TAG, "enqueueTranscodingRequest called.");
+            @NonNull OnTranscodingFinishedListener listener)
+            throws UnsupportedOperationException, FileNotFoundException {
+        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");
 
-        // Reserve a job ID.
-        // TODO(hkuang): Remove this.
-        long jobID = native_requestUniqueJobID();
-        if (jobID == ID_INVALID) {
-            throw new UnsupportedOperationException("Transcoding request could not be fulfilled");
+        TranscodingRequestParcel requestParcel = transcodingRequest.writeToParcel();
+
+        // TODO(hkuang): move to use Uri string instead of FileDescriptor.
+        // Open the source file descriptor.
+        AssetFileDescriptor sourceFd = mContentResolver.openAssetFileDescriptor(
+                transcodingRequest.getSourceUri(), "r");
+
+        // Open the destination file descriptor.
+        AssetFileDescriptor destinationFd = mContentResolver.openAssetFileDescriptor(
+                transcodingRequest.getDestinationUri(), "w");
+
+        // Submits the request to MediaTranscoding service.
+        TranscodingJobParcel jobParcel = new TranscodingJobParcel();
+        try {
+            // 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");
+                }
+
+                // Wraps the TranscodingJobParcel into a TranscodingJob and returns it to client for
+                // tracking.
+                TranscodingJob job = new TranscodingJob(mTranscodingClient, jobParcel,
+                        listenerExecutor,
+                        listener);
+
+                // Adds the new job into pending jobs.
+                mPendingTranscodingJobs.put(job.getJobId(), job);
+                return job;
+            }
+        } catch (RemoteException re) {
+            throw new UnsupportedOperationException(
+                    "Failed to submit request to Transcoding service");
         }
-
-        // Add the job to the tracking set.
-        TranscodingJob transcodingJob = new TranscodingJob(jobID, listenerExecutor, listener);
-        mPendingTranscodingJobs.put(jobID, transcodingJob);
-
-        // Enqueue the request with the native service.
-        boolean enqueued = native_enqueueTranscodingRequest(jobID, transcodingRequest, mContext);
-        if (!enqueued) {
-            mPendingTranscodingJobs.remove(jobID);
-            throw new UnsupportedOperationException("Transcoding request could not be fulfilled");
-        }
-
-        return transcodingJob;
-    }
-
-    static {
-        System.loadLibrary("mediatranscodemanager_jni");
-        native_init();
     }
 }
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/mediatranscodemanager/MediaTranscodeManagerTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/mediatranscodemanager/MediaTranscodeManagerTest.java
index 41a1f74..c07e38f 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/mediatranscodemanager/MediaTranscodeManagerTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/mediatranscodemanager/MediaTranscodeManagerTest.java
@@ -20,11 +20,18 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.media.IMediaTranscodingService;
+import android.media.ITranscodingClient;
+import android.media.ITranscodingClientCallback;
 import android.media.MediaFormat;
 import android.media.MediaTranscodeManager;
 import android.media.MediaTranscodeManager.TranscodingJob;
 import android.media.MediaTranscodeManager.TranscodingRequest;
+import android.media.TranscodingJobParcel;
+import android.media.TranscodingRequestParcel;
+import android.media.TranscodingResultParcel;
 import android.net.Uri;
+import android.os.RemoteException;
 import android.test.ActivityInstrumentationTestCase2;
 import android.util.Log;
 
@@ -34,10 +41,16 @@
 import org.junit.Test;
 
 import java.io.File;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /*
  * Functional tests for MediaTranscodeManager in the media framework.
@@ -63,12 +76,124 @@
     private Uri mSourceHEVCVideoUri = null;
     private Uri mDestinationUri = null;
 
+    // Use mock transcoding service for testing the api.
+    private MockTranscodingService mTranscodingService = null;
+
     // Setting for transcoding to H.264.
     private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
     private static final int BIT_RATE = 2000000;            // 2Mbps
     private static final int WIDTH = 1920;
     private static final int HEIGHT = 1080;
 
+    // A mock transcoding service that will take constant 300ms to process each transcoding job.
+    // Instead of doing real transcoding, it will return the dst uri directly.
+    class MockTranscodingService extends IMediaTranscodingService.Stub {
+        private final ScheduledExecutorService mJobScheduler = Executors.newScheduledThreadPool(1);
+        private int mNumOfClients = 0;
+        private AtomicInteger mJobId = new AtomicInteger();
+
+        // A runnable that will process the job.
+        private class ProcessingJobRunnable implements Runnable {
+            private TranscodingJobParcel mJob;
+            private ITranscodingClientCallback mCallback;
+            private ConcurrentMap<Integer, ScheduledFuture<?>> mJobMap;
+
+            ProcessingJobRunnable(ITranscodingClientCallback callback,
+                    TranscodingJobParcel job,
+                    ConcurrentMap<Integer, ScheduledFuture<?>> jobMap) {
+                mJob = job;
+                mCallback = callback;
+                mJobMap = jobMap;
+            }
+
+            @Override
+            public void run() {
+                Log.d(TAG, "Start to process job " + mJob.jobId);
+                TranscodingResultParcel result = new TranscodingResultParcel();
+                try {
+                    mCallback.onTranscodingFinished(mJob.jobId, result);
+                    // Removes the job from job map.
+                    mJobMap.remove(mJob.jobId);
+                } catch (RemoteException re) {
+                    Log.e(TAG, "Failed to callback to client");
+                }
+            }
+        }
+
+        @Override
+        public ITranscodingClient registerClient(ITranscodingClientCallback callback,
+                String clientName, String opPackageName, int clientUid, int clientPid)
+                throws RemoteException {
+            Log.d(TAG, "MockTranscodingService creates one client");
+
+            ITranscodingClient client = new ITranscodingClient.Stub() {
+                private final ConcurrentMap<Integer, ScheduledFuture<?>> mPendingTranscodingJobs =
+                        new ConcurrentHashMap<Integer, ScheduledFuture<?>>();
+
+                @Override
+                public boolean submitRequest(TranscodingRequestParcel inRequest,
+                        TranscodingJobParcel outjob) {
+                    Log.d(TAG, "Mock client gets submitRequest");
+                    try {
+                        outjob.request = inRequest;
+                        outjob.jobId = mJobId.getAndIncrement();
+                        Log.d(TAG, "Generate new job " + outjob.jobId);
+
+                        // Schedules the job to run after inRequest.processingDelayMs.
+                        ScheduledFuture<?> transcodingFuture = mJobScheduler.schedule(
+                                new ProcessingJobRunnable(callback, outjob,
+                                        mPendingTranscodingJobs),
+                                inRequest.testConfig == null ? 0
+                                        : inRequest.testConfig.processingDelayMs,
+                                TimeUnit.MILLISECONDS);
+                        mPendingTranscodingJobs.put(outjob.jobId, transcodingFuture);
+                    } catch (RejectedExecutionException e) {
+                        Log.e(TAG, "Failed to schedule transcoding job: " + e);
+                        return false;
+                    }
+
+                    return true;
+                }
+
+                @Override
+                public boolean cancelJob(int jobId) throws RemoteException {
+                    Log.d(TAG, "Mock client gets cancelJob " + jobId);
+                    // Cancels the job is still in the mPendingTranscodingJobs.
+                    if (mPendingTranscodingJobs.containsKey(jobId)) {
+                        // Cancel the future task for transcoding.
+                        mPendingTranscodingJobs.get(jobId).cancel(true);
+
+                        // Remove the job from the mPendingTranscodingJobs.
+                        mPendingTranscodingJobs.remove(jobId);
+                        return true;
+                    }
+                    return false;
+                }
+
+                @Override
+                public boolean getJobWithId(int jobId, TranscodingJobParcel job)
+                        throws RemoteException {
+                    // This will be implemented this if needed in the test.
+                    return true;
+                }
+
+                @Override
+                public void unregister() throws RemoteException {
+                    Log.d(TAG, "Mock client gets unregister");
+                    // This will be implemented this if needed in the test.
+                    mNumOfClients--;
+                }
+            };
+            mNumOfClients++;
+            return client;
+        }
+
+        @Override
+        public int getNumOfClients() throws RemoteException {
+            return mNumOfClients;
+        }
+    }
+
     public MediaTranscodeManagerTest() {
         super("com.android.MediaTranscodeManagerTest", MediaFrameworkTest.class);
     }
@@ -89,6 +214,12 @@
         return Uri.fromFile(outFile);
     }
 
+    // Generates a invalid uri which will let the mock service return transcoding failure.
+    private static Uri generateInvalidTranscodingUri(Context context) {
+        File outFile = new File(context.getExternalCacheDir(), "InvalidUri.mp4");
+        return Uri.fromFile(outFile);
+    }
+
     /**
      * Creates a MediaFormat with the basic set of values.
      */
@@ -102,8 +233,9 @@
     public void setUp() throws Exception {
         Log.d(TAG, "setUp");
         super.setUp();
+        mTranscodingService = new MockTranscodingService();
         mContext = getInstrumentation().getContext();
-        mMediaTranscodeManager = MediaTranscodeManager.getInstance(mContext);
+        mMediaTranscodeManager = MediaTranscodeManager.getInstance(mContext, mTranscodingService);
         assertNotNull(mMediaTranscodeManager);
 
         // Setup source HEVC file uri.
@@ -217,7 +349,7 @@
     }
 
     @Test
-    public void testNormalTranscoding() throws InterruptedException {
+    public void testTranscodingOneVideo() throws Exception {
         Log.d(TAG, "Starting: testMediaTranscodeManager");
 
         Semaphore transcodeCompleteSemaphore = new Semaphore(0);
@@ -228,20 +360,17 @@
                         .setType(MediaTranscodeManager.TRANSCODING_TYPE_VIDEO)
                         .setPriority(MediaTranscodeManager.PRIORITY_REALTIME)
                         .setVideoTrackFormat(createMediaFormat())
+                        .setProcessingDelayMs(300 /* delayMs */)
                         .build();
         Executor listenerExecutor = Executors.newSingleThreadExecutor();
 
-        TranscodingJob job;
-        job = mMediaTranscodeManager.enqueueTranscodingRequest(request, listenerExecutor,
+        TranscodingJob job = mMediaTranscodeManager.enqueueRequest(request, listenerExecutor,
                 transcodingJob -> {
-                    Log.d(TAG, "Transcoding completed with result: " + transcodingJob.getResult());
+                    Log.d(TAG, "Transcoding completed with result: ");
                     transcodeCompleteSemaphore.release();
                 });
         assertNotNull(job);
 
-        job.setOnProgressChangedListener(
-                listenerExecutor, progress -> Log.d(TAG, "Progress: " + progress));
-
         if (job != null) {
             Log.d(TAG, "testMediaTranscodeManager - Waiting for transcode to complete.");
             boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
@@ -249,4 +378,5 @@
             assertTrue("Transcode failed to complete in time.", finishedOnTime);
         }
     }
+
 }