Import Android SDK Platform P [4697573]

/google/data/ro/projects/android/fetch_artifact \
    --bid 4697573 \
    --target sdk_phone_armv7-win_sdk \
    sdk-repo-linux-sources-4697573.zip

AndroidVersion.ApiLevel has been modified to appear as 28

Change-Id: If80578c3c657366cc9cf75f8db13d46e2dd4e077
diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java
index 44a2ff9..d432658 100644
--- a/android/media/AudioAttributes.java
+++ b/android/media/AudioAttributes.java
@@ -19,7 +19,6 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
-import android.media.AudioAttributesProto;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -206,19 +205,26 @@
     /**
      * @hide
      * Denotes a usage for alarms,
-     * will be muted when the Zen mode doesn't allow alarms
+     * will be muted when the Zen mode priority doesn't allow alarms or in Alarms Only Mode
      * @see #SUPPRESSIBLE_USAGES
      */
     public final static int SUPPRESSIBLE_ALARM = 4;
     /**
      * @hide
-     * Denotes a usage for all other sounds not caught in SUPPRESSIBLE_NOTIFICATION,
-     * SUPPRESSIBLE_CALL,SUPPRESSIBLE_NEVER or SUPPRESSIBLE_ALARM.
-     * This includes media, system, game, navigation, the assistant, and more.
-     * These will be muted when the Zen mode doesn't allow media/system/other.
+     * Denotes a usage for media, game, assistant, and navigation
+     * will be muted when the Zen priority mode doesn't allow media
      * @see #SUPPRESSIBLE_USAGES
      */
-    public final static int SUPPRESSIBLE_MEDIA_SYSTEM_OTHER = 5;
+    public final static int SUPPRESSIBLE_MEDIA = 5;
+    /**
+     * @hide
+     * Denotes a usage for all other sounds not caught in SUPPRESSIBLE_NOTIFICATION,
+     * SUPPRESSIBLE_CALL,SUPPRESSIBLE_NEVER, SUPPRESSIBLE_ALARM or SUPPRESSIBLE_MEDIA.
+     * This includes system, sonification and unknown sounds.
+     * These will be muted when the Zen priority mode doesn't allow sytem sounds
+     * @see #SUPPRESSIBLE_USAGES
+     */
+    public final static int SUPPRESSIBLE_SYSTEM = 6;
 
     /**
      * @hide
@@ -239,13 +245,13 @@
         SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_ACCESSIBILITY,          SUPPRESSIBLE_NEVER);
         SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION,               SUPPRESSIBLE_NEVER);
         SUPPRESSIBLE_USAGES.put(USAGE_ALARM,                             SUPPRESSIBLE_ALARM);
-        SUPPRESSIBLE_USAGES.put(USAGE_MEDIA,                             SUPPRESSIBLE_MEDIA_SYSTEM_OTHER);
-        SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_SONIFICATION,           SUPPRESSIBLE_MEDIA_SYSTEM_OTHER);
-        SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,    SUPPRESSIBLE_MEDIA_SYSTEM_OTHER);
-        SUPPRESSIBLE_USAGES.put(USAGE_GAME,                              SUPPRESSIBLE_MEDIA_SYSTEM_OTHER);
-        SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING,    SUPPRESSIBLE_MEDIA_SYSTEM_OTHER);
-        SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT,                         SUPPRESSIBLE_MEDIA_SYSTEM_OTHER);
-        SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN,                           SUPPRESSIBLE_MEDIA_SYSTEM_OTHER);
+        SUPPRESSIBLE_USAGES.put(USAGE_MEDIA,                             SUPPRESSIBLE_MEDIA);
+        SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,    SUPPRESSIBLE_MEDIA);
+        SUPPRESSIBLE_USAGES.put(USAGE_GAME,                              SUPPRESSIBLE_MEDIA);
+        SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANT,                         SUPPRESSIBLE_MEDIA);
+        SUPPRESSIBLE_USAGES.put(USAGE_VOICE_COMMUNICATION_SIGNALLING,    SUPPRESSIBLE_SYSTEM);
+        SUPPRESSIBLE_USAGES.put(USAGE_ASSISTANCE_SONIFICATION,           SUPPRESSIBLE_SYSTEM);
+        SUPPRESSIBLE_USAGES.put(USAGE_UNKNOWN,                           SUPPRESSIBLE_SYSTEM);
     }
 
     /**
diff --git a/android/media/AudioDeviceInfo.java b/android/media/AudioDeviceInfo.java
index 1a97b6b..ca895fc 100644
--- a/android/media/AudioDeviceInfo.java
+++ b/android/media/AudioDeviceInfo.java
@@ -22,6 +22,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 import java.util.TreeSet;
 
 /**
@@ -122,6 +123,10 @@
      * A device type describing a USB audio headset.
      */
     public static final int TYPE_USB_HEADSET       = 22;
+    /**
+     * A device type describing a Hearing Aid.
+     */
+    public static final int TYPE_HEARING_AID   = 23;
 
     /** @hide */
     @IntDef(flag = false, prefix = "TYPE", value = {
@@ -142,7 +147,9 @@
             TYPE_LINE_DIGITAL,
             TYPE_FM,
             TYPE_AUX_LINE,
-            TYPE_IP }
+            TYPE_IP,
+            TYPE_BUS,
+            TYPE_HEARING_AID }
     )
     @Retention(RetentionPolicy.SOURCE)
     public @interface AudioDeviceTypeOut {}
@@ -168,12 +175,27 @@
             case TYPE_FM:
             case TYPE_AUX_LINE:
             case TYPE_IP:
+            case TYPE_BUS:
+            case TYPE_HEARING_AID:
                 return true;
             default:
                 return false;
         }
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        AudioDeviceInfo that = (AudioDeviceInfo) o;
+        return Objects.equals(getPort(), that.getPort());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getPort());
+    }
+
     private final AudioDevicePort mPort;
 
     AudioDeviceInfo(AudioDevicePort port) {
@@ -204,11 +226,10 @@
     }
 
     /**
-     * @hide
      * @return The "address" string of the device. This generally contains device-specific
      * parameters.
      */
-    public String getAddress() {
+    public @NonNull String getAddress() {
         return mPort.address();
     }
 
@@ -351,6 +372,7 @@
         INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_AUX_LINE, TYPE_AUX_LINE);
         INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_IP, TYPE_IP);
         INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_BUS, TYPE_BUS);
+        INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_HEARING_AID, TYPE_HEARING_AID);
 
         INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_BUILTIN_MIC, TYPE_BUILTIN_MIC);
         INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET, TYPE_BLUETOOTH_SCO);
@@ -399,6 +421,7 @@
         EXT_TO_INT_DEVICE_MAPPING.put(TYPE_AUX_LINE, AudioSystem.DEVICE_OUT_AUX_LINE);
         EXT_TO_INT_DEVICE_MAPPING.put(TYPE_IP, AudioSystem.DEVICE_OUT_IP);
         EXT_TO_INT_DEVICE_MAPPING.put(TYPE_BUS, AudioSystem.DEVICE_OUT_BUS);
+        EXT_TO_INT_DEVICE_MAPPING.put(TYPE_HEARING_AID, AudioSystem.DEVICE_OUT_HEARING_AID);
     }
 }
 
diff --git a/android/media/AudioFocusInfo.java b/android/media/AudioFocusInfo.java
index 5d0c8e2..5467a69 100644
--- a/android/media/AudioFocusInfo.java
+++ b/android/media/AudioFocusInfo.java
@@ -38,6 +38,10 @@
     private int mLossReceived;
     private int mFlags;
 
+    // generation count for the validity of a request/response async exchange between
+    // external focus policy and MediaFocusControl
+    private long mGenCount = -1;
+
 
     /**
      * Class constructor
@@ -61,6 +65,16 @@
         mSdkTarget = sdk;
     }
 
+    /** @hide */
+    public void setGen(long g) {
+        mGenCount = g;
+    }
+
+    /** @hide */
+    public long getGen() {
+        return mGenCount;
+    }
+
 
     /**
      * The audio attributes for the audio focus request.
@@ -128,6 +142,7 @@
         dest.writeInt(mLossReceived);
         dest.writeInt(mFlags);
         dest.writeInt(mSdkTarget);
+        dest.writeLong(mGenCount);
     }
 
     @Override
@@ -168,6 +183,8 @@
         if (mSdkTarget != other.mSdkTarget) {
             return false;
         }
+        // mGenCount is not used to verify equality between two focus holds as multiple requests
+        // (hence of different generations) could correspond to the same hold
         return true;
     }
 
@@ -175,7 +192,7 @@
             = new Parcelable.Creator<AudioFocusInfo>() {
 
         public AudioFocusInfo createFromParcel(Parcel in) {
-            return new AudioFocusInfo(
+            final AudioFocusInfo afi = new AudioFocusInfo(
                     AudioAttributes.CREATOR.createFromParcel(in), //AudioAttributes aa
                     in.readInt(), // int clientUid
                     in.readString(), //String clientId
@@ -185,6 +202,8 @@
                     in.readInt(), //int flags
                     in.readInt()  //int sdkTarget
                     );
+            afi.setGen(in.readLong());
+            return afi;
         }
 
         public AudioFocusInfo[] newArray(int size) {
diff --git a/android/media/AudioFormat.java b/android/media/AudioFormat.java
index b07d042..f98480b 100644
--- a/android/media/AudioFormat.java
+++ b/android/media/AudioFormat.java
@@ -265,6 +265,12 @@
     public static final int ENCODING_AAC_XHE = 16;
     /** Audio data format: AC-4 sync frame transport format */
     public static final int ENCODING_AC4 = 17;
+    /** Audio data format: E-AC-3-JOC compressed
+     * E-AC-3-JOC streams can be decoded by downstream devices supporting {@link #ENCODING_E_AC3}.
+     * Use {@link #ENCODING_E_AC3} as the AudioTrack encoding when the downstream device
+     * supports {@link #ENCODING_E_AC3} but not {@link #ENCODING_E_AC3_JOC}.
+     **/
+    public static final int ENCODING_E_AC3_JOC = 18;
 
     /** @hide */
     public static String toLogFriendlyEncoding(int enc) {
@@ -512,6 +518,7 @@
         case ENCODING_PCM_FLOAT:
         case ENCODING_AC3:
         case ENCODING_E_AC3:
+        case ENCODING_E_AC3_JOC:
         case ENCODING_DTS:
         case ENCODING_DTS_HD:
         case ENCODING_MP3:
@@ -537,6 +544,7 @@
         case ENCODING_PCM_FLOAT:
         case ENCODING_AC3:
         case ENCODING_E_AC3:
+        case ENCODING_E_AC3_JOC:
         case ENCODING_DTS:
         case ENCODING_DTS_HD:
         case ENCODING_IEC61937:
@@ -564,6 +572,7 @@
             return true;
         case ENCODING_AC3:
         case ENCODING_E_AC3:
+        case ENCODING_E_AC3_JOC:
         case ENCODING_DTS:
         case ENCODING_DTS_HD:
         case ENCODING_MP3:
@@ -593,6 +602,7 @@
             return true;
         case ENCODING_AC3:
         case ENCODING_E_AC3:
+        case ENCODING_E_AC3_JOC:
         case ENCODING_DTS:
         case ENCODING_DTS_HD:
         case ENCODING_MP3:
@@ -829,6 +839,7 @@
                 case ENCODING_PCM_FLOAT:
                 case ENCODING_AC3:
                 case ENCODING_E_AC3:
+                case ENCODING_E_AC3_JOC:
                 case ENCODING_DTS:
                 case ENCODING_DTS_HD:
                 case ENCODING_IEC61937:
@@ -1044,6 +1055,7 @@
         ENCODING_PCM_FLOAT,
         ENCODING_AC3,
         ENCODING_E_AC3,
+        ENCODING_E_AC3_JOC,
         ENCODING_DTS,
         ENCODING_DTS_HD,
         ENCODING_IEC61937,
diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java
index 2ac4063..aeef215 100644
--- a/android/media/AudioManager.java
+++ b/android/media/AudioManager.java
@@ -32,6 +32,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.media.audiopolicy.AudioPolicy;
+import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.MediaSessionLegacyHelper;
@@ -48,17 +49,25 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.util.Pair;
 import android.util.Slog;
 import android.view.KeyEvent;
 
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+
 
 /**
  * AudioManager provides access to volume and ringer mode control.
@@ -400,6 +409,18 @@
     public static final int ADJUST_TOGGLE_MUTE = 101;
 
     /** @hide */
+    @IntDef(flag = false, prefix = "ADJUST", value = {
+            ADJUST_RAISE,
+            ADJUST_LOWER,
+            ADJUST_SAME,
+            ADJUST_MUTE,
+            ADJUST_UNMUTE,
+            ADJUST_TOGGLE_MUTE }
+            )
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface VolumeAdjustment {}
+
+    /** @hide */
     public static final String adjustToString(int adj) {
         switch (adj) {
             case ADJUST_RAISE: return "ADJUST_RAISE";
@@ -1331,6 +1352,7 @@
     //====================================================================
     // Offload query
     /**
+     * @hide
      * Returns whether offloaded playback of an audio format is supported on the device.
      * Offloaded playback is where the decoding of an audio stream is not competing with other
      * software resources. In general, it is supported by dedicated hardware, such as audio DSPs.
@@ -2077,27 +2099,7 @@
      */
     private boolean querySoundEffectsEnabled(int user) {
         return Settings.System.getIntForUser(getContext().getContentResolver(),
-                Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0
-                && !areSystemSoundsZenModeBlocked(getContext());
-    }
-
-    private boolean areSystemSoundsZenModeBlocked(Context context) {
-        int zenMode = Settings.Global.getInt(context.getContentResolver(),
-                Settings.Global.ZEN_MODE, 0);
-
-        switch (zenMode) {
-            case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS:
-            case Settings.Global.ZEN_MODE_ALARMS:
-                return true;
-            case Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
-                final NotificationManager noMan = (NotificationManager) context
-                        .getSystemService(Context.NOTIFICATION_SERVICE);
-                return (noMan.getNotificationPolicy().priorityCategories
-                        & NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER) == 0;
-            case Settings.Global.ZEN_MODE_OFF:
-            default:
-                return false;
-        }
+                Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0;
     }
 
     /**
@@ -2324,6 +2326,20 @@
                 }
             }
         }
+
+        @Override
+        public void dispatchFocusResultFromExtPolicy(int requestResult, String clientId) {
+            synchronized (mFocusRequestsLock) {
+                // TODO use generation counter as the key instead
+                final BlockingFocusResultReceiver focusReceiver =
+                        mFocusRequestsAwaitingResult.remove(clientId);
+                if (focusReceiver != null) {
+                    focusReceiver.notifyResult(requestResult);
+                } else {
+                    Log.e(TAG, "dispatchFocusResultFromExtPolicy found no result receiver");
+                }
+            }
+        }
     };
 
     private String getIdForAudioFocusListener(OnAudioFocusChangeListener l) {
@@ -2376,6 +2392,40 @@
       */
     public static final int AUDIOFOCUS_REQUEST_DELAYED = 2;
 
+    /** @hide */
+    @IntDef(flag = false, prefix = "AUDIOFOCUS_REQUEST", value = {
+            AUDIOFOCUS_REQUEST_FAILED,
+            AUDIOFOCUS_REQUEST_GRANTED,
+            AUDIOFOCUS_REQUEST_DELAYED }
+    )
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FocusRequestResult {}
+
+    /**
+     * @hide
+     * code returned when a synchronous focus request on the client-side is to be blocked
+     * until the external audio focus policy decides on the response for the client
+     */
+    public static final int AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY = 100;
+
+    /**
+     * Timeout duration in ms when waiting on an external focus policy for the result for a
+     * focus request
+     */
+    private static final int EXT_FOCUS_POLICY_TIMEOUT_MS = 200;
+
+    private static final String FOCUS_CLIENT_ID_STRING = "android_audio_focus_client_id";
+
+    private final Object mFocusRequestsLock = new Object();
+    /**
+     * Map of all receivers of focus request results, one per unresolved focus request.
+     * Receivers are added before sending the request to the external focus policy,
+     * and are removed either after receiving the result, or after the timeout.
+     * This variable is lazily initialized.
+     */
+    @GuardedBy("mFocusRequestsLock")
+    private HashMap<String, BlockingFocusResultReceiver> mFocusRequestsAwaitingResult;
+
 
     /**
      *  Request audio focus.
@@ -2642,18 +2692,100 @@
             // some tests don't have a Context
             sdk = Build.VERSION.SDK_INT;
         }
-        try {
-            status = service.requestAudioFocus(afr.getAudioAttributes(),
-                    afr.getFocusGain(), mICallBack,
-                    mAudioFocusDispatcher,
-                    getIdForAudioFocusListener(afr.getOnAudioFocusChangeListener()),
-                    getContext().getOpPackageName() /* package name */, afr.getFlags(),
-                    ap != null ? ap.cb() : null,
-                    sdk);
-        } catch (RemoteException e) {
-            throw e.rethrowFromSystemServer();
+
+        final String clientId = getIdForAudioFocusListener(afr.getOnAudioFocusChangeListener());
+        final BlockingFocusResultReceiver focusReceiver;
+        synchronized (mFocusRequestsLock) {
+            try {
+                // TODO status contains result and generation counter for ext policy
+                status = service.requestAudioFocus(afr.getAudioAttributes(),
+                        afr.getFocusGain(), mICallBack,
+                        mAudioFocusDispatcher,
+                        clientId,
+                        getContext().getOpPackageName() /* package name */, afr.getFlags(),
+                        ap != null ? ap.cb() : null,
+                        sdk);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            if (status != AudioManager.AUDIOFOCUS_REQUEST_WAITING_FOR_EXT_POLICY) {
+                // default path with no external focus policy
+                return status;
+            }
+            if (mFocusRequestsAwaitingResult == null) {
+                mFocusRequestsAwaitingResult =
+                        new HashMap<String, BlockingFocusResultReceiver>(1);
+            }
+            focusReceiver = new BlockingFocusResultReceiver(clientId);
+            mFocusRequestsAwaitingResult.put(clientId, focusReceiver);
         }
-        return status;
+        focusReceiver.waitForResult(EXT_FOCUS_POLICY_TIMEOUT_MS);
+        if (DEBUG && !focusReceiver.receivedResult()) {
+            Log.e(TAG, "requestAudio response from ext policy timed out, denying request");
+        }
+        synchronized (mFocusRequestsLock) {
+            mFocusRequestsAwaitingResult.remove(clientId);
+        }
+        return focusReceiver.requestResult();
+    }
+
+    // helper class that abstracts out the handling of spurious wakeups in Object.wait()
+    private static final class SafeWaitObject {
+        private boolean mQuit = false;
+
+        public void safeNotify() {
+            synchronized (this) {
+                mQuit = true;
+                this.notify();
+            }
+        }
+
+        public void safeWait(long millis) throws InterruptedException {
+            final long timeOutTime = java.lang.System.currentTimeMillis() + millis;
+            synchronized (this) {
+                while (!mQuit) {
+                    final long timeToWait = timeOutTime - java.lang.System.currentTimeMillis();
+                    if (timeToWait < 0) { break; }
+                    this.wait(timeToWait);
+                }
+            }
+        }
+    }
+
+    private static final class BlockingFocusResultReceiver {
+        private final SafeWaitObject mLock = new SafeWaitObject();
+        @GuardedBy("mLock")
+        private boolean mResultReceived = false;
+        // request denied by default (e.g. timeout)
+        private int mFocusRequestResult = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
+        private final String mFocusClientId;
+
+        BlockingFocusResultReceiver(String clientId) {
+            mFocusClientId = clientId;
+        }
+
+        boolean receivedResult() { return mResultReceived; }
+        int requestResult() { return mFocusRequestResult; }
+
+        void notifyResult(int requestResult) {
+            synchronized (mLock) {
+                mResultReceived = true;
+                mFocusRequestResult = requestResult;
+                mLock.safeNotify();
+            }
+        }
+
+        public void waitForResult(long timeOutMs) {
+            synchronized (mLock) {
+                if (mResultReceived) {
+                    // the result was received before waiting
+                    return;
+                }
+                try {
+                    mLock.safeWait(timeOutMs);
+                } catch (InterruptedException e) { }
+            }
+        }
     }
 
     /**
@@ -2700,6 +2832,32 @@
 
     /**
      * @hide
+     * Set the result to the audio focus request received through
+     * {@link AudioPolicyFocusListener#onAudioFocusRequest(AudioFocusInfo, int)}.
+     * @param afi the information about the focus requester
+     * @param requestResult the result to the focus request to be passed to the requester
+     * @param ap a valid registered {@link AudioPolicy} configured as a focus policy.
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public void setFocusRequestResult(@NonNull AudioFocusInfo afi,
+            @FocusRequestResult int requestResult, @NonNull AudioPolicy ap) {
+        if (afi == null) {
+            throw new IllegalArgumentException("Illegal null AudioFocusInfo");
+        }
+        if (ap == null) {
+            throw new IllegalArgumentException("Illegal null AudioPolicy");
+        }
+        final IAudioService service = getService();
+        try {
+            service.setFocusRequestResultFromExtPolicy(afi, requestResult, ap.cb());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
      * Notifies an application with a focus listener of gain or loss of audio focus.
      * This method can only be used by owners of an {@link AudioPolicy} configured with
      * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to true.
@@ -2989,7 +3147,7 @@
         final IAudioService service = getService();
         try {
             String regId = service.registerAudioPolicy(policy.getConfig(), policy.cb(),
-                    policy.hasFocusListener(), policy.isFocusPolicy());
+                    policy.hasFocusListener(), policy.isFocusPolicy(), policy.isVolumeController());
             if (regId == null) {
                 return ERROR;
             } else {
@@ -3736,6 +3894,21 @@
     }
 
      /**
+     * Indicate Hearing Aid connection state change.
+     * @param device Bluetooth device connected/disconnected
+     * @param state new connection state (BluetoothProfile.STATE_xxx)
+     * {@hide}
+     */
+    public void setHearingAidDeviceConnectionState(BluetoothDevice device, int state) {
+        final IAudioService service = getService();
+        try {
+            service.setHearingAidDeviceConnectionState(device, state);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+     /**
      * Indicate A2DP source or sink connection state change.
      * @param device Bluetooth device connected/disconnected
      * @param state  new connection state (BluetoothProfile.STATE_xxx)
@@ -4423,8 +4596,7 @@
 
     private static boolean checkTypes(AudioDevicePort port) {
         return AudioDeviceInfo.convertInternalDeviceToDeviceType(port.type()) !=
-                    AudioDeviceInfo.TYPE_UNKNOWN &&
-                port.type() != AudioSystem.DEVICE_IN_BACK_MIC;
+                    AudioDeviceInfo.TYPE_UNKNOWN;
     }
 
     /**
@@ -4566,6 +4738,82 @@
         }
     }
 
+    /**
+     * Set port id for microphones by matching device type and address.
+     * @hide
+     */
+    public static void setPortIdForMicrophones(ArrayList<MicrophoneInfo> microphones) {
+        AudioDeviceInfo[] devices = getDevicesStatic(AudioManager.GET_DEVICES_INPUTS);
+        for (int i = microphones.size() - 1; i >= 0; i--) {
+            boolean foundPortId = false;
+            for (AudioDeviceInfo device : devices) {
+                if (device.getPort().type() == microphones.get(i).getInternalDeviceType()
+                        && TextUtils.equals(device.getAddress(), microphones.get(i).getAddress())) {
+                    microphones.get(i).setId(device.getId());
+                    foundPortId = true;
+                    break;
+                }
+            }
+            if (!foundPortId) {
+                Log.i(TAG, "Failed to find port id for device with type:"
+                        + microphones.get(i).getType() + " address:"
+                        + microphones.get(i).getAddress());
+                microphones.remove(i);
+            }
+        }
+    }
+
+    /**
+     * Convert {@link AudioDeviceInfo} to {@link MicrophoneInfo}.
+     * @hide
+     */
+    public static MicrophoneInfo microphoneInfoFromAudioDeviceInfo(AudioDeviceInfo deviceInfo) {
+        int deviceType = deviceInfo.getType();
+        int micLocation = (deviceType == AudioDeviceInfo.TYPE_BUILTIN_MIC
+                || deviceType == AudioDeviceInfo.TYPE_TELEPHONY) ? MicrophoneInfo.LOCATION_MAINBODY
+                : deviceType == AudioDeviceInfo.TYPE_UNKNOWN ? MicrophoneInfo.LOCATION_UNKNOWN
+                        : MicrophoneInfo.LOCATION_PERIPHERAL;
+        MicrophoneInfo microphone = new MicrophoneInfo(
+                deviceInfo.getPort().name() + deviceInfo.getId(),
+                deviceInfo.getPort().type(), deviceInfo.getAddress(), micLocation,
+                MicrophoneInfo.GROUP_UNKNOWN, MicrophoneInfo.INDEX_IN_THE_GROUP_UNKNOWN,
+                MicrophoneInfo.POSITION_UNKNOWN, MicrophoneInfo.ORIENTATION_UNKNOWN,
+                new ArrayList<Pair<Float, Float>>(), new ArrayList<Pair<Integer, Integer>>(),
+                MicrophoneInfo.SENSITIVITY_UNKNOWN, MicrophoneInfo.SPL_UNKNOWN,
+                MicrophoneInfo.SPL_UNKNOWN, MicrophoneInfo.DIRECTIONALITY_UNKNOWN);
+        microphone.setId(deviceInfo.getId());
+        return microphone;
+    }
+
+    /**
+     * Returns a list of {@link MicrophoneInfo} that corresponds to the characteristics
+     * of all available microphones. The list is empty when no microphones are available
+     * on the device. An error during the query will result in an IOException being thrown.
+     *
+     * @return a list that contains all microphones' characteristics
+     * @throws IOException if an error occurs.
+     */
+    public List<MicrophoneInfo> getMicrophones() throws IOException {
+        ArrayList<MicrophoneInfo> microphones = new ArrayList<MicrophoneInfo>();
+        int status = AudioSystem.getMicrophones(microphones);
+        if (status != AudioManager.SUCCESS) {
+            // fail and bail!
+            Log.e(TAG, "getMicrophones failed:" + status);
+            return new ArrayList<MicrophoneInfo>(); // Always return a list.
+        }
+        setPortIdForMicrophones(microphones);
+        AudioDeviceInfo[] devices = getDevicesStatic(GET_DEVICES_INPUTS);
+        for (AudioDeviceInfo device : devices) {
+            if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC ||
+                    device.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
+                continue;
+            }
+            MicrophoneInfo microphone = microphoneInfoFromAudioDeviceInfo(device);
+            microphones.add(microphone);
+        }
+        return microphones;
+    }
+
     // Since we need to calculate the changes since THE LAST NOTIFICATION, and not since the
     // (unpredictable) last time updateAudioPortCache() was called by someone, keep a list
     // of the ports that exist at the time of the last notification.
@@ -4645,6 +4893,114 @@
         }
     }
 
+
+    /**
+     * @hide
+     * Abstract class to receive event notification about audioserver process state.
+     */
+    @SystemApi
+    public abstract static class AudioServerStateCallback {
+        public void onAudioServerDown() { }
+        public void onAudioServerUp() { }
+    }
+
+    private Executor mAudioServerStateExec;
+    private AudioServerStateCallback mAudioServerStateCb;
+    private final Object mAudioServerStateCbLock = new Object();
+
+    private final IAudioServerStateDispatcher mAudioServerStateDispatcher =
+            new IAudioServerStateDispatcher.Stub() {
+        @Override
+        public void dispatchAudioServerStateChange(boolean state) {
+            Executor exec;
+            AudioServerStateCallback cb;
+
+            synchronized (mAudioServerStateCbLock) {
+                exec = mAudioServerStateExec;
+                cb = mAudioServerStateCb;
+            }
+
+            if ((exec == null) || (cb == null)) {
+                return;
+            }
+            if (state) {
+                exec.execute(() -> cb.onAudioServerUp());
+            } else {
+                exec.execute(() -> cb.onAudioServerDown());
+            }
+        }
+    };
+
+    /**
+     * @hide
+     * Registers a callback for notification of audio server state changes.
+     * @param executor {@link Executor} to handle the callbacks
+     * @param stateCallback the callback to receive the audio server state changes
+     *        To remove the callabck, pass a null reference for both executor and stateCallback.
+     */
+    @SystemApi
+    public void setAudioServerStateCallback(@NonNull Executor executor,
+            @NonNull AudioServerStateCallback stateCallback) {
+        if (stateCallback == null) {
+            throw new IllegalArgumentException("Illegal null AudioServerStateCallback");
+        }
+        if (executor == null) {
+            throw new IllegalArgumentException(
+                    "Illegal null Executor for the AudioServerStateCallback");
+        }
+
+        synchronized (mAudioServerStateCbLock) {
+            if (mAudioServerStateCb != null) {
+                throw new IllegalStateException(
+                    "setAudioServerStateCallback called with already registered callabck");
+            }
+            final IAudioService service = getService();
+            try {
+                service.registerAudioServerStateDispatcher(mAudioServerStateDispatcher);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            mAudioServerStateExec = executor;
+            mAudioServerStateCb = stateCallback;
+        }
+    }
+
+    /**
+     * @hide
+     * Unregisters the callback for notification of audio server state changes.
+     */
+    @SystemApi
+    public void clearAudioServerStateCallback() {
+        synchronized (mAudioServerStateCbLock) {
+            if (mAudioServerStateCb != null) {
+                final IAudioService service = getService();
+                try {
+                    service.unregisterAudioServerStateDispatcher(
+                            mAudioServerStateDispatcher);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            }
+            mAudioServerStateExec = null;
+            mAudioServerStateCb = null;
+        }
+    }
+
+    /**
+     * @hide
+     * Checks if native audioservice is running or not.
+     * @return true if native audioservice runs, false otherwise.
+     */
+    @SystemApi
+    public boolean isAudioServerRunning() {
+        final IAudioService service = getService();
+        try {
+            return service.isAudioServerRunning();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     //---------------------------------------------------------
     // Inner classes
     //--------------------
diff --git a/android/media/AudioManagerInternal.java b/android/media/AudioManagerInternal.java
index 0a1de33..98c2d7f 100644
--- a/android/media/AudioManagerInternal.java
+++ b/android/media/AudioManagerInternal.java
@@ -42,6 +42,8 @@
 
     public abstract void setRingerModeInternal(int ringerMode, String caller);
 
+    public abstract void silenceRingerModeInternal(String caller);
+
     public abstract void updateRingerModeAffectedStreamsInternal();
 
     public abstract void setAccessibilityServiceUids(IntArray uids);
diff --git a/android/media/AudioPresentation.java b/android/media/AudioPresentation.java
new file mode 100644
index 0000000..e39cb7d
--- /dev/null
+++ b/android/media/AudioPresentation.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2018 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.IntDef;
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+
+/**
+ * The AudioPresentation class encapsulates the information that describes an audio presentation
+ * which is available in next generation audio content.
+ *
+ * Used by {@link MediaExtractor} {@link MediaExtractor#getAudioPresentations(int)} and
+ * {@link AudioTrack} {@link AudioTrack#setPresentation(AudioPresentation)} to query available
+ * presentations and to select one.
+ *
+ * A list of available audio presentations in a media source can be queried using
+ * {@link MediaExtractor#getAudioPresentations(int)}. This list can be presented to a user for
+ * selection.
+ * An AudioPresentation can be passed to an offloaded audio decoder via
+ * {@link AudioTrack#setPresentation(AudioPresentation)} to request decoding of the selected
+ * presentation. An audio stream may contain multiple presentations that differ by language,
+ * accessibility, end point mastering and dialogue enhancement. An audio presentation may also have
+ * a set of description labels in different languages to help the user to make an informed
+ * selection.
+ */
+public final class AudioPresentation {
+    private final int mPresentationId;
+    private final int mProgramId;
+    private final Map<String, String> mLabels;
+    private final String mLanguage;
+
+    /** @hide */
+    @IntDef(
+        value = {
+            MASTERING_NOT_INDICATED,
+            MASTERED_FOR_STEREO,
+            MASTERED_FOR_SURROUND,
+            MASTERED_FOR_3D,
+            MASTERED_FOR_HEADPHONE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MasteringIndicationType {}
+
+    private final @MasteringIndicationType int mMasteringIndication;
+    private final boolean mAudioDescriptionAvailable;
+    private final boolean mSpokenSubtitlesAvailable;
+    private final boolean mDialogueEnhancementAvailable;
+
+    /**
+     * No preferred reproduction channel layout.
+     */
+    public static final int MASTERING_NOT_INDICATED         = 0;
+    /**
+     * Stereo speaker layout.
+     */
+    public static final int MASTERED_FOR_STEREO             = 1;
+    /**
+     * Two-dimensional (e.g. 5.1) speaker layout.
+     */
+    public static final int MASTERED_FOR_SURROUND           = 2;
+    /**
+     * Three-dimensional (e.g. 5.1.2) speaker layout.
+     */
+    public static final int MASTERED_FOR_3D                 = 3;
+    /**
+     * Prerendered for headphone playback.
+     */
+    public static final int MASTERED_FOR_HEADPHONE          = 4;
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    public AudioPresentation(int presentationId,
+                        int programId,
+                        @NonNull Map<String, String> labels,
+                        @NonNull String language,
+                        @MasteringIndicationType int masteringIndication,
+                        boolean audioDescriptionAvailable,
+                        boolean spokenSubtitlesAvailable,
+                        boolean dialogueEnhancementAvailable) {
+        this.mPresentationId = presentationId;
+        this.mProgramId = programId;
+        this.mLanguage = language;
+        this.mMasteringIndication = masteringIndication;
+        this.mAudioDescriptionAvailable = audioDescriptionAvailable;
+        this.mSpokenSubtitlesAvailable = spokenSubtitlesAvailable;
+        this.mDialogueEnhancementAvailable = dialogueEnhancementAvailable;
+
+        this.mLabels = new HashMap<String, String>(labels);
+    }
+
+    /**
+     * The framework uses this presentation id to select an audio presentation rendered by a
+     * decoder. Presentation id is typically sequential, but does not have to be.
+     * @hide
+     */
+    @VisibleForTesting
+    public int getPresentationId() {
+        return mPresentationId;
+    }
+
+    /**
+     * The framework uses this program id to select an audio presentation rendered by a decoder.
+     * Program id can be used to further uniquely identify the presentation to a decoder.
+     * @hide
+     */
+    @VisibleForTesting
+    public int getProgramId() {
+        return mProgramId;
+    }
+
+    /**
+     * @return a map of available text labels for this presentation. Each label is indexed by its
+     * locale corresponding to the language code as specified by ISO 639-2. Either ISO 639-2/B
+     * or ISO 639-2/T could be used.
+     */
+    public Map<Locale, String> getLabels() {
+        Map<Locale, String> localeLabels = new HashMap<>();
+        for (Map.Entry<String, String> entry : mLabels.entrySet()) {
+            localeLabels.put(new Locale(entry.getKey()), entry.getValue());
+        }
+        return localeLabels;
+    }
+
+    /**
+     * @return the locale corresponding to audio presentation's ISO 639-1/639-2 language code.
+     */
+    public Locale getLocale() {
+        return new Locale(mLanguage);
+    }
+
+    /**
+     * @return the mastering indication of the audio presentation.
+     * See {@link #MASTERING_NOT_INDICATED}, {@link #MASTERED_FOR_STEREO},
+     * {@link #MASTERED_FOR_SURROUND}, {@link #MASTERED_FOR_3D}, {@link #MASTERED_FOR_HEADPHONE}
+     */
+    @MasteringIndicationType
+    public int getMasteringIndication() {
+        return mMasteringIndication;
+    }
+
+    /**
+     * Indicates whether an audio description for the visually impaired is available.
+     * @return {@code true} if audio description is available.
+     */
+    public boolean hasAudioDescription() {
+        return mAudioDescriptionAvailable;
+    }
+
+    /**
+     * Indicates whether spoken subtitles for the visually impaired are available.
+     * @return {@code true} if spoken subtitles are available.
+     */
+    public boolean hasSpokenSubtitles() {
+        return mSpokenSubtitlesAvailable;
+    }
+
+    /**
+     * Indicates whether dialogue enhancement is available.
+     * @return {@code true} if dialogue enhancement is available.
+     */
+    public boolean hasDialogueEnhancement() {
+        return mDialogueEnhancementAvailable;
+    }
+}
diff --git a/android/media/AudioRecord.java b/android/media/AudioRecord.java
index 27784e9..4f0dccb 100644
--- a/android/media/AudioRecord.java
+++ b/android/media/AudioRecord.java
@@ -16,12 +16,15 @@
 
 package android.media;
 
+import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
 import java.nio.ByteBuffer;
 import java.util.Collection;
 import java.util.Iterator;
+import java.util.ArrayList;
+import java.util.List;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -32,10 +35,13 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.util.Pair;
 
 import com.android.internal.annotations.GuardedBy;
 
@@ -1314,6 +1320,23 @@
         return native_read_in_direct_buffer(audioBuffer, sizeInBytes, readMode == READ_BLOCKING);
     }
 
+    /**
+     *  Return Metrics data about the current AudioTrack instance.
+     *
+     * @return a {@link PersistableBundle} containing the set of attributes and values
+     * available for the media being handled by this instance of AudioRecord
+     * The attributes are descibed in {@link MetricsConstants}.
+     *
+     * Additional vendor-specific fields may also be present in
+     * the return value.
+     */
+    public PersistableBundle getMetrics() {
+        PersistableBundle bundle = native_getMetrics();
+        return bundle;
+    }
+
+    private native PersistableBundle native_getMetrics();
+
     //--------------------------------------------------------------------------
     // Initialization / configuration
     //--------------------
@@ -1394,6 +1417,7 @@
     /*
      * Call BEFORE adding a routing callback handler.
      */
+    @GuardedBy("mRoutingChangeListeners")
     private void testEnableNativeRoutingCallbacksLocked() {
         if (mRoutingChangeListeners.size() == 0) {
             native_enableDeviceCallback();
@@ -1403,6 +1427,7 @@
     /*
      * Call AFTER removing a routing callback handler.
      */
+    @GuardedBy("mRoutingChangeListeners")
     private void testDisableNativeRoutingCallbacksLocked() {
         if (mRoutingChangeListeners.size() == 0) {
             native_disableDeviceCallback();
@@ -1583,6 +1608,46 @@
         }
     }
 
+    //--------------------------------------------------------------------------
+    // Microphone information
+    //--------------------
+    /**
+     * Returns a lists of {@link MicrophoneInfo} representing the active microphones.
+     * By querying channel mapping for each active microphone, developer can know how
+     * the microphone is used by each channels or a capture stream.
+     * Note that the information about the active microphones may change during a recording.
+     * See {@link AudioManager#registerAudioDeviceCallback} to be notified of changes
+     * in the audio devices, querying the active microphones then will return the latest
+     * information.
+     *
+     * @return a lists of {@link MicrophoneInfo} representing the active microphones.
+     * @throws IOException if an error occurs
+     */
+    public List<MicrophoneInfo> getActiveMicrophones() throws IOException {
+        ArrayList<MicrophoneInfo> activeMicrophones = new ArrayList<>();
+        int status = native_get_active_microphones(activeMicrophones);
+        if (status != AudioManager.SUCCESS) {
+            Log.e(TAG, "getActiveMicrophones failed:" + status);
+            return new ArrayList<MicrophoneInfo>();
+        }
+        AudioManager.setPortIdForMicrophones(activeMicrophones);
+
+        // Use routed device when there is not information returned by hal.
+        if (activeMicrophones.size() == 0) {
+            AudioDeviceInfo device = getRoutedDevice();
+            if (device != null) {
+                MicrophoneInfo microphone = AudioManager.microphoneInfoFromAudioDeviceInfo(device);
+                ArrayList<Pair<Integer, Integer>> channelMapping = new ArrayList<>();
+                for (int i = 0; i < mChannelCount; i++) {
+                    channelMapping.add(new Pair(i, MicrophoneInfo.CHANNEL_MAPPING_DIRECT));
+                }
+                microphone.setChannelMapping(channelMapping);
+                activeMicrophones.add(microphone);
+            }
+        }
+        return activeMicrophones;
+    }
+
     //---------------------------------------------------------
     // Interface definitions
     //--------------------
@@ -1728,6 +1793,9 @@
     private native final int native_get_timestamp(@NonNull AudioTimestamp outTimestamp,
             @AudioTimestamp.Timebase int timebase);
 
+    private native final int native_get_active_microphones(
+            ArrayList<MicrophoneInfo> activeMicrophones);
+
     //---------------------------------------------------------
     // Utility methods
     //------------------
@@ -1739,4 +1807,46 @@
     private static void loge(String msg) {
         Log.e(TAG, msg);
     }
+
+    public static final class MetricsConstants
+    {
+        private MetricsConstants() {}
+
+        /**
+         * Key to extract the output format being recorded
+         * from the {@link AudioRecord#getMetrics} return value.
+         * The value is a String.
+         */
+        public static final String ENCODING = "android.media.audiorecord.encoding";
+
+        /**
+         * Key to extract the Source Type for this track
+         * from the {@link AudioRecord#getMetrics} return value.
+         * The value is a String.
+         */
+        public static final String SOURCE = "android.media.audiorecord.source";
+
+        /**
+         * Key to extract the estimated latency through the recording pipeline
+         * from the {@link AudioRecord#getMetrics} return value.
+         * This is in units of milliseconds.
+         * The value is an integer.
+         */
+        public static final String LATENCY = "android.media.audiorecord.latency";
+
+        /**
+         * Key to extract the sink sample rate for this record track in Hz
+         * from the {@link AudioRecord#getMetrics} return value.
+         * The value is an integer.
+         */
+        public static final String SAMPLERATE = "android.media.audiorecord.samplerate";
+
+        /**
+         * Key to extract the number of channels being recorded in this record track
+         * from the {@link AudioRecord#getMetrics} return value.
+         * The value is an integer.
+         */
+        public static final String CHANNELS = "android.media.audiorecord.channels";
+
+    }
 }
diff --git a/android/media/AudioSystem.java b/android/media/AudioSystem.java
index dcd37da..aaba1e3 100644
--- a/android/media/AudioSystem.java
+++ b/android/media/AudioSystem.java
@@ -401,6 +401,7 @@
     public static final int DEVICE_OUT_BUS = 0x1000000;
     public static final int DEVICE_OUT_PROXY = 0x2000000;
     public static final int DEVICE_OUT_USB_HEADSET = 0x4000000;
+    public static final int DEVICE_OUT_HEARING_AID = 0x8000000;
 
     public static final int DEVICE_OUT_DEFAULT = DEVICE_BIT_DEFAULT;
 
@@ -431,6 +432,7 @@
                                               DEVICE_OUT_BUS |
                                               DEVICE_OUT_PROXY |
                                               DEVICE_OUT_USB_HEADSET |
+                                              DEVICE_OUT_HEARING_AID |
                                               DEVICE_OUT_DEFAULT);
     public static final int DEVICE_OUT_ALL_A2DP = (DEVICE_OUT_BLUETOOTH_A2DP |
                                                    DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES |
@@ -546,6 +548,7 @@
     public static final String DEVICE_OUT_BUS_NAME = "bus";
     public static final String DEVICE_OUT_PROXY_NAME = "proxy";
     public static final String DEVICE_OUT_USB_HEADSET_NAME = "usb_headset";
+    public static final String DEVICE_OUT_HEARING_AID_NAME = "hearing_aid_out";
 
     public static final String DEVICE_IN_COMMUNICATION_NAME = "communication";
     public static final String DEVICE_IN_AMBIENT_NAME = "ambient";
@@ -628,6 +631,8 @@
             return DEVICE_OUT_PROXY_NAME;
         case DEVICE_OUT_USB_HEADSET:
             return DEVICE_OUT_USB_HEADSET_NAME;
+        case DEVICE_OUT_HEARING_AID:
+            return DEVICE_OUT_HEARING_AID_NAME;
         case DEVICE_OUT_DEFAULT:
         default:
             return Integer.toString(device);
@@ -742,7 +747,8 @@
     public static final int FOR_SYSTEM = 4;
     public static final int FOR_HDMI_SYSTEM_AUDIO = 5;
     public static final int FOR_ENCODED_SURROUND = 6;
-    private static final int NUM_FORCE_USE = 7;
+    public static final int FOR_VIBRATE_RINGING = 7;
+    private static final int NUM_FORCE_USE = 8;
 
     public static String forceUseUsageToString(int usage) {
         switch (usage) {
@@ -753,6 +759,7 @@
             case FOR_SYSTEM: return "FOR_SYSTEM";
             case FOR_HDMI_SYSTEM_AUDIO: return "FOR_HDMI_SYSTEM_AUDIO";
             case FOR_ENCODED_SURROUND: return "FOR_ENCODED_SURROUND";
+            case FOR_VIBRATE_RINGING: return "FOR_VIBRATE_RINGING";
             default: return "unknown usage (" + usage + ")" ;
         }
     }
@@ -827,6 +834,8 @@
     private static native boolean native_is_offload_supported(int encoding, int sampleRate,
             int channelMask, int channelIndexMask);
 
+    public static native int getMicrophones(ArrayList<MicrophoneInfo> microphonesInfo);
+
     // Items shared with audio service
 
     /**
diff --git a/android/media/AudioTrack.java b/android/media/AudioTrack.java
index 5928d03..87b5d43 100644
--- a/android/media/AudioTrack.java
+++ b/android/media/AudioTrack.java
@@ -36,6 +36,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
+import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
@@ -895,6 +896,7 @@
         }
 
         /**
+         * @hide
          * Sets whether this track will play through the offloaded audio path.
          * When set to true, at build time, the audio format will be checked against
          * {@link AudioManager#isOffloadedPlaybackSupported(AudioFormat)} to verify the audio format
@@ -1718,6 +1720,23 @@
          return ret;
      }
 
+    /**
+     *  Return Metrics data about the current AudioTrack instance.
+     *
+     * @return a {@link PersistableBundle} containing the set of attributes and values
+     * available for the media being handled by this instance of AudioTrack
+     * The attributes are descibed in {@link MetricsConstants}.
+     *
+     * Additional vendor-specific fields may also be present in
+     * the return value.
+     */
+    public PersistableBundle getMetrics() {
+        PersistableBundle bundle = native_getMetrics();
+        return bundle;
+    }
+
+    private native PersistableBundle native_getMetrics();
+
     //--------------------------------------------------------------------------
     // Initialization / configuration
     //--------------------
@@ -1990,6 +2009,26 @@
     }
 
     /**
+     * Sets the audio presentation.
+     * If the audio presentation is invalid then {@link #ERROR_BAD_VALUE} will be returned.
+     * If a multi-stream decoder (MSD) is not present, or the format does not support
+     * multiple presentations, then {@link #ERROR_INVALID_OPERATION} will be returned.
+     * {@link #ERROR} is returned in case of any other error.
+     * @param presentation see {@link AudioPresentation}. In particular, id should be set.
+     * @return error code or success, see {@link #SUCCESS}, {@link #ERROR},
+     *    {@link #ERROR_BAD_VALUE}, {@link #ERROR_INVALID_OPERATION}
+     * @throws IllegalArgumentException if the audio presentation is null.
+     * @throws IllegalStateException if track is not initialized.
+     */
+    public int setPresentation(@NonNull AudioPresentation presentation) {
+        if (presentation == null) {
+            throw new IllegalArgumentException("audio presentation is null");
+        }
+        return native_setPresentation(presentation.getPresentationId(),
+                presentation.getProgramId());
+    }
+
+    /**
      * Sets the initialization state of the instance. This method was originally intended to be used
      * in an AudioTrack subclass constructor to set a subclass-specific post-initialization state.
      * However, subclasses of AudioTrack are no longer recommended, so this method is obsolete.
@@ -2784,6 +2823,7 @@
     /*
      * Call BEFORE adding a routing callback handler.
      */
+    @GuardedBy("mRoutingChangeListeners")
     private void testEnableNativeRoutingCallbacksLocked() {
         if (mRoutingChangeListeners.size() == 0) {
             native_enableDeviceCallback();
@@ -2793,6 +2833,7 @@
     /*
      * Call AFTER removing a routing callback handler.
      */
+    @GuardedBy("mRoutingChangeListeners")
     private void testDisableNativeRoutingCallbacksLocked() {
         if (mRoutingChangeListeners.size() == 0) {
             native_disableDeviceCallback();
@@ -2939,6 +2980,7 @@
     }
 
     /**
+     * @hide
      * Abstract class to receive event notification about the stream playback.
      * See {@link AudioTrack#setStreamEventCallback(Executor, StreamEventCallback)} to register
      * the callback on the given {@link AudioTrack} instance.
@@ -2972,6 +3014,7 @@
     private final Object mStreamEventCbLock = new Object();
 
     /**
+     * @hide
      * Sets the callback for the notification of stream events.
      * @param executor {@link Executor} to handle the callbacks
      * @param eventCallback the callback to receive the stream event notifications
@@ -2991,6 +3034,7 @@
     }
 
     /**
+     * @hide
      * Unregisters the callback for notification of stream events, previously set
      * by {@link #setStreamEventCallback(Executor, StreamEventCallback)}.
      */
@@ -3227,6 +3271,7 @@
             @NonNull VolumeShaper.Operation operation);
 
     private native @Nullable VolumeShaper.State native_getVolumeShaperState(int id);
+    private native final int native_setPresentation(int presentationId, int programId);
 
     //---------------------------------------------------------
     // Utility methods
@@ -3239,4 +3284,46 @@
     private static void loge(String msg) {
         Log.e(TAG, msg);
     }
+
+    public final static class MetricsConstants
+    {
+        private MetricsConstants() {}
+
+        /**
+         * Key to extract the Stream Type for this track
+         * from the {@link AudioTrack#getMetrics} return value.
+         * The value is a String.
+         */
+        public static final String STREAMTYPE = "android.media.audiotrack.streamtype";
+
+        /**
+         * Key to extract the Content Type for this track
+         * from the {@link AudioTrack#getMetrics} return value.
+         * The value is a String.
+         */
+        public static final String CONTENTTYPE = "android.media.audiotrack.type";
+
+        /**
+         * Key to extract the Content Type for this track
+         * from the {@link AudioTrack#getMetrics} return value.
+         * The value is a String.
+         */
+        public static final String USAGE = "android.media.audiotrack.usage";
+
+        /**
+         * Key to extract the sample rate for this track in Hz
+         * from the {@link AudioTrack#getMetrics} return value.
+         * The value is an integer.
+         */
+        public static final String SAMPLERATE = "android.media.audiorecord.samplerate";
+
+        /**
+         * Key to extract the channel mask information for this track
+         * from the {@link AudioTrack#getMetrics} return value.
+         *
+         * The value is a Long integer.
+         */
+        public static final String CHANNELMASK = "android.media.audiorecord.channelmask";
+
+    }
 }
diff --git a/android/media/ClosedCaptionRenderer.java b/android/media/ClosedCaptionRenderer.java
index cc7722a..66759e5 100644
--- a/android/media/ClosedCaptionRenderer.java
+++ b/android/media/ClosedCaptionRenderer.java
@@ -59,7 +59,7 @@
     public boolean supports(MediaFormat format) {
         if (format.containsKey(MediaFormat.KEY_MIME)) {
             String mimeType = format.getString(MediaFormat.KEY_MIME);
-            return MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608.equals(mimeType);
+            return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType);
         }
         return false;
     }
@@ -67,7 +67,7 @@
     @Override
     public SubtitleTrack createTrack(MediaFormat format) {
         String mimeType = format.getString(MediaFormat.KEY_MIME);
-        if (MediaPlayer.MEDIA_MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
+        if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
             if (mCCWidget == null) {
                 mCCWidget = new Cea608CCWidget(mContext);
             }
diff --git a/android/media/DataSourceDesc.java b/android/media/DataSourceDesc.java
index 73fad7a..a53fa11 100644
--- a/android/media/DataSourceDesc.java
+++ b/android/media/DataSourceDesc.java
@@ -30,6 +30,8 @@
 import java.io.FileDescriptor;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.net.CookieHandler;
+import java.net.CookieManager;
 import java.net.HttpCookie;
 
 import java.util.ArrayList;
@@ -38,6 +40,7 @@
 import java.util.Map;
 
 /**
+ * @hide
  * Structure for data source descriptor.
  *
  * Used by {@link MediaPlayer2#setDataSource(DataSourceDesc)}
@@ -72,7 +75,7 @@
     private List<HttpCookie> mUriCookies;
     private Context mUriContext;
 
-    private long mId = 0;
+    private String mMediaId;
     private long mStartPositionMs = 0;
     private long mEndPositionMs = LONG_MAX;
 
@@ -80,11 +83,11 @@
     }
 
     /**
-     * Return the Id of data source.
-     * @return the Id of data source
+     * Return the media Id of data source.
+     * @return the media Id of data source
      */
-    public long getId() {
-        return mId;
+    public String getMediaId() {
+        return mMediaId;
     }
 
     /**
@@ -220,7 +223,7 @@
         private List<HttpCookie> mUriCookies;
         private Context mUriContext;
 
-        private long mId = 0;
+        private String mMediaId;
         private long mStartPositionMs = 0;
         private long mEndPositionMs = LONG_MAX;
 
@@ -246,7 +249,7 @@
             mUriCookies = dsd.mUriCookies;
             mUriContext = dsd.mUriContext;
 
-            mId = dsd.mId;
+            mMediaId = dsd.mMediaId;
             mStartPositionMs = dsd.mStartPositionMs;
             mEndPositionMs = dsd.mEndPositionMs;
         }
@@ -280,7 +283,7 @@
             dsd.mUriCookies = mUriCookies;
             dsd.mUriContext = mUriContext;
 
-            dsd.mId = mId;
+            dsd.mMediaId = mMediaId;
             dsd.mStartPositionMs = mStartPositionMs;
             dsd.mEndPositionMs = mEndPositionMs;
 
@@ -288,13 +291,13 @@
         }
 
         /**
-         * Sets the Id of this data source.
+         * Sets the media Id of this data source.
          *
-         * @param id the Id of this data source
+         * @param mediaId the media Id of this data source
          * @return the same Builder instance.
          */
-        public Builder setId(long id) {
-            mId = id;
+        public Builder setMediaId(String mediaId) {
+            mMediaId = mediaId;
             return this;
         }
 
@@ -433,10 +436,22 @@
          * @param cookies the cookies to be sent together with the request
          * @return the same Builder instance.
          * @throws NullPointerException if context or uri is null.
+         * @throws IllegalArgumentException if the cookie handler is not of CookieManager type
+         *                                  when cookies are provided.
          */
         public Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
                 @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
+            Preconditions.checkNotNull(context, "context cannot be null");
             Preconditions.checkNotNull(uri);
+            if (cookies != null) {
+                CookieHandler cookieHandler = CookieHandler.getDefault();
+                if (cookieHandler != null && !(cookieHandler instanceof CookieManager)) {
+                    throw new IllegalArgumentException(
+                            "The cookie handler has to be of CookieManager type "
+                            + "when cookies are provided.");
+                }
+            }
+
             resetDataSource();
             mType = TYPE_URI;
             mUri = uri;
diff --git a/android/media/ExifInterface.java b/android/media/ExifInterface.java
index 9175416..bc0e43b 100644
--- a/android/media/ExifInterface.java
+++ b/android/media/ExifInterface.java
@@ -3226,9 +3226,18 @@
 
         if (stripOffsetsAttribute != null && stripByteCountsAttribute != null) {
             long[] stripOffsets =
-                    (long[]) stripOffsetsAttribute.getValue(mExifByteOrder);
+                    convertToLongArray(stripOffsetsAttribute.getValue(mExifByteOrder));
             long[] stripByteCounts =
-                    (long[]) stripByteCountsAttribute.getValue(mExifByteOrder);
+                    convertToLongArray(stripByteCountsAttribute.getValue(mExifByteOrder));
+
+            if (stripOffsets == null) {
+                Log.w(TAG, "stripOffsets should not be null.");
+                return;
+            }
+            if (stripByteCounts == null) {
+                Log.w(TAG, "stripByteCounts should not be null.");
+                return;
+            }
 
             // Set thumbnail byte array data for non-consecutive strip bytes
             byte[] totalStripBytes =
@@ -4025,4 +4034,22 @@
         }
         return false;
     }
+
+    /**
+     * Convert given int[] to long[]. If long[] is given, just return it.
+     * Return null for other types of input.
+     */
+    private static long[] convertToLongArray(Object inputObj) {
+        if (inputObj instanceof int[]) {
+            int[] input = (int[]) inputObj;
+            long[] result = new long[input.length];
+            for (int i = 0; i < input.length; i++) {
+                result[i] = input[i];
+            }
+            return result;
+        } else if (inputObj instanceof long[]) {
+            return (long[]) inputObj;
+        }
+        return null;
+    }
 }
diff --git a/android/media/Image.java b/android/media/Image.java
index fbe5561..37c5785 100644
--- a/android/media/Image.java
+++ b/android/media/Image.java
@@ -19,7 +19,9 @@
 import java.nio.ByteBuffer;
 import java.lang.AutoCloseable;
 
+import android.annotation.Nullable;
 import android.graphics.Rect;
+import android.hardware.HardwareBuffer;
 
 /**
  * <p>A single complete image buffer to use with a media source such as a
@@ -184,6 +186,30 @@
     public abstract long getTimestamp();
 
     /**
+     * Get the transformation associated with this frame.
+     * @return The window transformation that needs to be applied for this frame.
+     * @hide
+     */
+    public abstract int getTransform();
+
+    /**
+     * Get the {@link android.hardware.HardwareBuffer HardwareBuffer} handle of the input image
+     * intended for GPU and/or hardware access.
+     * <p>
+     * The returned {@link android.hardware.HardwareBuffer HardwareBuffer} shall not be used
+     * after  {@link Image#close Image.close()} has been called.
+     * </p>
+     * @return the HardwareBuffer associated with this Image or null if this Image doesn't support
+     * this feature (e.g. {@link android.media.ImageWriter ImageWriter} or
+     * {@link android.media.MediaCodec MediaCodec} don't).
+     */
+    @Nullable
+    public HardwareBuffer getHardwareBuffer() {
+        throwISEIfImageIsInvalid();
+        return null;
+    }
+
+    /**
      * Set the timestamp associated with this frame.
      * <p>
      * The timestamp is measured in nanoseconds, and is normally monotonically
diff --git a/android/media/ImageReader.java b/android/media/ImageReader.java
index 1019580..72d52d3 100644
--- a/android/media/ImageReader.java
+++ b/android/media/ImageReader.java
@@ -727,18 +727,7 @@
             return false;
         }
 
-        if (format == ImageFormat.PRIVATE) {
-            // Usage need to be either USAGE0_GPU_SAMPLED_IMAGE or USAGE0_VIDEO_ENCODE or combined.
-            boolean isAllowed = (usage == HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
-            isAllowed = isAllowed || (usage == HardwareBuffer.USAGE_VIDEO_ENCODE);
-            isAllowed = isAllowed || (usage ==
-                    (HardwareBuffer.USAGE_VIDEO_ENCODE | HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE));
-            return isAllowed;
-        } else {
-            // Usage need to make the buffer CPU readable for explicit format.
-            return ((usage == HardwareBuffer.USAGE_CPU_READ_RARELY) ||
-                    (usage == HardwareBuffer.USAGE_CPU_READ_OFTEN));
-        }
+        return true;
     }
 
     /**
@@ -876,6 +865,18 @@
         }
 
         @Override
+        public int getTransform() {
+            throwISEIfImageIsInvalid();
+            return mTransform;
+        }
+
+        @Override
+        public HardwareBuffer getHardwareBuffer() {
+            throwISEIfImageIsInvalid();
+            return nativeGetHardwareBuffer();
+        }
+
+        @Override
         public void setTimestamp(long timestampNs) {
             throwISEIfImageIsInvalid();
             mTimestamp = timestampNs;
@@ -1007,6 +1008,11 @@
          */
         private long mTimestamp;
 
+        /**
+         * This field is set by native code during nativeImageSetup().
+         */
+        private int mTransform;
+
         private SurfacePlane[] mPlanes;
         private int mFormat = ImageFormat.UNKNOWN;
         // If this image is detached from the ImageReader.
@@ -1017,6 +1023,7 @@
         private synchronized native int nativeGetWidth();
         private synchronized native int nativeGetHeight();
         private synchronized native int nativeGetFormat(int readerFormat);
+        private synchronized native HardwareBuffer nativeGetHardwareBuffer();
     }
 
     private synchronized native void nativeInit(Object weakSelf, int w, int h,
diff --git a/android/media/ImageWriter.java b/android/media/ImageWriter.java
index 2b7309f..8ee27ae 100644
--- a/android/media/ImageWriter.java
+++ b/android/media/ImageWriter.java
@@ -371,7 +371,7 @@
 
         Rect crop = image.getCropRect();
         nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), crop.left, crop.top,
-                crop.right, crop.bottom);
+                crop.right, crop.bottom, image.getTransform());
 
         /**
          * Only remove and cleanup the Images that are owned by this
@@ -557,7 +557,8 @@
         // buffer caused leak.
         Rect crop = image.getCropRect();
         nativeAttachAndQueueImage(mNativeContext, image.getNativeContext(), image.getFormat(),
-                image.getTimestamp(), crop.left, crop.top, crop.right, crop.bottom);
+                image.getTimestamp(), crop.left, crop.top, crop.right, crop.bottom,
+                image.getTransform());
     }
 
     /**
@@ -674,6 +675,8 @@
         private final long DEFAULT_TIMESTAMP = Long.MIN_VALUE;
         private long mTimestamp = DEFAULT_TIMESTAMP;
 
+        private int mTransform = 0; //Default no transform
+
         public WriterSurfaceImage(ImageWriter writer) {
             mOwner = writer;
         }
@@ -711,6 +714,13 @@
         }
 
         @Override
+        public int getTransform() {
+            throwISEIfImageIsInvalid();
+
+            return mTransform;
+        }
+
+        @Override
         public long getTimestamp() {
             throwISEIfImageIsInvalid();
 
@@ -856,11 +866,11 @@
     private synchronized native void nativeDequeueInputImage(long nativeCtx, Image wi);
 
     private synchronized native void nativeQueueInputImage(long nativeCtx, Image image,
-            long timestampNs, int left, int top, int right, int bottom);
+            long timestampNs, int left, int top, int right, int bottom, int transform);
 
     private synchronized native int nativeAttachAndQueueImage(long nativeCtx,
             long imageNativeBuffer, int imageFormat, long timestampNs, int left,
-            int top, int right, int bottom);
+            int top, int right, int bottom, int transform);
 
     private synchronized native void cancelImage(long nativeCtx, Image image);
 
diff --git a/android/media/Media2DataSource.java b/android/media/Media2DataSource.java
index 8ee4a70..08df632 100644
--- a/android/media/Media2DataSource.java
+++ b/android/media/Media2DataSource.java
@@ -21,6 +21,7 @@
 import java.io.IOException;
 
 /**
+ * @hide
  * For supplying media data to the framework. Implement this if your app has
  * special requirements for the way media data is obtained.
  *
diff --git a/android/media/MediaActionSound.java b/android/media/MediaActionSound.java
index 983ca75..dcd4dce 100644
--- a/android/media/MediaActionSound.java
+++ b/android/media/MediaActionSound.java
@@ -47,11 +47,16 @@
     private SoundPool mSoundPool;
     private SoundState[] mSounds;
 
+    private static final String[] SOUND_DIRS = {
+        "/product/media/audio/ui/",
+        "/system/media/audio/ui/",
+    };
+
     private static final String[] SOUND_FILES = {
-        "/system/media/audio/ui/camera_click.ogg",
-        "/system/media/audio/ui/camera_focus.ogg",
-        "/system/media/audio/ui/VideoRecord.ogg",
-        "/system/media/audio/ui/VideoStop.ogg"
+        "camera_click.ogg",
+        "camera_focus.ogg",
+        "VideoRecord.ogg",
+        "VideoStop.ogg"
     };
 
     private static final String TAG = "MediaActionSound";
@@ -132,12 +137,16 @@
     }
 
     private int loadSound(SoundState sound) {
-        int id = mSoundPool.load(SOUND_FILES[sound.name], 1);
-        if (id > 0) {
-            sound.state = STATE_LOADING;
-            sound.id = id;
+        final String soundFileName = SOUND_FILES[sound.name];
+        for (String soundDir : SOUND_DIRS) {
+            int id = mSoundPool.load(soundDir + soundFileName, 1);
+            if (id > 0) {
+                sound.state = STATE_LOADING;
+                sound.id = id;
+                return id;
+            }
         }
-        return id;
+        return 0;
     }
 
     /**
diff --git a/android/media/MediaBrowser2.java b/android/media/MediaBrowser2.java
index be4be3f..452371a 100644
--- a/android/media/MediaBrowser2.java
+++ b/android/media/MediaBrowser2.java
@@ -16,9 +16,12 @@
 
 package android.media;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
+import android.media.MediaLibraryService2.MediaLibrarySession;
+import android.media.MediaSession2.ControllerInfo;
 import android.media.update.ApiLoader;
 import android.media.update.MediaBrowser2Provider;
 import android.os.Bundle;
@@ -27,8 +30,8 @@
 import java.util.concurrent.Executor;
 
 /**
- * Browses media content offered by a {@link MediaLibraryService2}.
  * @hide
+ * Browses media content offered by a {@link MediaLibraryService2}.
  */
 public class MediaBrowser2 extends MediaController2 {
     // Equals to the ((MediaBrowser2Provider) getProvider())
@@ -39,138 +42,197 @@
      */
     public static class BrowserCallback extends MediaController2.ControllerCallback {
         /**
-         * Called with the result of {@link #getBrowserRoot(Bundle)}.
+         * Called with the result of {@link #getLibraryRoot(Bundle)}.
          * <p>
-         * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the browser root isn't
+         * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the library root isn't
          * available.
          *
+         * @param browser the browser for this event
          * @param rootHints rootHints that you previously requested.
-         * @param rootMediaId media id of the browser root. Can be {@code null}
-         * @param rootExtra extra of the browser root. Can be {@code null}
+         * @param rootMediaId media id of the library root. Can be {@code null}
+         * @param rootExtra extra of the library root. Can be {@code null}
          */
-        public void onGetRootResult(Bundle rootHints, @Nullable String rootMediaId,
-                @Nullable Bundle rootExtra) { }
+        public void onGetLibraryRootDone(@NonNull MediaBrowser2 browser, @Nullable Bundle rootHints,
+                @Nullable String rootMediaId, @Nullable Bundle rootExtra) { }
 
         /**
-         * Called when the item has been returned by the library service for the previous
-         * {@link MediaBrowser2#getItem} call.
+         * Called when there's change in the parent's children.
          * <p>
-         * Result can be null if there had been error.
+         * This API is called when the library service called
+         * {@link MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, Bundle)} or
+         * {@link MediaLibrarySession#notifyChildrenChanged(String, int, Bundle)} for the parent.
          *
-         * @param mediaId media id
-         * @param result result. Can be {@code null}
+         * @param browser the browser for this event
+         * @param parentId parent id that you've specified with {@link #subscribe(String, Bundle)}
+         * @param itemCount number of children
+         * @param extras extra bundle from the library service. Can be differ from extras that
+         *               you've specified with {@link #subscribe(String, Bundle)}.
          */
-        public void onItemLoaded(@NonNull String mediaId, @Nullable MediaItem2 result) { }
+        public void onChildrenChanged(@NonNull MediaBrowser2 browser, @NonNull String parentId,
+                int itemCount, @Nullable Bundle extras) { }
 
         /**
          * Called when the list of items has been returned by the library service for the previous
          * {@link MediaBrowser2#getChildren(String, int, int, Bundle)}.
          *
+         * @param browser the browser for this event
          * @param parentId parent id
-         * @param page page number that you've specified
-         * @param pageSize page size that you've specified
-         * @param options optional bundle that you've specified
+         * @param page page number that you've specified with
+         *             {@link #getChildren(String, int, int, Bundle)}
+         * @param pageSize page size that you've specified with
+         *                 {@link #getChildren(String, int, int, Bundle)}
          * @param result result. Can be {@code null}
+         * @param extras extra bundle from the library service
          */
-        public void onChildrenLoaded(@NonNull String parentId, int page, int pageSize,
-                @Nullable Bundle options, @Nullable List<MediaItem2> result) { }
+        public void onGetChildrenDone(@NonNull MediaBrowser2 browser, @NonNull String parentId,
+                int page, int pageSize, @Nullable List<MediaItem2> result,
+                @Nullable Bundle extras) { }
 
         /**
-         * Called when there's change in the parent's children.
-         *
-         * @param parentId parent id that you've specified with subscribe
-         * @param options optional bundle that you've specified with subscribe
-         */
-        public void onChildrenChanged(@NonNull String parentId, @Nullable Bundle options) { }
-
-        /**
-         * Called when the search result has been returned by the library service for the previous
-         * {@link MediaBrowser2#search(String, int, int, Bundle)}.
+         * Called when the item has been returned by the library service for the previous
+         * {@link MediaBrowser2#getItem(String)} call.
          * <p>
          * Result can be null if there had been error.
          *
-         * @param query query string that you've specified
-         * @param page page number that you've specified
-         * @param pageSize page size that you've specified
-         * @param options optional bundle that you've specified
+         * @param browser the browser for this event
+         * @param mediaId media id
          * @param result result. Can be {@code null}
          */
-        public void onSearchResult(@NonNull String query, int page, int pageSize,
-                @Nullable Bundle options, @Nullable List<MediaItem2> result) { }
+        public void onGetItemDone(@NonNull MediaBrowser2 browser, @NonNull String mediaId,
+                @Nullable MediaItem2 result) { }
+
+        /**
+         * Called when there's change in the search result requested by the previous
+         * {@link MediaBrowser2#search(String, Bundle)}.
+         *
+         * @param browser the browser for this event
+         * @param query search query that you've specified with {@link #search(String, Bundle)}
+         * @param itemCount The item count for the search result
+         * @param extras extra bundle from the library service
+         */
+        public void onSearchResultChanged(@NonNull MediaBrowser2 browser, @NonNull String query,
+                int itemCount, @Nullable Bundle extras) { }
+
+        /**
+         * Called when the search result has been returned by the library service for the previous
+         * {@link MediaBrowser2#getSearchResult(String, int, int, Bundle)}.
+         * <p>
+         * Result can be null if there had been error.
+         *
+         * @param browser the browser for this event
+         * @param query search query that you've specified with
+         *              {@link #getSearchResult(String, int, int, Bundle)}
+         * @param page page number that you've specified with
+         *             {@link #getSearchResult(String, int, int, Bundle)}
+         * @param pageSize page size that you've specified with
+         *                 {@link #getSearchResult(String, int, int, Bundle)}
+         * @param result result. Can be {@code null}.
+         * @param extras extra bundle from the library service
+         */
+        public void onGetSearchResultDone(@NonNull MediaBrowser2 browser, @NonNull String query,
+                int page, int pageSize, @Nullable List<MediaItem2> result,
+                @Nullable Bundle extras) { }
     }
 
-    public MediaBrowser2(Context context, SessionToken2 token, BrowserCallback callback,
-            Executor executor) {
-        super(context, token, callback, executor);
+    public MediaBrowser2(@NonNull Context context, @NonNull SessionToken2 token,
+            @NonNull @CallbackExecutor Executor executor, @NonNull BrowserCallback callback) {
+        super(context, token, executor, callback);
         mProvider = (MediaBrowser2Provider) getProvider();
     }
 
     @Override
     MediaBrowser2Provider createProvider(Context context, SessionToken2 token,
-            ControllerCallback callback, Executor executor) {
-        return ApiLoader.getProvider(context)
-                .createMediaBrowser2(this, context, token, (BrowserCallback) callback, executor);
+            Executor executor, ControllerCallback callback) {
+        return ApiLoader.getProvider().createMediaBrowser2(
+                context, this, token, executor, (BrowserCallback) callback);
     }
 
-    public void getBrowserRoot(Bundle rootHints) {
-        mProvider.getBrowserRoot_impl(rootHints);
+    /**
+     * Get the library root. Result would be sent back asynchronously with the
+     * {@link BrowserCallback#onGetLibraryRootDone(MediaBrowser2, Bundle, String, Bundle)}.
+     *
+     * @param rootHints hint for the root
+     * @see BrowserCallback#onGetLibraryRootDone(MediaBrowser2, Bundle, String, Bundle)
+     */
+    public void getLibraryRoot(@Nullable Bundle rootHints) {
+        mProvider.getLibraryRoot_impl(rootHints);
     }
 
     /**
      * Subscribe to a parent id for the change in its children. When there's a change,
-     * {@link BrowserCallback#onChildrenChanged(String, Bundle)} will be called with the bundle
-     * that you've specified. You should call {@link #getChildren(String, int, int, Bundle)} to get
-     * the actual contents for the parent.
+     * {@link BrowserCallback#onChildrenChanged(MediaBrowser2, String, int, Bundle)} will be called
+     * with the bundle that you've specified. You should call
+     * {@link #getChildren(String, int, int, Bundle)} to get the actual contents for the parent.
      *
      * @param parentId parent id
-     * @param options optional bundle
+     * @param extras extra bundle
      */
-    public void subscribe(String parentId, @Nullable Bundle options) {
-        mProvider.subscribe_impl(parentId, options);
+    public void subscribe(@NonNull String parentId, @Nullable Bundle extras) {
+        mProvider.subscribe_impl(parentId, extras);
     }
 
     /**
      * Unsubscribe for changes to the children of the parent, which was previously subscribed with
      * {@link #subscribe(String, Bundle)}.
+     * <p>
+     * This unsubscribes all previous subscription with the parent id, regardless of the extra
+     * that was previously sent to the library service.
      *
      * @param parentId parent id
-     * @param options optional bundle
      */
-    public void unsubscribe(String parentId, @Nullable Bundle options) {
-        mProvider.unsubscribe_impl(parentId, options);
-    }
-
-    /**
-     * Get the media item with the given media id. Result would be sent back asynchronously with the
-     * {@link BrowserCallback#onItemLoaded(String, MediaItem2)}.
-     *
-     * @param mediaId media id
-     */
-    public void getItem(String mediaId) {
-        mProvider.getItem_impl(mediaId);
+    public void unsubscribe(@NonNull String parentId) {
+        mProvider.unsubscribe_impl(parentId);
     }
 
     /**
      * Get list of children under the parent. Result would be sent back asynchronously with the
-     * {@link BrowserCallback#onChildrenLoaded(String, int, int, Bundle, List)}.
+     * {@link BrowserCallback#onGetChildrenDone(MediaBrowser2, String, int, int, List, Bundle)}.
      *
-     * @param parentId
-     * @param page
-     * @param pageSize
-     * @param options
+     * @param parentId parent id for getting the children.
+     * @param page page number to get the result. Starts from {@code 1}
+     * @param pageSize page size. Should be greater or equal to {@code 1}
+     * @param extras extra bundle
      */
-    public void getChildren(String parentId, int page, int pageSize, @Nullable Bundle options) {
-        mProvider.getChildren_impl(parentId, page, pageSize, options);
+    public void getChildren(@NonNull String parentId, int page, int pageSize,
+            @Nullable Bundle extras) {
+        mProvider.getChildren_impl(parentId, page, pageSize, extras);
     }
 
     /**
+     * Get the media item with the given media id. Result would be sent back asynchronously with the
+     * {@link BrowserCallback#onGetItemDone(MediaBrowser2, String, MediaItem2)}.
      *
-     * @param query search query deliminated by string
+     * @param mediaId media id for specifying the item
+     */
+    public void getItem(@NonNull String mediaId) {
+        mProvider.getItem_impl(mediaId);
+    }
+
+    /**
+     * Send a search request to the library service. When the search result is changed,
+     * {@link BrowserCallback#onSearchResultChanged(MediaBrowser2, String, int, Bundle)} will be
+     * called. You should call {@link #getSearchResult(String, int, int, Bundle)} to get the actual
+     * search result.
+     *
+     * @param query search query. Should not be an empty string.
+     * @param extras extra bundle
+     */
+    public void search(@NonNull String query, @Nullable Bundle extras) {
+        mProvider.search_impl(query, extras);
+    }
+
+    /**
+     * Get the search result from lhe library service. Result would be sent back asynchronously with
+     * the
+     * {@link BrowserCallback#onGetSearchResultDone(MediaBrowser2, String, int, int, List, Bundle)}.
+     *
+     * @param query search query that you've specified with {@link #search(String, Bundle)}
      * @param page page number to get search result. Starts from {@code 1}
      * @param pageSize page size. Should be greater or equal to {@code 1}
      * @param extras extra bundle
      */
-    public void search(String query, int page, int pageSize, Bundle extras) {
-        mProvider.search_impl(query, page, pageSize, extras);
+    public void getSearchResult(@NonNull String query, int page, int pageSize,
+            @Nullable Bundle extras) {
+        mProvider.getSearchResult_impl(query, page, pageSize, extras);
     }
 }
diff --git a/android/media/MediaBrowser2Test.java b/android/media/MediaBrowser2Test.java
deleted file mode 100644
index 5c960c8..0000000
--- a/android/media/MediaBrowser2Test.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright 2018 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 static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import android.content.Context;
-import android.media.MediaBrowser2.BrowserCallback;
-import android.media.MediaSession2.CommandGroup;
-import android.os.Bundle;
-import android.support.annotation.CallSuper;
-import android.support.annotation.NonNull;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Tests {@link MediaBrowser2}.
- * <p>
- * This test inherits {@link MediaController2Test} to ensure that inherited APIs from
- * {@link MediaController2} works cleanly.
- */
-// TODO(jaewan): Implement host-side test so browser and service can run in different processes.
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class MediaBrowser2Test extends MediaController2Test {
-    private static final String TAG = "MediaBrowser2Test";
-
-    @Override
-    TestControllerInterface onCreateController(@NonNull SessionToken2 token,
-            @NonNull TestControllerCallbackInterface callback) {
-        return new TestMediaBrowser(mContext, token, new TestBrowserCallback(callback));
-    }
-
-    @Test
-    public void testGetBrowserRoot() throws InterruptedException {
-        final Bundle param = new Bundle();
-        param.putString(TAG, TAG);
-
-        final CountDownLatch latch = new CountDownLatch(1);
-        final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
-            @Override
-            public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
-                assertTrue(TestUtils.equals(param, rootHints));
-                assertEquals(MockMediaLibraryService2.ROOT_ID, rootMediaId);
-                assertTrue(TestUtils.equals(MockMediaLibraryService2.EXTRA, rootExtra));
-                latch.countDown();
-            }
-        };
-
-        final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
-        MediaBrowser2 browser =
-                (MediaBrowser2) createController(token,true, callback);
-        browser.getBrowserRoot(param);
-        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-    }
-
-    public static class TestBrowserCallback extends BrowserCallback
-            implements WaitForConnectionInterface {
-        private final TestControllerCallbackInterface mCallbackProxy;
-        public final CountDownLatch connectLatch = new CountDownLatch(1);
-        public final CountDownLatch disconnectLatch = new CountDownLatch(1);
-
-        TestBrowserCallback(TestControllerCallbackInterface callbackProxy) {
-            mCallbackProxy = callbackProxy;
-        }
-
-        @CallSuper
-        @Override
-        public void onConnected(CommandGroup commands) {
-            super.onConnected(commands);
-            connectLatch.countDown();
-        }
-
-        @CallSuper
-        @Override
-        public void onDisconnected() {
-            super.onDisconnected();
-            disconnectLatch.countDown();
-        }
-
-        @Override
-        public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
-            mCallbackProxy.onGetRootResult(rootHints, rootMediaId, rootExtra);
-        }
-
-        @Override
-        public void waitForConnect(boolean expect) throws InterruptedException {
-            if (expect) {
-                assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-            } else {
-                assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-            }
-        }
-
-        @Override
-        public void waitForDisconnect(boolean expect) throws InterruptedException {
-            if (expect) {
-                assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-            } else {
-                assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-            }
-        }
-    }
-
-    public class TestMediaBrowser extends MediaBrowser2 implements TestControllerInterface {
-        private final BrowserCallback mCallback;
-
-        public TestMediaBrowser(@NonNull Context context, @NonNull SessionToken2 token,
-                @NonNull ControllerCallback callback) {
-            super(context, token, (BrowserCallback) callback, sHandlerExecutor);
-            mCallback = (BrowserCallback) callback;
-        }
-
-        @Override
-        public BrowserCallback getCallback() {
-            return mCallback;
-        }
-    }
-}
\ No newline at end of file
diff --git a/android/media/MediaCodec.java b/android/media/MediaCodec.java
index 3d5f6bc..e3fba0c 100644
--- a/android/media/MediaCodec.java
+++ b/android/media/MediaCodec.java
@@ -1602,7 +1602,9 @@
     private EventHandler mCallbackHandler;
     private Callback mCallback;
     private OnFrameRenderedListener mOnFrameRenderedListener;
-    private Object mListenerLock = new Object();
+    private final Object mListenerLock = new Object();
+    private MediaCodecInfo mCodecInfo;
+    private final Object mCodecInfoLock = new Object();
 
     private static final int EVENT_CALLBACK = 1;
     private static final int EVENT_SET_CALLBACK = 2;
@@ -2357,17 +2359,61 @@
     public static final int CRYPTO_MODE_AES_CBC     = 2;
 
     /**
-     * Metadata describing the structure of a (at least partially) encrypted
-     * input sample.
-     * A buffer's data is considered to be partitioned into "subSamples",
-     * each subSample starts with a (potentially empty) run of plain,
-     * unencrypted bytes followed by a (also potentially empty) run of
-     * encrypted bytes. If pattern encryption applies, each of the latter runs
-     * is encrypted only partly, according to a repeating pattern of "encrypt"
-     * and "skip" blocks. numBytesOfClearData can be null to indicate that all
-     * data is encrypted. This information encapsulates per-sample metadata as
-     * outlined in ISO/IEC FDIS 23001-7:2011 "Common encryption in ISO base
-     * media file format files".
+     * Metadata describing the structure of an encrypted input sample.
+     * <p>
+     * A buffer's data is considered to be partitioned into "subSamples". Each subSample starts with
+     * a run of plain, unencrypted bytes followed by a run of encrypted bytes. Either of these runs
+     * may be empty. If pattern encryption applies, each of the encrypted runs is encrypted only
+     * partly, according to a repeating pattern of "encrypt" and "skip" blocks.
+     * {@link #numBytesOfClearData} can be null to indicate that all data is encrypted, and
+     * {@link #numBytesOfEncryptedData} can be null to indicate that all data is clear. At least one
+     * of {@link #numBytesOfClearData} and {@link #numBytesOfEncryptedData} must be non-null.
+     * <p>
+     * This information encapsulates per-sample metadata as outlined in ISO/IEC FDIS 23001-7:2016
+     * "Common encryption in ISO base media file format files".
+     * <p>
+     * <h3>ISO-CENC Schemes</h3>
+     * ISO/IEC FDIS 23001-7:2016 defines four possible schemes by which media may be encrypted,
+     * corresponding to each possible combination of an AES mode with the presence or absence of
+     * patterned encryption.
+     *
+     * <table style="width: 0%">
+     *   <thead>
+     *     <tr>
+     *       <th>&nbsp;</th>
+     *       <th>AES-CTR</th>
+     *       <th>AES-CBC</th>
+     *     </tr>
+     *   </thead>
+     *   <tbody>
+     *     <tr>
+     *       <th>Without Patterns</th>
+     *       <td>cenc</td>
+     *       <td>cbc1</td>
+     *     </tr><tr>
+     *       <th>With Patterns</th>
+     *       <td>cens</td>
+     *       <td>cbcs</td>
+     *     </tr>
+     *   </tbody>
+     * </table>
+     *
+     * For {@code CryptoInfo}, the scheme is selected implicitly by the combination of the
+     * {@link #mode} field and the value set with {@link #setPattern}. For the pattern, setting the
+     * pattern to all zeroes (that is, both {@code blocksToEncrypt} and {@code blocksToSkip} are
+     * zero) is interpreted as turning patterns off completely. A scheme that does not use patterns
+     * will be selected, either cenc or cbc1. Setting the pattern to any nonzero value will choose
+     * one of the pattern-supporting schemes, cens or cbcs. The default pattern if
+     * {@link #setPattern} is never called is all zeroes.
+     * <p>
+     * <h4>HLS SAMPLE-AES Audio</h4>
+     * HLS SAMPLE-AES audio is encrypted in a manner compatible with the cbcs scheme, except that it
+     * does not use patterned encryption. However, if {@link #setPattern} is used to set the pattern
+     * to all zeroes, this will be interpreted as selecting the cbc1 scheme. The cbc1 scheme cannot
+     * successfully decrypt HLS SAMPLE-AES audio because of differences in how the IVs are handled.
+     * For this reason, it is recommended that a pattern of {@code 1} encrypted block and {@code 0}
+     * skip blocks be used with HLS SAMPLE-AES audio. This will trigger decryption to use cbcs mode
+     * while still decrypting every block.
      */
     public final static class CryptoInfo {
         /**
@@ -2375,11 +2421,13 @@
          */
         public int numSubSamples;
         /**
-         * The number of leading unencrypted bytes in each subSample.
+         * The number of leading unencrypted bytes in each subSample. If null, all bytes are treated
+         * as encrypted and {@link #numBytesOfEncryptedData} must be specified.
          */
         public int[] numBytesOfClearData;
         /**
-         * The number of trailing encrypted bytes in each subSample.
+         * The number of trailing encrypted bytes in each subSample. If null, all bytes are treated
+         * as clear and {@link #numBytesOfClearData} must be specified.
          */
         public int[] numBytesOfEncryptedData;
         /**
@@ -2398,35 +2446,34 @@
         public int mode;
 
         /**
-         * Metadata describing an encryption pattern for the protected bytes in
-         * a subsample.  An encryption pattern consists of a repeating sequence
-         * of crypto blocks comprised of a number of encrypted blocks followed
-         * by a number of unencrypted, or skipped, blocks.
+         * Metadata describing an encryption pattern for the protected bytes in a subsample.  An
+         * encryption pattern consists of a repeating sequence of crypto blocks comprised of a
+         * number of encrypted blocks followed by a number of unencrypted, or skipped, blocks.
          */
         public final static class Pattern {
             /**
-             * Number of blocks to be encrypted in the pattern. If zero, pattern
-             * encryption is inoperative.
+             * Number of blocks to be encrypted in the pattern. If both this and
+             * {@link #mSkipBlocks} are zero, pattern encryption is inoperative.
              */
             private int mEncryptBlocks;
 
             /**
-             * Number of blocks to be skipped (left clear) in the pattern. If zero,
-             * pattern encryption is inoperative.
+             * Number of blocks to be skipped (left clear) in the pattern. If both this and
+             * {@link #mEncryptBlocks} are zero, pattern encryption is inoperative.
              */
             private int mSkipBlocks;
 
             /**
-             * Construct a sample encryption pattern given the number of blocks to
-             * encrypt and skip in the pattern.
+             * Construct a sample encryption pattern given the number of blocks to encrypt and skip
+             * in the pattern. If both parameters are zero, pattern encryption is inoperative.
              */
             public Pattern(int blocksToEncrypt, int blocksToSkip) {
                 set(blocksToEncrypt, blocksToSkip);
             }
 
             /**
-             * Set the number of blocks to encrypt and skip in a sample encryption
-             * pattern.
+             * Set the number of blocks to encrypt and skip in a sample encryption pattern. If both
+             * parameters are zero, pattern encryption is inoperative.
              */
             public void set(int blocksToEncrypt, int blocksToSkip) {
                 mEncryptBlocks = blocksToEncrypt;
@@ -3469,10 +3516,26 @@
      */
     @NonNull
     public MediaCodecInfo getCodecInfo() {
-        return MediaCodecList.getInfoFor(getName());
+        // Get the codec name first. If the codec is already released,
+        // IllegalStateException will be thrown here.
+        String name = getName();
+        synchronized (mCodecInfoLock) {
+            if (mCodecInfo == null) {
+                // Get the codec info for this codec itself first. Only initialize
+                // the full codec list if this somehow fails because it can be slow.
+                mCodecInfo = getOwnCodecInfo();
+                if (mCodecInfo == null) {
+                    mCodecInfo = MediaCodecList.getInfoFor(name);
+                }
+            }
+            return mCodecInfo;
+        }
     }
 
     @NonNull
+    private native final MediaCodecInfo getOwnCodecInfo();
+
+    @NonNull
     private native final ByteBuffer[] getBuffers(boolean input);
 
     @Nullable
@@ -3510,6 +3573,8 @@
 
         private final static int TYPE_YUV = 1;
 
+        private final int mTransform = 0; //Default no transform
+
         @Override
         public int getFormat() {
             throwISEIfImageIsInvalid();
@@ -3529,6 +3594,12 @@
         }
 
         @Override
+        public int getTransform() {
+            throwISEIfImageIsInvalid();
+            return mTransform;
+        }
+
+        @Override
         public long getTimestamp() {
             throwISEIfImageIsInvalid();
             return mTimestamp;
diff --git a/android/media/MediaCodecInfo.java b/android/media/MediaCodecInfo.java
index 44d9099..2a601f9 100644
--- a/android/media/MediaCodecInfo.java
+++ b/android/media/MediaCodecInfo.java
@@ -829,14 +829,24 @@
 
         /** @hide */
         public CodecCapabilities dup() {
-            return new CodecCapabilities(
-                // clone writable arrays
-                Arrays.copyOf(profileLevels, profileLevels.length),
-                Arrays.copyOf(colorFormats, colorFormats.length),
-                isEncoder(),
-                mFlagsVerified,
-                mDefaultFormat,
-                mCapabilitiesInfo);
+            CodecCapabilities caps = new CodecCapabilities();
+
+            // profileLevels and colorFormats may be modified by client.
+            caps.profileLevels = Arrays.copyOf(profileLevels, profileLevels.length);
+            caps.colorFormats = Arrays.copyOf(colorFormats, colorFormats.length);
+
+            caps.mMime = mMime;
+            caps.mMaxSupportedInstances = mMaxSupportedInstances;
+            caps.mFlagsRequired = mFlagsRequired;
+            caps.mFlagsSupported = mFlagsSupported;
+            caps.mFlagsVerified = mFlagsVerified;
+            caps.mAudioCaps = mAudioCaps;
+            caps.mVideoCaps = mVideoCaps;
+            caps.mEncoderCaps = mEncoderCaps;
+            caps.mDefaultFormat = mDefaultFormat;
+            caps.mCapabilitiesInfo = mCapabilitiesInfo;
+
+            return caps;
         }
 
         /**
@@ -898,13 +908,13 @@
 
             if (mMime.toLowerCase().startsWith("audio/")) {
                 mAudioCaps = AudioCapabilities.create(info, this);
-                mAudioCaps.setDefaultFormat(mDefaultFormat);
+                mAudioCaps.getDefaultFormat(mDefaultFormat);
             } else if (mMime.toLowerCase().startsWith("video/")) {
                 mVideoCaps = VideoCapabilities.create(info, this);
             }
             if (encoder) {
                 mEncoderCaps = EncoderCapabilities.create(info, this);
-                mEncoderCaps.setDefaultFormat(mDefaultFormat);
+                mEncoderCaps.getDefaultFormat(mDefaultFormat);
             }
 
             final Map<String, Object> global = MediaCodecList.getGlobalSettings();
@@ -990,8 +1000,7 @@
             return caps;
         }
 
-        /** @hide */
-        public void init(MediaFormat info, CodecCapabilities parent) {
+        private void init(MediaFormat info, CodecCapabilities parent) {
             mParent = parent;
             initWithPlatformLimits();
             applyLevelLimits();
@@ -1171,7 +1180,7 @@
         }
 
         /** @hide */
-        public void setDefaultFormat(MediaFormat format) {
+        public void getDefaultFormat(MediaFormat format) {
             // report settings that have only a single choice
             if (mBitrateRange.getLower().equals(mBitrateRange.getUpper())) {
                 format.setInteger(MediaFormat.KEY_BIT_RATE, mBitrateRange.getLower());
@@ -1585,8 +1594,7 @@
             return caps;
         }
 
-        /** @hide */
-        public void init(MediaFormat info, CodecCapabilities parent) {
+        private void init(MediaFormat info, CodecCapabilities parent) {
             mParent = parent;
             initWithPlatformLimits();
             applyLevelLimits();
@@ -2707,8 +2715,7 @@
             return caps;
         }
 
-        /** @hide */
-        public void init(MediaFormat info, CodecCapabilities parent) {
+        private void init(MediaFormat info, CodecCapabilities parent) {
             // no support for complexity or quality yet
             mParent = parent;
             mComplexityRange = Range.create(0, 0);
@@ -2789,7 +2796,7 @@
         }
 
         /** @hide */
-        public void setDefaultFormat(MediaFormat format) {
+        public void getDefaultFormat(MediaFormat format) {
             // don't list trivial quality/complexity as default for now
             if (!mQualityRange.getUpper().equals(mQualityRange.getLower())
                     && mDefaultQuality != null) {
@@ -3002,6 +3009,7 @@
         // from OMX_VIDEO_HEVCPROFILETYPE
         public static final int HEVCProfileMain        = 0x01;
         public static final int HEVCProfileMain10      = 0x02;
+        public static final int HEVCProfileMainStill   = 0x04;
         public static final int HEVCProfileMain10HDR10 = 0x1000;
 
         // from OMX_VIDEO_HEVCLEVELTYPE
@@ -3078,6 +3086,23 @@
          * {@link VideoCapabilities} to determine the codec capabilities.
          */
         public int level;
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
+            }
+            if (obj instanceof CodecProfileLevel) {
+                CodecProfileLevel other = (CodecProfileLevel)obj;
+                return other.profile == profile && other.level == level;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Long.hashCode(((long)profile << Integer.SIZE) | level);
+        }
     };
 
     /**
diff --git a/android/media/MediaController2.java b/android/media/MediaController2.java
index d669bc1..591f33f 100644
--- a/android/media/MediaController2.java
+++ b/android/media/MediaController2.java
@@ -16,18 +16,22 @@
 
 package android.media;
 
+import static android.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
+
+import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.PendingIntent;
 import android.content.Context;
-import android.media.MediaSession2.Command;
+import android.media.MediaPlaylistAgent.RepeatMode;
+import android.media.MediaPlaylistAgent.ShuffleMode;
 import android.media.MediaSession2.CommandButton;
-import android.media.MediaSession2.CommandGroup;
 import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.PlaylistParam;
+import android.media.MediaSession2.ErrorCode;
 import android.media.session.MediaSessionManager;
 import android.media.update.ApiLoader;
 import android.media.update.MediaController2Provider;
+import android.media.update.MediaController2Provider.PlaybackInfoProvider;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.ResultReceiver;
@@ -36,6 +40,7 @@
 import java.util.concurrent.Executor;
 
 /**
+ * @hide
  * Allows an app to interact with an active {@link MediaSession2} or a
  * {@link MediaSessionService2} in any status. Media buttons and other commands can be sent to
  * the session.
@@ -48,9 +53,9 @@
  * <p>
  * When controlling {@link MediaSessionService2}, the {@link MediaController2} would be
  * available only if the session service allows this controller by
- * {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)} for the service. Wait
- * {@link ControllerCallback#onConnected(CommandGroup)} or
- * {@link ControllerCallback#onDisconnected()} for the result.
+ * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)} for the service.
+ * Wait {@link ControllerCallback#onConnected(MediaController2, SessionCommandGroup2)} or
+ * {@link ControllerCallback#onDisconnected(MediaController2)} for the result.
  * <p>
  * A controller can be created through token from {@link MediaSessionManager} if you hold the
  * signature|privileged permission "android.permission.MEDIA_CONTENT_CONTROL" permission or are
@@ -61,10 +66,7 @@
  * <p>
  * @see MediaSession2
  * @see MediaSessionService2
- * @hide
  */
-// TODO(jaewan): Unhide
-// TODO(jaewan): Revisit comments. Currently MediaBrowser case is missing.
 public class MediaController2 implements AutoCloseable {
     /**
      * Interface for listening to change in activeness of the {@link MediaSession2}.  It's
@@ -75,9 +77,11 @@
          * Called when the controller is successfully connected to the session. The controller
          * becomes available afterwards.
          *
+         * @param controller the controller for this event
          * @param allowedCommands commands that's allowed by the session.
          */
-        public void onConnected(CommandGroup allowedCommands) { }
+        public void onConnected(@NonNull MediaController2 controller,
+                @NonNull SessionCommandGroup2 allowedCommands) { }
 
         /**
          * Called when the session refuses the controller or the controller is disconnected from
@@ -86,58 +90,159 @@
          * <p>
          * It will be also called after the {@link #close()}, so you can put clean up code here.
          * You don't need to call {@link #close()} after this.
+         *
+         * @param controller the controller for this event
+         * @param controller controller for this event
          */
-        public void onDisconnected() { }
+        public void onDisconnected(@NonNull MediaController2 controller) { }
 
         /**
          * Called when the session set the custom layout through the
          * {@link MediaSession2#setCustomLayout(ControllerInfo, List)}.
          * <p>
-         * Can be called before {@link #onConnected(CommandGroup)} is called.
+         * Can be called before {@link #onConnected(MediaController2, SessionCommandGroup2)} is
+         * called.
          *
+         * @param controller the controller for this event
          * @param layout
          */
-        public void onCustomLayoutChanged(List<CommandButton> layout) { }
+        public void onCustomLayoutChanged(@NonNull MediaController2 controller,
+                @NonNull List<CommandButton> layout) { }
 
         /**
          * Called when the session has changed anything related with the {@link PlaybackInfo}.
          *
+         * @param controller the controller for this event
          * @param info new playback info
          */
-        public void onAudioInfoChanged(PlaybackInfo info) { }
+        public void onPlaybackInfoChanged(@NonNull MediaController2 controller,
+                @NonNull PlaybackInfo info) { }
 
         /**
          * Called when the allowed commands are changed by session.
          *
+         * @param controller the controller for this event
          * @param commands newly allowed commands
          */
-        public void onAllowedCommandsChanged(CommandGroup commands) { }
+        public void onAllowedCommandsChanged(@NonNull MediaController2 controller,
+                @NonNull SessionCommandGroup2 commands) { }
 
         /**
          * Called when the session sent a custom command.
          *
+         * @param controller the controller for this event
          * @param command
          * @param args
          * @param receiver
          */
-        public void onCustomCommand(Command command, @Nullable Bundle args,
+        public void onCustomCommand(@NonNull MediaController2 controller,
+                @NonNull SessionCommand2 command, @Nullable Bundle args,
                 @Nullable ResultReceiver receiver) { }
 
         /**
-         * Called when the playlist is changed.
+         * Called when the player state is changed.
          *
-         * @param list
-         * @param param
-         */
-        public void onPlaylistChanged(
-                @NonNull List<MediaItem2> list, @NonNull PlaylistParam param) { }
-
-        /**
-         * Called when the playback state is changed.
-         *
+         * @param controller the controller for this event
          * @param state
          */
-        public void onPlaybackStateChanged(@NonNull PlaybackState2 state) { }
+        public void onPlayerStateChanged(@NonNull MediaController2 controller, int state) { }
+
+        /**
+         * Called when playback speed is changed.
+         *
+         * @param controller the controller for this event
+         * @param speed speed
+         */
+        public void onPlaybackSpeedChanged(@NonNull MediaController2 controller,
+                float speed) { }
+
+        /**
+         * Called to report buffering events for a data source.
+         * <p>
+         * Use {@link #getBufferedPosition()} for current buffering position.
+         *
+         * @param controller the controller for this event
+         * @param item the media item for which buffering is happening.
+         * @param state the new buffering state.
+         */
+        public void onBufferingStateChanged(@NonNull MediaController2 controller,
+                @NonNull MediaItem2 item, @MediaPlayerBase.BuffState int state) { }
+
+        /**
+         * Called to indicate that seeking is completed.
+         *
+         * @param controller the controller for this event.
+         * @param position the previous seeking request.
+         */
+        public void onSeekCompleted(@NonNull MediaController2 controller, long position) { }
+
+        /**
+         * Called when a error from
+         *
+         * @param controller the controller for this event
+         * @param errorCode error code
+         * @param extras extra information
+         */
+        public void onError(@NonNull MediaController2 controller, @ErrorCode int errorCode,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when the player's currently playing item is changed
+         * <p>
+         * When it's called, you should invalidate previous playback information and wait for later
+         * callbacks.
+         *
+         * @param controller the controller for this event
+         * @param item new item
+         * @see #onBufferingStateChanged(MediaController2, MediaItem2, int)
+         */
+        // TODO(jaewan): Use this (b/74316764)
+        public void onCurrentMediaItemChanged(@NonNull MediaController2 controller,
+                @NonNull MediaItem2 item) { }
+
+        /**
+         * Called when a playlist is changed.
+         *
+         * @param controller the controller for this event
+         * @param list new playlist
+         * @param metadata new metadata
+         */
+        public void onPlaylistChanged(@NonNull MediaController2 controller,
+                @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when a playlist metadata is changed.
+         *
+         * @param controller the controller for this event
+         * @param metadata new metadata
+         */
+        public void onPlaylistMetadataChanged(@NonNull MediaController2 controller,
+                @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when the shuffle mode is changed.
+         *
+         * @param controller the controller for this event
+         * @param shuffleMode repeat mode
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+         */
+        public void onShuffleModeChanged(@NonNull MediaController2 controller,
+                @MediaPlaylistAgent.ShuffleMode int shuffleMode) { }
+
+        /**
+         * Called when the repeat mode is changed.
+         *
+         * @param controller the controller for this event
+         * @param repeatMode repeat mode
+         * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+         * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+         * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+         * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+         */
+        public void onRepeatModeChanged(@NonNull MediaController2 controller,
+                @MediaPlaylistAgent.RepeatMode int repeatMode) { }
     }
 
     /**
@@ -155,21 +260,20 @@
          */
         public static final int PLAYBACK_TYPE_LOCAL = 1;
 
-        private final int mVolumeType;
-        private final int mVolumeControl;
-        private final int mMaxVolume;
-        private final int mCurrentVolume;
-        private final AudioAttributes mAudioAttrs;
+        private final PlaybackInfoProvider mProvider;
 
         /**
          * @hide
          */
-        public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
-            mVolumeType = type;
-            mAudioAttrs = attrs;
-            mVolumeControl = control;
-            mMaxVolume = max;
-            mCurrentVolume = current;
+        public PlaybackInfo(PlaybackInfoProvider provider) {
+            mProvider = provider;
+        }
+
+        /**
+         * @hide
+         */
+        public PlaybackInfoProvider getProvider() {
+            return mProvider;
         }
 
         /**
@@ -182,7 +286,7 @@
          * @return The type of playback this session is using.
          */
         public int getPlaybackType() {
-            return mVolumeType;
+            return mProvider.getPlaybackType_impl();
         }
 
         /**
@@ -194,22 +298,21 @@
          * @return The attributes for this session.
          */
         public AudioAttributes getAudioAttributes() {
-            return mAudioAttrs;
+            return mProvider.getAudioAttributes_impl();
         }
 
         /**
          * Get the type of volume control that can be used. One of:
          * <ul>
-         * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li>
-         * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li>
-         * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
+         * <li>{@link VolumeProvider2#VOLUME_CONTROL_ABSOLUTE}</li>
+         * <li>{@link VolumeProvider2#VOLUME_CONTROL_RELATIVE}</li>
+         * <li>{@link VolumeProvider2#VOLUME_CONTROL_FIXED}</li>
          * </ul>
          *
-         * @return The type of volume control that may be used with this
-         *         session.
+         * @return The type of volume control that may be used with this session.
          */
-        public int getVolumeControl() {
-            return mVolumeControl;
+        public int getControlType() {
+            return mProvider.getControlType_impl();
         }
 
         /**
@@ -218,7 +321,7 @@
          * @return The maximum allowed volume where this session is playing.
          */
         public int getMaxVolume() {
-            return mMaxVolume;
+            return mProvider.getMaxVolume_impl();
         }
 
         /**
@@ -227,39 +330,39 @@
          * @return The current volume where this session is playing.
          */
         public int getCurrentVolume() {
-            return mCurrentVolume;
+            return mProvider.getCurrentVolume_impl();
         }
     }
 
     private final MediaController2Provider mProvider;
 
     /**
-     * Create a {@link MediaController2} from the {@link SessionToken2}. This connects to the session
-     * and may wake up the service if it's not available.
+     * Create a {@link MediaController2} from the {@link SessionToken2}.
+     * This connects to the session and may wake up the service if it's not available.
      *
      * @param context Context
      * @param token token to connect to
-     * @param callback controller callback to receive changes in
      * @param executor executor to run callbacks on.
+     * @param callback controller callback to receive changes in
      */
-    // TODO(jaewan): Put @CallbackExecutor to the constructor.
     public MediaController2(@NonNull Context context, @NonNull SessionToken2 token,
-            @NonNull ControllerCallback callback, @NonNull Executor executor) {
+            @NonNull @CallbackExecutor Executor executor, @NonNull ControllerCallback callback) {
         super();
 
+        mProvider = createProvider(context, token, executor, callback);
         // This also connects to the token.
         // Explicit connect() isn't added on purpose because retrying connect() is impossible with
         // session whose session binder is only valid while it's active.
         // prevent a controller from reusable after the
         // session is released and recreated.
-        mProvider = createProvider(context, token, callback, executor);
+        mProvider.initialize();
     }
 
     MediaController2Provider createProvider(@NonNull Context context,
-            @NonNull SessionToken2 token, @NonNull ControllerCallback callback,
-            @NonNull Executor executor) {
-        return ApiLoader.getProvider(context)
-                .createMediaController2(this, context, token, callback, executor);
+            @NonNull SessionToken2 token, @NonNull Executor executor,
+            @NonNull ControllerCallback callback) {
+        return ApiLoader.getProvider().createMediaController2(
+                context, this, token, executor, callback);
     }
 
     /**
@@ -281,8 +384,7 @@
     /**
      * @return token
      */
-    public @NonNull
-    SessionToken2 getSessionToken() {
+    public @NonNull SessionToken2 getSessionToken() {
         return mProvider.getSessionToken_impl();
     }
 
@@ -305,36 +407,26 @@
         mProvider.stop_impl();
     }
 
-    public void skipToPrevious() {
-        mProvider.skipToPrevious_impl();
-    }
-
-    public void skipToNext() {
-        mProvider.skipToNext_impl();
-    }
-
     /**
      * Request that the player prepare its playback. In other words, other sessions can continue
      * to play during the preparation of this session. This method can be used to speed up the
      * start of the playback. Once the preparation is done, the session will change its playback
-     * state to {@link PlaybackState2#STATE_PAUSED}. Afterwards, {@link #play} can be called to
-     * start playback.
+     * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
+     * to start playback.
      */
     public void prepare() {
         mProvider.prepare_impl();
     }
 
     /**
-     * Start fast forwarding. If playback is already fast forwarding this
-     * may increase the rate.
+     * Fast forwards playback. If playback is already fast forwarding this may increase the rate.
      */
     public void fastForward() {
         mProvider.fastForward_impl();
     }
 
     /**
-     * Start rewinding. If playback is already rewinding this may increase
-     * the rate.
+     * Rewinds playback. If playback is already rewinding this may increase the rate.
      */
     public void rewind() {
         mProvider.rewind_impl();
@@ -350,20 +442,11 @@
     }
 
     /**
-     * Sets the index of current DataSourceDesc in the play list to be played.
-     *
-     * @param index the index of DataSourceDesc in the play list you want to play
-     * @throws IllegalArgumentException if the play list is null
-     * @throws NullPointerException if index is outside play list range
-     */
-    public void setCurrentPlaylistItem(int index) {
-        mProvider.setCurrentPlaylistItem_impl(index);
-    }
-
-    /**
+     * Revisit this API later.
      * @hide
      */
     public void skipForward() {
+        // TODO(jaewan): (Post-P) Discuss this API later.
         // To match with KEYCODE_MEDIA_SKIP_FORWARD
     }
 
@@ -371,6 +454,7 @@
      * @hide
      */
     public void skipBackward() {
+        // TODO(jaewan): (Post-P) Discuss this API later.
         // To match with KEYCODE_MEDIA_SKIP_BACKWARD
     }
 
@@ -387,12 +471,9 @@
 
     /**
      * Request that the player start playback for a specific search query.
-     * An empty or null query should be treated as a request to play any
-     * music.
      *
-     * @param query The search query.
-     * @param extras Optional extras that can include extra information
-     *               about the query.
+     * @param query The search query. Should not be an empty string.
+     * @param extras Optional extras that can include extra information about the query.
      */
     public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
         mProvider.playFromSearch_impl(query, extras);
@@ -405,16 +486,15 @@
      * @param extras Optional extras that can include extra information about the media item
      *               to be played.
      */
-    public void playFromUri(@NonNull String uri, @Nullable Bundle extras) {
+    public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
         mProvider.playFromUri_impl(uri, extras);
     }
 
-
     /**
      * Request that the player prepare playback for a specific media id. In other words, other
      * sessions can continue to play during the preparation of this session. This method can be
      * used to speed up the start of the playback. Once the preparation is done, the session
-     * will change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards,
+     * will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards,
      * {@link #play} can be called to start playback. If the preparation is not needed,
      * {@link #playFromMediaId} can be directly called without this method.
      *
@@ -423,21 +503,20 @@
      *               to be prepared.
      */
     public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
-        mProvider.prepareMediaId_impl(mediaId, extras);
+        mProvider.prepareFromMediaId_impl(mediaId, extras);
     }
 
     /**
-     * Request that the player prepare playback for a specific search query. An empty or null
-     * query should be treated as a request to prepare any music. In other words, other sessions
-     * can continue to play during the preparation of this session. This method can be used to
-     * speed up the start of the playback. Once the preparation is done, the session will
-     * change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards,
+     * Request that the player prepare playback for a specific search query.
+     * In other words, other sessions can continue to play during the preparation of this session.
+     * This method can be used to speed up the start of the playback.
+     * Once the preparation is done, the session will change its playback state to
+     * {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards,
      * {@link #play} can be called to start playback. If the preparation is not needed,
      * {@link #playFromSearch} can be directly called without this method.
      *
-     * @param query The search query.
-     * @param extras Optional extras that can include extra information
-     *               about the query.
+     * @param query The search query. Should not be an empty string.
+     * @param extras Optional extras that can include extra information about the query.
      */
     public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
         mProvider.prepareFromSearch_impl(query, extras);
@@ -447,8 +526,8 @@
      * Request that the player prepare playback for a specific {@link Uri}. In other words,
      * other sessions can continue to play during the preparation of this session. This method
      * can be used to speed up the start of the playback. Once the preparation is done, the
-     * session will change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards,
-     * {@link #play} can be called to start playback. If the preparation is not needed,
+     * session will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}.
+     * Afterwards, {@link #play} can be called to start playback. If the preparation is not needed,
      * {@link #playFromUri} can be directly called without this method.
      *
      * @param uri The URI of the requested media.
@@ -461,7 +540,7 @@
 
     /**
      * Set the volume of the output this session is playing on. The command will be ignored if it
-     * does not support {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}.
+     * does not support {@link VolumeProvider2#VOLUME_CONTROL_ABSOLUTE}.
      * <p>
      * If the session is local playback, this changes the device's volume with the stream that
      * session's player is using. Flags will be specified for the {@link AudioManager}.
@@ -483,8 +562,8 @@
      * must be one of {@link AudioManager#ADJUST_LOWER},
      * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
      * The command will be ignored if the session does not support
-     * {@link VolumeProvider#VOLUME_CONTROL_RELATIVE} or
-     * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}.
+     * {@link VolumeProvider2#VOLUME_CONTROL_RELATIVE} or
+     * {@link VolumeProvider2#VOLUME_CONTROL_ABSOLUTE}.
      * <p>
      * If the session is local playback, this changes the device's volume with the stream that
      * session's player is using. Flags will be specified for the {@link AudioManager}.
@@ -502,24 +581,6 @@
     }
 
     /**
-     * Get the rating type supported by the session. One of:
-     * <ul>
-     * <li>{@link Rating2#RATING_NONE}</li>
-     * <li>{@link Rating2#RATING_HEART}</li>
-     * <li>{@link Rating2#RATING_THUMB_UP_DOWN}</li>
-     * <li>{@link Rating2#RATING_3_STARS}</li>
-     * <li>{@link Rating2#RATING_4_STARS}</li>
-     * <li>{@link Rating2#RATING_5_STARS}</li>
-     * <li>{@link Rating2#RATING_PERCENTAGE}</li>
-     * </ul>
-     *
-     * @return The supported rating type
-     */
-    public int getRatingType() {
-        return mProvider.getRatingType_impl();
-    }
-
-    /**
      * Get an intent for launching UI associated with this session if one exists.
      *
      * @return A {@link PendingIntent} to launch UI or null.
@@ -529,12 +590,65 @@
     }
 
     /**
-     * Get the latest {@link PlaybackState2} from the session.
+     * Get the lastly cached player state from
+     * {@link ControllerCallback#onPlayerStateChanged(MediaController2, int)}.
      *
-     * @return a playback state
+     * @return player state
      */
-    public PlaybackState2 getPlaybackState() {
-        return mProvider.getPlaybackState_impl();
+    public int getPlayerState() {
+        return mProvider.getPlayerState_impl();
+    }
+
+    /**
+     * Gets the current playback position.
+     * <p>
+     * This returns the calculated value of the position, based on the difference between the
+     * update time and current time.
+     *
+     * @return position
+     */
+    public long getCurrentPosition() {
+        return mProvider.getCurrentPosition_impl();
+    }
+
+    /**
+     * Get the lastly cached playback speed from
+     * {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}.
+     *
+     * @return speed
+     */
+    public float getPlaybackSpeed() {
+        return mProvider.getPlaybackSpeed_impl();
+    }
+
+    /**
+     * Set the playback speed.
+     */
+    public void setPlaybackSpeed(float speed) {
+        // TODO(jaewan): implement this (b/74093080)
+    }
+
+
+    /**
+     * Gets the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     * @return the buffering state.
+     */
+    public @MediaPlayerBase.BuffState int getBufferingState() {
+        // TODO(jaewan): Implement.
+        return BUFFERING_STATE_UNKNOWN;
+    }
+
+    /**
+     * Gets the lastly cached buffered position from the session when
+     * {@link ControllerCallback#onBufferingStateChanged(MediaController2, MediaItem2, int)} is
+     * called.
+     *
+     * @return buffering position in millis
+     */
+    public long getBufferedPosition() {
+        return mProvider.getBufferedPosition_impl();
     }
 
     /**
@@ -547,14 +661,19 @@
     }
 
     /**
-     * Rate the current content. This will cause the rating to be set for
-     * the current user. The Rating type must match the type returned by
-     * {@link #getRatingType()}.
+     * Rate the media. This will cause the rating to be set for the current user.
+     * The rating style must follow the user rating style from the session.
+     * You can get the rating style from the session through the
+     * {@link MediaMetadata#getRating(String)} with the key
+     * {@link MediaMetadata#METADATA_KEY_USER_RATING}.
+     * <p>
+     * If the user rating was {@code null}, the media item does not accept setting user rating.
      *
-     * @param rating The rating to set for the current content
+     * @param mediaId The id of the media
+     * @param rating The rating to set
      */
-    public void setRating(Rating2 rating) {
-        mProvider.setRating_impl(rating);
+    public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) {
+        mProvider.setRating_impl(mediaId, rating);
     }
 
     /**
@@ -564,53 +683,189 @@
      * @param args optional argument
      * @param cb optional result receiver
      */
-    public void sendCustomCommand(@NonNull Command command, @Nullable Bundle args,
+    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args,
             @Nullable ResultReceiver cb) {
         mProvider.sendCustomCommand_impl(command, args, cb);
     }
 
     /**
-     * Return playlist from the session.
+     * Returns the cached playlist from
+     * {@link ControllerCallback#onPlaylistChanged(MediaController2, List, MediaMetadata2)}.
+     * <p>
+     * This list may differ with the list that was specified with
+     * {@link #setPlaylist(List, MediaMetadata2)} depending on the session implementation. Use media
+     * items returned here for other playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
      *
-     * @return playlist. Can be {@code null} if the controller doesn't have enough permission.
+     * @return The playlist. Can be {@code null} if the controller doesn't have enough permission or
+     *         the session hasn't set any playlist.
      */
     public @Nullable List<MediaItem2> getPlaylist() {
         return mProvider.getPlaylist_impl();
     }
 
-    public @Nullable PlaylistParam getPlaylistParam() {
-        return mProvider.getPlaylistParam_impl();
+    /**
+     * Sets the playlist.
+     * <p>
+     * Even when the playlist is successfully set, use the playlist returned from
+     * {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}.
+     * Otherwise the session in the remote process can't distinguish between media items.
+     *
+     * @param list playlist
+     * @param metadata metadata of the playlist
+     * @see #getPlaylist()
+     * @see ControllerCallback#onPlaylistChanged(MediaController2, List, MediaMetadata2)
+     */
+    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+        mProvider.setPlaylist_impl(list, metadata);
     }
 
     /**
-     * Removes the media item at index in the play list.
+     * Updates the playlist metadata
+     *
+     * @param metadata metadata of the playlist
+     */
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        mProvider.updatePlaylistMetadata_impl(metadata);
+    }
+
+    /**
+     * Gets the lastly cached playlist playlist metadata either from
+     * {@link ControllerCallback#onPlaylistMetadataChanged(MediaController2,  MediaMetadata2)} or
+     * {@link ControllerCallback#onPlaylistChanged(MediaController2, List, MediaMetadata2)}.
+     *
+     * @return metadata metadata of the playlist, or null if none is set
+     */
+    public @Nullable MediaMetadata2 getPlaylistMetadata() {
+        return mProvider.getPlaylistMetadata_impl();
+    }
+
+
+    /**
+     * Adds the media item to the playlist at position index. Index equals or greater than
+     * the current playlist size will add the item at the end of the playlist.
+     * <p>
+     * This will not change the currently playing media item.
+     * If index is less than or equal to the current index of the playlist,
+     * the current index of the playlist will be incremented correspondingly.
+     *
+     * @param index the index you want to add
+     * @param item the media item you want to add
+     */
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        mProvider.addPlaylistItem_impl(index, item);
+    }
+
+    /**
+     * Removes the media item at index in the playlist.
      *<p>
-     * If index is same as the current index of the playlist, current playback
+     * If the item is the currently playing item of the playlist, current playback
      * will be stopped and playback moves to next source in the list.
      *
-     * @return the removed DataSourceDesc at index in the play list
-     * @throws IllegalArgumentException if the play list is null
-     * @throws IndexOutOfBoundsException if index is outside play list range
+     * @param item the media item you want to add
      */
-    // TODO(jaewan): Remove with index was previously rejected by council (b/36524925)
-    // TODO(jaewan): Should we also add movePlaylistItem from index to index?
-    public void removePlaylistItem(MediaItem2 item) {
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
         mProvider.removePlaylistItem_impl(item);
     }
 
     /**
-     * Inserts the media item to the play list at position index.
-     * <p>
-     * This will not change the currently playing media item.
-     * If index is less than or equal to the current index of the play list,
-     * the current index of the play list will be incremented correspondingly.
+     * Replace the media item at index in the playlist. This can be also used to update metadata of
+     * an item.
      *
-     * @param index the index you want to add dsd to the play list
-     * @param item the media item you want to add to the play list
-     * @throws IndexOutOfBoundsException if index is outside play list range
-     * @throws NullPointerException if dsd is null
+     * @param index the index of the item to replace
+     * @param item the new item
      */
-    public void addPlaylistItem(int index, MediaItem2 item) {
-        mProvider.addPlaylistItem_impl(index, item);
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        mProvider.replacePlaylistItem_impl(index, item);
+    }
+
+    /**
+     * Get the lastly cached current item from
+     * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController2, MediaItem2)}.
+     *
+     * @return index of the current item
+     */
+    public MediaItem2 getCurrentMediaItem() {
+        return mProvider.getCurrentMediaItem_impl();
+    }
+
+    /**
+     * Skips to the previous item in the playlist.
+     * <p>
+     * This calls {@link MediaSession2#skipToPreviousItem()} if the session allows.
+     */
+     public void skipToPreviousItem() {
+         mProvider.skipToPreviousItem_impl();
+     }
+
+    /**
+     * Skips to the next item in the playlist.
+     * <p>
+     * This calls {@link MediaSession2#skipToNextItem()} if the session allows.
+     */
+    public void skipToNextItem() {
+        mProvider.skipToNextItem_impl();
+    }
+
+    /**
+     * Skips to the item in the playlist.
+     * <p>
+     * This calls {@link MediaSession2#skipToPlaylistItem(MediaItem2)} if the session allows.
+     *
+     * @param item The item in the playlist you want to play
+     */
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        mProvider.skipToPlaylistItem_impl(item);
+    }
+
+    /**
+     * Gets the cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged(
+     * MediaController2, int)}.
+     *
+     * @return repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    public @RepeatMode int getRepeatMode() {
+        return mProvider.getRepeatMode_impl();
+    }
+
+    /**
+     * Sets the repeat mode.
+     *
+     * @param repeatMode repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    public void setRepeatMode(@RepeatMode int repeatMode) {
+        mProvider.setRepeatMode_impl(repeatMode);
+    }
+
+    /**
+     * Gets the cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged(
+     * MediaController2, int)}.
+     *
+     * @return The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    public @ShuffleMode int getShuffleMode() {
+        return mProvider.getShuffleMode_impl();
+    }
+
+    /**
+     * Sets the shuffle mode.
+     *
+     * @param shuffleMode The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    public void setShuffleMode(@ShuffleMode int shuffleMode) {
+        mProvider.setShuffleMode_impl(shuffleMode);
     }
 }
diff --git a/android/media/MediaController2Test.java b/android/media/MediaController2Test.java
deleted file mode 100644
index ae67a95..0000000
--- a/android/media/MediaController2Test.java
+++ /dev/null
@@ -1,487 +0,0 @@
-/*
- * Copyright 2018 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.media.MediaPlayerBase.PlaybackListener;
-import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
-import android.media.TestUtils.SyncHandler;
-import android.media.session.PlaybackState;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
-import android.support.test.filters.FlakyTest;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static android.media.TestUtils.createPlaybackState;
-import static org.junit.Assert.*;
-
-/**
- * Tests {@link MediaController2}.
- */
-// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
-// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-@FlakyTest
-public class MediaController2Test extends MediaSession2TestBase {
-    private static final String TAG = "MediaController2Test";
-
-    MediaSession2 mSession;
-    MediaController2 mController;
-    MockPlayer mPlayer;
-
-    @Before
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        // Create this test specific MediaSession2 to use our own Handler.
-        sHandler.postAndSync(()->{
-            mPlayer = new MockPlayer(1);
-            mSession = new MediaSession2.Builder(mContext, mPlayer).setId(TAG).build();
-        });
-
-        mController = createController(mSession.getToken());
-        TestServiceRegistry.getInstance().setHandler(sHandler);
-    }
-
-    @After
-    @Override
-    public void cleanUp() throws Exception {
-        super.cleanUp();
-        sHandler.postAndSync(() -> {
-            if (mSession != null) {
-                mSession.close();
-            }
-        });
-        TestServiceRegistry.getInstance().cleanUp();
-    }
-
-    @Test
-    public void testPlay() throws InterruptedException {
-        mController.play();
-        try {
-            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        } catch (InterruptedException e) {
-            fail(e.getMessage());
-        }
-        assertTrue(mPlayer.mPlayCalled);
-    }
-
-    @Test
-    public void testPause() throws InterruptedException {
-        mController.pause();
-        try {
-            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        } catch (InterruptedException e) {
-            fail(e.getMessage());
-        }
-        assertTrue(mPlayer.mPauseCalled);
-    }
-
-
-    @Test
-    public void testSkipToPrevious() throws InterruptedException {
-        mController.skipToPrevious();
-        try {
-            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        } catch (InterruptedException e) {
-            fail(e.getMessage());
-        }
-        assertTrue(mPlayer.mSkipToPreviousCalled);
-    }
-
-    @Test
-    public void testSkipToNext() throws InterruptedException {
-        mController.skipToNext();
-        try {
-            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        } catch (InterruptedException e) {
-            fail(e.getMessage());
-        }
-        assertTrue(mPlayer.mSkipToNextCalled);
-    }
-
-    @Test
-    public void testStop() throws InterruptedException {
-        mController.stop();
-        try {
-            assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        } catch (InterruptedException e) {
-            fail(e.getMessage());
-        }
-        assertTrue(mPlayer.mStopCalled);
-    }
-
-    @Test
-    public void testGetPackageName() {
-        assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
-    }
-
-    @Test
-    public void testGetPlaybackState() throws InterruptedException {
-        // TODO(jaewan): add equivalent test later
-        /*
-        final CountDownLatch latch = new CountDownLatch(1);
-        final MediaPlayerBase.PlaybackListener listener = (state) -> {
-            assertEquals(PlaybackState.STATE_BUFFERING, state.getState());
-            latch.countDown();
-        };
-        assertNull(mController.getPlaybackState());
-        mController.addPlaybackListener(listener, sHandler);
-
-        mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_BUFFERING));
-        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        assertEquals(PlaybackState.STATE_BUFFERING, mController.getPlaybackState().getState());
-        */
-    }
-
-    // TODO(jaewan): add equivalent test later
-    /*
-    @Test
-    public void testAddPlaybackListener() throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(2);
-        final MediaPlayerBase.PlaybackListener listener = (state) -> {
-            switch ((int) latch.getCount()) {
-                case 2:
-                    assertEquals(PlaybackState.STATE_PLAYING, state.getState());
-                    break;
-                case 1:
-                    assertEquals(PlaybackState.STATE_PAUSED, state.getState());
-                    break;
-            }
-            latch.countDown();
-        };
-
-        mController.addPlaybackListener(listener, sHandler);
-        sHandler.postAndSync(()->{
-            mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
-            mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
-        });
-        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-    }
-
-    @Test
-    public void testRemovePlaybackListener() throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(1);
-        final MediaPlayerBase.PlaybackListener listener = (state) -> {
-            fail();
-            latch.countDown();
-        };
-        mController.addPlaybackListener(listener, sHandler);
-        mController.removePlaybackListener(listener);
-        mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
-        assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-    }
-    */
-
-    @Test
-    public void testControllerCallback_onConnected() throws InterruptedException {
-        // createController() uses controller callback to wait until the controller becomes
-        // available.
-        MediaController2 controller = createController(mSession.getToken());
-        assertNotNull(controller);
-    }
-
-    @Test
-    public void testControllerCallback_sessionRejects() throws InterruptedException {
-        final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
-            @Override
-            public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
-                return null;
-            }
-        };
-        sHandler.postAndSync(() -> {
-            mSession.close();
-            mSession = new MediaSession2.Builder(mContext, mPlayer)
-                    .setSessionCallback(sHandlerExecutor, sessionCallback).build();
-        });
-        MediaController2 controller =
-                createController(mSession.getToken(), false, null);
-        assertNotNull(controller);
-        waitForConnect(controller, false);
-        waitForDisconnect(controller, true);
-    }
-
-    @Test
-    public void testControllerCallback_releaseSession() throws InterruptedException {
-        sHandler.postAndSync(() -> {
-            mSession.close();
-        });
-        waitForDisconnect(mController, true);
-    }
-
-    @Test
-    public void testControllerCallback_release() throws InterruptedException {
-        mController.close();
-        waitForDisconnect(mController, true);
-    }
-
-    @Test
-    public void testIsConnected() throws InterruptedException {
-        assertTrue(mController.isConnected());
-        sHandler.postAndSync(()->{
-            mSession.close();
-        });
-        // postAndSync() to wait until the disconnection is propagated.
-        sHandler.postAndSync(()->{
-            assertFalse(mController.isConnected());
-        });
-    }
-
-    /**
-     * Test potential deadlock for calls between controller and session.
-     */
-    @Test
-    public void testDeadlock() throws InterruptedException {
-        sHandler.postAndSync(() -> {
-            mSession.close();
-            mSession = null;
-        });
-
-        // Two more threads are needed not to block test thread nor test wide thread (sHandler).
-        final HandlerThread sessionThread = new HandlerThread("testDeadlock_session");
-        final HandlerThread testThread = new HandlerThread("testDeadlock_test");
-        sessionThread.start();
-        testThread.start();
-        final SyncHandler sessionHandler = new SyncHandler(sessionThread.getLooper());
-        final Handler testHandler = new Handler(testThread.getLooper());
-        final CountDownLatch latch = new CountDownLatch(1);
-        try {
-            final MockPlayer player = new MockPlayer(0);
-            sessionHandler.postAndSync(() -> {
-                mSession = new MediaSession2.Builder(mContext, mPlayer)
-                        .setId("testDeadlock").build();
-            });
-            final MediaController2 controller = createController(mSession.getToken());
-            testHandler.post(() -> {
-                final PlaybackState2 state = createPlaybackState(PlaybackState.STATE_ERROR);
-                for (int i = 0; i < 100; i++) {
-                    // triggers call from session to controller.
-                    player.notifyPlaybackState(state);
-                    // triggers call from controller to session.
-                    controller.play();
-
-                    // Repeat above
-                    player.notifyPlaybackState(state);
-                    controller.pause();
-                    player.notifyPlaybackState(state);
-                    controller.stop();
-                    player.notifyPlaybackState(state);
-                    controller.skipToNext();
-                    player.notifyPlaybackState(state);
-                    controller.skipToPrevious();
-                }
-                // This may hang if deadlock happens.
-                latch.countDown();
-            });
-            assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        } finally {
-            if (mSession != null) {
-                sessionHandler.postAndSync(() -> {
-                    // Clean up here because sessionHandler will be removed afterwards.
-                    mSession.close();
-                    mSession = null;
-                });
-            }
-            if (sessionThread != null) {
-                sessionThread.quitSafely();
-            }
-            if (testThread != null) {
-                testThread.quitSafely();
-            }
-        }
-    }
-
-    @Ignore
-    @Test
-    public void testGetServiceToken() {
-        SessionToken2 token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
-        assertNotNull(token);
-        assertEquals(mContext.getPackageName(), token.getPackageName());
-        assertEquals(MockMediaSessionService2.ID, token.getId());
-        assertNull(token.getSessionBinder());
-        assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
-    }
-
-    private void connectToService(SessionToken2 token) throws InterruptedException {
-        mController = createController(token);
-        mSession = TestServiceRegistry.getInstance().getServiceInstance().getSession();
-        mPlayer = (MockPlayer) mSession.getPlayer();
-    }
-
-    // TODO(jaewan): Reenable when session manager detects app installs
-    @Ignore
-    @Test
-    public void testConnectToService_sessionService() throws InterruptedException {
-        connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
-        testConnectToService();
-    }
-
-    // TODO(jaewan): Reenable when session manager detects app installs
-    @Ignore
-    @Test
-    public void testConnectToService_libraryService() throws InterruptedException {
-        connectToService(TestUtils.getServiceToken(mContext, MockMediaLibraryService2.ID));
-        testConnectToService();
-    }
-
-    public void testConnectToService() throws InterruptedException {
-        TestServiceRegistry serviceInfo = TestServiceRegistry.getInstance();
-        ControllerInfo info = serviceInfo.getOnConnectControllerInfo();
-        assertEquals(mContext.getPackageName(), info.getPackageName());
-        assertEquals(Process.myUid(), info.getUid());
-        assertFalse(info.isTrusted());
-
-        // Test command from controller to session service
-        mController.play();
-        assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-        assertTrue(mPlayer.mPlayCalled);
-
-        // Test command from session service to controller
-        // TODO(jaewan): Add equivalent tests again
-        /*
-        final CountDownLatch latch = new CountDownLatch(1);
-        mController.addPlaybackListener((state) -> {
-            assertNotNull(state);
-            assertEquals(PlaybackState.STATE_REWINDING, state.getState());
-            latch.countDown();
-        }, sHandler);
-        mPlayer.notifyPlaybackState(
-                TestUtils.createPlaybackState(PlaybackState.STATE_REWINDING));
-        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        */
-    }
-
-    @Test
-    public void testControllerAfterSessionIsGone_session() throws InterruptedException {
-        testControllerAfterSessionIsGone(mSession.getToken().getId());
-    }
-
-    @Ignore
-    @Test
-    public void testControllerAfterSessionIsGone_sessionService() throws InterruptedException {
-        connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
-        testControllerAfterSessionIsGone(MockMediaSessionService2.ID);
-    }
-
-    @Test
-    public void testClose_beforeConnected() throws InterruptedException {
-        MediaController2 controller =
-                createController(mSession.getToken(), false, null);
-        controller.close();
-    }
-
-    @Test
-    public void testClose_twice() throws InterruptedException {
-        mController.close();
-        mController.close();
-    }
-
-    @Test
-    public void testClose_session() throws InterruptedException {
-        final String id = mSession.getToken().getId();
-        mController.close();
-        // close is done immediately for session.
-        testNoInteraction();
-
-        // Test whether the controller is notified about later close of the session or
-        // re-creation.
-        testControllerAfterSessionIsGone(id);
-    }
-
-    // TODO(jaewan): Reenable when session manager detects app installs
-    @Ignore
-    @Test
-    public void testClose_sessionService() throws InterruptedException {
-        connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
-        testCloseFromService();
-    }
-
-    // TODO(jaewan): Reenable when session manager detects app installs
-    @Ignore
-    @Test
-    public void testClose_libraryService() throws InterruptedException {
-        connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
-        testCloseFromService();
-    }
-
-    private void testCloseFromService() throws InterruptedException {
-        final String id = mController.getSessionToken().getId();
-        final CountDownLatch latch = new CountDownLatch(1);
-        TestServiceRegistry.getInstance().setServiceInstanceChangedCallback((service) -> {
-            if (service == null) {
-                // Destroying..
-                latch.countDown();
-            }
-        });
-        mController.close();
-        // Wait until close triggers onDestroy() of the session service.
-        assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        assertNull(TestServiceRegistry.getInstance().getServiceInstance());
-        testNoInteraction();
-
-        // Test whether the controller is notified about later close of the session or
-        // re-creation.
-        testControllerAfterSessionIsGone(id);
-    }
-
-    private void testControllerAfterSessionIsGone(final String id) throws InterruptedException {
-        sHandler.postAndSync(() -> {
-            // TODO(jaewan): Use Session.close later when we add the API.
-            mSession.close();
-        });
-        waitForDisconnect(mController, true);
-        testNoInteraction();
-
-        // Test with the newly created session.
-        sHandler.postAndSync(() -> {
-            // Recreated session has different session stub, so previously created controller
-            // shouldn't be available.
-            mSession = new MediaSession2.Builder(mContext, mPlayer).setId(id).build();
-        });
-        testNoInteraction();
-    }
-
-    private void testNoInteraction() throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(1);
-        final PlaybackListener playbackListener = (state) -> {
-            fail("Controller shouldn't be notified about change in session after the close.");
-            latch.countDown();
-        };
-        // TODO(jaewan): Add equivalent tests again
-        /*
-        mController.addPlaybackListener(playbackListener, sHandler);
-        mPlayer.notifyPlaybackState(TestUtils.createPlaybackState(PlaybackState.STATE_BUFFERING));
-        assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-        mController.removePlaybackListener(playbackListener);
-        */
-    }
-
-    // TODO(jaewan): Add  test for service connect rejection, when we differentiate session
-    //               active/inactive and connection accept/refuse
-}
diff --git a/android/media/MediaDataSource.java b/android/media/MediaDataSource.java
index 948da0b..4ba2120 100644
--- a/android/media/MediaDataSource.java
+++ b/android/media/MediaDataSource.java
@@ -34,8 +34,8 @@
     /**
      * Called to request data from the given position.
      *
-     * Implementations should should write up to {@code size} bytes into
-     * {@code buffer}, and return the number of bytes written.
+     * Implementations should fill {@code buffer} with up to {@code size}
+     * bytes of data, and return the number of valid bytes in the buffer.
      *
      * Return {@code 0} if size is zero (thus no bytes are read).
      *
diff --git a/android/media/MediaDescrambler.java b/android/media/MediaDescrambler.java
index 40c837b..99bd254 100644
--- a/android/media/MediaDescrambler.java
+++ b/android/media/MediaDescrambler.java
@@ -125,6 +125,38 @@
     }
 
     /**
+     * Scramble control value indicating that the samples are not scrambled.
+     * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo)
+     */
+    public static final byte SCRAMBLE_CONTROL_UNSCRAMBLED = 0;
+
+    /**
+     * Scramble control value reserved and shouldn't be used currently.
+     * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo)
+     */
+    public static final byte SCRAMBLE_CONTROL_RESERVED    = 1;
+
+    /**
+     * Scramble control value indicating that the even key is used.
+     * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo)
+     */
+    public static final byte SCRAMBLE_CONTROL_EVEN_KEY     = 2;
+
+    /**
+     * Scramble control value indicating that the odd key is used.
+     * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo)
+     */
+    public static final byte SCRAMBLE_CONTROL_ODD_KEY      = 3;
+
+    /**
+     * Scramble flag for a hint indicating that the descrambling request is for
+     * retrieving the PES header info only.
+     *
+     * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo)
+     */
+    public static final byte SCRAMBLE_FLAG_PES_HEADER = (1 << 0);
+
+    /**
      * Descramble a ByteBuffer of data described by a
      * {@link android.media.MediaCodec.CryptoInfo} structure.
      *
@@ -133,7 +165,15 @@
      * @param dstBuf ByteBuffer to hold the descrambled data, which starts at
      * dstBuf.position().
      * @param cryptoInfo a {@link android.media.MediaCodec.CryptoInfo} structure
-     * describing the subsamples contained in src.
+     * describing the subsamples contained in srcBuf. The iv and mode fields in
+     * CryptoInfo are not used. key[0] contains the MPEG2TS scrambling control bits
+     * (as defined in ETSI TS 100 289 (2011): "Digital Video Broadcasting (DVB);
+     * Support for use of the DVB Scrambling Algorithm version 3 within digital
+     * broadcasting systems"), and the value must be one of {@link #SCRAMBLE_CONTROL_UNSCRAMBLED},
+     * {@link #SCRAMBLE_CONTROL_RESERVED}, {@link #SCRAMBLE_CONTROL_EVEN_KEY} or
+     * {@link #SCRAMBLE_CONTROL_ODD_KEY}. key[1] is a set of bit flags, with the
+     * only possible bit being {@link #SCRAMBLE_FLAG_PES_HEADER} currently.
+     * key[2~15] are not used.
      *
      * @return number of bytes that have been successfully descrambled, with negative
      * values indicating errors.
@@ -169,6 +209,7 @@
         try {
             return native_descramble(
                     cryptoInfo.key[0],
+                    cryptoInfo.key[1],
                     cryptoInfo.numSubSamples,
                     cryptoInfo.numBytesOfClearData,
                     cryptoInfo.numBytesOfEncryptedData,
@@ -204,7 +245,8 @@
     private native final void native_setup(@NonNull IHwBinder decramblerBinder);
     private native final void native_release();
     private native final int native_descramble(
-            byte key, int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
+            byte key, byte flags, int numSubSamples,
+            int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
             @NonNull ByteBuffer srcBuf, int srcOffset, int srcLimit,
             ByteBuffer dstBuf, int dstOffset, int dstLimit) throws RemoteException;
 
diff --git a/android/media/MediaDrm.java b/android/media/MediaDrm.java
index 063186d..7ac1529 100644
--- a/android/media/MediaDrm.java
+++ b/android/media/MediaDrm.java
@@ -628,14 +628,48 @@
     }
 
     /**
-     * Open a new session with the MediaDrm object.  A session ID is returned.
+     * Open a new session with the MediaDrm object. A session ID is returned.
+     * By default, sessions are opened at the native security level of the device.
      *
      * @throws NotProvisionedException if provisioning is needed
      * @throws ResourceBusyException if required resources are in use
      */
     @NonNull
-    public native byte[] openSession() throws NotProvisionedException,
-            ResourceBusyException;
+    public byte[] openSession() throws NotProvisionedException,
+            ResourceBusyException {
+        return openSession(getMaxSecurityLevel());
+    }
+
+    /**
+     * Open a new session at a requested security level. The security level
+     * represents the robustness of the device's DRM implementation. By default,
+     * sessions are opened at the native security level of the device.
+     * Overriding the security level is necessary when the decrypted frames need
+     * to be manipulated, such as for image compositing. The security level
+     * parameter must be lower than the native level. Reducing the security
+     * level will typically limit the content to lower resolutions, as
+     * determined by the license policy. If the requested level is not
+     * supported, the next lower supported security level will be set. The level
+     * can be queried using {@link #getSecurityLevel}. A session
+     * ID is returned.
+     *
+     * @param level the new security level, one of
+     * {@link #SECURITY_LEVEL_SW_SECURE_CRYPTO},
+     * {@link #SECURITY_LEVEL_SW_SECURE_DECODE},
+     * {@link #SECURITY_LEVEL_HW_SECURE_CRYPTO},
+     * {@link #SECURITY_LEVEL_HW_SECURE_DECODE} or
+     * {@link #SECURITY_LEVEL_HW_SECURE_ALL}.
+     *
+     * @throws NotProvisionedException if provisioning is needed
+     * @throws ResourceBusyException if required resources are in use
+     * @throws IllegalArgumentException if the requested security level is
+     * higher than the native level or lower than the lowest supported level or
+     * if the device does not support specifying the security level when opening
+     * a session
+     */
+    @NonNull
+    public native byte[] openSession(@SecurityLevel int level) throws
+            NotProvisionedException, ResourceBusyException;
 
     /**
      * Close a session on the MediaDrm object that was previously opened
@@ -671,7 +705,9 @@
     public @interface KeyType {}
 
     /**
-     * Contains the opaque data an app uses to request keys from a license server
+     * Contains the opaque data an app uses to request keys from a license server.
+     * These request types may or may not be generated by a given plugin. Refer
+     * to plugin vendor documentation for more information.
      */
     public static final class KeyRequest {
         private byte[] mData;
@@ -696,8 +732,8 @@
         public static final int REQUEST_TYPE_RELEASE = 2;
 
         /**
-         * Keys are already loaded. No license request is necessary, and no
-         * key request data is returned.
+         * Keys are already loaded and are available for use. No license request is necessary, and
+         * no key request data is returned.
          */
         public static final int REQUEST_TYPE_NONE = 3;
 
@@ -942,43 +978,84 @@
             throws DeniedByServerException;
 
     /**
-     * A means of enforcing limits on the number of concurrent streams per subscriber
-     * across devices is provided via SecureStop. This is achieved by securely
-     * monitoring the lifetime of sessions.
+     * Secure stops are a way to enforce limits on the number of concurrent
+     * streams per subscriber across devices. They provide secure monitoring of
+     * the lifetime of content decryption keys in MediaDrm sessions.
      * <p>
-     * Information from the server related to the current playback session is written
-     * to persistent storage on the device when each MediaCrypto object is created.
+     * A secure stop is written to secure persistent memory when keys are loaded
+     * into a MediaDrm session. The secure stop state indicates that the keys
+     * are available for use. When playback completes and the keys are removed
+     * or the session is destroyed, the secure stop state is updated to indicate
+     * that keys are no longer usable.
      * <p>
-     * In the normal case, playback will be completed, the session destroyed and the
-     * Secure Stops will be queried. The app queries secure stops and forwards the
-     * secure stop message to the server which verifies the signature and notifies the
-     * server side database that the session destruction has been confirmed. The persisted
-     * record on the client is only removed after positive confirmation that the server
-     * received the message using releaseSecureStops().
+     * After playback, the app can query the secure stop and send it in a
+     * message to the license server confirming that the keys are no longer
+     * active. The license server returns a secure stop release response
+     * message to the app which then deletes the secure stop from persistent
+     * memory using {@link #releaseSecureStops}.
+     * <p>
+     * Each secure stop has a unique ID that can be used to identify it during
+     * enumeration, access and removal.
+     * @return a list of all secure stops from secure persistent memory
      */
     @NonNull
     public native List<byte[]> getSecureStops();
 
     /**
-     * Access secure stop by secure stop ID.
+     * Return a list of all secure stop IDs currently in persistent memory.
+     * The secure stop ID can be used to access or remove the corresponding
+     * secure stop.
      *
-     * @param ssid - The secure stop ID provided by the license server.
+     * @return a list of secure stop IDs
+     */
+    @NonNull
+    public native List<byte[]> getSecureStopIds();
+
+    /**
+     * Access a specific secure stop given its secure stop ID.
+     * Each secure stop has a unique ID.
+     *
+     * @param ssid the ID of the secure stop to return
+     * @return the secure stop identified by ssid
      */
     @NonNull
     public native byte[] getSecureStop(@NonNull byte[] ssid);
 
     /**
-     * Process the SecureStop server response message ssRelease.  After authenticating
-     * the message, remove the SecureStops identified in the response.
+     * Process the secure stop server response message ssRelease.  After
+     * authenticating the message, remove the secure stops identified in the
+     * response.
      *
      * @param ssRelease the server response indicating which secure stops to release
      */
     public native void releaseSecureStops(@NonNull byte[] ssRelease);
 
     /**
-     * Remove all secure stops without requiring interaction with the server.
+     * Remove a specific secure stop without requiring a secure stop release message
+     * from the license server.
+     * @param ssid the ID of the secure stop to remove
      */
-    public native void releaseAllSecureStops();
+    public native void removeSecureStop(@NonNull byte[] ssid);
+
+    /**
+     * Remove all secure stops without requiring a secure stop release message from
+     * the license server.
+     *
+     * This method was added in API 28. In API versions 18 through 27,
+     * {@link #releaseAllSecureStops} should be called instead. There is no need to
+     * do anything for API versions prior to 18.
+     */
+    public native void removeAllSecureStops();
+
+    /**
+     * Remove all secure stops without requiring a secure stop release message from
+     * the license server.
+     *
+     * @deprecated Remove all secure stops using {@link #removeAllSecureStops} instead.
+     */
+    public void releaseAllSecureStops() {
+        removeAllSecureStops();;
+    }
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({HDCP_LEVEL_UNKNOWN, HDCP_NONE, HDCP_V1, HDCP_V2,
@@ -1073,8 +1150,9 @@
      * implementation.
      */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({SECURITY_LEVEL_UNKNOWN, SW_SECURE_CRYPTO, SW_SECURE_DECODE,
-                        HW_SECURE_CRYPTO, HW_SECURE_DECODE, HW_SECURE_ALL})
+    @IntDef({SECURITY_LEVEL_UNKNOWN, SECURITY_LEVEL_SW_SECURE_CRYPTO,
+            SECURITY_LEVEL_SW_SECURE_DECODE, SECURITY_LEVEL_HW_SECURE_CRYPTO,
+            SECURITY_LEVEL_HW_SECURE_DECODE, SECURITY_LEVEL_HW_SECURE_ALL})
     public @interface SecurityLevel {}
 
     /**
@@ -1084,65 +1162,66 @@
     public static final int SECURITY_LEVEL_UNKNOWN = 0;
 
     /**
-     *  Software-based whitebox crypto
+     * DRM key management uses software-based whitebox crypto.
      */
-    public static final int SW_SECURE_CRYPTO = 1;
+    public static final int SECURITY_LEVEL_SW_SECURE_CRYPTO = 1;
 
     /**
-     * Software-based whitebox crypto and an obfuscated decoder
+     * DRM key management and decoding use software-based whitebox crypto.
      */
-     public static final int SW_SECURE_DECODE = 2;
+    public static final int SECURITY_LEVEL_SW_SECURE_DECODE = 2;
 
     /**
-     * DRM key management and crypto operations are performed within a
-     * hardware backed trusted execution environment
+     * DRM key management and crypto operations are performed within a hardware
+     * backed trusted execution environment.
      */
-    public static final int HW_SECURE_CRYPTO = 3;
+    public static final int SECURITY_LEVEL_HW_SECURE_CRYPTO = 3;
 
     /**
-     * DRM key management, crypto operations and decoding of content
-     * are performed within a hardware backed trusted execution environment
+     * DRM key management, crypto operations and decoding of content are
+     * performed within a hardware backed trusted execution environment.
      */
-     public static final int HW_SECURE_DECODE = 4;
+    public static final int SECURITY_LEVEL_HW_SECURE_DECODE = 4;
 
     /**
      * DRM key management, crypto operations, decoding of content and all
-     * handling of the media (compressed and uncompressed) is handled within
-     * a hardware backed trusted execution environment.
+     * handling of the media (compressed and uncompressed) is handled within a
+     * hardware backed trusted execution environment.
      */
-    public static final int HW_SECURE_ALL = 5;
+    public static final int SECURITY_LEVEL_HW_SECURE_ALL = 5;
 
     /**
-     * Return the current security level of a session. A session
-     * has an initial security level determined by the robustness of
-     * the DRM system's implementation on the device. The security
-     * level may be adjusted using {@link #setSecurityLevel}.
+     * The maximum security level supported by the device. This is the default
+     * security level when a session is opened.
+     * @hide
+     */
+    public static final int SECURITY_LEVEL_MAX = 6;
+
+    /**
+     * The maximum security level supported by the device. This is the default
+     * security level when a session is opened.
+     */
+    @SecurityLevel
+    public static final int getMaxSecurityLevel() {
+        return SECURITY_LEVEL_MAX;
+    }
+
+    /**
+     * Return the current security level of a session. A session has an initial
+     * security level determined by the robustness of the DRM system's
+     * implementation on the device. The security level may be changed at the
+     * time a session is opened using {@link #openSession}.
      * @param sessionId the session to query.
      * <p>
      * @return one of {@link #SECURITY_LEVEL_UNKNOWN},
-     * {@link #SW_SECURE_CRYPTO}, {@link #SW_SECURE_DECODE},
-     * {@link #HW_SECURE_CRYPTO}, {@link #HW_SECURE_DECODE} or
-     * {@link #HW_SECURE_ALL}.
+     * {@link #SECURITY_LEVEL_SW_SECURE_CRYPTO}, {@link #SECURITY_LEVEL_SW_SECURE_DECODE},
+     * {@link #SECURITY_LEVEL_HW_SECURE_CRYPTO}, {@link #SECURITY_LEVEL_HW_SECURE_DECODE} or
+     * {@link #SECURITY_LEVEL_HW_SECURE_ALL}.
      */
     @SecurityLevel
     public native int getSecurityLevel(@NonNull byte[] sessionId);
 
     /**
-     * Set the security level of a session. This can be useful if specific
-     * attributes of a lower security level are needed by an application,
-     * such as image manipulation or compositing. Reducing the security
-     * level will typically limit decryption to lower content resolutions,
-     * depending on the license policy.
-     * @param sessionId the session to set the security level on.
-     * @param level the new security level, one of
-     * {@link #SW_SECURE_CRYPTO}, {@link #SW_SECURE_DECODE},
-     * {@link #HW_SECURE_CRYPTO}, {@link #HW_SECURE_DECODE} or
-     * {@link #HW_SECURE_ALL}.
-     */
-    public native void setSecurityLevel(@NonNull byte[] sessionId,
-            @SecurityLevel int level);
-
-    /**
      * String property name: identifies the maker of the DRM plugin
      */
     public static final String PROPERTY_VENDOR = "vendor";
@@ -1253,8 +1332,6 @@
      *
      * Additional vendor-specific fields may also be present in
      * the return value.
-     *
-     * @hide - not part of the public API at this time
      */
     public PersistableBundle getMetrics() {
         PersistableBundle bundle = getMetricsNative();
@@ -1571,8 +1648,6 @@
     /**
      * Definitions for the metrics that are reported via the
      * {@link #getMetrics} call.
-     *
-     * @hide - not part of the public API at this time
      */
     public final static class MetricsConstants
     {
@@ -1582,16 +1657,350 @@
          * Key to extract the number of successful {@link #openSession} calls
          * from the {@link PersistableBundle} returned by a
          * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
          */
         public static final String OPEN_SESSION_OK_COUNT
-            = "/drm/mediadrm/open_session/ok/count";
+            = "drm.mediadrm.open_session.ok.count";
 
         /**
          * Key to extract the number of failed {@link #openSession} calls
          * from the {@link PersistableBundle} returned by a
          * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
          */
         public static final String OPEN_SESSION_ERROR_COUNT
-            = "/drm/mediadrm/open_session/error/count";
+            = "drm.mediadrm.open_session.error.count";
+
+        /**
+         * Key to extract the list of error codes that were returned from
+         * {@link #openSession} calls. The key is used to lookup the list
+         * in the {@link PersistableBundle} returned by a {@link #getMetrics}
+         * call.
+         * The list is an array of Long values
+         * ({@link android.os.BaseBundle#getLongArray}).
+         */
+        public static final String OPEN_SESSION_ERROR_LIST
+            = "drm.mediadrm.open_session.error.list";
+
+        /**
+         * Key to extract the number of successful {@link #closeSession} calls
+         * from the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String CLOSE_SESSION_OK_COUNT
+            = "drm.mediadrm.close_session.ok.count";
+
+        /**
+         * Key to extract the number of failed {@link #closeSession} calls
+         * from the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String CLOSE_SESSION_ERROR_COUNT
+            = "drm.mediadrm.close_session.error.count";
+
+        /**
+         * Key to extract the list of error codes that were returned from
+         * {@link #closeSession} calls. The key is used to lookup the list
+         * in the {@link PersistableBundle} returned by a {@link #getMetrics}
+         * call.
+         * The list is an array of Long values
+         * ({@link android.os.BaseBundle#getLongArray}).
+         */
+        public static final String CLOSE_SESSION_ERROR_LIST
+            = "drm.mediadrm.close_session.error.list";
+
+        /**
+         * Key to extract the start times of sessions. Times are
+         * represented as milliseconds since epoch (1970-01-01T00:00:00Z).
+         * The start times are returned from the {@link PersistableBundle}
+         * from a {@link #getMetrics} call.
+         * The start times are returned as another {@link PersistableBundle}
+         * containing the session ids as keys and the start times as long
+         * values. Use {@link android.os.BaseBundle#keySet} to get the list of
+         * session ids, and then {@link android.os.BaseBundle#getLong} to get
+         * the start time for each session.
+         */
+        public static final String SESSION_START_TIMES_MS
+            = "drm.mediadrm.session_start_times_ms";
+
+        /**
+         * Key to extract the end times of sessions. Times are
+         * represented as milliseconds since epoch (1970-01-01T00:00:00Z).
+         * The end times are returned from the {@link PersistableBundle}
+         * from a {@link #getMetrics} call.
+         * The end times are returned as another {@link PersistableBundle}
+         * containing the session ids as keys and the end times as long
+         * values. Use {@link android.os.BaseBundle#keySet} to get the list of
+         * session ids, and then {@link android.os.BaseBundle#getLong} to get
+         * the end time for each session.
+         */
+        public static final String SESSION_END_TIMES_MS
+            = "drm.mediadrm.session_end_times_ms";
+
+        /**
+         * Key to extract the number of successful {@link #getKeyRequest} calls
+         * from the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String GET_KEY_REQUEST_OK_COUNT
+            = "drm.mediadrm.get_key_request.ok.count";
+
+        /**
+         * Key to extract the number of failed {@link #getKeyRequest}
+         * calls from the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String GET_KEY_REQUEST_ERROR_COUNT
+            = "drm.mediadrm.get_key_request.error.count";
+
+        /**
+         * Key to extract the list of error codes that were returned from
+         * {@link #getKeyRequest} calls. The key is used to lookup the list
+         * in the {@link PersistableBundle} returned by a {@link #getMetrics}
+         * call.
+         * The list is an array of Long values
+         * ({@link android.os.BaseBundle#getLongArray}).
+         */
+        public static final String GET_KEY_REQUEST_ERROR_LIST
+            = "drm.mediadrm.get_key_request.error.list";
+
+        /**
+         * Key to extract the average time in microseconds of calls to
+         * {@link #getKeyRequest}. The value is retrieved from the
+         * {@link PersistableBundle} returned from {@link #getMetrics}.
+         * The time is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String GET_KEY_REQUEST_OK_TIME_MICROS
+            = "drm.mediadrm.get_key_request.ok.average_time_micros";
+
+        /**
+         * Key to extract the number of successful {@link #provideKeyResponse}
+         * calls from the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String PROVIDE_KEY_RESPONSE_OK_COUNT
+            = "drm.mediadrm.provide_key_response.ok.count";
+
+        /**
+         * Key to extract the number of failed {@link #provideKeyResponse}
+         * calls from the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String PROVIDE_KEY_RESPONSE_ERROR_COUNT
+            = "drm.mediadrm.provide_key_response.error.count";
+
+        /**
+         * Key to extract the list of error codes that were returned from
+         * {@link #provideKeyResponse} calls. The key is used to lookup the
+         * list in the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The list is an array of Long values
+         * ({@link android.os.BaseBundle#getLongArray}).
+         */
+        public static final String PROVIDE_KEY_RESPONSE_ERROR_LIST
+            = "drm.mediadrm.provide_key_response.error.list";
+
+        /**
+         * Key to extract the average time in microseconds of calls to
+         * {@link #provideKeyResponse}. The valus is retrieved from the
+         * {@link PersistableBundle} returned from {@link #getMetrics}.
+         * The time is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String PROVIDE_KEY_RESPONSE_OK_TIME_MICROS
+            = "drm.mediadrm.provide_key_response.ok.average_time_micros";
+
+        /**
+         * Key to extract the number of successful {@link #getProvisionRequest}
+         * calls from the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String GET_PROVISION_REQUEST_OK_COUNT
+            = "drm.mediadrm.get_provision_request.ok.count";
+
+        /**
+         * Key to extract the number of failed {@link #getProvisionRequest}
+         * calls from the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String GET_PROVISION_REQUEST_ERROR_COUNT
+            = "drm.mediadrm.get_provision_request.error.count";
+
+        /**
+         * Key to extract the list of error codes that were returned from
+         * {@link #getProvisionRequest} calls. The key is used to lookup the
+         * list in the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The list is an array of Long values
+         * ({@link android.os.BaseBundle#getLongArray}).
+         */
+        public static final String GET_PROVISION_REQUEST_ERROR_LIST
+            = "drm.mediadrm.get_provision_request.error.list";
+
+        /**
+         * Key to extract the number of successful
+         * {@link #provideProvisionResponse} calls from the
+         * {@link PersistableBundle} returned by a {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String PROVIDE_PROVISION_RESPONSE_OK_COUNT
+            = "drm.mediadrm.provide_provision_response.ok.count";
+
+        /**
+         * Key to extract the number of failed
+         * {@link #provideProvisionResponse} calls from the
+         * {@link PersistableBundle} returned by a {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String PROVIDE_PROVISION_RESPONSE_ERROR_COUNT
+            = "drm.mediadrm.provide_provision_response.error.count";
+
+        /**
+         * Key to extract the list of error codes that were returned from
+         * {@link #provideProvisionResponse} calls. The key is used to lookup
+         * the list in the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The list is an array of Long values
+         * ({@link android.os.BaseBundle#getLongArray}).
+         */
+        public static final String PROVIDE_PROVISION_RESPONSE_ERROR_LIST
+            = "drm.mediadrm.provide_provision_response.error.list";
+
+        /**
+         * Key to extract the number of successful
+         * {@link #getPropertyByteArray} calls were made with the
+         * {@link #PROPERTY_DEVICE_UNIQUE_ID} value. The key is used to lookup
+         * the value in the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String GET_DEVICE_UNIQUE_ID_OK_COUNT
+            = "drm.mediadrm.get_device_unique_id.ok.count";
+
+        /**
+         * Key to extract the number of failed
+         * {@link #getPropertyByteArray} calls were made with the
+         * {@link #PROPERTY_DEVICE_UNIQUE_ID} value. The key is used to lookup
+         * the value in the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String GET_DEVICE_UNIQUE_ID_ERROR_COUNT
+            = "drm.mediadrm.get_device_unique_id.error.count";
+
+        /**
+         * Key to extract the list of error codes that were returned from
+         * {@link #getPropertyByteArray} calls with the
+         * {@link #PROPERTY_DEVICE_UNIQUE_ID} value. The key is used to lookup
+         * the list in the {@link PersistableBundle} returned by a
+         * {@link #getMetrics} call.
+         * The list is an array of Long values
+         * ({@link android.os.BaseBundle#getLongArray}).
+         */
+        public static final String GET_DEVICE_UNIQUE_ID_ERROR_LIST
+            = "drm.mediadrm.get_device_unique_id.error.list";
+
+        /**
+         * Key to extraact the count of {@link KeyStatus#STATUS_EXPIRED} events
+         * that occured. The count is extracted from the
+         * {@link PersistableBundle} returned from a {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String KEY_STATUS_EXPIRED_COUNT
+            = "drm.mediadrm.key_status.EXPIRED.count";
+
+        /**
+         * Key to extract the count of {@link KeyStatus#STATUS_INTERNAL_ERROR}
+         * events that occured. The count is extracted from the
+         * {@link PersistableBundle} returned from a {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String KEY_STATUS_INTERNAL_ERROR_COUNT
+            = "drm.mediadrm.key_status.INTERNAL_ERROR.count";
+
+        /**
+         * Key to extract the count of
+         * {@link KeyStatus#STATUS_OUTPUT_NOT_ALLOWED} events that occured.
+         * The count is extracted from the
+         * {@link PersistableBundle} returned from a {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String KEY_STATUS_OUTPUT_NOT_ALLOWED_COUNT
+            = "drm.mediadrm.key_status_change.OUTPUT_NOT_ALLOWED.count";
+
+        /**
+         * Key to extract the count of {@link KeyStatus#STATUS_PENDING}
+         * events that occured. The count is extracted from the
+         * {@link PersistableBundle} returned from a {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String KEY_STATUS_PENDING_COUNT
+            = "drm.mediadrm.key_status_change.PENDING.count";
+
+        /**
+         * Key to extract the count of {@link KeyStatus#STATUS_USABLE}
+         * events that occured. The count is extracted from the
+         * {@link PersistableBundle} returned from a {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String KEY_STATUS_USABLE_COUNT
+            = "drm.mediadrm.key_status_change.USABLE.count";
+
+        /**
+         * Key to extract the count of {@link OnEventListener#onEvent}
+         * calls of type PROVISION_REQUIRED occured. The count is
+         * extracted from the {@link PersistableBundle} returned from a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String EVENT_PROVISION_REQUIRED_COUNT
+            = "drm.mediadrm.event.PROVISION_REQUIRED.count";
+
+        /**
+         * Key to extract the count of {@link OnEventListener#onEvent}
+         * calls of type KEY_NEEDED occured. The count is
+         * extracted from the {@link PersistableBundle} returned from a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String EVENT_KEY_NEEDED_COUNT
+            = "drm.mediadrm.event.KEY_NEEDED.count";
+
+        /**
+         * Key to extract the count of {@link OnEventListener#onEvent}
+         * calls of type KEY_EXPIRED occured. The count is
+         * extracted from the {@link PersistableBundle} returned from a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String EVENT_KEY_EXPIRED_COUNT
+            = "drm.mediadrm.event.KEY_EXPIRED.count";
+
+        /**
+         * Key to extract the count of {@link OnEventListener#onEvent}
+         * calls of type VENDOR_DEFINED. The count is
+         * extracted from the {@link PersistableBundle} returned from a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String EVENT_VENDOR_DEFINED_COUNT
+            = "drm.mediadrm.event.VENDOR_DEFINED.count";
+
+        /**
+         * Key to extract the count of {@link OnEventListener#onEvent}
+         * calls of type SESSION_RECLAIMED. The count is
+         * extracted from the {@link PersistableBundle} returned from a
+         * {@link #getMetrics} call.
+         * The count is a Long value ({@link android.os.BaseBundle#getLong}).
+         */
+        public static final String EVENT_SESSION_RECLAIMED_COUNT
+            = "drm.mediadrm.event.SESSION_RECLAIMED.count";
     }
 }
diff --git a/android/media/MediaExtractor.java b/android/media/MediaExtractor.java
index 2c1b4b3..4919eeb 100644
--- a/android/media/MediaExtractor.java
+++ b/android/media/MediaExtractor.java
@@ -22,6 +22,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
+import android.media.AudioPresentation;
 import android.media.MediaCodec;
 import android.media.MediaFormat;
 import android.media.MediaHTTPService;
@@ -40,6 +41,7 @@
 import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
@@ -396,6 +398,17 @@
     }
 
     /**
+     * Get the list of available audio presentations for the track.
+     * @param trackIndex index of the track.
+     * @return a list of available audio presentations for a given valid audio track index.
+     * The list will be empty if the source does not contain any audio presentations.
+     */
+    @NonNull
+    public List<AudioPresentation> getAudioPresentations(int trackIndex) {
+        return new ArrayList<AudioPresentation>();
+    }
+
+    /**
      * Get the PSSH info if present.
      * @return a map of uuid-to-bytes, with the uuid specifying
      * the crypto scheme, and the bytes being the data specific to that scheme.
@@ -626,6 +639,12 @@
      */
     public native long getSampleTime();
 
+    /**
+     * @return size of the current sample in bytes or -1 if no more
+     * samples are available.
+     */
+    public native long getSampleSize();
+
     // Keep these in sync with their equivalents in NuMediaExtractor.h
     /**
      * The sample is a sync sample (or in {@link MediaCodec}'s terminology
diff --git a/android/media/MediaFormat.java b/android/media/MediaFormat.java
index e9128e4..384326f 100644
--- a/android/media/MediaFormat.java
+++ b/android/media/MediaFormat.java
@@ -87,6 +87,7 @@
  * <tr><td>{@link #KEY_AAC_DRC_ATTENUATION_FACTOR}</td><td>Integer</td><td><b>decoder-only</b>, optional, if content is AAC audio, specifies the DRC attenuation factor.</td></tr>
  * <tr><td>{@link #KEY_AAC_DRC_HEAVY_COMPRESSION}</td><td>Integer</td><td><b>decoder-only</b>, optional, if content is AAC audio, specifies whether to use heavy compression.</td></tr>
  * <tr><td>{@link #KEY_AAC_MAX_OUTPUT_CHANNEL_COUNT}</td><td>Integer</td><td><b>decoder-only</b>, optional, if content is AAC audio, specifies the maximum number of channels the decoder outputs.</td></tr>
+ * <tr><td>{@link #KEY_AAC_DRC_EFFECT_TYPE}</td><td>Integer</td><td><b>decoder-only</b>, optional, if content is AAC audio, specifies the DRC effect type to use.</td></tr>
  * <tr><td>{@link #KEY_CHANNEL_MASK}</td><td>Integer</td><td>optional, a mask of audio channel assignments</td></tr>
  * <tr><td>{@link #KEY_FLAC_COMPRESSION_LEVEL}</td><td>Integer</td><td><b>encoder-only</b>, optional, if content is FLAC audio, specifies the desired compression level.</td></tr>
  * </table>
@@ -104,10 +105,10 @@
  * <tr><td>{@link #KEY_HEIGHT}</td><td>Integer</td><td></td></tr>
  * <tr><td>{@link #KEY_COLOR_FORMAT}</td><td>Integer</td><td>set by the user
  *         for encoders, readable in the output format of decoders</b></td></tr>
- * <tr><td>{@link #KEY_GRID_WIDTH}</td><td>Integer</td><td>required if the image has grid</td></tr>
- * <tr><td>{@link #KEY_GRID_HEIGHT}</td><td>Integer</td><td>required if the image has grid</td></tr>
+ * <tr><td>{@link #KEY_TILE_WIDTH}</td><td>Integer</td><td>required if the image has grid</td></tr>
+ * <tr><td>{@link #KEY_TILE_HEIGHT}</td><td>Integer</td><td>required if the image has grid</td></tr>
  * <tr><td>{@link #KEY_GRID_ROWS}</td><td>Integer</td><td>required if the image has grid</td></tr>
- * <tr><td>{@link #KEY_GRID_COLS}</td><td>Integer</td><td>required if the image has grid</td></tr>
+ * <tr><td>{@link #KEY_GRID_COLUMNS}</td><td>Integer</td><td>required if the image has grid</td></tr>
  * </table>
  */
 public final class MediaFormat {
@@ -149,17 +150,17 @@
      * The track's MediaFormat will come with {@link #KEY_WIDTH} and
      * {@link #KEY_HEIGHT} keys, which describes the width and height
      * of the image. If the image doesn't contain grid (i.e. none of
-     * {@link #KEY_GRID_WIDTH}, {@link #KEY_GRID_HEIGHT},
-     * {@link #KEY_GRID_ROWS}, {@link #KEY_GRID_COLS} are present}), the
+     * {@link #KEY_TILE_WIDTH}, {@link #KEY_TILE_HEIGHT},
+     * {@link #KEY_GRID_ROWS}, {@link #KEY_GRID_COLUMNS} are present}), the
      * track will contain a single sample of coded data for the entire image,
      * and the image width and height should be used to set up the decoder.
      *
      * If the image does come with grid, each sample from the track will
      * contain one tile in the grid, of which the size is described by
-     * {@link #KEY_GRID_WIDTH} and {@link #KEY_GRID_HEIGHT}. This size
+     * {@link #KEY_TILE_WIDTH} and {@link #KEY_TILE_HEIGHT}. This size
      * (instead of {@link #KEY_WIDTH} and {@link #KEY_HEIGHT}) should be
      * used to set up the decoder. The track contains {@link #KEY_GRID_ROWS}
-     * by {@link #KEY_GRID_COLS} samples in row-major, top-row first,
+     * by {@link #KEY_GRID_COLUMNS} samples in row-major, top-row first,
      * left-to-right order. The output image should be reconstructed by
      * first tiling the decoding results of the tiles in the correct order,
      * then trimming (before rotation is applied) on the bottom and right
@@ -173,10 +174,20 @@
     public static final String MIMETYPE_TEXT_VTT = "text/vtt";
 
     /**
+     * MIME type for SubRip (SRT) container.
+     */
+    public static final String MIMETYPE_TEXT_SUBRIP = "application/x-subrip";
+
+    /**
      * MIME type for CEA-608 closed caption data.
      */
     public static final String MIMETYPE_TEXT_CEA_608 = "text/cea-608";
 
+    /**
+     * MIME type for CEA-708 closed caption data.
+     */
+    public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708";
+
     private Map<String, Object> mMap;
 
     /**
@@ -274,28 +285,28 @@
     public static final String KEY_FRAME_RATE = "frame-rate";
 
     /**
-     * A key describing the grid width of the content in a {@link #MIMETYPE_IMAGE_ANDROID_HEIC}
-     * track. The associated value is an integer.
+     * A key describing the width (in pixels) of each tile of the content in a
+     * {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track. The associated value is an integer.
      *
      * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks.
      *
-     * @see #KEY_GRID_HEIGHT
+     * @see #KEY_TILE_HEIGHT
      * @see #KEY_GRID_ROWS
-     * @see #KEY_GRID_COLS
+     * @see #KEY_GRID_COLUMNS
      */
-    public static final String KEY_GRID_WIDTH = "grid-width";
+    public static final String KEY_TILE_WIDTH = "tile-width";
 
     /**
-     * A key describing the grid height of the content in a {@link #MIMETYPE_IMAGE_ANDROID_HEIC}
-     * track. The associated value is an integer.
+     * A key describing the height (in pixels) of each tile of the content in a
+     * {@link #MIMETYPE_IMAGE_ANDROID_HEIC} track. The associated value is an integer.
      *
      * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks.
      *
-     * @see #KEY_GRID_WIDTH
+     * @see #KEY_TILE_WIDTH
      * @see #KEY_GRID_ROWS
-     * @see #KEY_GRID_COLS
+     * @see #KEY_GRID_COLUMNS
      */
-    public static final String KEY_GRID_HEIGHT = "grid-height";
+    public static final String KEY_TILE_HEIGHT = "tile-height";
 
     /**
      * A key describing the number of grid rows in the content in a
@@ -303,9 +314,9 @@
      *
      * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks.
      *
-     * @see #KEY_GRID_WIDTH
-     * @see #KEY_GRID_HEIGHT
-     * @see #KEY_GRID_COLS
+     * @see #KEY_TILE_WIDTH
+     * @see #KEY_TILE_HEIGHT
+     * @see #KEY_GRID_COLUMNS
      */
     public static final String KEY_GRID_ROWS = "grid-rows";
 
@@ -315,11 +326,11 @@
      *
      * Refer to {@link #MIMETYPE_IMAGE_ANDROID_HEIC} on decoding instructions of such tracks.
      *
-     * @see #KEY_GRID_WIDTH
-     * @see #KEY_GRID_HEIGHT
+     * @see #KEY_TILE_WIDTH
+     * @see #KEY_TILE_HEIGHT
      * @see #KEY_GRID_ROWS
      */
-    public static final String KEY_GRID_COLS = "grid-cols";
+    public static final String KEY_GRID_COLUMNS = "grid-cols";
 
     /**
      * A key describing the raw audio sample encoding/format.
@@ -512,6 +523,31 @@
     public static final String KEY_AAC_DRC_TARGET_REFERENCE_LEVEL = "aac-target-ref-level";
 
     /**
+     * A key describing for selecting the DRC effect type for MPEG-D DRC.
+     * The supported values are defined in ISO/IEC 23003-4:2015 and are described as follows:
+     * <table>
+     * <tr><th>Value</th><th>Effect</th></tr>
+     * <tr><th>-1</th><th>Off</th></tr>
+     * <tr><th>0</th><th>None</th></tr>
+     * <tr><th>1</th><th>Late night</th></tr>
+     * <tr><th>2</th><th>Noisy environment</th></tr>
+     * <tr><th>3</th><th>Limited playback range</th></tr>
+     * <tr><th>4</th><th>Low playback level</th></tr>
+     * <tr><th>5</th><th>Dialog enhancement</th></tr>
+     * <tr><th>6</th><th>General compression</th></tr>
+     * </table>
+     * <p>The value -1 (Off) disables DRC processing, while loudness normalization may still be
+     * active and dependent on KEY_AAC_DRC_TARGET_REFERENCE_LEVEL.<br>
+     * The value 0 (None) automatically enables DRC processing if necessary to prevent signal
+     * clipping<br>
+     * The value 6 (General compression) can be used for enabling MPEG-D DRC without particular
+     * DRC effect type request.<br>
+     * The default is DRC effect type "None".
+     * <p>This key is only used during decoding.
+     */
+    public static final String KEY_AAC_DRC_EFFECT_TYPE = "aac-drc-effect-type";
+
+    /**
      * A key describing the target reference level that was assumed at the encoder for
      * calculation of attenuation gains for clipping prevention. This information can be provided
      * if it is known, otherwise a worst-case assumption is used.
diff --git a/android/media/MediaItem2.java b/android/media/MediaItem2.java
index 96a87d5..423a1fd 100644
--- a/android/media/MediaItem2.java
+++ b/android/media/MediaItem2.java
@@ -19,31 +19,23 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.media.update.ApiLoader;
+import android.media.update.MediaItem2Provider;
 import android.os.Bundle;
-import android.text.TextUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 /**
+ * @hide
  * A class with information on a single media item with the metadata information.
  * Media item are application dependent so we cannot guarantee that they contain the right values.
  * <p>
  * When it's sent to a controller or browser, it's anonymized and data descriptor wouldn't be sent.
  * <p>
  * This object isn't a thread safe.
- *
- * @hide
  */
-// TODO(jaewan): Unhide and extends from DataSourceDesc.
-//               Note) Feels like an anti-pattern. We should anonymize MediaItem2 to remove *all*
-//                     information in the DataSourceDesc. Why it should extends from this?
-// TODO(jaewan): Move this to updatable
-// Previously MediaBrowser.MediaItem
 public class MediaItem2 {
-    private final int mFlags;
-    private MediaMetadata2 mMetadata;
-
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
@@ -62,15 +54,21 @@
      */
     public static final int FLAG_PLAYABLE = 1 << 1;
 
+    private final MediaItem2Provider mProvider;
+
     /**
-     * Create a new media item.
-     *
-     * @param metadata metadata with the media id.
-     * @param flags The flags for this item.
+     * Create a new media item
+     * @hide
      */
-    public MediaItem2(@NonNull MediaMetadata2 metadata, @Flags int flags) {
-        mFlags = flags;
-        setMetadata(metadata);
+    public MediaItem2(MediaItem2Provider provider) {
+        mProvider = provider;
+    }
+
+    /**
+     * @hide
+     */
+    public MediaItem2Provider getProvider() {
+        return mProvider;
     }
 
     /**
@@ -79,23 +77,22 @@
      * @return a new bundle instance
      */
     public Bundle toBundle() {
-        // TODO(jaewan): Fill here when we rebase.
-        return new Bundle();
+        return mProvider.toBundle_impl();
+    }
+
+    public static MediaItem2 fromBundle(Bundle bundle) {
+        return ApiLoader.getProvider().fromBundle_MediaItem2(bundle);
     }
 
     public String toString() {
-        final StringBuilder sb = new StringBuilder("MediaItem2{");
-        sb.append("mFlags=").append(mFlags);
-        sb.append(", mMetadata=").append(mMetadata);
-        sb.append('}');
-        return sb.toString();
+        return mProvider.toString_impl();
     }
 
     /**
      * Gets the flags of the item.
      */
     public @Flags int getFlags() {
-        return mFlags;
+        return mProvider.getFlags_impl();
     }
 
     /**
@@ -103,7 +100,7 @@
      * @see #FLAG_BROWSABLE
      */
     public boolean isBrowsable() {
-        return (mFlags & FLAG_BROWSABLE) != 0;
+        return mProvider.isBrowsable_impl();
     }
 
     /**
@@ -111,36 +108,113 @@
      * @see #FLAG_PLAYABLE
      */
     public boolean isPlayable() {
-        return (mFlags & FLAG_PLAYABLE) != 0;
+        return mProvider.isPlayable_impl();
     }
 
     /**
-     * Set a metadata. Metadata shouldn't be null and should have non-empty media id.
+     * Set a metadata. If the metadata is not null, its id should be matched with this instance's
+     * media id.
      *
-     * @param metadata
+     * @param metadata metadata to update
      */
-    public void setMetadata(@NonNull MediaMetadata2 metadata) {
-        if (metadata == null) {
-            throw new IllegalArgumentException("metadata cannot be null");
-        }
-        if (TextUtils.isEmpty(metadata.getMediaId())) {
-            throw new IllegalArgumentException("metadata must have a non-empty media id");
-        }
-        mMetadata = metadata;
+    public void setMetadata(@Nullable MediaMetadata2 metadata) {
+        mProvider.setMetadata_impl(metadata);
     }
 
     /**
      * Returns the metadata of the media.
      */
-    public @NonNull MediaMetadata2 getMetadata() {
-        return mMetadata;
+    public @Nullable MediaMetadata2 getMetadata() {
+        return mProvider.getMetadata_impl();
     }
 
     /**
-     * Returns the media id in the {@link MediaMetadata2} for this item.
-     * @see MediaMetadata2#METADATA_KEY_MEDIA_ID
+     * Returns the media id for this item.
      */
-    public @Nullable String getMediaId() {
-        return mMetadata.getMediaId();
+    public @NonNull String getMediaId() {
+        return mProvider.getMediaId_impl();
+    }
+
+    /**
+     * Return the {@link DataSourceDesc}
+     * <p>
+     * Can be {@code null} if the MediaItem2 came from another process and anonymized
+     *
+     * @return data source descriptor
+     */
+    public @Nullable DataSourceDesc getDataSourceDesc() {
+        return mProvider.getDataSourceDesc_impl();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return mProvider.equals_impl(obj);
+    }
+
+    /**
+     * Build {@link MediaItem2}
+     */
+    public static final class Builder {
+        private final MediaItem2Provider.BuilderProvider mProvider;
+
+        /**
+         * Constructor for {@link Builder}
+         *
+         * @param flags
+         */
+        public Builder(@Flags int flags) {
+            mProvider = ApiLoader.getProvider().createMediaItem2Builder(this, flags);
+        }
+
+        /**
+         * Set the media id of this instance. {@code null} for unset.
+         * <p>
+         * Media id is used to identify a media contents between session and controller.
+         * <p>
+         * If the metadata is set with the {@link #setMetadata(MediaMetadata2)} and it has
+         * media id, id from {@link #setMediaId(String)} will be ignored and metadata's id will be
+         * used instead. If the id isn't set neither by {@link #setMediaId(String)} nor
+         * {@link #setMetadata(MediaMetadata2)}, id will be automatically generated.
+         *
+         * @param mediaId media id
+         * @return this instance for chaining
+         */
+        public Builder setMediaId(@Nullable String mediaId) {
+            return mProvider.setMediaId_impl(mediaId);
+        }
+
+        /**
+         * Set the metadata of this instance. {@code null} for unset.
+         * <p>
+         * If the metadata is set with the {@link #setMetadata(MediaMetadata2)} and it has
+         * media id, id from {@link #setMediaId(String)} will be ignored and metadata's id will be
+         * used instead. If the id isn't set neither by {@link #setMediaId(String)} nor
+         * {@link #setMetadata(MediaMetadata2)}, id will be automatically generated.
+         *
+         * @param metadata metadata
+         * @return this instance for chaining
+         */
+        public Builder setMetadata(@Nullable MediaMetadata2 metadata) {
+            return mProvider.setMetadata_impl(metadata);
+        }
+
+        /**
+         * Set the data source descriptor for this instance. {@code null} for unset.
+         *
+         * @param dataSourceDesc data source descriptor
+         * @return this instance for chaining
+         */
+        public Builder setDataSourceDesc(@Nullable DataSourceDesc dataSourceDesc) {
+            return mProvider.setDataSourceDesc_impl(dataSourceDesc);
+        }
+
+        /**
+         * Build {@link MediaItem2}.
+         *
+         * @return a new {@link MediaItem2}.
+         */
+        public MediaItem2 build() {
+            return mProvider.build_impl();
+        }
     }
 }
diff --git a/android/media/MediaLibraryService2.java b/android/media/MediaLibraryService2.java
index d7e43ec..f29d386 100644
--- a/android/media/MediaLibraryService2.java
+++ b/android/media/MediaLibraryService2.java
@@ -20,27 +20,27 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.PendingIntent;
-import android.content.Context;
-import android.media.MediaSession2.BuilderBase;
+import android.media.MediaLibraryService2.MediaLibrarySession.Builder;
+import android.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
 import android.media.MediaSession2.ControllerInfo;
 import android.media.update.ApiLoader;
+import android.media.update.MediaLibraryService2Provider.LibraryRootProvider;
 import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
-import android.media.update.MediaSession2Provider;
 import android.media.update.MediaSessionService2Provider;
 import android.os.Bundle;
-import android.service.media.MediaBrowserService.BrowserRoot;
 
 import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
+ * @hide
  * Base class for media library services.
  * <p>
  * Media library services enable applications to browse media content provided by an application
  * and ask the application to start playing it. They may also be used to control content that
  * is already playing by way of a {@link MediaSession2}.
  * <p>
- * To extend this class, adding followings directly to your {@code AndroidManifest.xml}.
+ * When extending this class, also add the following to your {@code AndroidManifest.xml}.
  * <pre>
  * &lt;service android:name="component_name_of_your_implementation" &gt;
  *   &lt;intent-filter&gt;
@@ -48,13 +48,13 @@
  *   &lt;/intent-filter&gt;
  * &lt;/service&gt;</pre>
  * <p>
- * A {@link MediaLibraryService2} is extension of {@link MediaSessionService2}. IDs shouldn't
+ * The {@link MediaLibraryService2} class derives from {@link MediaSessionService2}. IDs shouldn't
  * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By
  * default, an empty string will be used for ID of the service. If you want to specify an ID,
  * declare metadata in the manifest as follows.
- * @hide
+ *
+ * @see MediaSessionService2
  */
-// TODO(jaewan): Unhide
 public abstract class MediaLibraryService2 extends MediaSessionService2 {
     /**
      * This is the interface name that a service implementing a session service should say that it
@@ -63,178 +63,252 @@
     public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2";
 
     /**
-     * Session for the media library service.
+     * Session for the {@link MediaLibraryService2}. Build this object with
+     * {@link Builder} and return in {@link #onCreateSession(String)}.
      */
-    public class MediaLibrarySession extends MediaSession2 {
+    public static final class MediaLibrarySession extends MediaSession2 {
         private final MediaLibrarySessionProvider mProvider;
 
-        MediaLibrarySession(Context context, MediaPlayerBase player, String id,
-                Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
-                int ratingType, PendingIntent sessionActivity) {
-            super(context, player, id, callbackExecutor, callback, volumeProvider, ratingType,
-                    sessionActivity);
-            mProvider = (MediaLibrarySessionProvider) getProvider();
-        }
+        /**
+         * Callback for the {@link MediaLibrarySession}.
+         */
+        public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback {
+            public MediaLibrarySessionCallback() {
+                super();
+            }
 
-        @Override
-        MediaSession2Provider createProvider(Context context, MediaPlayerBase player, String id,
-                Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
-                int ratingType, PendingIntent sessionActivity) {
-            return ApiLoader.getProvider(context)
-                    .createMediaLibraryService2MediaLibrarySession(this, context, player, id,
-                            callbackExecutor, (MediaLibrarySessionCallback) callback,
-                            volumeProvider, ratingType, sessionActivity);
+            /**
+             * Called to get the root information for browsing by a particular client.
+             * <p>
+             * The implementation should verify that the client package has permission
+             * to access browse media information before returning the root id; it
+             * should return null if the client is not allowed to access this
+             * information.
+             *
+             * @param session the session for this event
+             * @param controllerInfo information of the controller requesting access to browse media.
+             * @param rootHints An optional bundle of service-specific arguments to send
+             * to the media library service when connecting and retrieving the
+             * root id for browsing, or null if none. The contents of this
+             * bundle may affect the information returned when browsing.
+             * @return The {@link LibraryRoot} for accessing this app's content or null.
+             * @see LibraryRoot#EXTRA_RECENT
+             * @see LibraryRoot#EXTRA_OFFLINE
+             * @see LibraryRoot#EXTRA_SUGGESTED
+             */
+            public @Nullable LibraryRoot onGetLibraryRoot(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controllerInfo, @Nullable Bundle rootHints) {
+                return null;
+            }
+
+            /**
+             * Called to get an item. Return result here for the browser.
+             * <p>
+             * Return {@code null} for no result or error.
+             *
+             * @param session the session for this event
+             * @param mediaId item id to get media item.
+             * @return a media item. {@code null} for no result or error.
+             */
+            public @Nullable MediaItem2 onGetItem(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controllerInfo, @NonNull String mediaId) {
+                return null;
+            }
+
+            /**
+             * Called to get children of given parent id. Return the children here for the browser.
+             * <p>
+             * Return an empty list for no children, and return {@code null} for the error.
+             *
+             * @param session the session for this event
+             * @param parentId parent id to get children
+             * @param page number of page
+             * @param pageSize size of the page
+             * @param extras extra bundle
+             * @return list of children. Can be {@code null}.
+             */
+            public @Nullable List<MediaItem2> onGetChildren(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controller, @NonNull String parentId, int page,
+                    int pageSize, @Nullable Bundle extras) {
+                return null;
+            }
+
+            /**
+             * Called when a controller subscribes to the parent.
+             * <p>
+             * It's your responsibility to keep subscriptions by your own and call
+             * {@link MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, Bundle)}
+             * when the parent is changed.
+             *
+             * @param session the session for this event
+             * @param controller controller
+             * @param parentId parent id
+             * @param extras extra bundle
+             */
+            public void onSubscribe(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controller, @NonNull String parentId,
+                    @Nullable Bundle extras) {
+            }
+
+            /**
+             * Called when a controller unsubscribes to the parent.
+             *
+             * @param session the session for this event
+             * @param controller controller
+             * @param parentId parent id
+             */
+            public void onUnsubscribe(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controller, @NonNull String parentId) {
+            }
+
+            /**
+             * Called when a controller requests search.
+             *
+             * @param session the session for this event
+             * @param query The search query sent from the media browser. It contains keywords
+             *              separated by space.
+             * @param extras The bundle of service-specific arguments sent from the media browser.
+             */
+            public void onSearch(@NonNull MediaLibrarySession session,
+                    @NonNull ControllerInfo controllerInfo, @NonNull String query,
+                    @Nullable Bundle extras) {
+            }
+
+            /**
+             * Called to get the search result. Return search result here for the browser which has
+             * requested search previously.
+             * <p>
+             * Return an empty list for no search result, and return {@code null} for the error.
+             *
+             * @param session the session for this event
+             * @param controllerInfo Information of the controller requesting the search result.
+             * @param query The search query which was previously sent through
+             *              {@link #onSearch(MediaLibrarySession, ControllerInfo, String, Bundle)}.
+             * @param page page number. Starts from {@code 1}.
+             * @param pageSize page size. Should be greater or equal to {@code 1}.
+             * @param extras The bundle of service-specific arguments sent from the media browser.
+             * @return search result. {@code null} for error.
+             */
+            public @Nullable List<MediaItem2> onGetSearchResult(
+                    @NonNull MediaLibrarySession session, @NonNull ControllerInfo controllerInfo,
+                    @NonNull String query, int page, int pageSize, @Nullable Bundle extras) {
+                return null;
+            }
         }
 
         /**
-         * Notify subscribed controller about change in a parent's children.
+         * Builder for {@link MediaLibrarySession}.
+         */
+        // Override all methods just to show them with the type instead of generics in Javadoc.
+        // This workarounds javadoc issue described in the MediaSession2.BuilderBase.
+        public static final class Builder extends BuilderBase<MediaLibrarySession, Builder,
+                MediaLibrarySessionCallback> {
+            // Builder requires MediaLibraryService2 instead of Context just to ensure that the
+            // builder can be only instantiated within the MediaLibraryService2.
+            // Ideally it's better to make it inner class of service to enforce, it violates API
+            // guideline that Builders should be the inner class of the building target.
+            public Builder(@NonNull MediaLibraryService2 service,
+                    @NonNull @CallbackExecutor Executor callbackExecutor,
+                    @NonNull MediaLibrarySessionCallback callback) {
+                super((instance) -> ApiLoader.getProvider().createMediaLibraryService2Builder(
+                        service, (Builder) instance, callbackExecutor, callback));
+            }
+
+            @Override
+            public Builder setPlayer(@NonNull MediaPlayerBase player) {
+                return super.setPlayer(player);
+            }
+
+            @Override
+            public Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+                return super.setPlaylistAgent(playlistAgent);
+            }
+
+            @Override
+            public Builder setVolumeProvider(@Nullable VolumeProvider2 volumeProvider) {
+                return super.setVolumeProvider(volumeProvider);
+            }
+
+            @Override
+            public Builder setSessionActivity(@Nullable PendingIntent pi) {
+                return super.setSessionActivity(pi);
+            }
+
+            @Override
+            public Builder setId(@NonNull String id) {
+                return super.setId(id);
+            }
+
+            @Override
+            public Builder setSessionCallback(@NonNull @CallbackExecutor Executor executor,
+                    @NonNull MediaLibrarySessionCallback callback) {
+                return super.setSessionCallback(executor, callback);
+            }
+
+            @Override
+            public MediaLibrarySession build() {
+                return super.build();
+            }
+        }
+
+        /**
+         * @hide
+         */
+        public MediaLibrarySession(MediaLibrarySessionProvider provider) {
+            super(provider);
+            mProvider = provider;
+        }
+
+        /**
+         * Notify the controller of the change in a parent's children.
+         * <p>
+         * If the controller hasn't subscribed to the parent, the API will do nothing.
+         * <p>
+         * Controllers will use {@link MediaBrowser2#getChildren(String, int, int, Bundle)} to get
+         * the list of children.
          *
          * @param controller controller to notify
-         * @param parentId
-         * @param options
+         * @param parentId parent id with changes in its children
+         * @param itemCount number of children.
+         * @param extras extra information from session to controller
          */
         public void notifyChildrenChanged(@NonNull ControllerInfo controller,
-                @NonNull String parentId, @NonNull Bundle options) {
-            mProvider.notifyChildrenChanged_impl(controller, parentId, options);
+                @NonNull String parentId, int itemCount, @Nullable Bundle extras) {
+            mProvider.notifyChildrenChanged_impl(controller, parentId, itemCount, extras);
         }
 
         /**
-         * Notify subscribed controller about change in a parent's children.
+         * Notify all controllers that subscribed to the parent about change in the parent's
+         * children, regardless of the extra bundle supplied by
+         * {@link MediaBrowser2#subscribe(String, Bundle)}.
          *
          * @param parentId parent id
-         * @param options optional bundle
+         * @param itemCount number of children
+         * @param extras extra information from session to controller
          */
         // This is for the backward compatibility.
-        public void notifyChildrenChanged(@NonNull String parentId, @Nullable Bundle options) {
-            mProvider.notifyChildrenChanged_impl(parentId, options);
-        }
-    }
-
-    public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback {
-        /**
-         * Called to get the root information for browsing by a particular client.
-         * <p>
-         * The implementation should verify that the client package has permission
-         * to access browse media information before returning the root id; it
-         * should return null if the client is not allowed to access this
-         * information.
-         *
-         * @param controllerInfo information of the controller requesting access to browse media.
-         * @param rootHints An optional bundle of service-specific arguments to send
-         * to the media browser service when connecting and retrieving the
-         * root id for browsing, or null if none. The contents of this
-         * bundle may affect the information returned when browsing.
-         * @return The {@link BrowserRoot} for accessing this app's content or null.
-         * @see BrowserRoot#EXTRA_RECENT
-         * @see BrowserRoot#EXTRA_OFFLINE
-         * @see BrowserRoot#EXTRA_SUGGESTED
-         */
-        public @Nullable BrowserRoot onGetRoot(@NonNull ControllerInfo controllerInfo,
-                @Nullable Bundle rootHints) {
-            return null;
+        public void notifyChildrenChanged(@NonNull String parentId, int itemCount,
+                @Nullable Bundle extras) {
+            mProvider.notifyChildrenChanged_impl(parentId, itemCount, extras);
         }
 
         /**
-         * Called to get the search result. Return search result here for the browser.
-         * <p>
-         * Return an empty list for no search result, and return {@code null} for the error.
+         * Notify controller about change in the search result.
          *
-         * @param query The search query sent from the media browser. It contains keywords separated
-         *            by space.
-         * @param extras The bundle of service-specific arguments sent from the media browser.
-         * @return search result. {@code null} for error.
+         * @param controller controller to notify
+         * @param query previously sent search query from the controller.
+         * @param itemCount the number of items that have been found in the search.
+         * @param extras extra bundle
          */
-        public @Nullable List<MediaItem2> onSearch(@NonNull ControllerInfo controllerInfo,
-                @NonNull String query, @Nullable Bundle extras) {
-            return null;
-        }
-
-        /**
-         * Called to get the search result . Return result here for the browser.
-         * <p>
-         * Return an empty list for no search result, and return {@code null} for the error.
-         *
-         * @param itemId item id to get media item.
-         * @return media item2. {@code null} for error.
-         */
-        public @Nullable MediaItem2 onLoadItem(@NonNull ControllerInfo controllerInfo,
-                @NonNull String itemId) {
-            return null;
-        }
-
-        /**
-         * Called to get the search result. Return search result here for the browser.
-         * <p>
-         * Return an empty list for no search result, and return {@code null} for the error.
-         *
-         * @param parentId parent id to get children
-         * @param page number of page
-         * @param pageSize size of the page
-         * @param options
-         * @return list of children. Can be {@code null}.
-         */
-        public @Nullable List<MediaItem2> onLoadChildren(@NonNull ControllerInfo controller,
-                @NonNull String parentId, int page, int pageSize, @Nullable Bundle options) {
-            return null;
-        }
-
-        /**
-         * Called when a controller subscribes to the parent.
-         *
-         * @param controller controller
-         * @param parentId parent id
-         * @param options optional bundle
-         */
-        public void onSubscribed(@NonNull ControllerInfo controller,
-                String parentId, @Nullable Bundle options) {
-        }
-
-        /**
-         * Called when a controller unsubscribes to the parent.
-         *
-         * @param controller controller
-         * @param parentId parent id
-         * @param options optional bundle
-         */
-        public void onUnsubscribed(@NonNull ControllerInfo controller,
-                String parentId, @Nullable Bundle options) {
-        }
-    }
-
-    /**
-     * Builder for {@link MediaLibrarySession}.
-     */
-    // TODO(jaewan): Move this to updatable.
-    public class MediaLibrarySessionBuilder
-            extends BuilderBase<MediaLibrarySessionBuilder, MediaLibrarySessionCallback> {
-        public MediaLibrarySessionBuilder(
-                @NonNull Context context, @NonNull MediaPlayerBase player,
-                @NonNull @CallbackExecutor Executor callbackExecutor,
-                @NonNull MediaLibrarySessionCallback callback) {
-            super(context, player);
-            setSessionCallback(callbackExecutor, callback);
-        }
-
-        @Override
-        public MediaLibrarySessionBuilder setSessionCallback(
-                @NonNull @CallbackExecutor Executor callbackExecutor,
-                @NonNull MediaLibrarySessionCallback callback) {
-            if (callback == null) {
-                throw new IllegalArgumentException("MediaLibrarySessionCallback cannot be null");
-            }
-            return super.setSessionCallback(callbackExecutor, callback);
-        }
-
-        @Override
-        public MediaLibrarySession build() throws IllegalStateException {
-            return new MediaLibrarySession(mContext, mPlayer, mId, mCallbackExecutor, mCallback,
-                    mVolumeProvider, mRatingType, mSessionActivity);
+        public void notifySearchResultChanged(@NonNull ControllerInfo controller,
+                @NonNull String query, int itemCount, @NonNull Bundle extras) {
+            mProvider.notifySearchResultChanged_impl(controller, query, itemCount, extras);
         }
     }
 
     @Override
     MediaSessionService2Provider createProvider() {
-        return ApiLoader.getProvider(this).createMediaLibraryService2(this);
+        return ApiLoader.getProvider().createMediaLibraryService2(this);
     }
 
     /**
@@ -249,8 +323,8 @@
      * This method will be called on the main thread.
      *
      * @param sessionId session id written in the AndroidManifest.xml.
-     * @return a new browser session
-     * @see MediaLibrarySessionBuilder
+     * @return a new library session
+     * @see Builder
      * @see #getSession()
      * @throws RuntimeException if returned session is invalid
      */
@@ -258,93 +332,91 @@
     public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId);
 
     /**
-     * Contains information that the browser service needs to send to the client
-     * when first connected.
+     * Contains information that the library service needs to send to the client when
+     * {@link MediaBrowser2#getLibraryRoot(Bundle)} is called.
      */
-    public static final class BrowserRoot {
+    public static final class LibraryRoot {
         /**
-         * The lookup key for a boolean that indicates whether the browser service should return a
-         * browser root for recently played media items.
+         * The lookup key for a boolean that indicates whether the library service should return a
+         * librar root for recently played media items.
          *
-         * <p>When creating a media browser for a given media browser service, this key can be
+         * <p>When creating a media browser for a given media library service, this key can be
          * supplied as a root hint for retrieving media items that are recently played.
-         * If the media browser service can provide such media items, the implementation must return
+         * If the media library service can provide such media items, the implementation must return
          * the key in the root hint when
-         * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back.
+         * {@link MediaLibrarySessionCallback#onGetLibraryRoot(MediaLibrarySession, ControllerInfo, Bundle)}
+         * is called back.
          *
          * <p>The root hint may contain multiple keys.
          *
          * @see #EXTRA_OFFLINE
          * @see #EXTRA_SUGGESTED
          */
-        public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
+        public static final String EXTRA_RECENT = "android.media.extra.RECENT";
 
         /**
-         * The lookup key for a boolean that indicates whether the browser service should return a
-         * browser root for offline media items.
+         * The lookup key for a boolean that indicates whether the library service should return a
+         * library root for offline media items.
          *
-         * <p>When creating a media browser for a given media browser service, this key can be
+         * <p>When creating a media browser for a given media library service, this key can be
          * supplied as a root hint for retrieving media items that are can be played without an
          * internet connection.
-         * If the media browser service can provide such media items, the implementation must return
+         * If the media library service can provide such media items, the implementation must return
          * the key in the root hint when
-         * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back.
+         * {@link MediaLibrarySessionCallback#onGetLibraryRoot(MediaLibrarySession, ControllerInfo, Bundle)}
+         * is called back.
          *
          * <p>The root hint may contain multiple keys.
          *
          * @see #EXTRA_RECENT
          * @see #EXTRA_SUGGESTED
          */
-        public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
+        public static final String EXTRA_OFFLINE = "android.media.extra.OFFLINE";
 
         /**
-         * The lookup key for a boolean that indicates whether the browser service should return a
-         * browser root for suggested media items.
+         * The lookup key for a boolean that indicates whether the library service should return a
+         * library root for suggested media items.
          *
-         * <p>When creating a media browser for a given media browser service, this key can be
-         * supplied as a root hint for retrieving the media items suggested by the media browser
-         * service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
-         * is considered ordered by relevance, first being the top suggestion.
-         * If the media browser service can provide such media items, the implementation must return
+         * <p>When creating a media browser for a given media library service, this key can be
+         * supplied as a root hint for retrieving the media items suggested by the media library
+         * service. The list of media items is considered ordered by relevance, first being the top
+         * suggestion.
+         * If the media library service can provide such media items, the implementation must return
          * the key in the root hint when
-         * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back.
+         * {@link MediaLibrarySessionCallback#onGetLibraryRoot(MediaLibrarySession, ControllerInfo, Bundle)}
+         * is called back.
          *
          * <p>The root hint may contain multiple keys.
          *
          * @see #EXTRA_RECENT
          * @see #EXTRA_OFFLINE
          */
-        public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
+        public static final String EXTRA_SUGGESTED = "android.media.extra.SUGGESTED";
 
-        final private String mRootId;
-        final private Bundle mExtras;
+        private final LibraryRootProvider mProvider;
 
         /**
-         * Constructs a browser root.
+         * Constructs a library root.
          * @param rootId The root id for browsing.
-         * @param extras Any extras about the browser service.
+         * @param extras Any extras about the library service.
          */
-        public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
-            if (rootId == null) {
-                throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
-                        "Use null for BrowserRoot instead.");
-            }
-            mRootId = rootId;
-            mExtras = extras;
+        public LibraryRoot(@NonNull String rootId, @Nullable Bundle extras) {
+            mProvider = ApiLoader.getProvider().createMediaLibraryService2LibraryRoot(
+                    this, rootId, extras);
         }
 
         /**
          * Gets the root id for browsing.
          */
         public String getRootId() {
-            return mRootId;
+            return mProvider.getRootId_impl();
         }
 
         /**
-         * Gets any extras about the browser service.
+         * Gets any extras about the library service.
          */
         public Bundle getExtras() {
-            return mExtras;
+            return mProvider.getExtras_impl();
         }
     }
 }
diff --git a/android/media/MediaMetadata2.java b/android/media/MediaMetadata2.java
index 0e24db6..7b03ae0 100644
--- a/android/media/MediaMetadata2.java
+++ b/android/media/MediaMetadata2.java
@@ -16,216 +16,348 @@
 
 package android.media;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StringDef;
 import android.graphics.Bitmap;
+import android.media.update.ApiLoader;
+import android.media.update.MediaMetadata2Provider;
 import android.net.Uri;
-import android.os.Build;
 import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.ArrayMap;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Set;
 
 /**
- * Contains metadata about an item, such as the title, artist, etc.
  * @hide
+ * Contains metadata about an item, such as the title, artist, etc.
  */
-// TODO(jaewan): Move this to updatable
+// New version of MediaMetadata with following changes
+//   - Don't implement Parcelable for updatable support.
+//   - Also support MediaDescription features. MediaDescription is deprecated instead because
+//     it was insufficient for controller to display media contents.
 public final class MediaMetadata2 {
-    private static final String TAG = "MediaMetadata2";
-
     /**
-     * The title of the media.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the title of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
 
     /**
-     * The artist of the media.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the artist of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
 
     /**
-     * The duration of the media in ms. A negative duration indicates that the
-     * duration is unknown (or infinite).
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * duration of the media in ms. A negative duration indicates that the duration is unknown
+     * (or infinite).
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
      */
     public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
 
     /**
-     * The album title for the media.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the album title for the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
 
     /**
-     * The author of the media.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the author of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
 
     /**
-     * The writer of the media.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the writer of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
 
     /**
-     * The composer of the media.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the composer of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
 
     /**
-     * The compilation status of the media.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the compilation status of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
 
     /**
-     * The date the media was created or published. The format is unspecified
-     * but RFC 3339 is recommended.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the date the media was created or published.
+     * The format is unspecified but RFC 3339 is recommended.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
 
     /**
-     * The year the media was created or published as a long.
+     * The metadata key for a {@link Long} typed value to retrieve the information about the year
+     * the media was created or published.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
      */
     public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
 
     /**
-     * The genre of the media.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the genre of the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
 
     /**
-     * The track number for the media.
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * track number for the media.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
      */
     public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
 
     /**
-     * The number of tracks in the media's original source.
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * number of tracks in the media's original source.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
      */
     public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
 
     /**
-     * The disc number for the media's original source.
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * disc number for the media's original source.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
      */
     public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
 
     /**
-     * The artist for the album of the media's original source.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the artist for the album of the media's original source.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
 
     /**
-     * The artwork for the media as a {@link Bitmap}.
+     * The metadata key for a {@link Bitmap} typed value to retrieve the information about the
+     * artwork for the media.
+     * The artwork should be relatively small and may be scaled down if it is too large.
+     * For higher resolution artwork, {@link #METADATA_KEY_ART_URI} should be used instead.
      *
-     * The artwork should be relatively small and may be scaled down
-     * if it is too large. For higher resolution artwork
-     * {@link #METADATA_KEY_ART_URI} should be used instead.
+     * @see Builder#putBitmap(String, Bitmap)
+     * @see #getBitmap(String)
      */
     public static final String METADATA_KEY_ART = "android.media.metadata.ART";
 
     /**
-     * The artwork for the media as a Uri style String.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about Uri of the artwork for the media.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
 
     /**
-     * The artwork for the album of the media's original source as a
-     * {@link Bitmap}.
-     * The artwork should be relatively small and may be scaled down
-     * if it is too large. For higher resolution artwork
-     * {@link #METADATA_KEY_ALBUM_ART_URI} should be used instead.
+     * The metadata key for a {@link Bitmap} typed value to retrieve the information about the
+     * artwork for the album of the media's original source.
+     * The artwork should be relatively small and may be scaled down if it is too large.
+     * For higher resolution artwork, {@link #METADATA_KEY_ALBUM_ART_URI} should be used instead.
+     *
+     * @see Builder#putBitmap(String, Bitmap)
+     * @see #getBitmap(String)
      */
     public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
 
     /**
-     * The artwork for the album of the media's original source as a Uri style
-     * String.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the Uri of the artwork for the album of the media's original source.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
 
     /**
-     * The user's rating for the media.
+     * The metadata key for a {@link Rating2} typed value to retrieve the information about the
+     * user's rating for the media.
      *
-     * @see Rating
+     * @see Builder#putRating(String, Rating2)
+     * @see #getRating(String)
      */
     public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
 
     /**
-     * The overall rating for the media.
+     * The metadata key for a {@link Rating2} typed value to retrieve the information about the
+     * overall rating for the media.
      *
-     * @see Rating
+     * @see Builder#putRating(String, Rating2)
+     * @see #getRating(String)
      */
     public static final String METADATA_KEY_RATING = "android.media.metadata.RATING";
 
     /**
-     * A title that is suitable for display to the user. This will generally be
-     * the same as {@link #METADATA_KEY_TITLE} but may differ for some formats.
-     * When displaying media described by this metadata this should be preferred
-     * if present.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the title that is suitable for display to the user.
+     * It will generally be the same as {@link #METADATA_KEY_TITLE} but may differ for some formats.
+     * When displaying media described by this metadata, this should be preferred if present.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE";
 
     /**
-     * A subtitle that is suitable for display to the user. When displaying a
-     * second line for media described by this metadata this should be preferred
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the subtitle that is suitable for display to the user.
+     * When displaying a second line for media described by this metadata, this should be preferred
      * to other fields if present.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_DISPLAY_SUBTITLE
             = "android.media.metadata.DISPLAY_SUBTITLE";
 
     /**
-     * A description that is suitable for display to the user. When displaying
-     * more information for media described by this metadata this should be
-     * preferred to other fields if present.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the description that is suitable for display to the user.
+     * When displaying more information for media described by this metadata,
+     * this should be preferred to other fields if present.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_DISPLAY_DESCRIPTION
             = "android.media.metadata.DISPLAY_DESCRIPTION";
 
     /**
-     * An icon or thumbnail that is suitable for display to the user. When
-     * displaying an icon for media described by this metadata this should be
-     * preferred to other fields if present. This must be a {@link Bitmap}.
+     * The metadata key for a {@link Bitmap} typed value to retrieve the information about the icon
+     * or thumbnail that is suitable for display to the user.
+     * When displaying an icon for media described by this metadata, this should be preferred to
+     * other fields if present.
+     * <p>
+     * The icon should be relatively small and may be scaled down if it is too large.
+     * For higher resolution artwork, {@link #METADATA_KEY_DISPLAY_ICON_URI} should be used instead.
      *
-     * The icon should be relatively small and may be scaled down
-     * if it is too large. For higher resolution artwork
-     * {@link #METADATA_KEY_DISPLAY_ICON_URI} should be used instead.
+     * @see Builder#putBitmap(String, Bitmap)
+     * @see #getBitmap(String)
      */
-    public static final String METADATA_KEY_DISPLAY_ICON
-            = "android.media.metadata.DISPLAY_ICON";
+    public static final String METADATA_KEY_DISPLAY_ICON = "android.media.metadata.DISPLAY_ICON";
 
     /**
-     * An icon or thumbnail that is suitable for display to the user. When
-     * displaying more information for media described by this metadata the
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the Uri of icon or thumbnail that is suitable for display to the user.
+     * When displaying more information for media described by this metadata, the
      * display description should be preferred to other fields when present.
-     * This must be a Uri style String.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_DISPLAY_ICON_URI
             = "android.media.metadata.DISPLAY_ICON_URI";
 
     /**
-     * A String key for identifying the content. This value is specific to the
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the media ID of the content. This value is specific to the
      * service providing the content. If used, this should be a persistent
      * unique key for the underlying content.  It may be used with
      * {@link MediaController2#playFromMediaId(String, Bundle)}
      * to initiate playback when provided by a {@link MediaBrowser2} connected to
      * the same app.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID";
 
     /**
-     * A Uri formatted String representing the content. This value is specific to the
-     * service providing the content. It may be used with
-     * {@link MediaController2#playFromUri(Uri, Bundle)}
-     * to initiate playback when provided by a {@link MediaBrowser2} connected to
-     * the same app.
+     * The metadata key for a {@link CharSequence} or {@link String} typed value to retrieve the
+     * information about the Uri of the content. This value is specific to the service providing the
+     * content. It may be used with {@link MediaController2#playFromUri(Uri, Bundle)}
+     * to initiate playback when provided by a {@link MediaBrowser2} connected to the same app.
+     *
+     * @see Builder#putText(String, CharSequence)
+     * @see Builder#putString(String, String)
+     * @see #getText(String)
+     * @see #getString(String)
      */
     public static final String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI";
 
     /**
-     * The bluetooth folder type of the media specified in the section 6.10.2.2 of the Bluetooth
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * bluetooth folder type of the media specified in the section 6.10.2.2 of the Bluetooth
      * AVRCP 1.5. It should be one of the following:
      * <ul>
      * <li>{@link #BT_FOLDER_TYPE_MIXED}</li>
@@ -236,6 +368,9 @@
      * <li>{@link #BT_FOLDER_TYPE_PLAYLISTS}</li>
      * <li>{@link #BT_FOLDER_TYPE_YEARS}</li>
      * </ul>
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
      */
     public static final String METADATA_KEY_BT_FOLDER_TYPE
             = "android.media.metadata.BT_FOLDER_TYPE";
@@ -283,14 +418,19 @@
     public static final long BT_FOLDER_TYPE_YEARS = 6;
 
     /**
-     * Whether the media is an advertisement. A value of 0 indicates it is not an advertisement. A
-     * value of 1 or non-zero indicates it is an advertisement. If not specified, this value is set
-     * to 0 by default.
+     * The metadata key for a {@link Long} typed value to retrieve the information about whether
+     * the media is an advertisement. A value of 0 indicates it is not an advertisement.
+     * A value of 1 or non-zero indicates it is an advertisement.
+     * If not specified, this value is set to 0 by default.
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
      */
     public static final String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
 
     /**
-     * The download status of the media which will be used for later offline playback. It should be
+     * The metadata key for a {@link Long} typed value to retrieve the information about the
+     * download status of the media which will be used for later offline playback. It should be
      * one of the following:
      *
      * <ul>
@@ -298,6 +438,9 @@
      * <li>{@link #STATUS_DOWNLOADING}</li>
      * <li>{@link #STATUS_DOWNLOADED}</li>
      * </ul>
+     *
+     * @see Builder#putLong(String, long)
+     * @see #getLong(String)
      */
     public static final String METADATA_KEY_DOWNLOAD_STATUS =
             "android.media.metadata.DOWNLOAD_STATUS";
@@ -325,9 +468,8 @@
 
     /**
      * A {@link Bundle} extra.
-     * @hide
      */
-    public static final String METADATA_KEY_EXTRA = "android.media.metadata.EXTRA";
+    public static final String METADATA_KEY_EXTRAS = "android.media.metadata.EXTRAS";
 
     /**
      * @hide
@@ -364,76 +506,20 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface RatingKey {}
 
-    static final int METADATA_TYPE_LONG = 0;
-    static final int METADATA_TYPE_TEXT = 1;
-    static final int METADATA_TYPE_BITMAP = 2;
-    static final int METADATA_TYPE_RATING = 3;
-    static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
+    /**
+     * @hide
+     */
+    // TODO(jaewan): Add predefined float key.
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface FloatKey {}
 
-    static {
-        METADATA_KEYS_TYPE = new ArrayMap<String, Integer>();
-        METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_TITLE, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_SUBTITLE, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON, METADATA_TYPE_BITMAP);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON_URI, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_ID, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_BT_FOLDER_TYPE, METADATA_TYPE_LONG);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_URI, METADATA_TYPE_TEXT);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_ADVERTISEMENT, METADATA_TYPE_LONG);
-        METADATA_KEYS_TYPE.put(METADATA_KEY_DOWNLOAD_STATUS, METADATA_TYPE_LONG);
-    }
-
-    private static final @TextKey String[] PREFERRED_DESCRIPTION_ORDER = {
-            METADATA_KEY_TITLE,
-            METADATA_KEY_ARTIST,
-            METADATA_KEY_ALBUM,
-            METADATA_KEY_ALBUM_ARTIST,
-            METADATA_KEY_WRITER,
-            METADATA_KEY_AUTHOR,
-            METADATA_KEY_COMPOSER
-    };
-
-    private static final @BitmapKey String[] PREFERRED_BITMAP_ORDER = {
-            METADATA_KEY_DISPLAY_ICON,
-            METADATA_KEY_ART,
-            METADATA_KEY_ALBUM_ART
-    };
-
-    private static final @TextKey String[] PREFERRED_URI_ORDER = {
-            METADATA_KEY_DISPLAY_ICON_URI,
-            METADATA_KEY_ART_URI,
-            METADATA_KEY_ALBUM_ART_URI
-    };
-
-    final Bundle mBundle;
+    private final MediaMetadata2Provider mProvider;
 
     /**
      * @hide
      */
-    public MediaMetadata2(Bundle bundle) {
-        mBundle = new Bundle(bundle);
+    public MediaMetadata2(MediaMetadata2Provider provider) {
+        mProvider = provider;
     }
 
     /**
@@ -442,8 +528,8 @@
      * @param key a String key
      * @return true if the key exists in this metadata, false otherwise
      */
-    public boolean containsKey(String key) {
-        return mBundle.containsKey(key);
+    public boolean containsKey(@NonNull String key) {
+        return mProvider.containsKey_impl(key);
     }
 
     /**
@@ -454,20 +540,20 @@
      * @param key The key the value is stored under
      * @return a CharSequence value, or null
      */
-    public CharSequence getText(@TextKey String key) {
-        return mBundle.getCharSequence(key);
+    public @Nullable CharSequence getText(@NonNull @TextKey String key) {
+        return mProvider.getText_impl(key);
     }
 
     /**
-     * Returns the value associated with the given key, or null if no mapping of
-     * the desired type exists for the given key or a null value is explicitly
-     * associated with the key.
+     * Returns the media id, or {@code null} if the id doesn't exist.
+     *<p>
+     * This is equivalent to the {@link #getString(String)} with the {@link #METADATA_KEY_MEDIA_ID}.
      *
-     * @
      * @return media id. Can be {@code null}
+     * @see #METADATA_KEY_MEDIA_ID
      */
     public @Nullable String getMediaId() {
-        return getString(METADATA_KEY_MEDIA_ID);
+        return mProvider.getMediaId_impl();
     }
 
     /**
@@ -478,12 +564,8 @@
      * @param key The key the value is stored under
      * @return a String value, or null
      */
-    public String getString(@TextKey String key) {
-        CharSequence text = mBundle.getCharSequence(key);
-        if (text != null) {
-            return text.toString();
-        }
-        return null;
+    public @Nullable String getString(@NonNull @TextKey String key) {
+        return mProvider.getString_impl(key);
     }
 
     /**
@@ -493,27 +575,22 @@
      * @param key The key the value is stored under
      * @return a long value
      */
-    public long getLong(@LongKey String key) {
-        return mBundle.getLong(key, 0);
+    public long getLong(@NonNull @LongKey String key) {
+        return mProvider.getLong_impl(key);
     }
 
     /**
      * Return a {@link Rating2} for the given key or null if no rating exists for
      * the given key.
+     * <p>
+     * For the {@link #METADATA_KEY_USER_RATING}, A {@code null} return value means that user rating
+     * cannot be set by {@link MediaController2}.
      *
      * @param key The key the value is stored under
-     * @return A {@link Rating2} or null
+     * @return A {@link Rating2} or {@code null}
      */
-    public Rating2 getRating(@RatingKey String key) {
-        // TODO(jaewan): Add backward compatibility
-        Rating2 rating = null;
-        try {
-            rating = Rating2.fromBundle(mBundle.getBundle(key));
-        } catch (Exception e) {
-            // ignore, value was not a rating
-            Log.w(TAG, "Failed to retrieve a key as Rating.", e);
-        }
-        return rating;
+    public @Nullable Rating2 getRating(@NonNull @RatingKey String key) {
+        return mProvider.getRating_impl(key);
     }
 
     /**
@@ -523,15 +600,19 @@
      * @param key The key the value is stored under
      * @return A {@link Bitmap} or null
      */
-    public Bitmap getBitmap(@BitmapKey String key) {
-        Bitmap bmp = null;
-        try {
-            bmp = mBundle.getParcelable(key);
-        } catch (Exception e) {
-            // ignore, value was not a bitmap
-            Log.w(TAG, "Failed to retrieve a key as Bitmap.", e);
-        }
-        return bmp;
+    public @Nullable Bitmap getBitmap(@NonNull @BitmapKey String key) {
+        return mProvider.getBitmap_impl(key);
+    }
+
+    /**
+     * Return the value associated with the given key, or 0.0f if no long exists
+     * for the given key.
+     *
+     * @param key The key the value is stored under
+     * @return a float value
+     */
+    public float getFloat(@NonNull @FloatKey String key) {
+        return mProvider.getFloat_impl(key);
     }
 
     /**
@@ -539,14 +620,8 @@
      *
      * @return A {@link Bundle} or {@code null}
      */
-    public Bundle getExtra() {
-        try {
-            return mBundle.getBundle(METADATA_KEY_EXTRA);
-        } catch (Exception e) {
-            // ignore, value was not an bundle
-            Log.w(TAG, "Failed to retrieve an extra");
-        }
-        return null;
+    public @Nullable Bundle getExtras() {
+        return mProvider.getExtras_impl();
     }
 
     /**
@@ -555,7 +630,7 @@
      * @return The number of fields in the metadata.
      */
     public int size() {
-        return mBundle.size();
+        return mProvider.size_impl();
     }
 
     /**
@@ -563,8 +638,8 @@
      *
      * @return a Set of String keys
      */
-    public Set<String> keySet() {
-        return mBundle.keySet();
+    public @NonNull Set<String> keySet() {
+        return mProvider.keySet_impl();
     }
 
     /**
@@ -573,8 +648,19 @@
      *
      * @return The Bundle backing this metadata.
      */
-    public Bundle getBundle() {
-        return mBundle;
+    public @NonNull Bundle toBundle() {
+        return mProvider.toBundle_impl();
+    }
+
+    /**
+     * Creates the {@link MediaMetadata2} from the bundle that previously returned by
+     * {@link #toBundle()}.
+     *
+     * @param bundle bundle for the metadata
+     * @return a new MediaMetadata2
+     */
+    public static @NonNull MediaMetadata2 fromBundle(@Nullable Bundle bundle) {
+        return ApiLoader.getProvider().fromBundle_MediaMetadata2(bundle);
     }
 
     /**
@@ -582,14 +668,14 @@
      * use the appropriate data type.
      */
     public static final class Builder {
-        private final Bundle mBundle;
+        private final MediaMetadata2Provider.BuilderProvider mProvider;
 
         /**
          * Create an empty Builder. Any field that should be included in the
          * {@link MediaMetadata2} must be added.
          */
         public Builder() {
-            mBundle = new Bundle();
+            mProvider = ApiLoader.getProvider().createMediaMetadata2Builder(this);
         }
 
         /**
@@ -599,31 +685,15 @@
          *
          * @param source
          */
-        public Builder(MediaMetadata2 source) {
-            mBundle = new Bundle(source.mBundle);
+        public Builder(@NonNull MediaMetadata2 source) {
+            mProvider = ApiLoader.getProvider().createMediaMetadata2Builder(this, source);
         }
 
         /**
-         * Create a Builder using a {@link MediaMetadata2} instance to set
-         * initial values, but replace bitmaps with a scaled down copy if they
-         * are larger than maxBitmapSize.
-         *
-         * @param source The original metadata to copy.
-         * @param maxBitmapSize The maximum height/width for bitmaps contained
-         *            in the metadata.
          * @hide
          */
-        public Builder(MediaMetadata2 source, int maxBitmapSize) {
-            this(source);
-            for (String key : mBundle.keySet()) {
-                Object value = mBundle.get(key);
-                if (value instanceof Bitmap) {
-                    Bitmap bmp = (Bitmap) value;
-                    if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) {
-                        putBitmap(key, scaleBitmap(bmp, maxBitmapSize));
-                    }
-                }
-            }
+        public Builder(@NonNull MediaMetadata2Provider.BuilderProvider provider) {
+            mProvider = provider;
         }
 
         /**
@@ -652,15 +722,9 @@
          * @param value The CharSequence value to store
          * @return The Builder to allow chaining
          */
-        public Builder putText(@TextKey String key, CharSequence value) {
-            if (METADATA_KEYS_TYPE.containsKey(key)) {
-                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
-                    throw new IllegalArgumentException("The " + key
-                            + " key cannot be used to put a CharSequence");
-                }
-            }
-            mBundle.putCharSequence(key, value);
-            return this;
+        public @NonNull Builder putText(@NonNull @TextKey String key,
+                @Nullable CharSequence value) {
+            return mProvider.putText_impl(key, value);
         }
 
         /**
@@ -689,15 +753,9 @@
          * @param value The String value to store
          * @return The Builder to allow chaining
          */
-        public Builder putString(@TextKey String key, String value) {
-            if (METADATA_KEYS_TYPE.containsKey(key)) {
-                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
-                    throw new IllegalArgumentException("The " + key
-                            + " key cannot be used to put a String");
-                }
-            }
-            mBundle.putCharSequence(key, value);
-            return this;
+        public @NonNull Builder putString(@NonNull @TextKey String key,
+                @Nullable String value) {
+            return mProvider.putString_impl(key, value);
         }
 
         /**
@@ -719,15 +777,8 @@
          * @param value The String value to store
          * @return The Builder to allow chaining
          */
-        public Builder putLong(@LongKey String key, long value) {
-            if (METADATA_KEYS_TYPE.containsKey(key)) {
-                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) {
-                    throw new IllegalArgumentException("The " + key
-                            + " key cannot be used to put a long");
-                }
-            }
-            mBundle.putLong(key, value);
-            return this;
+        public @NonNull Builder putLong(@NonNull @LongKey String key, long value) {
+            return mProvider.putLong_impl(key, value);
         }
 
         /**
@@ -743,16 +794,8 @@
          * @param value The String value to store
          * @return The Builder to allow chaining
          */
-        public Builder putRating(@RatingKey String key, Rating2 value) {
-            if (METADATA_KEYS_TYPE.containsKey(key)) {
-                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) {
-                    throw new IllegalArgumentException("The " + key
-                            + " key cannot be used to put a Rating");
-                }
-            }
-            mBundle.putBundle(key, value.toBundle());
-
-            return this;
+        public @NonNull Builder putRating(@NonNull @RatingKey String key, @Nullable Rating2 value) {
+            return mProvider.putRating_impl(key, value);
         }
 
         /**
@@ -773,23 +816,29 @@
          * @param value The Bitmap to store
          * @return The Builder to allow chaining
          */
-        public Builder putBitmap(@BitmapKey String key, Bitmap value) {
-            if (METADATA_KEYS_TYPE.containsKey(key)) {
-                if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
-                    throw new IllegalArgumentException("The " + key
-                            + " key cannot be used to put a Bitmap");
-                }
-            }
-            mBundle.putParcelable(key, value);
-            return this;
+        public @NonNull Builder putBitmap(@NonNull @BitmapKey String key, @Nullable Bitmap value) {
+            return mProvider.putBitmap_impl(key, value);
         }
 
         /**
-         * Set an extra {@link Bundle} into the metadata.
+         * Put a float value into the metadata. Custom keys may be used.
+         *
+         * @param key The key for referencing this value
+         * @param value The float value to store
+         * @return The Builder to allow chaining
          */
-        public Builder setExtra(Bundle bundle) {
-            mBundle.putBundle(METADATA_KEY_EXTRA, bundle);
-            return this;
+        public @NonNull Builder putFloat(@NonNull @LongKey String key, float value) {
+            return mProvider.putFloat_impl(key, value);
+        }
+
+        /**
+         * Set a bundle of extras.
+         *
+         * @param extras The extras to include with this description or null.
+         * @return The Builder to allow chaining
+         */
+        public Builder setExtras(@Nullable Bundle extras) {
+            return mProvider.setExtras_impl(extras);
         }
 
         /**
@@ -797,18 +846,8 @@
          *
          * @return The new MediaMetadata2 instance
          */
-        public MediaMetadata2 build() {
-            return new MediaMetadata2(mBundle);
-        }
-
-        private Bitmap scaleBitmap(Bitmap bmp, int maxSize) {
-            float maxSizeF = maxSize;
-            float widthScale = maxSizeF / bmp.getWidth();
-            float heightScale = maxSizeF / bmp.getHeight();
-            float scale = Math.min(widthScale, heightScale);
-            int height = (int) (bmp.getHeight() * scale);
-            int width = (int) (bmp.getWidth() * scale);
-            return Bitmap.createScaledBitmap(bmp, width, height, true);
+        public @NonNull MediaMetadata2 build() {
+            return mProvider.build_impl();
         }
     }
 }
diff --git a/android/media/MediaMetadataRetriever.java b/android/media/MediaMetadataRetriever.java
index 745eb74..0955dd6 100644
--- a/android/media/MediaMetadataRetriever.java
+++ b/android/media/MediaMetadataRetriever.java
@@ -17,6 +17,8 @@
 package android.media;
 
 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;
@@ -30,7 +32,7 @@
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -367,10 +369,83 @@
 
     private native Bitmap _getFrameAtTime(long timeUs, int option, int width, int height);
 
+    public static final class BitmapParams {
+        private Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;
+        private Bitmap.Config outActualConfig = Bitmap.Config.ARGB_8888;
+
+        /**
+         * Create a default BitmapParams object. By default, it uses {@link Bitmap.Config#ARGB_8888}
+         * as the preferred bitmap config.
+         */
+        public BitmapParams() {}
+
+        /**
+         * Set the preferred bitmap config for the decoder to decode into.
+         *
+         * If not set, or the request cannot be met, the decoder will output
+         * in {@link Bitmap.Config#ARGB_8888} config by default.
+         *
+         * After decode, the actual config used can be retrieved by {@link #getActualConfig()}.
+         *
+         * @param config the preferred bitmap config to use.
+         */
+        public void setPreferredConfig(@NonNull Bitmap.Config config) {
+            if (config == null) {
+                throw new IllegalArgumentException("preferred config can't be null");
+            }
+            inPreferredConfig = config;
+        }
+
+        /**
+         * Retrieve the preferred bitmap config in the params.
+         *
+         * @return the preferred bitmap config.
+         */
+        public @NonNull Bitmap.Config getPreferredConfig() {
+            return inPreferredConfig;
+        }
+
+        /**
+         * Get the actual bitmap config used to decode the bitmap after the decoding.
+         *
+         * @return the actual bitmap config used.
+         */
+        public @NonNull Bitmap.Config getActualConfig() {
+            return outActualConfig;
+        }
+    }
+
     /**
      * This method retrieves a video frame by its index. It should only be called
      * after {@link #setDataSource}.
      *
+     * After the bitmap is returned, you can query the actual parameters that were
+     * used to create the bitmap from the {@code BitmapParams} argument, for instance
+     * to query the bitmap config used for the bitmap with {@link BitmapParams#getActualConfig}.
+     *
+     * @param frameIndex 0-based index of the video frame. The frame index must be that of
+     *        a valid frame. The total number of frames available for retrieval can be queried
+     *        via the {@link #METADATA_KEY_VIDEO_FRAME_COUNT} key.
+     * @param params BitmapParams that controls the returned bitmap config (such as pixel formats).
+     *
+     * @throws IllegalStateException if the container doesn't contain video or image sequences.
+     * @throws IllegalArgumentException if the requested frame index does not exist.
+     *
+     * @return A Bitmap containing the requested video frame, or null if the retrieval fails.
+     *
+     * @see #getFrameAtIndex(int)
+     * @see #getFramesAtIndex(int, int, BitmapParams)
+     * @see #getFramesAtIndex(int, int)
+     */
+    public Bitmap getFrameAtIndex(int frameIndex, @NonNull BitmapParams params) {
+        List<Bitmap> bitmaps = getFramesAtIndex(frameIndex, 1, params);
+        return bitmaps.get(0);
+    }
+
+    /**
+     * This method is similar to {@link #getFrameAtIndex(int, BitmapParams)} except that
+     * the default for {@link BitmapParams} will be used.
+     *
      * @param frameIndex 0-based index of the video frame. The frame index must be that of
      *        a valid frame. The total number of frames available for retrieval can be queried
      *        via the {@link #METADATA_KEY_VIDEO_FRAME_COUNT} key.
@@ -380,14 +455,13 @@
      *
      * @return A Bitmap containing the requested video frame, or null if the retrieval fails.
      *
+     * @see #getFrameAtIndex(int, BitmapParams)
+     * @see #getFramesAtIndex(int, int, BitmapParams)
      * @see #getFramesAtIndex(int, int)
      */
     public Bitmap getFrameAtIndex(int frameIndex) {
-        Bitmap[] bitmaps = getFramesAtIndex(frameIndex, 1);
-        if (bitmaps == null || bitmaps.length < 1) {
-            return null;
-        }
-        return bitmaps[0];
+        List<Bitmap> bitmaps = getFramesAtIndex(frameIndex, 1);
+        return bitmaps.get(0);
     }
 
     /**
@@ -395,7 +469,38 @@
      * specified index. It should only be called after {@link #setDataSource}.
      *
      * If the caller intends to retrieve more than one consecutive video frames,
-     * this method is preferred over {@link #getFrameAtIndex(int)} for efficiency.
+     * this method is preferred over {@link #getFrameAtIndex(int, BitmapParams)} for efficiency.
+     *
+     * After the bitmaps are returned, you can query the actual parameters that were
+     * used to create the bitmaps from the {@code BitmapParams} argument, for instance
+     * to query the bitmap config used for the bitmaps with {@link BitmapParams#getActualConfig}.
+     *
+     * @param frameIndex 0-based index of the first video frame to retrieve. The frame index
+     *        must be that of a valid frame. The total number of frames available for retrieval
+     *        can be queried via the {@link #METADATA_KEY_VIDEO_FRAME_COUNT} key.
+     * @param numFrames number of consecutive video frames to retrieve. Must be a positive
+     *        value. The stream must contain at least numFrames frames starting at frameIndex.
+     * @param params BitmapParams that controls the returned bitmap config (such as pixel formats).
+     *
+     * @throws IllegalStateException if the container doesn't contain video or image sequences.
+     * @throws IllegalArgumentException if the frameIndex or numFrames is invalid, or the
+     *         stream doesn't contain at least numFrames starting at frameIndex.
+
+     * @return An list of Bitmaps containing the requested video frames. The returned
+     *         array could contain less frames than requested if the retrieval fails.
+     *
+     * @see #getFrameAtIndex(int, BitmapParams)
+     * @see #getFrameAtIndex(int)
+     * @see #getFramesAtIndex(int, int)
+     */
+    public @NonNull List<Bitmap> getFramesAtIndex(
+            int frameIndex, int numFrames, @NonNull BitmapParams params) {
+        return getFramesAtIndexInternal(frameIndex, numFrames, params);
+    }
+
+    /**
+     * This method is similar to {@link #getFramesAtIndex(int, int, BitmapParams)} except that
+     * the default for {@link BitmapParams} will be used.
      *
      * @param frameIndex 0-based index of the first video frame to retrieve. The frame index
      *        must be that of a valid frame. The total number of frames available for retrieval
@@ -407,12 +512,19 @@
      * @throws IllegalArgumentException if the frameIndex or numFrames is invalid, or the
      *         stream doesn't contain at least numFrames starting at frameIndex.
 
-     * @return An array of Bitmaps containing the requested video frames. The returned
+     * @return An list of Bitmaps containing the requested video frames. The returned
      *         array could contain less frames than requested if the retrieval fails.
      *
+     * @see #getFrameAtIndex(int, BitmapParams)
      * @see #getFrameAtIndex(int)
+     * @see #getFramesAtIndex(int, int, BitmapParams)
      */
-    public Bitmap[] getFramesAtIndex(int frameIndex, int numFrames) {
+    public @NonNull List<Bitmap> getFramesAtIndex(int frameIndex, int numFrames) {
+        return getFramesAtIndexInternal(frameIndex, numFrames, null);
+    }
+
+    private @NonNull List<Bitmap> getFramesAtIndexInternal(
+            int frameIndex, int numFrames, @Nullable BitmapParams params) {
         if (!"yes".equals(extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO))) {
             throw new IllegalStateException("Does not contail video or image sequences");
         }
@@ -424,24 +536,94 @@
             throw new IllegalArgumentException("Invalid frameIndex or numFrames: "
                 + frameIndex + ", " + numFrames);
         }
-        return _getFrameAtIndex(frameIndex, numFrames);
+        return _getFrameAtIndex(frameIndex, numFrames, params);
     }
-    private native Bitmap[] _getFrameAtIndex(int frameIndex, int numFrames);
+
+    private native @NonNull List<Bitmap> _getFrameAtIndex(
+            int frameIndex, int numFrames, @Nullable BitmapParams params);
 
     /**
      * This method retrieves a still image by its index. It should only be called
      * after {@link #setDataSource}.
      *
-     * @param imageIndex 0-based index of the image, with negative value indicating
-     *        the primary image.
+     * After the bitmap is returned, you can query the actual parameters that were
+     * used to create the bitmap from the {@code BitmapParams} argument, for instance
+     * to query the bitmap config used for the bitmap with {@link BitmapParams#getActualConfig}.
+     *
+     * @param imageIndex 0-based index of the image.
+     * @param params BitmapParams that controls the returned bitmap config (such as pixel formats).
+     *
      * @throws IllegalStateException if the container doesn't contain still images.
      * @throws IllegalArgumentException if the requested image does not exist.
      *
      * @return the requested still image, or null if the image cannot be retrieved.
      *
-     * @see #getPrimaryImage
+     * @see #getImageAtIndex(int)
+     * @see #getPrimaryImage(BitmapParams)
+     * @see #getPrimaryImage()
+     */
+    public Bitmap getImageAtIndex(int imageIndex, @NonNull BitmapParams params) {
+        return getImageAtIndexInternal(imageIndex, params);
+    }
+
+    /**
+     * This method is similar to {@link #getImageAtIndex(int, BitmapParams)} except that
+     * the default for {@link BitmapParams} will be used.
+     *
+     * @param imageIndex 0-based index of the image.
+     *
+     * @throws IllegalStateException if the container doesn't contain still images.
+     * @throws IllegalArgumentException if the requested image does not exist.
+     *
+     * @return the requested still image, or null if the image cannot be retrieved.
+     *
+     * @see #getImageAtIndex(int, BitmapParams)
+     * @see #getPrimaryImage(BitmapParams)
+     * @see #getPrimaryImage()
      */
     public Bitmap getImageAtIndex(int imageIndex) {
+        return getImageAtIndexInternal(imageIndex, null);
+    }
+
+    /**
+     * This method retrieves the primary image of the media content. It should only
+     * be called after {@link #setDataSource}.
+     *
+     * After the bitmap is returned, you can query the actual parameters that were
+     * used to create the bitmap from the {@code BitmapParams} argument, for instance
+     * to query the bitmap config used for the bitmap with {@link BitmapParams#getActualConfig}.
+     *
+     * @param params BitmapParams that controls the returned bitmap config (such as pixel formats).
+     *
+     * @return the primary image, or null if it cannot be retrieved.
+     *
+     * @throws IllegalStateException if the container doesn't contain still images.
+     *
+     * @see #getImageAtIndex(int, BitmapParams)
+     * @see #getImageAtIndex(int)
+     * @see #getPrimaryImage()
+     */
+    public Bitmap getPrimaryImage(@NonNull BitmapParams params) {
+        return getImageAtIndexInternal(-1, params);
+    }
+
+    /**
+     * This method is similar to {@link #getPrimaryImage(BitmapParams)} except that
+     * the default for {@link BitmapParams} will be used.
+     *
+     * @return the primary image, or null if it cannot be retrieved.
+     *
+     * @throws IllegalStateException if the container doesn't contain still images.
+     *
+     * @see #getImageAtIndex(int, BitmapParams)
+     * @see #getImageAtIndex(int)
+     * @see #getPrimaryImage(BitmapParams)
+     */
+    public Bitmap getPrimaryImage() {
+        return getImageAtIndexInternal(-1, null);
+    }
+
+    private Bitmap getImageAtIndexInternal(int imageIndex, @Nullable BitmapParams params) {
         if (!"yes".equals(extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE))) {
             throw new IllegalStateException("Does not contail still images");
         }
@@ -451,24 +633,10 @@
             throw new IllegalArgumentException("Invalid image index: " + imageCount);
         }
 
-        return _getImageAtIndex(imageIndex);
+        return _getImageAtIndex(imageIndex, params);
     }
 
-    /**
-     * This method retrieves the primary image of the media content. It should only
-     * be called after {@link #setDataSource}.
-     *
-     * @return the primary image, or null if it cannot be retrieved.
-     *
-     * @throws IllegalStateException if the container doesn't contain still images.
-     *
-     * @see #getImageAtIndex(int)
-     */
-    public Bitmap getPrimaryImage() {
-        return getImageAtIndex(-1);
-    }
-
-    private native Bitmap _getImageAtIndex(int imageIndex);
+    private native Bitmap _getImageAtIndex(int imageIndex, @Nullable BitmapParams params);
 
     /**
      * Call this method after setDataSource(). This method finds the optional
@@ -712,7 +880,8 @@
     public static final int METADATA_KEY_IMAGE_HEIGHT    = 30;
     /**
      * If the media contains still images, this key retrieves the rotation
-     * of the primary image.
+     * angle (in degrees clockwise) of the primary image. The image rotation
+     * angle must be one of 0, 90, 180, or 270 degrees.
      */
     public static final int METADATA_KEY_IMAGE_ROTATION  = 31;
     /**
diff --git a/android/media/MediaMuxer.java b/android/media/MediaMuxer.java
index 02c71b2..205ce8d 100644
--- a/android/media/MediaMuxer.java
+++ b/android/media/MediaMuxer.java
@@ -328,6 +328,7 @@
         RandomAccessFile file = null;
         try {
             file = new RandomAccessFile(path, "rws");
+            file.setLength(0);
             FileDescriptor fd = file.getFD();
             setUpMediaMuxer(fd, format);
         } finally {
diff --git a/android/media/MediaPlayer.java b/android/media/MediaPlayer.java
index 1bc3dfa..aef31b1 100644
--- a/android/media/MediaPlayer.java
+++ b/android/media/MediaPlayer.java
@@ -1484,6 +1484,7 @@
     /*
      * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler.
      */
+    @GuardedBy("mRoutingChangeListeners")
     private void enableNativeRoutingCallbacksLocked(boolean enabled) {
         if (mRoutingChangeListeners.size() == 0) {
             native_enableDeviceCallback(enabled);
@@ -2415,7 +2416,7 @@
          * Gets the track type.
          * @return TrackType which indicates if the track is video, audio, timed text.
          */
-        public int getTrackType() {
+        public @TrackType int getTrackType() {
             return mTrackType;
         }
 
@@ -2449,6 +2450,19 @@
         public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4;
         public static final int MEDIA_TRACK_TYPE_METADATA = 5;
 
+        /** @hide */
+        @IntDef(flag = false, prefix = "MEDIA_TRACK_TYPE", value = {
+                MEDIA_TRACK_TYPE_UNKNOWN,
+                MEDIA_TRACK_TYPE_VIDEO,
+                MEDIA_TRACK_TYPE_AUDIO,
+                MEDIA_TRACK_TYPE_TIMEDTEXT,
+                MEDIA_TRACK_TYPE_SUBTITLE,
+                MEDIA_TRACK_TYPE_METADATA }
+        )
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface TrackType {}
+
+
         final int mTrackType;
         final MediaFormat mFormat;
 
@@ -2599,26 +2613,30 @@
      */
     /**
      * MIME type for SubRip (SRT) container. Used in addTimedTextSource APIs.
+     * @deprecated use {@link MediaFormat#MIMETYPE_TEXT_SUBRIP}
      */
-    public static final String MEDIA_MIMETYPE_TEXT_SUBRIP = "application/x-subrip";
+    public static final String MEDIA_MIMETYPE_TEXT_SUBRIP = MediaFormat.MIMETYPE_TEXT_SUBRIP;
 
     /**
      * MIME type for WebVTT subtitle data.
      * @hide
+     * @deprecated
      */
-    public static final String MEDIA_MIMETYPE_TEXT_VTT = "text/vtt";
+    public static final String MEDIA_MIMETYPE_TEXT_VTT = MediaFormat.MIMETYPE_TEXT_VTT;
 
     /**
      * MIME type for CEA-608 closed caption data.
      * @hide
+     * @deprecated
      */
-    public static final String MEDIA_MIMETYPE_TEXT_CEA_608 = "text/cea-608";
+    public static final String MEDIA_MIMETYPE_TEXT_CEA_608 = MediaFormat.MIMETYPE_TEXT_CEA_608;
 
     /**
      * MIME type for CEA-708 closed caption data.
      * @hide
+     * @deprecated
      */
-    public static final String MEDIA_MIMETYPE_TEXT_CEA_708 = "text/cea-708";
+    public static final String MEDIA_MIMETYPE_TEXT_CEA_708 = MediaFormat.MIMETYPE_TEXT_CEA_708;
 
     /*
      * A helper function to check if the mime type is supported by media framework.
@@ -3107,7 +3125,7 @@
      * this function is called.
      * </p>
      * <p>
-     * Currently, only timed text tracks or audio tracks can be selected via this method.
+     * Currently, only timed text, subtitle or audio tracks can be selected via this method.
      * In addition, the support for selecting an audio track at runtime is pretty limited
      * in that an audio track can only be selected in the <em>Prepared</em> state.
      * </p>
@@ -3794,29 +3812,158 @@
     private OnTimedTextListener mOnTimedTextListener;
 
     /**
-     * Interface definition of a callback to be invoked when a
-     * track has data available.
-     *
-     * @hide
+     * Interface definition of a callback to be invoked when a player subtitle track has new
+     * subtitle data available.
+     * See the {@link MediaPlayer#setOnSubtitleDataListener(OnSubtitleDataListener, Handler)}
+     * method for the description of which track will report data through this listener.
      */
-    public interface OnSubtitleDataListener
-    {
-        public void onSubtitleData(MediaPlayer mp, SubtitleData data);
+    public interface OnSubtitleDataListener {
+        /**
+         * Method called when new subtitle data is available
+         * @param mp the player that reports the new subtitle data
+         * @param data the subtitle data
+         */
+        public void onSubtitleData(@NonNull MediaPlayer mp, @NonNull SubtitleData data);
     }
 
     /**
-     * Register a callback to be invoked when a track has data available.
-     *
-     * @param listener the callback that will be run
-     *
-     * @hide
+     * Sets the listener to be invoked when a subtitle track has new data available.
+     * The subtitle data comes from a subtitle track previously selected with
+     * {@link #selectTrack(int)}. Use {@link #getTrackInfo()} to determine which tracks are
+     * subtitles (of type {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}), Subtitle track encodings
+     * can be determined by {@link TrackInfo#getFormat()}).<br>
+     * See {@link SubtitleData} for an example of querying subtitle encoding.
+     * @param listener the listener called when new data is available
+     * @param handler the {@link Handler} that receives the listener events
      */
-    public void setOnSubtitleDataListener(OnSubtitleDataListener listener)
+    public void setOnSubtitleDataListener(@NonNull OnSubtitleDataListener listener,
+            @NonNull Handler handler) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Illegal null listener");
+        }
+        if (handler == null) {
+            throw new IllegalArgumentException("Illegal null handler");
+        }
+        setOnSubtitleDataListenerInt(listener, handler);
+    }
+    /**
+     * Sets the listener to be invoked when a subtitle track has new data available.
+     * The subtitle data comes from a subtitle track previously selected with
+     * {@link #selectTrack(int)}. Use {@link #getTrackInfo()} to determine which tracks are
+     * subtitles (of type {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}), Subtitle track encodings
+     * can be determined by {@link TrackInfo#getFormat()}).<br>
+     * See {@link SubtitleData} for an example of querying subtitle encoding.<br>
+     * The listener will be called on the same thread as the one in which the MediaPlayer was
+     * created.
+     * @param listener the listener called when new data is available
+     */
+    public void setOnSubtitleDataListener(@NonNull OnSubtitleDataListener listener)
     {
-        mOnSubtitleDataListener = listener;
+        if (listener == null) {
+            throw new IllegalArgumentException("Illegal null listener");
+        }
+        setOnSubtitleDataListenerInt(listener, null);
+    }
+
+    /**
+     * Clears the listener previously set with
+     * {@link #setOnSubtitleDataListener(OnSubtitleDataListener)} or
+     * {@link #setOnSubtitleDataListener(OnSubtitleDataListener, Handler)}.
+     */
+    public void clearOnSubtitleDataListener() {
+        setOnSubtitleDataListenerInt(null, null);
+    }
+
+    private void setOnSubtitleDataListenerInt(
+            @Nullable OnSubtitleDataListener listener, @Nullable Handler handler) {
+        synchronized (this) {
+            mOnSubtitleDataListener = listener;
+            mOnSubtitleDataHandler = handler;
+        }
     }
 
     private OnSubtitleDataListener mOnSubtitleDataListener;
+    private Handler mOnSubtitleDataHandler;
+
+    /**
+     * Interface definition of a callback to be invoked when discontinuity in the normal progression
+     * of the media time is detected.
+     * The "normal progression" of media time is defined as the expected increase of the playback
+     * position when playing media, relative to the playback speed (for instance every second, media
+     * time increases by two seconds when playing at 2x).<br>
+     * Discontinuities are encountered in the following cases:
+     * <ul>
+     * <li>when the player is starved for data and cannot play anymore</li>
+     * <li>when the player encounters a playback error</li>
+     * <li>when the a seek operation starts, and when it's completed</li>
+     * <li>when the playback speed changes</li>
+     * <li>when the playback state changes</li>
+     * <li>when the player is reset</li>
+     * </ul>
+     * See the
+     * {@link MediaPlayer#setOnMediaTimeDiscontinuityListener(OnMediaTimeDiscontinuityListener, Handler)}
+     * method to set a listener for these events.
+     */
+    public interface OnMediaTimeDiscontinuityListener {
+        /**
+         * Called to indicate a time discontinuity has occured.
+         * @param mp the MediaPlayer for which the discontinuity has occured.
+         * @param mts the timestamp that correlates media time, system time and clock rate,
+         *     or {@link MediaTimestamp#TIMESTAMP_UNKNOWN} in an error case.
+         */
+        public void onMediaTimeDiscontinuity(@NonNull MediaPlayer mp, @NonNull MediaTimestamp mts);
+    }
+
+    /**
+     * Sets the listener to be invoked when a media time discontinuity is encountered.
+     * @param listener the listener called after a discontinuity
+     * @param handler the {@link Handler} that receives the listener events
+     */
+    public void setOnMediaTimeDiscontinuityListener(
+            @NonNull OnMediaTimeDiscontinuityListener listener, @NonNull Handler handler) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Illegal null listener");
+        }
+        if (handler == null) {
+            throw new IllegalArgumentException("Illegal null handler");
+        }
+        setOnMediaTimeDiscontinuityListenerInt(listener, handler);
+    }
+
+    /**
+     * Sets the listener to be invoked when a media time discontinuity is encountered.
+     * The listener will be called on the same thread as the one in which the MediaPlayer was
+     * created.
+     * @param listener the listener called after a discontinuity
+     */
+    public void setOnMediaTimeDiscontinuityListener(
+            @NonNull OnMediaTimeDiscontinuityListener listener)
+    {
+        if (listener == null) {
+            throw new IllegalArgumentException("Illegal null listener");
+        }
+        setOnMediaTimeDiscontinuityListenerInt(listener, null);
+    }
+
+    /**
+     * Clears the listener previously set with
+     * {@link #setOnMediaTimeDiscontinuityListener(OnMediaTimeDiscontinuityListener)}
+     * or {@link #setOnMediaTimeDiscontinuityListener(OnMediaTimeDiscontinuityListener, Handler)}
+     */
+    public void clearOnMediaTimeDiscontinuityListener() {
+        setOnMediaTimeDiscontinuityListenerInt(null, null);
+    }
+
+    private void setOnMediaTimeDiscontinuityListenerInt(
+            @Nullable OnMediaTimeDiscontinuityListener listener, @Nullable Handler handler) {
+        synchronized (this) {
+            mOnMediaTimeDiscontinuityListener = listener;
+            mOnMediaTimeDiscontinuityHandler = handler;
+        }
+    }
+
+    private OnMediaTimeDiscontinuityListener mOnMediaTimeDiscontinuityListener;
+    private Handler mOnMediaTimeDiscontinuityHandler;
 
     /**
      * Interface definition of a callback to be invoked when a
@@ -3952,8 +4099,8 @@
 
     /** The player was started because it was used as the next player for another
      * player, which just completed playback.
+     * @see android.media.MediaPlayer#setNextMediaPlayer(MediaPlayer)
      * @see android.media.MediaPlayer.OnInfoListener
-     * @hide
      */
     public static final int MEDIA_INFO_STARTED_AS_NEXT = 2;
 
diff --git a/android/media/MediaPlayer2.java b/android/media/MediaPlayer2.java
index d36df84..dcc872c 100644
--- a/android/media/MediaPlayer2.java
+++ b/android/media/MediaPlayer2.java
@@ -28,32 +28,22 @@
 import android.os.PersistableBundle;
 import android.view.Surface;
 import android.view.SurfaceHolder;
-import android.media.MediaDrm;
-import android.media.MediaFormat;
-import android.media.MediaPlayer2Impl;
-import android.media.MediaTimeProvider;
-import android.media.PlaybackParams;
-import android.media.SubtitleController;
-import android.media.SubtitleController.Anchor;
-import android.media.SubtitleData;
-import android.media.SubtitleTrack.RenderingWidget;
-import android.media.SyncParams;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.AutoCloseable;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.net.InetSocketAddress;
-import java.util.concurrent.Executor;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.concurrent.Executor;
 
 
 /**
+ * @hide
  * MediaPlayer2 class can be used to control playback
  * of audio/video files and streams. An example on how to use the methods in
  * this class can be found in {@link android.widget.VideoView}.
@@ -91,27 +81,18 @@
  * <p>From this state diagram, one can see that a MediaPlayer2 object has the
  *    following states:</p>
  * <ul>
- *     <li>When a MediaPlayer2 object is just created using <code>new</code> or
+ *     <li>When a MediaPlayer2 object is just created using <code>create</code> or
  *         after {@link #reset()} is called, it is in the <em>Idle</em> state; and after
  *         {@link #close()} is called, it is in the <em>End</em> state. Between these
  *         two states is the life cycle of the MediaPlayer2 object.
  *         <ul>
- *         <li>There is a subtle but important difference between a newly constructed
- *         MediaPlayer2 object and the MediaPlayer2 object after {@link #reset()}
- *         is called. It is a programming error to invoke methods such
+ *         <li> It is a programming error to invoke methods such
  *         as {@link #getCurrentPosition()},
  *         {@link #getDuration()}, {@link #getVideoHeight()},
  *         {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)},
- *         {@link #setVolume(float, float)}, {@link #pause()}, {@link #play()},
+ *         {@link #setPlayerVolume(float)}, {@link #pause()}, {@link #play()},
  *         {@link #seekTo(long, int)} or
- *         {@link #prepareAsync()} in the <em>Idle</em> state for both cases. If any of these
- *         methods is called right after a MediaPlayer2 object is constructed,
- *         the user supplied callback method OnErrorListener.onError() won't be
- *         called by the internal player engine and the object state remains
- *         unchanged; but if these methods are called right after {@link #reset()},
- *         the user supplied callback method OnErrorListener.onError() will be
- *         invoked by the internal player engine and the object will be
- *         transfered to the <em>Error</em> state. </li>
+ *         {@link #prepare()} in the <em>Idle</em> state.
  *         <li>It is also recommended that once
  *         a MediaPlayer2 object is no longer being used, call {@link #close()} immediately
  *         so that resources used by the internal player engine associated with the
@@ -135,9 +116,9 @@
  *         these circumstances. Sometimes, due to programming errors, invoking a playback
  *         control operation in an invalid state may also occur. Under all these
  *         error conditions, the internal player engine invokes a user supplied
- *         EventCallback.onError() method if an EventCallback has been
+ *         MediaPlayer2EventCallback.onError() method if an MediaPlayer2EventCallback has been
  *         registered beforehand via
- *         {@link #registerEventCallback(Executor, EventCallback)}.
+ *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
  *         <ul>
  *         <li>It is important to note that once an error occurs, the
  *         MediaPlayer2 object enters the <em>Error</em> state (except as noted
@@ -151,43 +132,42 @@
  *         the internal player engine.</li>
  *         <li>IllegalStateException is
  *         thrown to prevent programming errors such as calling
- *         {@link #prepareAsync()}, {@link #setDataSource(DataSourceDesc)}, or
- *         {@code setPlaylist} methods in an invalid state. </li>
+ *         {@link #prepare()}, {@link #setDataSource(DataSourceDesc)}
+ *         methods in an invalid state. </li>
  *         </ul>
  *         </li>
  *     <li>Calling
- *         {@link #setDataSource(DataSourceDesc)}, or
- *         {@code setPlaylist} transfers a
+ *         {@link #setDataSource(DataSourceDesc)} transfers a
  *         MediaPlayer2 object in the <em>Idle</em> state to the
  *         <em>Initialized</em> state.
  *         <ul>
  *         <li>An IllegalStateException is thrown if
- *         setDataSource() or setPlaylist() is called in any other state.</li>
+ *         setDataSource() is called in any other state.</li>
  *         <li>It is good programming
  *         practice to always look out for <code>IllegalArgumentException</code>
  *         and <code>IOException</code> that may be thrown from
- *         <code>setDataSource</code> and <code>setPlaylist</code> methods.</li>
+ *         <code>setDataSource</code>.</li>
  *         </ul>
  *         </li>
  *     <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state
  *         before playback can be started.
  *         <ul>
  *         <li>There are an asynchronous way that the <em>Prepared</em> state can be reached:
- *         a call to {@link #prepareAsync()} (asynchronous) which
+ *         a call to {@link #prepare()} (asynchronous) which
  *         first transfers the object to the <em>Preparing</em> state after the
  *         call returns (which occurs almost right way) while the internal
  *         player engine continues working on the rest of preparation work
  *         until the preparation work completes. When the preparation completes,
  *         the internal player engine then calls a user supplied callback method,
- *         onInfo() of the EventCallback interface with {@link #MEDIA_INFO_PREPARED}, if an
- *         EventCallback is registered beforehand via
- *         {@link #registerEventCallback(Executor, EventCallback)}.</li>
+ *         onInfo() of the MediaPlayer2EventCallback interface with {@link #MEDIA_INFO_PREPARED},
+ *         if an MediaPlayer2EventCallback is registered beforehand via
+ *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</li>
  *         <li>It is important to note that
  *         the <em>Preparing</em> state is a transient state, and the behavior
  *         of calling any method with side effect while a MediaPlayer2 object is
  *         in the <em>Preparing</em> state is undefined.</li>
  *         <li>An IllegalStateException is
- *         thrown if {@link #prepareAsync()} is called in
+ *         thrown if {@link #prepare()} is called in
  *         any other state.</li>
  *         <li>While in the <em>Prepared</em> state, properties
  *         such as audio/sound volume, screenOnWhilePlaying, looping can be
@@ -196,13 +176,14 @@
  *         </li>
  *     <li>To start the playback, {@link #play()} must be called. After
  *         {@link #play()} returns successfully, the MediaPlayer2 object is in the
- *         <em>Started</em> state. {@link #isPlaying()} can be called to test
+ *         <em>Started</em> state. {@link #getPlayerState()} can be called to test
  *         whether the MediaPlayer2 object is in the <em>Started</em> state.
  *         <ul>
  *         <li>While in the <em>Started</em> state, the internal player engine calls
- *         a user supplied EventCallback.onBufferingUpdate() callback
- *         method if an EventCallback has been registered beforehand
- *         via {@link #registerEventCallback(Executor, EventCallback)}.
+ *         a user supplied callback method MediaPlayer2EventCallback.onInfo() with
+ *         {@link #MEDIA_INFO_BUFFERING_UPDATE} if an MediaPlayer2EventCallback has been
+ *         registered beforehand via
+ *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
  *         This callback allows applications to keep track of the buffering status
  *         while streaming audio/video.</li>
  *         <li>Calling {@link #play()} has not effect
@@ -215,7 +196,7 @@
  *         <em>Paused</em> state. Note that the transition from the <em>Started</em>
  *         state to the <em>Paused</em> state and vice versa happens
  *         asynchronously in the player engine. It may take some time before
- *         the state is updated in calls to {@link #isPlaying()}, and it can be
+ *         the state is updated in calls to {@link #getPlayerState()}, and it can be
  *         a number of seconds in the case of streamed content.
  *         <ul>
  *         <li>Calling {@link #play()} to resume playback for a paused
@@ -234,9 +215,10 @@
  *         call returns right away, the actual seek operation may take a while to
  *         finish, especially for audio/video being streamed. When the actual
  *         seek operation completes, the internal player engine calls a user
- *         supplied EventCallback.onInfo() with {@link #MEDIA_INFO_COMPLETE_CALL_SEEK}
- *         if an EventCallback has been registered beforehand via
- *         {@link #registerEventCallback(Executor, EventCallback)}.</li>
+ *         supplied MediaPlayer2EventCallback.onCallCompleted() with
+ *         {@link #CALL_COMPLETED_SEEK_TO}
+ *         if an MediaPlayer2EventCallback has been registered beforehand via
+ *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.</li>
  *         <li>Please
  *         note that {@link #seekTo(long, int)} can also be called in the other states,
  *         such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted
@@ -252,15 +234,13 @@
  *         </li>
  *     <li>When the playback reaches the end of stream, the playback completes.
  *         <ul>
- *         <li>If the looping mode was being set to one of the values of
- *         {@link #LOOPING_MODE_FULL}, {@link #LOOPING_MODE_SINGLE} or
- *         {@link #LOOPING_MODE_SHUFFLE} with
- *         {@link #setLoopingMode(int)}, the MediaPlayer2 object shall remain in
- *         the <em>Started</em> state.</li>
+ *         <li>If current source is set to loop by {@link #loopCurrent(boolean)},
+ *         the MediaPlayer2 object shall remain in the <em>Started</em> state.</li>
  *         <li>If the looping mode was set to <var>false
  *         </var>, the player engine calls a user supplied callback method,
- *         EventCallback.onCompletion(), if an EventCallback is registered
- *         beforehand via {@link #registerEventCallback(Executor, EventCallback)}.
+ *         MediaPlayer2EventCallback.onCompletion(), if an MediaPlayer2EventCallback is
+ *         registered beforehand via
+ *         {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)}.
  *         The invoke of the callback signals that the object is now in the <em>
  *         PlaybackCompleted</em> state.</li>
  *         <li>While in the <em>PlaybackCompleted</em>
@@ -280,7 +260,7 @@
  * <tr><td>attachAuxEffect </p></td>
  *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
  *     <td>{Idle, Error} </p></td>
- *     <td>This method must be called after setDataSource or setPlaylist.
+ *     <td>This method must be called after setDataSource.
  *     Calling it does not change the object state. </p></td></tr>
  * <tr><td>getAudioSessionId </p></td>
  *     <td>any </p></td>
@@ -314,7 +294,7 @@
  *     <td>Successful invoke of this method in a valid state does not change
  *         the state. Calling this method in an invalid state transfers the
  *         object to the <em>Error</em> state. </p></td></tr>
- * <tr><td>isPlaying </p></td>
+ * <tr><td>getPlayerState </p></td>
  *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
  *          PlaybackCompleted}</p></td>
  *     <td>{Error}</p></td>
@@ -327,7 +307,7 @@
  *     <td>Successful invoke of this method in a valid state transfers the
  *         object to the <em>Paused</em> state. Calling this method in an
  *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>prepareAsync </p></td>
+ * <tr><td>prepare </p></td>
  *     <td>{Initialized, Stopped} </p></td>
  *     <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
  *     <td>Successful invoke of this method in a valid state transfers the
@@ -354,13 +334,13 @@
  *     <td>{Error}</p></td>
  *     <td>Successful invoke of this method does not change the state. In order for the
  *         target audio attributes type to become effective, this method must be called before
- *         prepareAsync().</p></td></tr>
+ *         prepare().</p></td></tr>
  * <tr><td>setAudioSessionId </p></td>
  *     <td>{Idle} </p></td>
  *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
  *          Error} </p></td>
  *     <td>This method must be called in idle state as the audio session ID must be known before
- *         calling setDataSource or setPlaylist. Calling it does not change the object
+ *         calling setDataSource. Calling it does not change the object
  *         state. </p></td></tr>
  * <tr><td>setAudioStreamType (deprecated)</p></td>
  *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
@@ -368,7 +348,7 @@
  *     <td>{Error}</p></td>
  *     <td>Successful invoke of this method does not change the state. In order for the
  *         target audio stream type to become effective, this method must be called before
- *         prepareAsync().</p></td></tr>
+ *         prepare().</p></td></tr>
  * <tr><td>setAuxEffectSendLevel </p></td>
  *     <td>any</p></td>
  *     <td>{} </p></td>
@@ -380,13 +360,6 @@
  *     <td>Successful invoke of this method in a valid state transfers the
  *         object to the <em>Initialized</em> state. Calling this method in an
  *         invalid state throws an IllegalStateException.</p></td></tr>
- * <tr><td>setPlaylist </p></td>
- *     <td>{Idle} </p></td>
- *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
- *          Error} </p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Initialized</em> state. Calling this method in an
- *         invalid state throws an IllegalStateException.</p></td></tr>
  * <tr><td>setDisplay </p></td>
  *     <td>any </p></td>
  *     <td>{} </p></td>
@@ -397,7 +370,7 @@
  *     <td>{} </p></td>
  *     <td>This method can be called in any state and calling it does not change
  *         the object state. </p></td></tr>
- * <tr><td>setLoopingMode </p></td>
+ * <tr><td>loopCurrent </p></td>
  *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
  *         PlaybackCompleted}</p></td>
  *     <td>{Error}</p></td>
@@ -409,12 +382,12 @@
  *     <td>{} </p></td>
  *     <td>This method can be called in any state and calling it does not change
  *         the object state. </p></td></tr>
- * <tr><td>registerDrmEventCallback </p></td>
+ * <tr><td>setDrmEventCallback </p></td>
  *     <td>any </p></td>
  *     <td>{} </p></td>
  *     <td>This method can be called in any state and calling it does not change
  *         the object state. </p></td></tr>
- * <tr><td>registerEventCallback </p></td>
+ * <tr><td>setMediaPlayer2EventCallback </p></td>
  *     <td>any </p></td>
  *     <td>{} </p></td>
  *     <td>This method can be called in any state and calling it does not change
@@ -424,7 +397,7 @@
  *     <td>{Idle, Stopped} </p></td>
  *     <td>This method will change state in some cases, depending on when it's called.
  *         </p></td></tr>
- * <tr><td>setVolume </p></td>
+ * <tr><td>setPlayerVolume </p></td>
  *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
  *          PlaybackCompleted}</p></td>
  *     <td>{Error}</p></td>
@@ -472,51 +445,17 @@
  * possible runtime errors during playback or streaming. Registration for
  * these events is done by properly setting the appropriate listeners (via calls
  * to
- * {@link #registerEventCallback(Executor, EventCallback)},
- * {@link #registerDrmEventCallback(Executor, DrmEventCallback)}).
+ * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)},
+ * {@link #setDrmEventCallback(Executor, DrmEventCallback)}).
  * In order to receive the respective callback
  * associated with these listeners, applications are required to create
  * MediaPlayer2 objects on a thread with its own Looper running (main UI
  * thread by default has a Looper running).
  *
  */
-public abstract class MediaPlayer2 implements SubtitleController.Listener
-                                            , AudioRouting
-                                            , AutoCloseable
-{
-    /**
-       Constant to retrieve only the new metadata since the last
-       call.
-       // FIXME: unhide.
-       // FIXME: add link to getMetadata(boolean, boolean)
-       {@hide}
-     */
-    public static final boolean METADATA_UPDATE_ONLY = true;
-
-    /**
-       Constant to retrieve all the metadata.
-       // FIXME: unhide.
-       // FIXME: add link to getMetadata(boolean, boolean)
-       {@hide}
-     */
-    public static final boolean METADATA_ALL = false;
-
-    /**
-       Constant to enable the metadata filter during retrieval.
-       // FIXME: unhide.
-       // FIXME: add link to getMetadata(boolean, boolean)
-       {@hide}
-     */
-    public static final boolean APPLY_METADATA_FILTER = true;
-
-    /**
-       Constant to disable the metadata filter during retrieval.
-       // FIXME: unhide.
-       // FIXME: add link to getMetadata(boolean, boolean)
-       {@hide}
-     */
-    public static final boolean BYPASS_METADATA_FILTER = false;
-
+public abstract class MediaPlayer2 extends MediaPlayerBase
+                                   implements SubtitleController.Listener
+                                            , AudioRouting {
     /**
      * Create a MediaPlayer2 object.
      *
@@ -527,6 +466,47 @@
         return new MediaPlayer2Impl();
     }
 
+    private static final String[] decodeMediaPlayer2Uri(String location) {
+        Uri uri = Uri.parse(location);
+        if (!"mediaplayer2".equals(uri.getScheme())) {
+            return new String[] {location};
+        }
+
+        List<String> uris = uri.getQueryParameters("uri");
+        if (uris.isEmpty()) {
+            return new String[] {location};
+        }
+
+        List<String> keys = uri.getQueryParameters("key");
+        List<String> values = uri.getQueryParameters("value");
+        if (keys.size() != values.size()) {
+            return new String[] {uris.get(0)};
+        }
+
+        List<String> ls = new ArrayList();
+        ls.add(uris.get(0));
+        for (int i = 0; i < keys.size() ; i++) {
+            ls.add(keys.get(i));
+            ls.add(values.get(i));
+        }
+
+        return ls.toArray(new String[ls.size()]);
+    }
+
+    private static final String encodeMediaPlayer2Uri(String uri, String[] keys, String[] values) {
+        Uri.Builder builder = new Uri.Builder();
+        builder.scheme("mediaplayer2").path("/").appendQueryParameter("uri", uri);
+        if (keys == null || values == null || keys.length != values.length) {
+            return builder.build().toString();
+        }
+        for (int i = 0; i < keys.length ; i++) {
+            builder
+                .appendQueryParameter("key", keys[i])
+                .appendQueryParameter("value", values[i]);
+        }
+        return builder.build().toString();
+    }
+
     /**
      * @hide
      */
@@ -534,6 +514,271 @@
     public MediaPlayer2() { }
 
     /**
+     * Releases the resources held by this {@code MediaPlayer2} object.
+     *
+     * It is considered good practice to call this method when you're
+     * done using the MediaPlayer2. In particular, whenever an Activity
+     * of an application is paused (its onPause() method is called),
+     * or stopped (its onStop() method is called), this method should be
+     * invoked to release the MediaPlayer2 object, unless the application
+     * has a special need to keep the object around. In addition to
+     * unnecessary resources (such as memory and instances of codecs)
+     * being held, failure to call this method immediately if a
+     * MediaPlayer2 object is no longer needed may also lead to
+     * continuous battery consumption for mobile devices, and playback
+     * failure for other applications if no multiple instances of the
+     * same codec are supported on a device. Even if multiple instances
+     * of the same codec are supported, some performance degradation
+     * may be expected when unnecessary multiple instances are used
+     * at the same time.
+     *
+     * {@code close()} may be safely called after a prior {@code close()}.
+     * This class implements the Java {@code AutoCloseable} interface and
+     * may be used with try-with-resources.
+     */
+    // This is a synchronous call.
+    @Override
+    public abstract void close();
+
+    /**
+     * Starts or resumes playback. If playback had previously been paused,
+     * playback will continue from where it was paused. If playback had
+     * reached end of stream and been paused, or never started before,
+     * playback will start at the beginning. If the source had not been
+     * prepared, the player will prepare the source and play.
+     *
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void play();
+
+    /**
+     * Prepares the player for playback, asynchronously.
+     *
+     * After setting the datasource and the display surface, you need to
+     * call prepare().
+     *
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void prepare();
+
+    /**
+     * Pauses playback. Call play() to resume.
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void pause();
+
+    /**
+     * Tries to play next data source if applicable.
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void skipToNext();
+
+    /**
+     * Moves the media to specified time position.
+     * Same as {@link #seekTo(long, int)} with {@code mode = SEEK_PREVIOUS_SYNC}.
+     *
+     * @param msec the offset in milliseconds from the start to seek to
+     */
+    // This is an asynchronous call.
+    @Override
+    public void seekTo(long msec) {
+        seekTo(msec, SEEK_PREVIOUS_SYNC /* mode */);
+    }
+
+    /**
+     * Gets the current playback position.
+     *
+     * @return the current position in milliseconds
+     */
+    @Override
+    public abstract long getCurrentPosition();
+
+    /**
+     * Gets the duration of the file.
+     *
+     * @return the duration in milliseconds, if no duration is available
+     *         (for example, if streaming live content), -1 is returned.
+     */
+    @Override
+    public abstract long getDuration();
+
+    /**
+     * Gets the current buffered media source position received through progressive downloading.
+     * The received buffering percentage indicates how much of the content has been buffered
+     * or played. For example a buffering update of 80 percent when half the content
+     * has already been played indicates that the next 30 percent of the
+     * content to play has been buffered.
+     *
+     * @return the current buffered media source position in milliseconds
+     */
+    @Override
+    public abstract long getBufferedPosition();
+
+    /**
+     * Gets the current player state.
+     *
+     * @return the current player state.
+     */
+    @Override
+    public abstract @PlayerState int getPlayerState();
+
+    /**
+     * Gets the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     * @return the buffering state, one of the following:
+     */
+    @Override
+    public abstract @BuffState int getBufferingState();
+
+    /**
+     * Sets the audio attributes for this MediaPlayer2.
+     * See {@link AudioAttributes} for how to build and configure an instance of this class.
+     * You must call this method before {@link #prepare()} in order
+     * for the audio attributes to become effective thereafter.
+     * @param attributes a non-null set of audio attributes
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setAudioAttributes(@NonNull AudioAttributes attributes);
+
+    /**
+     * Gets the audio attributes for this MediaPlayer2.
+     * @return attributes a set of audio attributes
+     */
+    @Override
+    public abstract @Nullable AudioAttributes getAudioAttributes();
+
+    /**
+     * Sets the data source as described by a DataSourceDesc.
+     *
+     * @param dsd the descriptor of data source you want to play
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setDataSource(@NonNull DataSourceDesc dsd);
+
+    /**
+     * Sets a single data source as described by a DataSourceDesc which will be played
+     * after current data source is finished.
+     *
+     * @param dsd the descriptor of data source you want to play after current one
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
+
+    /**
+     * Sets a list of data sources to be played sequentially after current data source is done.
+     *
+     * @param dsds the list of data sources you want to play after current one
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
+
+    /**
+     * Gets the current data source as described by a DataSourceDesc.
+     *
+     * @return the current DataSourceDesc
+     */
+    @Override
+    public abstract @NonNull DataSourceDesc getCurrentDataSource();
+
+    /**
+     * Configures the player to loop on the current data source.
+     * @param loop true if the current data source is meant to loop.
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void loopCurrent(boolean loop);
+
+    /**
+     * Sets the playback speed.
+     * A value of 1.0f is the default playback value.
+     * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+     * before using negative values.<br>
+     * After changing the playback speed, it is recommended to query the actual speed supported
+     * by the player, see {@link #getPlaybackSpeed()}.
+     * @param speed the desired playback speed
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setPlaybackSpeed(float speed);
+
+    /**
+     * Returns the actual playback speed to be used by the player when playing.
+     * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+     * @return the actual playback speed
+     */
+    @Override
+    public float getPlaybackSpeed() {
+        return 1.0f;
+    }
+
+    /**
+     * Indicates whether reverse playback is supported.
+     * Reverse playback is indicated by negative playback speeds, see
+     * {@link #setPlaybackSpeed(float)}.
+     * @return true if reverse playback is supported.
+     */
+    @Override
+    public boolean isReversePlaybackSupported() {
+        return false;
+    }
+
+    /**
+     * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+     * on the audio samples.
+     * Note that this volume is specific to the player, and is separate from stream volume
+     * used across the platform.<br>
+     * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+     * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+     * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+     */
+    // This is an asynchronous call.
+    @Override
+    public abstract void setPlayerVolume(float volume);
+
+    /**
+     * Returns the current volume of this player to this player.
+     * Note that it does not take into account the associated stream volume.
+     * @return the player volume.
+     */
+    @Override
+    public abstract float getPlayerVolume();
+
+    /**
+     * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+     */
+    @Override
+    public float getMaxPlayerVolume() {
+        return 1.0f;
+    }
+
+    /**
+     * Adds a callback to be notified of events for this player.
+     * @param e the {@link Executor} to be used for the events.
+     * @param cb the callback to receive the events.
+     */
+    // This is a synchronous call.
+    @Override
+    public abstract void registerPlayerEventCallback(@NonNull Executor e,
+            @NonNull PlayerEventCallback cb);
+
+    /**
+     * Removes a previously registered callback for player events
+     * @param cb the callback to remove
+     */
+    // This is a synchronous call.
+    @Override
+    public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
+
+    /**
      * Create a request parcel which can be routed to the native media
      * player using {@link #invoke(Parcel, Parcel)}. The Parcel
      * returned has the proper InterfaceToken set. The caller should
@@ -552,7 +797,7 @@
      * Invoke a generic method on the native player using opaque
      * parcels for the request and reply. Both payloads' format is a
      * convention between the java caller and the native player.
-     * Must be called after setDataSource or setPlaylist to make sure a native player
+     * Must be called after setDataSource to make sure a native player
      * exists. On failure, a RuntimeException is thrown.
      *
      * @param request Parcel with the data for the extension. The
@@ -565,6 +810,20 @@
     public void invoke(Parcel request, Parcel reply) { }
 
     /**
+     * Insert a task in the command queue to help the client to identify whether a batch
+     * of commands has been finished. When this command is processed, a notification
+     * {@code MediaPlayer2EventCallback.onCommandLabelReached} will be fired with the
+     * given {@code label}.
+     *
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCommandLabelReached
+     *
+     * @param label An application specific Object used to help to identify the completeness
+     * of a batch of commands.
+     */
+    // This is an asynchronous call.
+    public void notifyWhenCommandLabelReached(@NonNull Object label) { }
+
+    /**
      * Sets the {@link SurfaceHolder} to use for displaying the video
      * portion of the media.
      *
@@ -600,6 +859,7 @@
      * @throws IllegalStateException if the internal player engine has not been
      * initialized or has been released.
      */
+    // This is an asynchronous call.
     public abstract void setSurface(Surface surface);
 
     /* Do not change these video scaling mode values below without updating
@@ -648,204 +908,10 @@
     /**
      * Discards all pending commands.
      */
+    // This is a synchronous call.
     public abstract void clearPendingCommands();
 
     /**
-     * Sets the data source as described by a DataSourceDesc.
-     *
-     * @param dsd the descriptor of data source you want to play
-     * @throws IllegalStateException if it is called in an invalid state
-     * @throws NullPointerException if dsd is null
-     */
-    public abstract void setDataSource(@NonNull DataSourceDesc dsd) throws IOException;
-
-    /**
-     * Gets the current data source as described by a DataSourceDesc.
-     *
-     * @return the current DataSourceDesc
-     */
-    public abstract DataSourceDesc getCurrentDataSource();
-
-    /**
-     * Sets the play list.
-     *
-     * If startIndex falls outside play list range, it will be clamped to the nearest index
-     * in the play list.
-     *
-     * @param pl the play list of data source you want to play
-     * @param startIndex the index of the DataSourceDesc in the play list you want to play first
-     * @throws IllegalStateException if it is called in an invalid state
-     * @throws IllegalArgumentException if pl is null or empty, or pl contains null DataSourceDesc
-     */
-    public abstract void setPlaylist(@NonNull List<DataSourceDesc> pl, int startIndex)
-            throws IOException;
-
-    /**
-     * Gets a copy of the play list.
-     *
-     * @return a copy of the play list used by {@link MediaPlayer2}
-     */
-    public abstract List<DataSourceDesc> getPlaylist();
-
-    /**
-     * Sets the index of current DataSourceDesc in the play list to be played.
-     *
-     * @param index the index of DataSourceDesc in the play list you want to play
-     * @throws IllegalArgumentException if the play list is null
-     * @throws NullPointerException if index is outside play list range
-     */
-    public abstract void setCurrentPlaylistItem(int index);
-
-    /**
-     * Sets the index of next-to-be-played DataSourceDesc in the play list.
-     *
-     * @param index the index of next-to-be-played DataSourceDesc in the play list
-     * @throws IllegalArgumentException if the play list is null
-     * @throws NullPointerException if index is outside play list range
-     */
-    public abstract void setNextPlaylistItem(int index);
-
-    /**
-     * Gets the current index of play list.
-     *
-     * @return the index of the current DataSourceDesc in the play list
-     */
-    public abstract int getCurrentPlaylistItemIndex();
-
-    /**
-     * Specifies a playback looping mode. The source will not be played in looping mode.
-     */
-    public static final int LOOPING_MODE_NONE = 0;
-    /**
-     * Specifies a playback looping mode. The full list of source will be played in looping mode,
-     * and in the order specified in the play list.
-     */
-    public static final int LOOPING_MODE_FULL = 1;
-    /**
-     * Specifies a playback looping mode. The current DataSourceDesc will be played in looping mode.
-     */
-    public static final int LOOPING_MODE_SINGLE = 2;
-    /**
-     * Specifies a playback looping mode. The full list of source will be played in looping mode,
-     * and in a random order.
-     */
-    public static final int LOOPING_MODE_SHUFFLE = 3;
-
-    /** @hide */
-    @IntDef(
-        value = {
-            LOOPING_MODE_NONE,
-            LOOPING_MODE_FULL,
-            LOOPING_MODE_SINGLE,
-            LOOPING_MODE_SHUFFLE,
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface LoopingMode {}
-
-    /**
-     * Sets the looping mode of the play list.
-     * The mode shall be one of {@link #LOOPING_MODE_NONE}, {@link #LOOPING_MODE_FULL},
-     * {@link #LOOPING_MODE_SINGLE}, {@link #LOOPING_MODE_SHUFFLE}.
-     *
-     * @param mode the mode in which the play list will be played
-     * @throws IllegalArgumentException if mode is not supported
-     */
-    public abstract void setLoopingMode(@LoopingMode int mode);
-
-    /**
-     * Gets the looping mode of play list.
-     *
-     * @return the looping mode of the play list
-     */
-    public abstract int getLoopingMode();
-
-    /**
-     * Moves the DataSourceDesc at indexFrom in the play list to indexTo.
-     *
-     * @throws IllegalArgumentException if the play list is null
-     * @throws IndexOutOfBoundsException if indexFrom or indexTo is outside play list range
-     */
-    public abstract void movePlaylistItem(int indexFrom, int indexTo);
-
-    /**
-     * Removes the DataSourceDesc at index in the play list.
-     *
-     * If index is same as the current index of the play list, current DataSourceDesc
-     * will be stopped and playback moves to next source in the list.
-     *
-     * @return the removed DataSourceDesc at index in the play list
-     * @throws IllegalArgumentException if the play list is null
-     * @throws IndexOutOfBoundsException if index is outside play list range
-     */
-    public abstract DataSourceDesc removePlaylistItem(int index);
-
-    /**
-     * Inserts the DataSourceDesc to the play list at position index.
-     *
-     * This will not change the DataSourceDesc currently being played.
-     * If index is less than or equal to the current index of the play list,
-     * the current index of the play list will be incremented correspondingly.
-     *
-     * @param index the index you want to add dsd to the play list
-     * @param dsd the descriptor of data source you want to add to the play list
-     * @throws IndexOutOfBoundsException if index is outside play list range
-     * @throws NullPointerException if dsd is null
-     */
-    public abstract void addPlaylistItem(int index, DataSourceDesc dsd);
-
-    /**
-     * replaces the DataSourceDesc at index in the play list with given dsd.
-     *
-     * When index is same as the current index of the play list, the current source
-     * will be stopped and the new source will be played, except that if new
-     * and old source only differ on end position and current media position is
-     * smaller then the new end position.
-     *
-     * This will not change the DataSourceDesc currently being played.
-     * If index is less than or equal to the current index of the play list,
-     * the current index of the play list will be incremented correspondingly.
-     *
-     * @param index the index you want to add dsd to the play list
-     * @param dsd the descriptor of data source you want to add to the play list
-     * @throws IndexOutOfBoundsException if index is outside play list range
-     * @throws NullPointerException if dsd is null
-     */
-    public abstract DataSourceDesc editPlaylistItem(int index, DataSourceDesc dsd);
-
-    /**
-     * Prepares the player for playback, synchronously.
-     *
-     * After setting the datasource and the display surface, you need to either
-     * call prepare() or prepareAsync(). For files, it is OK to call prepare(),
-     * which blocks until MediaPlayer2 is ready for playback.
-     *
-     * @throws IOException if source can not be accessed
-     * @throws IllegalStateException if it is called in an invalid state
-     * @hide
-     */
-    public void prepare() throws IOException { }
-
-    /**
-     * Prepares the player for playback, asynchronously.
-     *
-     * After setting the datasource and the display surface, you need to
-     * call prepareAsync().
-     *
-     * @throws IllegalStateException if it is called in an invalid state
-     */
-    public abstract void prepareAsync();
-
-    /**
-     * Starts or resumes playback. If playback had previously been paused,
-     * playback will continue from where it was paused. If playback had
-     * been stopped, or never started before, playback will start at the
-     * beginning.
-     *
-     * @throws IllegalStateException if it is called in an invalid state
-     */
-    public abstract void play();
-
-    /**
      * Stops playback after playback has been started or paused.
      *
      * @throws IllegalStateException if the internal player engine has not been
@@ -854,14 +920,6 @@
      */
     public void stop() { }
 
-    /**
-     * Pauses playback. Call play() to resume.
-     *
-     * @throws IllegalStateException if the internal player engine has not been
-     * initialized.
-     */
-    public abstract void pause();
-
     //--------------------------------------------------------------------------
     // Explicit Routing
     //--------------------
@@ -874,6 +932,7 @@
      * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and
      * does not correspond to a valid audio device.
      */
+    // This is an asynchronous call.
     @Override
     public abstract boolean setPreferredDevice(AudioDeviceInfo deviceInfo);
 
@@ -901,6 +960,7 @@
      * @param handler  Specifies the {@link Handler} object for the thread on which to execute
      * the callback. If <code>null</code>, the handler on the main looper will be used.
      */
+    // This is a synchronous call.
     @Override
     public abstract void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener,
             Handler handler);
@@ -911,6 +971,7 @@
      * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface
      * to remove.
      */
+    // This is a synchronous call.
     @Override
     public abstract void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener);
 
@@ -949,9 +1010,10 @@
      *
      * @return the width of the video, or 0 if there is no video,
      * no display surface was set, or the width has not been determined
-     * yet. The {@code EventCallback} can be registered via
-     * {@link #registerEventCallback(Executor, EventCallback)} to provide a
-     * notification {@code EventCallback.onVideoSizeChanged} when the width is available.
+     * yet. The {@code MediaPlayer2EventCallback} can be registered via
+     * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+     * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the width
+     * is available.
      */
     public abstract int getVideoWidth();
 
@@ -960,9 +1022,9 @@
      *
      * @return the height of the video, or 0 if there is no video,
      * no display surface was set, or the height has not been determined
-     * yet. The {@code EventCallback} can be registered via
-     * {@link #registerEventCallback(Executor, EventCallback)} to provide a
-     * notification {@code EventCallback.onVideoSizeChanged} when the height is available.
+     * yet. The {@code MediaPlayer2EventCallback} can be registered via
+     * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+     * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the height is available.
      */
     public abstract int getVideoHeight();
 
@@ -984,10 +1046,65 @@
      * @return true if currently playing, false otherwise
      * @throws IllegalStateException if the internal player engine has not been
      * initialized or has been released.
+     * @hide
      */
     public abstract boolean isPlaying();
 
     /**
+     * MediaPlayer2 has not been prepared or just has been reset.
+     * In this state, MediaPlayer2 doesn't fetch data.
+     * @hide
+     */
+    public static final int MEDIAPLAYER2_STATE_IDLE = 1;
+
+    /**
+     * MediaPlayer2 has been just prepared.
+     * In this state, MediaPlayer2 just fetches data from media source,
+     * but doesn't actively render data.
+     * @hide
+     */
+    public static final int MEDIAPLAYER2_STATE_PREPARED = 2;
+
+    /**
+     * MediaPlayer2 is paused.
+     * In this state, MediaPlayer2 doesn't actively render data.
+     * @hide
+     */
+    public static final int MEDIAPLAYER2_STATE_PAUSED = 3;
+
+    /**
+     * MediaPlayer2 is actively playing back data.
+     * @hide
+     */
+    public static final int MEDIAPLAYER2_STATE_PLAYING = 4;
+
+    /**
+     * MediaPlayer2 has hit some fatal error and cannot continue playback.
+     * @hide
+     */
+    public static final int MEDIAPLAYER2_STATE_ERROR = 5;
+
+    /**
+     * @hide
+     */
+    @IntDef(flag = false, prefix = "MEDIAPLAYER2_STATE", value = {
+        MEDIAPLAYER2_STATE_IDLE,
+        MEDIAPLAYER2_STATE_PREPARED,
+        MEDIAPLAYER2_STATE_PAUSED,
+        MEDIAPLAYER2_STATE_PLAYING,
+        MEDIAPLAYER2_STATE_ERROR })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MediaPlayer2State {}
+
+    /**
+     * Gets the current MediaPlayer2 state.
+     *
+     * @return the current MediaPlayer2 state.
+     * @hide
+     */
+    public abstract @MediaPlayer2State int getMediaPlayer2State();
+
+    /**
      * Gets the current buffering management params used by the source component.
      * Calling it only after {@code setDataSource} has been called.
      * Each type of data source might have different set of default params.
@@ -1016,6 +1133,7 @@
      * @throws IllegalArgumentException if params is invalid or not supported.
      * @hide
      */
+    // This is an asynchronous call.
     public void setBufferingParams(@NonNull BufferingParams params) { }
 
     /**
@@ -1060,8 +1178,7 @@
     public static final int PLAYBACK_RATE_AUDIO_MODE_DEFAULT = 0;
 
     /** @hide */
-    @IntDef(
-        value = {
+    @IntDef(flag = false, prefix = "PLAYBACK_RATE_AUDIO_MODE", value = {
             PLAYBACK_RATE_AUDIO_MODE_DEFAULT,
             PLAYBACK_RATE_AUDIO_MODE_STRETCH,
             PLAYBACK_RATE_AUDIO_MODE_RESAMPLE,
@@ -1097,19 +1214,14 @@
      * non-zero speed is equivalent to calling play().
      *
      * @param params the playback params.
-     *
-     * @throws IllegalStateException if the internal player engine has not been
-     * initialized or has been released.
-     * @throws IllegalArgumentException if params is not supported.
      */
+    // This is an asynchronous call.
     public abstract void setPlaybackParams(@NonNull PlaybackParams params);
 
     /**
      * Gets the playback params, containing the current playback rate.
      *
      * @return the playback params.
-     * @throws IllegalStateException if the internal player engine has not been
-     * initialized.
      */
     @NonNull
     public abstract PlaybackParams getPlaybackParams();
@@ -1118,20 +1230,14 @@
      * Sets A/V sync mode.
      *
      * @param params the A/V sync params to apply
-     *
-     * @throws IllegalStateException if the internal player engine has not been
-     * initialized.
-     * @throws IllegalArgumentException if params are not supported.
      */
+    // This is an asynchronous call.
     public abstract void setSyncParams(@NonNull SyncParams params);
 
     /**
      * Gets the A/V sync mode.
      *
      * @return the A/V sync params
-     *
-     * @throws IllegalStateException if the internal player engine has not been
-     * initialized.
      */
     @NonNull
     public abstract SyncParams getSyncParams();
@@ -1177,8 +1283,7 @@
     public static final int SEEK_CLOSEST          = 0x03;
 
     /** @hide */
-    @IntDef(
-        value = {
+    @IntDef(flag = false, prefix = "SEEK", value = {
             SEEK_PREVIOUS_SYNC,
             SEEK_NEXT_SYNC,
             SEEK_CLOSEST_SYNC,
@@ -1203,20 +1308,8 @@
      * If msec is negative, time position zero will be used.
      * If msec is larger than duration, duration will be used.
      * @param mode the mode indicating where exactly to seek to.
-     * Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame
-     * that has a timestamp earlier than or the same as msec. Use
-     * {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame
-     * that has a timestamp later than or the same as msec. Use
-     * {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame
-     * that has a timestamp closest to or the same as msec. Use
-     * {@link #SEEK_CLOSEST} if one wants to seek to a frame that may
-     * or may not be a sync frame but is closest to or the same as msec.
-     * {@link #SEEK_CLOSEST} often has larger performance overhead compared
-     * to the other options if there is no sync frame located at msec.
-     * @throws IllegalStateException if the internal player engine has not been
-     * initialized
-     * @throws IllegalArgumentException if the mode is invalid.
      */
+    // This is an asynchronous call.
     public abstract void seekTo(long msec, @SeekMode int mode);
 
     /**
@@ -1241,21 +1334,6 @@
     public abstract MediaTimestamp getTimestamp();
 
     /**
-     * Gets the current playback position.
-     *
-     * @return the current position in milliseconds
-     */
-    public abstract int getCurrentPosition();
-
-    /**
-     * Gets the duration of the file.
-     *
-     * @return the duration in milliseconds, if no duration is available
-     *         (for example, if streaming live content), -1 is returned.
-     */
-    public abstract int getDuration();
-
-    /**
      * Gets the media metadata.
      *
      * @param update_only controls whether the full set of available
@@ -1300,31 +1378,12 @@
     }
 
     /**
-     * Set the MediaPlayer2 to start when this MediaPlayer2 finishes playback
-     * (i.e. reaches the end of the stream).
-     * The media framework will attempt to transition from this player to
-     * the next as seamlessly as possible. The next player can be set at
-     * any time before completion, but shall be after setDataSource has been
-     * called successfully. The next player must be prepared by the
-     * app, and the application should not call play() on it.
-     * The next MediaPlayer2 must be different from 'this'. An exception
-     * will be thrown if next == this.
-     * The application may call setNextMediaPlayer(null) to indicate no
-     * next player should be started at the end of playback.
-     * If the current player is looping, it will keep looping and the next
-     * player will not be started.
-     *
-     * @param next the player to start after this one completes playback.
-     *
-     * @hide
-     */
-    public void setNextMediaPlayer(MediaPlayer2 next) { }
-
-    /**
      * Resets the MediaPlayer2 to its uninitialized state. After calling
      * this method, you will have to initialize it again by setting the
-     * data source and calling prepareAsync().
+     * data source and calling prepare().
      */
+    // This is a synchronous call.
+    @Override
     public abstract void reset();
 
     /**
@@ -1338,24 +1397,6 @@
     public void notifyAt(long mediaTimeUs) { }
 
     /**
-     * Sets the audio attributes for this MediaPlayer2.
-     * See {@link AudioAttributes} for how to build and configure an instance of this class.
-     * You must call this method before {@link #prepareAsync()} in order
-     * for the audio attributes to become effective thereafter.
-     * @param attributes a non-null set of audio attributes
-     * @throws IllegalArgumentException if the attributes are null or invalid.
-     */
-    public abstract void setAudioAttributes(AudioAttributes attributes);
-
-    /**
-     * Sets the player to be looping or non-looping.
-     *
-     * @param looping whether to loop or not
-     * @hide
-     */
-    public void setLooping(boolean looping) { }
-
-    /**
      * Checks whether the MediaPlayer2 is looping or non-looping.
      *
      * @return true if the MediaPlayer2 is currently looping, false otherwise
@@ -1366,31 +1407,6 @@
     }
 
     /**
-     * Sets the volume on this player.
-     * This API is recommended for balancing the output of audio streams
-     * within an application. Unless you are writing an application to
-     * control user settings, this API should be used in preference to
-     * {@link AudioManager#setStreamVolume(int, int, int)} which sets the volume of ALL streams of
-     * a particular type. Note that the passed volume values are raw scalars in range 0.0 to 1.0.
-     * UI controls should be scaled logarithmically.
-     *
-     * @param leftVolume left volume scalar
-     * @param rightVolume right volume scalar
-     */
-    /*
-     * FIXME: Merge this into javadoc comment above when setVolume(float) is not @hide.
-     * The single parameter form below is preferred if the channel volumes don't need
-     * to be set independently.
-     */
-    public abstract void setVolume(float leftVolume, float rightVolume);
-
-    /**
-     * Similar, excepts sets volume of all channels to same value.
-     * @hide
-     */
-    public void setVolume(float volume) { }
-
-    /**
      * Sets the audio session ID.
      *
      * @param sessionId the audio session ID.
@@ -1404,9 +1420,8 @@
      * However, it is possible to force this player to be part of an already existing audio session
      * by calling this method.
      * This method must be called before one of the overloaded <code> setDataSource </code> methods.
-     * @throws IllegalStateException if it is called in an invalid state
-     * @throws IllegalArgumentException if the sessionId is invalid.
      */
+    // This is an asynchronous call.
     public abstract void setAudioSessionId(int sessionId);
 
     /**
@@ -1431,6 +1446,7 @@
      * methods.
      * @param effectId system wide unique id of the effect to attach
      */
+    // This is an asynchronous call.
     public abstract void attachAuxEffect(int effectId);
 
 
@@ -1446,6 +1462,7 @@
      * 0 < x <= R -> level = 10^(72*(x-R)/20/R)
      * @param level send level scalar
      */
+    // This is an asynchronous call.
     public abstract void setAuxEffectSendLevel(float level);
 
     /**
@@ -1494,7 +1511,6 @@
      * @return List of track info. The total number of tracks is the array length.
      * Must be called again if an external timed text source has been added after
      * addTimedTextSource method is called.
-     * @throws IllegalStateException if it is called in an invalid state.
      */
     public abstract List<TrackInfo> getTrackInfo();
 
@@ -1661,6 +1677,7 @@
      *
      * @see android.media.MediaPlayer2#getTrackInfo
      */
+    // This is an asynchronous call.
     public abstract void selectTrack(int index);
 
     /**
@@ -1677,63 +1694,9 @@
      *
      * @see android.media.MediaPlayer2#getTrackInfo
      */
+    // This is an asynchronous call.
     public abstract void deselectTrack(int index);
 
-    /**
-     * Sets the target UDP re-transmit endpoint for the low level player.
-     * Generally, the address portion of the endpoint is an IP multicast
-     * address, although a unicast address would be equally valid.  When a valid
-     * retransmit endpoint has been set, the media player will not decode and
-     * render the media presentation locally.  Instead, the player will attempt
-     * to re-multiplex its media data using the Android@Home RTP profile and
-     * re-transmit to the target endpoint.  Receiver devices (which may be
-     * either the same as the transmitting device or different devices) may
-     * instantiate, prepare, and start a receiver player using a setDataSource
-     * URL of the form...
-     *
-     * aahRX://&lt;multicastIP&gt;:&lt;port&gt;
-     *
-     * to receive, decode and render the re-transmitted content.
-     *
-     * setRetransmitEndpoint may only be called before setDataSource has been
-     * called; while the player is in the Idle state.
-     *
-     * @param endpoint the address and UDP port of the re-transmission target or
-     * null if no re-transmission is to be performed.
-     * @throws IllegalStateException if it is called in an invalid state
-     * @throws IllegalArgumentException if the retransmit endpoint is supplied,
-     * but invalid.
-     *
-     * {@hide} pending API council
-     */
-    public void setRetransmitEndpoint(InetSocketAddress endpoint) { }
-
-    /**
-     * Releases the resources held by this {@code MediaPlayer2} object.
-     *
-     * It is considered good practice to call this method when you're
-     * done using the MediaPlayer2. In particular, whenever an Activity
-     * of an application is paused (its onPause() method is called),
-     * or stopped (its onStop() method is called), this method should be
-     * invoked to release the MediaPlayer2 object, unless the application
-     * has a special need to keep the object around. In addition to
-     * unnecessary resources (such as memory and instances of codecs)
-     * being held, failure to call this method immediately if a
-     * MediaPlayer2 object is no longer needed may also lead to
-     * continuous battery consumption for mobile devices, and playback
-     * failure for other applications if no multiple instances of the
-     * same codec are supported on a device. Even if multiple instances
-     * of the same codec are supported, some performance degradation
-     * may be expected when unnecessary multiple instances are used
-     * at the same time.
-     *
-     * {@code close()} may be safely called after a prior {@code close()}.
-     * This class implements the Java {@code AutoCloseable} interface and
-     * may be used with try-with-resources.
-     */
-    @Override
-    public abstract void close();
-
     /** @hide */
     public MediaTimeProvider getMediaTimeProvider() {
         return null;
@@ -1743,22 +1706,7 @@
      * Interface definition for callbacks to be invoked when the player has the corresponding
      * events.
      */
-    public abstract static class EventCallback {
-        /**
-         * Called to update status in buffering a media source received through
-         * progressive downloading. The received buffering percentage
-         * indicates how much of the content has been buffered or played.
-         * For example a buffering update of 80 percent when half the content
-         * has already been played indicates that the next 30 percent of the
-         * content to play has been buffered.
-         *
-         * @param mp the MediaPlayer2 the update pertains to
-         * @param srcId the Id of this data source
-         * @param percent the percentage (0-100) of the content
-         *                that has been buffered or played thus far
-         */
-        public void onBufferingUpdate(MediaPlayer2 mp, long srcId, int percent) { }
-
+    public abstract static class MediaPlayer2EventCallback {
         /**
          * Called to indicate the video size
          *
@@ -1766,22 +1714,22 @@
          * no display surface was set, or the value was not determined yet.
          *
          * @param mp the MediaPlayer2 associated with this callback
-         * @param srcId the Id of this data source
+         * @param dsd the DataSourceDesc of this data source
          * @param width the width of the video
          * @param height the height of the video
          */
-        public void onVideoSizeChanged(MediaPlayer2 mp, long srcId, int width, int height) { }
+        public void onVideoSizeChanged(MediaPlayer2 mp, DataSourceDesc dsd, int width, int height) { }
 
         /**
          * Called to indicate an avaliable timed text
          *
          * @param mp the MediaPlayer2 associated with this callback
-         * @param srcId the Id of this data source
+         * @param dsd the DataSourceDesc of this data source
          * @param text the timed text sample which contains the text
          *             needed to be displayed and the display format.
          * @hide
          */
-        public void onTimedText(MediaPlayer2 mp, long srcId, TimedText text) { }
+        public void onTimedText(MediaPlayer2 mp, DataSourceDesc dsd, TimedText text) { }
 
         /**
          * Called to indicate avaliable timed metadata
@@ -1798,82 +1746,82 @@
          * @see TimedMetaData
          *
          * @param mp the MediaPlayer2 associated with this callback
-         * @param srcId the Id of this data source
+         * @param dsd the DataSourceDesc of this data source
          * @param data the timed metadata sample associated with this event
          */
-        public void onTimedMetaDataAvailable(MediaPlayer2 mp, long srcId, TimedMetaData data) { }
+        public void onTimedMetaDataAvailable(
+                MediaPlayer2 mp, DataSourceDesc dsd, TimedMetaData data) { }
 
         /**
          * Called to indicate an error.
          *
          * @param mp the MediaPlayer2 the error pertains to
-         * @param srcId the Id of this data source
-         * @param what the type of error that has occurred:
-         * <ul>
-         * <li>{@link #MEDIA_ERROR_UNKNOWN}
-         * </ul>
+         * @param dsd the DataSourceDesc of this data source
+         * @param what the type of error that has occurred.
          * @param extra an extra code, specific to the error. Typically
          * implementation dependent.
-         * <ul>
-         * <li>{@link #MEDIA_ERROR_IO}
-         * <li>{@link #MEDIA_ERROR_MALFORMED}
-         * <li>{@link #MEDIA_ERROR_UNSUPPORTED}
-         * <li>{@link #MEDIA_ERROR_TIMED_OUT}
-         * <li><code>MEDIA_ERROR_SYSTEM (-2147483648)</code> - low-level system error.
-         * </ul>
          */
-        public void onError(MediaPlayer2 mp, long srcId, int what, int extra) { }
+        public void onError(
+                MediaPlayer2 mp, DataSourceDesc dsd, @MediaError int what, int extra) { }
 
         /**
          * Called to indicate an info or a warning.
          *
          * @param mp the MediaPlayer2 the info pertains to.
-         * @param srcId the Id of this data source
+         * @param dsd the DataSourceDesc of this data source
          * @param what the type of info or warning.
-         * <ul>
-         * <li>{@link #MEDIA_INFO_UNKNOWN}
-         * <li>{@link #MEDIA_INFO_STARTED_AS_NEXT}
-         * <li>{@link #MEDIA_INFO_VIDEO_RENDERING_START}
-         * <li>{@link #MEDIA_INFO_AUDIO_RENDERING_START}
-         * <li>{@link #MEDIA_INFO_PLAYBACK_COMPLETE}
-         * <li>{@link #MEDIA_INFO_PLAYLIST_END}
-         * <li>{@link #MEDIA_INFO_PREPARED}
-         * <li>{@link #MEDIA_INFO_COMPLETE_CALL_PLAY}
-         * <li>{@link #MEDIA_INFO_COMPLETE_CALL_PAUSE}
-         * <li>{@link #MEDIA_INFO_COMPLETE_CALL_SEEK}
-         * <li>{@link #MEDIA_INFO_VIDEO_TRACK_LAGGING}
-         * <li>{@link #MEDIA_INFO_BUFFERING_START}
-         * <li>{@link #MEDIA_INFO_BUFFERING_END}
-         * <li><code>MEDIA_INFO_NETWORK_BANDWIDTH (703)</code> -
-         *     bandwidth information is available (as <code>extra</code> kbps)
-         * <li>{@link #MEDIA_INFO_BAD_INTERLEAVING}
-         * <li>{@link #MEDIA_INFO_NOT_SEEKABLE}
-         * <li>{@link #MEDIA_INFO_METADATA_UPDATE}
-         * <li>{@link #MEDIA_INFO_UNSUPPORTED_SUBTITLE}
-         * <li>{@link #MEDIA_INFO_SUBTITLE_TIMED_OUT}
-         * </ul>
          * @param extra an extra code, specific to the info. Typically
          * implementation dependent.
          */
-        public void onInfo(MediaPlayer2 mp, long srcId, int what, int extra) { }
+        public void onInfo(MediaPlayer2 mp, DataSourceDesc dsd, @MediaInfo int what, int extra) { }
+
+        /**
+         * Called to acknowledge an API call.
+         *
+         * @param mp the MediaPlayer2 the call was made on.
+         * @param dsd the DataSourceDesc of this data source
+         * @param what the enum for the API call.
+         * @param status the returned status code for the call.
+         */
+        public void onCallCompleted(
+                MediaPlayer2 mp, DataSourceDesc dsd, @CallCompleted int what,
+                @CallStatus int status) { }
+
+        /**
+         * Called to indicate media clock has changed.
+         *
+         * @param mp the MediaPlayer2 the media time pertains to.
+         * @param dsd the DataSourceDesc of this data source
+         * @param timestamp the new media clock.
+         */
+        public void onMediaTimeChanged(
+                MediaPlayer2 mp, DataSourceDesc dsd, MediaTimestamp timestamp) { }
+
+        /**
+         * Called to indicate {@link #notifyWhenCommandLabelReached(Object)} has been processed.
+         *
+         * @param mp the MediaPlayer2 {@link #notifyWhenCommandLabelReached(Object)} was called on.
+         * @param label the application specific Object given by
+         *        {@link #notifyWhenCommandLabelReached(Object)}.
+         */
+        public void onCommandLabelReached(MediaPlayer2 mp, @NonNull Object label) { }
     }
 
     /**
-     * Register a callback to be invoked when the media source is ready
-     * for playback.
+     * Sets the callback to be invoked when the media source is ready for playback.
      *
      * @param eventCallback the callback that will be run
      * @param executor the executor through which the callback should be invoked
      */
-    public abstract void registerEventCallback(@NonNull @CallbackExecutor Executor executor,
-            @NonNull EventCallback eventCallback);
+    // This is a synchronous call.
+    public abstract void setMediaPlayer2EventCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull MediaPlayer2EventCallback eventCallback);
 
     /**
-     * Unregisters an {@link EventCallback}.
-     *
-     * @param callback an {@link EventCallback} to unregister
+     * Clears the {@link MediaPlayer2EventCallback}.
      */
-    public abstract void unregisterEventCallback(EventCallback callback);
+    // This is a synchronous call.
+    public abstract void clearMediaPlayer2EventCallback();
 
     /**
      * Interface definition of a callback to be invoked when a
@@ -1893,6 +1841,7 @@
      *
      * @hide
      */
+    // This is a synchronous call.
     public void setOnSubtitleDataListener(OnSubtitleDataListener listener) { }
 
 
@@ -1900,14 +1849,14 @@
      * in include/media/mediaplayer2.h!
      */
     /** Unspecified media player error.
-     * @see android.media.MediaPlayer2.EventCallback.onError
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onError
      */
     public static final int MEDIA_ERROR_UNKNOWN = 1;
 
     /** The video is streamed and its container is not valid for progressive
      * playback i.e the video's index (e.g moov atom) is not at the start of the
      * file.
-     * @see android.media.MediaPlayer2.EventCallback.onError
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onError
      */
     public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200;
 
@@ -1923,106 +1872,117 @@
 
     /** Unspecified low-level system error. This value originated from UNKNOWN_ERROR in
      * system/core/include/utils/Errors.h
-     * @see android.media.MediaPlayer2.EventCallback.onError
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onError
      * @hide
      */
     public static final int MEDIA_ERROR_SYSTEM = -2147483648;
 
+    /**
+     * @hide
+     */
+    @IntDef(flag = false, prefix = "MEDIA_ERROR", value = {
+            MEDIA_ERROR_UNKNOWN,
+            MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,
+            MEDIA_ERROR_IO,
+            MEDIA_ERROR_MALFORMED,
+            MEDIA_ERROR_UNSUPPORTED,
+            MEDIA_ERROR_TIMED_OUT,
+            MEDIA_ERROR_SYSTEM
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MediaError {}
 
     /* Do not change these values without updating their counterparts
      * in include/media/mediaplayer2.h!
      */
     /** Unspecified media player info.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_UNKNOWN = 1;
 
     /** The player switched to this datas source because it is the
-     * next-to-be-played in the play list.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * next-to-be-played in the playlist.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_STARTED_AS_NEXT = 2;
 
     /** The player just pushed the very first video frame for rendering.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3;
 
     /** The player just rendered the very first audio sample.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_AUDIO_RENDERING_START = 4;
 
     /** The player just completed the playback of this data source.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_PLAYBACK_COMPLETE = 5;
 
-    /** The player just completed the playback of the full play list.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+    /** The player just completed the playback of the full playlist.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_PLAYLIST_END = 6;
 
     /** The player just prepared a data source.
-     * This also serves as call completion notification for {@link #prepareAsync()}.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_PREPARED = 100;
 
-    /** The player just completed a call {@link #play()}.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
-     */
-    public static final int MEDIA_INFO_COMPLETE_CALL_PLAY = 101;
-
-    /** The player just completed a call {@link #pause()}.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
-     */
-    public static final int MEDIA_INFO_COMPLETE_CALL_PAUSE = 102;
-
-    /** The player just completed a call {@link #seekTo(long, int)}.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
-     */
-    public static final int MEDIA_INFO_COMPLETE_CALL_SEEK = 103;
-
     /** The video is too complex for the decoder: it can't decode frames fast
      *  enough. Possibly only the audio plays fine at this stage.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700;
 
     /** MediaPlayer2 is temporarily pausing playback internally in order to
      * buffer more data.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_BUFFERING_START = 701;
 
     /** MediaPlayer2 is resuming playback after filling buffers.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_BUFFERING_END = 702;
 
     /** Estimated network bandwidth information (kbps) is available; currently this event fires
      * simultaneously as {@link #MEDIA_INFO_BUFFERING_START} and {@link #MEDIA_INFO_BUFFERING_END}
      * when playing network files.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      * @hide
      */
     public static final int MEDIA_INFO_NETWORK_BANDWIDTH = 703;
 
+    /**
+     * Update status in buffering a media source received through progressive downloading.
+     * The received buffering percentage indicates how much of the content has been buffered
+     * or played. For example a buffering update of 80 percent when half the content
+     * has already been played indicates that the next 30 percent of the
+     * content to play has been buffered.
+     *
+     * The {@code extra} parameter in {@code MediaPlayer2EventCallback.onInfo} is the
+     * percentage (0-100) of the content that has been buffered or played thus far.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
+     */
+    public static final int MEDIA_INFO_BUFFERING_UPDATE = 704;
+
     /** Bad interleaving means that a media has been improperly interleaved or
      * not interleaved at all, e.g has all the video samples first then all the
      * audio ones. Video is playing but a lot of disk seeks may be happening.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_BAD_INTERLEAVING = 800;
 
     /** The media cannot be seeked (e.g live stream)
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_NOT_SEEKABLE = 801;
 
     /** A new set of metadata is available.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_METADATA_UPDATE = 802;
 
@@ -2034,33 +1994,273 @@
 
     /** Informs that audio is not playing. Note that playback of the video
      * is not interrupted.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804;
 
     /** Informs that video is not playing. Note that playback of the audio
      * is not interrupted.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805;
 
     /** Failed to handle timed text track properly.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      *
      * {@hide}
      */
     public static final int MEDIA_INFO_TIMED_TEXT_ERROR = 900;
 
     /** Subtitle track was not supported by the media framework.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901;
 
     /** Reading the subtitle track takes too long.
-     * @see android.media.MediaPlayer2.EventCallback.onInfo
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onInfo
      */
     public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902;
 
+    /**
+     * @hide
+     */
+    @IntDef(flag = false, prefix = "MEDIA_INFO", value = {
+            MEDIA_INFO_UNKNOWN,
+            MEDIA_INFO_STARTED_AS_NEXT,
+            MEDIA_INFO_VIDEO_RENDERING_START,
+            MEDIA_INFO_AUDIO_RENDERING_START,
+            MEDIA_INFO_PLAYBACK_COMPLETE,
+            MEDIA_INFO_PLAYLIST_END,
+            MEDIA_INFO_PREPARED,
+            MEDIA_INFO_VIDEO_TRACK_LAGGING,
+            MEDIA_INFO_BUFFERING_START,
+            MEDIA_INFO_BUFFERING_END,
+            MEDIA_INFO_NETWORK_BANDWIDTH,
+            MEDIA_INFO_BUFFERING_UPDATE,
+            MEDIA_INFO_BAD_INTERLEAVING,
+            MEDIA_INFO_NOT_SEEKABLE,
+            MEDIA_INFO_METADATA_UPDATE,
+            MEDIA_INFO_EXTERNAL_METADATA_UPDATE,
+            MEDIA_INFO_AUDIO_NOT_PLAYING,
+            MEDIA_INFO_VIDEO_NOT_PLAYING,
+            MEDIA_INFO_TIMED_TEXT_ERROR,
+            MEDIA_INFO_UNSUPPORTED_SUBTITLE,
+            MEDIA_INFO_SUBTITLE_TIMED_OUT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MediaInfo {}
+
+    //--------------------------------------------------------------------------
+    /** The player just completed a call {@link #attachAuxEffect}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_ATTACH_AUX_EFFECT = 1;
+
+    /** The player just completed a call {@link #deselectTrack}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_DESELECT_TRACK = 2;
+
+    /** The player just completed a call {@link #loopCurrent}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_LOOP_CURRENT = 3;
+
+    /** The player just completed a call {@link #pause}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_PAUSE = 4;
+
+    /** The player just completed a call {@link #play}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_PLAY = 5;
+
+    /** The player just completed a call {@link #prepare}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_PREPARE = 6;
+
+    /** The player just completed a call {@link #releaseDrm}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_RELEASE_DRM = 12;
+
+    /** The player just completed a call {@link #restoreDrmKeys}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_RESTORE_DRM_KEYS = 13;
+
+    /** The player just completed a call {@link #seekTo}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SEEK_TO = 14;
+
+    /** The player just completed a call {@link #selectTrack}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SELECT_TRACK = 15;
+
+    /** The player just completed a call {@link #setAudioAttributes}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_AUDIO_ATTRIBUTES = 16;
+
+    /** The player just completed a call {@link #setAudioSessionId}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_AUDIO_SESSION_ID = 17;
+
+    /** The player just completed a call {@link #setAuxEffectSendLevel}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL = 18;
+
+    /** The player just completed a call {@link #setDataSource}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_DATA_SOURCE = 19;
+
+    /** The player just completed a call {@link #setNextDataSource}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCE = 22;
+
+    /** The player just completed a call {@link #setNextDataSources}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_NEXT_DATA_SOURCES = 23;
+
+    /** The player just completed a call {@link #setPlaybackParams}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_PLAYBACK_PARAMS = 24;
+
+    /** The player just completed a call {@link #setPlaybackSpeed}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_PLAYBACK_SPEED = 25;
+
+    /** The player just completed a call {@link #setPlayerVolume}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_PLAYER_VOLUME = 26;
+
+    /** The player just completed a call {@link #setSurface}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_SURFACE = 27;
+
+    /** The player just completed a call {@link #setSyncParams}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SET_SYNC_PARAMS = 28;
+
+    /** The player just completed a call {@link #skipToNext}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_COMPLETED_SKIP_TO_NEXT = 29;
+
+    /** The player just completed a call {@link #setBufferingParams}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     * @hide
+     */
+    public static final int CALL_COMPLETED_SET_BUFFERING_PARAMS = 1001;
+
+    /** The player just completed a call {@code setVideoScalingMode}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     * @hide
+     */
+    public static final int CALL_COMPLETED_SET_VIDEO_SCALING_MODE = 1002;
+
+    /** The player just completed a call {@code notifyWhenCommandLabelReached}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCommandLabelReached
+     * @hide
+     */
+    public static final int CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED = 1003;
+
+    /**
+     * @hide
+     */
+    @IntDef(flag = false, prefix = "CALL_COMPLETED", value = {
+            CALL_COMPLETED_ATTACH_AUX_EFFECT,
+            CALL_COMPLETED_DESELECT_TRACK,
+            CALL_COMPLETED_LOOP_CURRENT,
+            CALL_COMPLETED_PAUSE,
+            CALL_COMPLETED_PLAY,
+            CALL_COMPLETED_PREPARE,
+            CALL_COMPLETED_RELEASE_DRM,
+            CALL_COMPLETED_RESTORE_DRM_KEYS,
+            CALL_COMPLETED_SEEK_TO,
+            CALL_COMPLETED_SELECT_TRACK,
+            CALL_COMPLETED_SET_AUDIO_ATTRIBUTES,
+            CALL_COMPLETED_SET_AUDIO_SESSION_ID,
+            CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL,
+            CALL_COMPLETED_SET_DATA_SOURCE,
+            CALL_COMPLETED_SET_NEXT_DATA_SOURCE,
+            CALL_COMPLETED_SET_NEXT_DATA_SOURCES,
+            CALL_COMPLETED_SET_PLAYBACK_PARAMS,
+            CALL_COMPLETED_SET_PLAYBACK_SPEED,
+            CALL_COMPLETED_SET_PLAYER_VOLUME,
+            CALL_COMPLETED_SET_SURFACE,
+            CALL_COMPLETED_SET_SYNC_PARAMS,
+            CALL_COMPLETED_SKIP_TO_NEXT,
+            CALL_COMPLETED_SET_BUFFERING_PARAMS,
+            CALL_COMPLETED_SET_VIDEO_SCALING_MODE,
+            CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CallCompleted {}
+
+    /** Status code represents that call is completed without an error.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_NO_ERROR = 0;
+
+    /** Status code represents that call is ended with an unknown error.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_ERROR_UNKNOWN = Integer.MIN_VALUE;
+
+    /** Status code represents that the player is not in valid state for the operation.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_INVALID_OPERATION = 1;
+
+    /** Status code represents that the argument is illegal.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_BAD_VALUE = 2;
+
+    /** Status code represents that the operation is not allowed.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_PERMISSION_DENIED = 3;
+
+    /** Status code represents a file or network related operation error.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_ERROR_IO = 4;
+
+    /** Status code represents that DRM operation is called before preparing a DRM scheme through
+     *  {@link #prepareDrm}.
+     * @see android.media.MediaPlayer2.MediaPlayer2EventCallback#onCallCompleted
+     */
+    public static final int CALL_STATUS_NO_DRM_SCHEME = 5;
+
+    /**
+     * @hide
+     */
+    @IntDef(flag = false, prefix = "CALL_STATUS", value = {
+            CALL_STATUS_NO_ERROR,
+            CALL_STATUS_ERROR_UNKNOWN,
+            CALL_STATUS_INVALID_OPERATION,
+            CALL_STATUS_BAD_VALUE,
+            CALL_STATUS_PERMISSION_DENIED,
+            CALL_STATUS_ERROR_IO,
+            CALL_STATUS_NO_DRM_SCHEME})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CallStatus {}
 
     // Modular DRM begin
 
@@ -2071,8 +2271,8 @@
      * 'securityLevel', which has to be set after DRM scheme creation but
      * before the DRM session is opened.
      *
-     * The only allowed DRM calls in this listener are {@code getDrmPropertyString}
-     * and {@code setDrmPropertyString}.
+     * The only allowed DRM calls in this listener are {@link #getDrmPropertyString}
+     * and {@link #setDrmPropertyString}.
      */
     public interface OnDrmConfigHelper
     {
@@ -2080,8 +2280,9 @@
          * Called to give the app the opportunity to configure DRM before the session is created
          *
          * @param mp the {@code MediaPlayer2} associated with this callback
+         * @param dsd the DataSourceDesc of this data source
          */
-        public void onDrmConfig(MediaPlayer2 mp);
+        public void onDrmConfig(MediaPlayer2 mp, DataSourceDesc dsd);
     }
 
     /**
@@ -2092,6 +2293,7 @@
      *
      * @param listener the callback that will be run
      */
+    // This is a synchronous call.
     public abstract void setOnDrmConfigHelper(OnDrmConfigHelper listener);
 
     /**
@@ -2102,42 +2304,40 @@
         /**
          * Called to indicate DRM info is available
          *
-         * @param mp       the {@code MediaPlayer2} associated with this callback
-         * @param drmInfo  DRM info of the source including PSSH, and subset
-         *                 of crypto schemes supported by this device
+         * @param mp the {@code MediaPlayer2} associated with this callback
+         * @param dsd the DataSourceDesc of this data source
+         * @param drmInfo DRM info of the source including PSSH, and subset
+         *                of crypto schemes supported by this device
          */
-        public void onDrmInfo(MediaPlayer2 mp, DrmInfo drmInfo) { }
+        public void onDrmInfo(MediaPlayer2 mp, DataSourceDesc dsd, DrmInfo drmInfo) { }
 
         /**
-         * Called to notify the client that {@code prepareDrm} is finished and ready for key request/response.
+         * Called to notify the client that {@link #prepareDrm} is finished and ready for
+         * key request/response.
          *
-         * @param mp      the {@code MediaPlayer2} associated with this callback
-         * @param status  the result of DRM preparation which can be
-         * {@link #PREPARE_DRM_STATUS_SUCCESS},
-         * {@link #PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR},
-         * {@link #PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR}, or
-         * {@link #PREPARE_DRM_STATUS_PREPARATION_ERROR}.
+         * @param mp the {@code MediaPlayer2} associated with this callback
+         * @param dsd the DataSourceDesc of this data source
+         * @param status the result of DRM preparation.
          */
-        public void onDrmPrepared(MediaPlayer2 mp, @PrepareDrmStatusCode int status) { }
-
+        public void onDrmPrepared(
+                MediaPlayer2 mp, DataSourceDesc dsd, @PrepareDrmStatusCode int status) { }
     }
 
     /**
-     * Register a callback to be invoked when the media source is ready
-     * for playback.
+     * Sets the callback to be invoked when the media source is ready for playback.
      *
      * @param eventCallback the callback that will be run
      * @param executor the executor through which the callback should be invoked
      */
-    public abstract void registerDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
+    // This is a synchronous call.
+    public abstract void setDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
             @NonNull DrmEventCallback eventCallback);
 
     /**
-     * Unregisters a {@link DrmEventCallback}.
-     *
-     * @param callback a {@link DrmEventCallback} to unregister
+     * Clears the {@link DrmEventCallback}.
      */
-    public abstract void unregisterDrmEventCallback(DrmEventCallback callback);
+    // This is a synchronous call.
+    public abstract void clearDrmEventCallback();
 
     /**
      * The status codes for {@link DrmEventCallback#onDrmPrepared} listener.
@@ -2164,7 +2364,7 @@
 
 
     /** @hide */
-    @IntDef({
+    @IntDef(flag = false, prefix = "PREPARE_DRM_STATUS", value = {
         PREPARE_DRM_STATUS_SUCCESS,
         PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR,
         PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
@@ -2183,10 +2383,10 @@
     /**
      * Prepares the DRM for the current source
      * <p>
-     * If {@code OnDrmConfigHelper} is registered, it will be called during
+     * If {@link OnDrmConfigHelper} is registered, it will be called during
      * preparation to allow configuration of the DRM properties before opening the
      * DRM session. Note that the callback is called synchronously in the thread that called
-     * {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
+     * {@link #prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
      * and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
      * <p>
      * If the device has not been provisioned before, this call also provisions the device
@@ -2216,6 +2416,7 @@
      * @throws ProvisioningServerErrorException   if provisioning is required but failed due to
      *                                            the request denied by the provisioning server
      */
+    // This is a synchronous call.
     public abstract void prepareDrm(@NonNull UUID uuid)
             throws UnsupportedSchemeException, ResourceBusyException,
                    ProvisioningNetworkErrorException, ProvisioningServerErrorException;
@@ -2229,20 +2430,21 @@
      *
      * @throws NoDrmSchemeException if there is no active DRM session to release
      */
+    // This is an asynchronous call.
     public abstract void releaseDrm() throws NoDrmSchemeException;
 
     /**
      * A key request/response exchange occurs between the app and a license server
      * to obtain or release keys used to decrypt encrypted content.
      * <p>
-     * getKeyRequest() is used to obtain an opaque key request byte array that is
+     * getDrmKeyRequest() is used to obtain an opaque key request byte array that is
      * delivered to the license server.  The opaque key request byte array is returned
      * in KeyRequest.data.  The recommended URL to deliver the key request to is
      * returned in KeyRequest.defaultUrl.
      * <p>
      * After the app has received the key request response from the server,
      * it should deliver to the response to the DRM engine plugin using the method
-     * {@link #provideKeyResponse}.
+     * {@link #provideDrmKeyResponse}.
      *
      * @param keySetId is the key-set identifier of the offline keys being released when keyType is
      * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
@@ -2269,22 +2471,23 @@
      * @throws NoDrmSchemeException if there is no active DRM session
      */
     @NonNull
-    public abstract MediaDrm.KeyRequest getKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
+    public abstract MediaDrm.KeyRequest getDrmKeyRequest(
+            @Nullable byte[] keySetId, @Nullable byte[] initData,
             @Nullable String mimeType, @MediaDrm.KeyType int keyType,
             @Nullable Map<String, String> optionalParameters)
             throws NoDrmSchemeException;
 
     /**
      * A key response is received from the license server by the app, then it is
-     * provided to the DRM engine plugin using provideKeyResponse. When the
+     * provided to the DRM engine plugin using provideDrmKeyResponse. When the
      * response is for an offline key request, a key-set identifier is returned that
      * can be used to later restore the keys to a new session with the method
-     * {@ link # restoreKeys}.
+     * {@ link # restoreDrmKeys}.
      * When the response is for a streaming or release request, null is returned.
      *
      * @param keySetId When the response is for a release request, keySetId identifies
      * the saved key associated with the release request (i.e., the same keySetId
-     * passed to the earlier {@ link # getKeyRequest} call. It MUST be null when the
+     * passed to the earlier {@ link # getDrmKeyRequest} call. It MUST be null when the
      * response is for either streaming or offline key requests.
      *
      * @param response the byte array response from the server
@@ -2293,16 +2496,19 @@
      * @throws DeniedByServerException if the response indicates that the
      * server rejected the request
      */
-    public abstract byte[] provideKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
+    // This is a synchronous call.
+    public abstract byte[] provideDrmKeyResponse(
+            @Nullable byte[] keySetId, @NonNull byte[] response)
             throws NoDrmSchemeException, DeniedByServerException;
 
     /**
      * Restore persisted offline keys into a new session.  keySetId identifies the
-     * keys to load, obtained from a prior call to {@link #provideKeyResponse}.
+     * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
      *
      * @param keySetId identifies the saved key set to restore
      */
-    public abstract void restoreKeys(@NonNull byte[] keySetId)
+    // This is an asynchronous call.
+    public abstract void restoreDrmKeys(@NonNull byte[] keySetId)
             throws NoDrmSchemeException;
 
     /**
@@ -2315,7 +2521,8 @@
      * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
      */
     @NonNull
-    public abstract String getDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName)
+    public abstract String getDrmPropertyString(
+            @NonNull @MediaDrm.StringProperty String propertyName)
             throws NoDrmSchemeException;
 
     /**
@@ -2328,8 +2535,9 @@
      * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
      * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
      */
-    public abstract void setDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName,
-                                     @NonNull String value)
+    // This is a synchronous call.
+    public abstract void setDrmPropertyString(
+            @NonNull @MediaDrm.StringProperty String propertyName, @NonNull String value)
             throws NoDrmSchemeException;
 
     /**
@@ -2473,4 +2681,38 @@
         public static final String ERROR_CODE = "android.media.mediaplayer.errcode";
 
     }
+
+    /**
+       Constant to retrieve only the new metadata since the last
+       call.
+       // FIXME: unhide.
+       // FIXME: add link to getMetadata(boolean, boolean)
+       {@hide}
+     */
+    public static final boolean METADATA_UPDATE_ONLY = true;
+
+    /**
+       Constant to retrieve all the metadata.
+       // FIXME: unhide.
+       // FIXME: add link to getMetadata(boolean, boolean)
+       {@hide}
+     */
+    public static final boolean METADATA_ALL = false;
+
+    /**
+       Constant to enable the metadata filter during retrieval.
+       // FIXME: unhide.
+       // FIXME: add link to getMetadata(boolean, boolean)
+       {@hide}
+     */
+    public static final boolean APPLY_METADATA_FILTER = true;
+
+    /**
+       Constant to disable the metadata filter during retrieval.
+       // FIXME: unhide.
+       // FIXME: add link to getMetadata(boolean, boolean)
+       {@hide}
+     */
+    public static final boolean BYPASS_METADATA_FILTER = false;
+
 }
diff --git a/android/media/MediaPlayer2Impl.java b/android/media/MediaPlayer2Impl.java
index 86a285c..56423fd 100644
--- a/android/media/MediaPlayer2Impl.java
+++ b/android/media/MediaPlayer2Impl.java
@@ -17,7 +17,6 @@
 package android.media;
 
 import android.annotation.CallbackExecutor;
-import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityThread;
@@ -25,8 +24,10 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
+import android.graphics.SurfaceTexture;
+import android.media.SubtitleController.Anchor;
+import android.media.SubtitleTrack.RenderingWidget;
 import android.net.Uri;
-import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -34,31 +35,19 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.PersistableBundle;
-import android.os.Process;
 import android.os.PowerManager;
+import android.os.Process;
 import android.os.SystemProperties;
 import android.provider.Settings;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.system.OsConstants;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.Pair;
-import android.util.ArrayMap;
 import android.view.Surface;
 import android.view.SurfaceHolder;
 import android.widget.VideoView;
-import android.graphics.SurfaceTexture;
-import android.media.AudioManager;
-import android.media.MediaDrm;
-import android.media.MediaFormat;
-import android.media.MediaPlayer2;
-import android.media.MediaTimeProvider;
-import android.media.PlaybackParams;
-import android.media.SubtitleController;
-import android.media.SubtitleController.Anchor;
-import android.media.SubtitleData;
-import android.media.SubtitleTrack.RenderingWidget;
-import android.media.SyncParams;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
@@ -74,485 +63,26 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.AutoCloseable;
-import java.lang.Runnable;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
-import java.net.CookieHandler;
-import java.net.CookieManager;
 import java.net.HttpCookie;
 import java.net.HttpURLConnection;
-import java.net.InetSocketAddress;
 import java.net.URL;
 import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
-import java.util.Collections;
-import java.util.concurrent.Executor;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Scanner;
 import java.util.Set;
 import java.util.UUID;
 import java.util.Vector;
-
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
- * MediaPlayer2 class can be used to control playback
- * of audio/video files and streams. An example on how to use the methods in
- * this class can be found in {@link android.widget.VideoView}.
- *
- * <p>Topics covered here are:
- * <ol>
- * <li><a href="#StateDiagram">State Diagram</a>
- * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a>
- * <li><a href="#Permissions">Permissions</a>
- * <li><a href="#Callbacks">Register informational and error callbacks</a>
- * </ol>
- *
- * <div class="special reference">
- * <h3>Developer Guides</h3>
- * <p>For more information about how to use MediaPlayer2, read the
- * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p>
- * </div>
- *
- * <a name="StateDiagram"></a>
- * <h3>State Diagram</h3>
- *
- * <p>Playback control of audio/video files and streams is managed as a state
- * machine. The following diagram shows the life cycle and the states of a
- * MediaPlayer2 object driven by the supported playback control operations.
- * The ovals represent the states a MediaPlayer2 object may reside
- * in. The arcs represent the playback control operations that drive the object
- * state transition. There are two types of arcs. The arcs with a single arrow
- * head represent synchronous method calls, while those with
- * a double arrow head represent asynchronous method calls.</p>
- *
- * <p><img src="../../../images/mediaplayer_state_diagram.gif"
- *         alt="MediaPlayer State diagram"
- *         border="0" /></p>
- *
- * <p>From this state diagram, one can see that a MediaPlayer2 object has the
- *    following states:</p>
- * <ul>
- *     <li>When a MediaPlayer2 object is just created using <code>new</code> or
- *         after {@link #reset()} is called, it is in the <em>Idle</em> state; and after
- *         {@link #close()} is called, it is in the <em>End</em> state. Between these
- *         two states is the life cycle of the MediaPlayer2 object.
- *         <ul>
- *         <li>There is a subtle but important difference between a newly constructed
- *         MediaPlayer2 object and the MediaPlayer2 object after {@link #reset()}
- *         is called. It is a programming error to invoke methods such
- *         as {@link #getCurrentPosition()},
- *         {@link #getDuration()}, {@link #getVideoHeight()},
- *         {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)},
- *         {@link #setLooping(boolean)},
- *         {@link #setVolume(float, float)}, {@link #pause()}, {@link #play()},
- *         {@link #seekTo(long, int)}, {@link #prepare()} or
- *         {@link #prepareAsync()} in the <em>Idle</em> state for both cases. If any of these
- *         methods is called right after a MediaPlayer2 object is constructed,
- *         the user supplied callback method OnErrorListener.onError() won't be
- *         called by the internal player engine and the object state remains
- *         unchanged; but if these methods are called right after {@link #reset()},
- *         the user supplied callback method OnErrorListener.onError() will be
- *         invoked by the internal player engine and the object will be
- *         transfered to the <em>Error</em> state. </li>
- *         <li>It is also recommended that once
- *         a MediaPlayer2 object is no longer being used, call {@link #close()} immediately
- *         so that resources used by the internal player engine associated with the
- *         MediaPlayer2 object can be released immediately. Resource may include
- *         singleton resources such as hardware acceleration components and
- *         failure to call {@link #close()} may cause subsequent instances of
- *         MediaPlayer2 objects to fallback to software implementations or fail
- *         altogether. Once the MediaPlayer2
- *         object is in the <em>End</em> state, it can no longer be used and
- *         there is no way to bring it back to any other state. </li>
- *         <li>Furthermore,
- *         the MediaPlayer2 objects created using <code>new</code> is in the
- *         <em>Idle</em> state.
- *         </li>
- *         </ul>
- *         </li>
- *     <li>In general, some playback control operation may fail due to various
- *         reasons, such as unsupported audio/video format, poorly interleaved
- *         audio/video, resolution too high, streaming timeout, and the like.
- *         Thus, error reporting and recovery is an important concern under
- *         these circumstances. Sometimes, due to programming errors, invoking a playback
- *         control operation in an invalid state may also occur. Under all these
- *         error conditions, the internal player engine invokes a user supplied
- *         EventCallback.onError() method if an EventCallback has been
- *         registered beforehand via
- *         {@link #registerEventCallback(Executor, EventCallback)}.
- *         <ul>
- *         <li>It is important to note that once an error occurs, the
- *         MediaPlayer2 object enters the <em>Error</em> state (except as noted
- *         above), even if an error listener has not been registered by the application.</li>
- *         <li>In order to reuse a MediaPlayer2 object that is in the <em>
- *         Error</em> state and recover from the error,
- *         {@link #reset()} can be called to restore the object to its <em>Idle</em>
- *         state.</li>
- *         <li>It is good programming practice to have your application
- *         register a OnErrorListener to look out for error notifications from
- *         the internal player engine.</li>
- *         <li>IllegalStateException is
- *         thrown to prevent programming errors such as calling {@link #prepare()},
- *         {@link #prepareAsync()}, {@link #setDataSource(DataSourceDesc)}, or
- *         {@code setPlaylist} methods in an invalid state. </li>
- *         </ul>
- *         </li>
- *     <li>Calling
- *         {@link #setDataSource(DataSourceDesc)}, or
- *         {@code setPlaylist} transfers a
- *         MediaPlayer2 object in the <em>Idle</em> state to the
- *         <em>Initialized</em> state.
- *         <ul>
- *         <li>An IllegalStateException is thrown if
- *         setDataSource() or setPlaylist() is called in any other state.</li>
- *         <li>It is good programming
- *         practice to always look out for <code>IllegalArgumentException</code>
- *         and <code>IOException</code> that may be thrown from
- *         <code>setDataSource</code> and <code>setPlaylist</code> methods.</li>
- *         </ul>
- *         </li>
- *     <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state
- *         before playback can be started.
- *         <ul>
- *         <li>There are two ways (synchronous vs.
- *         asynchronous) that the <em>Prepared</em> state can be reached:
- *         either a call to {@link #prepare()} (synchronous) which
- *         transfers the object to the <em>Prepared</em> state once the method call
- *         returns, or a call to {@link #prepareAsync()} (asynchronous) which
- *         first transfers the object to the <em>Preparing</em> state after the
- *         call returns (which occurs almost right way) while the internal
- *         player engine continues working on the rest of preparation work
- *         until the preparation work completes. When the preparation completes or when {@link #prepare()} call returns,
- *         the internal player engine then calls a user supplied callback method,
- *         onPrepared() of the EventCallback interface, if an
- *         EventCallback is registered beforehand via {@link
- *         #registerEventCallback(Executor, EventCallback)}.</li>
- *         <li>It is important to note that
- *         the <em>Preparing</em> state is a transient state, and the behavior
- *         of calling any method with side effect while a MediaPlayer2 object is
- *         in the <em>Preparing</em> state is undefined.</li>
- *         <li>An IllegalStateException is
- *         thrown if {@link #prepare()} or {@link #prepareAsync()} is called in
- *         any other state.</li>
- *         <li>While in the <em>Prepared</em> state, properties
- *         such as audio/sound volume, screenOnWhilePlaying, looping can be
- *         adjusted by invoking the corresponding set methods.</li>
- *         </ul>
- *         </li>
- *     <li>To start the playback, {@link #play()} must be called. After
- *         {@link #play()} returns successfully, the MediaPlayer2 object is in the
- *         <em>Started</em> state. {@link #isPlaying()} can be called to test
- *         whether the MediaPlayer2 object is in the <em>Started</em> state.
- *         <ul>
- *         <li>While in the <em>Started</em> state, the internal player engine calls
- *         a user supplied EventCallback.onBufferingUpdate() callback
- *         method if an EventCallback has been registered beforehand
- *         via {@link #registerEventCallback(Executor, EventCallback)}.
- *         This callback allows applications to keep track of the buffering status
- *         while streaming audio/video.</li>
- *         <li>Calling {@link #play()} has not effect
- *         on a MediaPlayer2 object that is already in the <em>Started</em> state.</li>
- *         </ul>
- *         </li>
- *     <li>Playback can be paused and stopped, and the current playback position
- *         can be adjusted. Playback can be paused via {@link #pause()}. When the call to
- *         {@link #pause()} returns, the MediaPlayer2 object enters the
- *         <em>Paused</em> state. Note that the transition from the <em>Started</em>
- *         state to the <em>Paused</em> state and vice versa happens
- *         asynchronously in the player engine. It may take some time before
- *         the state is updated in calls to {@link #isPlaying()}, and it can be
- *         a number of seconds in the case of streamed content.
- *         <ul>
- *         <li>Calling {@link #play()} to resume playback for a paused
- *         MediaPlayer2 object, and the resumed playback
- *         position is the same as where it was paused. When the call to
- *         {@link #play()} returns, the paused MediaPlayer2 object goes back to
- *         the <em>Started</em> state.</li>
- *         <li>Calling {@link #pause()} has no effect on
- *         a MediaPlayer2 object that is already in the <em>Paused</em> state.</li>
- *         </ul>
- *         </li>
- *     <li>The playback position can be adjusted with a call to
- *         {@link #seekTo(long, int)}.
- *         <ul>
- *         <li>Although the asynchronuous {@link #seekTo(long, int)}
- *         call returns right away, the actual seek operation may take a while to
- *         finish, especially for audio/video being streamed. When the actual
- *         seek operation completes, the internal player engine calls a user
- *         supplied EventCallback.onSeekComplete() if an EventCallback
- *         has been registered beforehand via
- *         {@link #registerEventCallback(Executor, EventCallback)}.</li>
- *         <li>Please
- *         note that {@link #seekTo(long, int)} can also be called in the other states,
- *         such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted
- *         </em> state. When {@link #seekTo(long, int)} is called in those states,
- *         one video frame will be displayed if the stream has video and the requested
- *         position is valid.
- *         </li>
- *         <li>Furthermore, the actual current playback position
- *         can be retrieved with a call to {@link #getCurrentPosition()}, which
- *         is helpful for applications such as a Music player that need to keep
- *         track of the playback progress.</li>
- *         </ul>
- *         </li>
- *     <li>When the playback reaches the end of stream, the playback completes.
- *         <ul>
- *         <li>If the looping mode was being set to <var>true</var>with
- *         {@link #setLooping(boolean)}, the MediaPlayer2 object shall remain in
- *         the <em>Started</em> state.</li>
- *         <li>If the looping mode was set to <var>false
- *         </var>, the player engine calls a user supplied callback method,
- *         EventCallback.onCompletion(), if an EventCallback is registered
- *         beforehand via {@link #registerEventCallback(Executor, EventCallback)}.
- *         The invoke of the callback signals that the object is now in the <em>
- *         PlaybackCompleted</em> state.</li>
- *         <li>While in the <em>PlaybackCompleted</em>
- *         state, calling {@link #play()} can restart the playback from the
- *         beginning of the audio/video source.</li>
- * </ul>
- *
- *
- * <a name="Valid_and_Invalid_States"></a>
- * <h3>Valid and invalid states</h3>
- *
- * <table border="0" cellspacing="0" cellpadding="0">
- * <tr><td>Method Name </p></td>
- *     <td>Valid Sates </p></td>
- *     <td>Invalid States </p></td>
- *     <td>Comments </p></td></tr>
- * <tr><td>attachAuxEffect </p></td>
- *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
- *     <td>{Idle, Error} </p></td>
- *     <td>This method must be called after setDataSource or setPlaylist.
- *     Calling it does not change the object state. </p></td></tr>
- * <tr><td>getAudioSessionId </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>getCurrentPosition </p></td>
- *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- *         PlaybackCompleted} </p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change the
- *         state. Calling this method in an invalid state transfers the object
- *         to the <em>Error</em> state. </p></td></tr>
- * <tr><td>getDuration </p></td>
- *     <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
- *     <td>{Idle, Initialized, Error} </p></td>
- *     <td>Successful invoke of this method in a valid state does not change the
- *         state. Calling this method in an invalid state transfers the object
- *         to the <em>Error</em> state. </p></td></tr>
- * <tr><td>getVideoHeight </p></td>
- *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- *         PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change the
- *         state. Calling this method in an invalid state transfers the object
- *         to the <em>Error</em> state.  </p></td></tr>
- * <tr><td>getVideoWidth </p></td>
- *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- *         PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change
- *         the state. Calling this method in an invalid state transfers the
- *         object to the <em>Error</em> state. </p></td></tr>
- * <tr><td>isPlaying </p></td>
- *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- *          PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change
- *         the state. Calling this method in an invalid state transfers the
- *         object to the <em>Error</em> state. </p></td></tr>
- * <tr><td>pause </p></td>
- *     <td>{Started, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Paused</em> state. Calling this method in an
- *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>prepare </p></td>
- *     <td>{Initialized, Stopped} </p></td>
- *     <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Prepared</em> state. Calling this method in an
- *         invalid state throws an IllegalStateException.</p></td></tr>
- * <tr><td>prepareAsync </p></td>
- *     <td>{Initialized, Stopped} </p></td>
- *     <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Preparing</em> state. Calling this method in an
- *         invalid state throws an IllegalStateException.</p></td></tr>
- * <tr><td>release </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>After {@link #close()}, the object is no longer available. </p></td></tr>
- * <tr><td>reset </p></td>
- *     <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
- *         PlaybackCompleted, Error}</p></td>
- *     <td>{}</p></td>
- *     <td>After {@link #reset()}, the object is like being just created.</p></td></tr>
- * <tr><td>seekTo </p></td>
- *     <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td>
- *     <td>{Idle, Initialized, Stopped, Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change
- *         the state. Calling this method in an invalid state transfers the
- *         object to the <em>Error</em> state. </p></td></tr>
- * <tr><td>setAudioAttributes </p></td>
- *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- *          PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method does not change the state. In order for the
- *         target audio attributes type to become effective, this method must be called before
- *         prepare() or prepareAsync().</p></td></tr>
- * <tr><td>setAudioSessionId </p></td>
- *     <td>{Idle} </p></td>
- *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
- *          Error} </p></td>
- *     <td>This method must be called in idle state as the audio session ID must be known before
- *         calling setDataSource or setPlaylist. Calling it does not change the object
- *         state. </p></td></tr>
- * <tr><td>setAudioStreamType (deprecated)</p></td>
- *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- *          PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method does not change the state. In order for the
- *         target audio stream type to become effective, this method must be called before
- *         prepare() or prepareAsync().</p></td></tr>
- * <tr><td>setAuxEffectSendLevel </p></td>
- *     <td>any</p></td>
- *     <td>{} </p></td>
- *     <td>Calling this method does not change the object state. </p></td></tr>
- * <tr><td>setDataSource </p></td>
- *     <td>{Idle} </p></td>
- *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
- *          Error} </p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Initialized</em> state. Calling this method in an
- *         invalid state throws an IllegalStateException.</p></td></tr>
- * <tr><td>setPlaylist </p></td>
- *     <td>{Idle} </p></td>
- *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
- *          Error} </p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Initialized</em> state. Calling this method in an
- *         invalid state throws an IllegalStateException.</p></td></tr>
- * <tr><td>setDisplay </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>setSurface </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>setVideoScalingMode </p></td>
- *     <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
- *     <td>{Idle, Error}</p></td>
- *     <td>Successful invoke of this method does not change the state.</p></td></tr>
- * <tr><td>setLooping </p></td>
- *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- *         PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method in a valid state does not change
- *         the state. Calling this method in an
- *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>isLooping </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>registerDrmEventCallback </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>registerEventCallback </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state. </p></td></tr>
- * <tr><td>setPlaybackParams</p></td>
- *     <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td>
- *     <td>{Idle, Stopped} </p></td>
- *     <td>This method will change state in some cases, depending on when it's called.
- *         </p></td></tr>
- * <tr><td>setScreenOnWhilePlaying</></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state.  </p></td></tr>
- * <tr><td>setVolume </p></td>
- *     <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
- *          PlaybackCompleted}</p></td>
- *     <td>{Error}</p></td>
- *     <td>Successful invoke of this method does not change the state.
- * <tr><td>setWakeMode </p></td>
- *     <td>any </p></td>
- *     <td>{} </p></td>
- *     <td>This method can be called in any state and calling it does not change
- *         the object state.</p></td></tr>
- * <tr><td>start </p></td>
- *     <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Stopped, Error}</p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Started</em> state. Calling this method in an
- *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>stop </p></td>
- *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Error}</p></td>
- *     <td>Successful invoke of this method in a valid state transfers the
- *         object to the <em>Stopped</em> state. Calling this method in an
- *         invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
- * <tr><td>getTrackInfo </p></td>
- *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Error}</p></td>
- *     <td>Successful invoke of this method does not change the state.</p></td></tr>
- * <tr><td>addTimedTextSource </p></td>
- *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Error}</p></td>
- *     <td>Successful invoke of this method does not change the state.</p></td></tr>
- * <tr><td>selectTrack </p></td>
- *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Error}</p></td>
- *     <td>Successful invoke of this method does not change the state.</p></td></tr>
- * <tr><td>deselectTrack </p></td>
- *     <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
- *     <td>{Idle, Initialized, Error}</p></td>
- *     <td>Successful invoke of this method does not change the state.</p></td></tr>
- *
- * </table>
- *
- * <a name="Permissions"></a>
- * <h3>Permissions</h3>
- * <p>One may need to declare a corresponding WAKE_LOCK permission {@link
- * android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
- * element.
- *
- * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission
- * when used with network-based content.
- *
- * <a name="Callbacks"></a>
- * <h3>Callbacks</h3>
- * <p>Applications may want to register for informational and error
- * events in order to be informed of some internal state update and
- * possible runtime errors during playback or streaming. Registration for
- * these events is done by properly setting the appropriate listeners (via calls
- * to
- * {@link #registerEventCallback(Executor, EventCallback)},
- * {@link #registerDrmEventCallback(Executor, DrmEventCallback)}).
- * In order to receive the respective callback
- * associated with these listeners, applications are required to create
- * MediaPlayer2 objects on a thread with its own Looper running (main UI
- * thread by default has a Looper running).
- *
  * @hide
  */
 public final class MediaPlayer2Impl extends MediaPlayer2 {
@@ -572,18 +102,27 @@
     private boolean mScreenOnWhilePlaying;
     private boolean mStayAwake;
     private int mStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE;
-    private int mUsage = -1;
-    private boolean mBypassInterruptionPolicy;
     private final CloseGuard mGuard = CloseGuard.get();
 
-    private List<DataSourceDesc> mPlaylist;
-    private int mPLCurrentIndex = 0;
-    private int mPLNextIndex = -1;
-    private int mLoopingMode = LOOPING_MODE_NONE;
+    private final Object mSrcLock = new Object();
+    //--- guarded by |mSrcLock| start
+    private long mSrcIdGenerator = 0;
+    private DataSourceDesc mCurrentDSD;
+    private long mCurrentSrcId = mSrcIdGenerator++;
+    private List<DataSourceDesc> mNextDSDs;
+    private long mNextSrcId = mSrcIdGenerator++;
+    private int mNextSourceState = NEXT_SOURCE_STATE_INIT;
+    private boolean mNextSourcePlayPending = false;
+    //--- guarded by |mSrcLock| end
+
+    private AtomicInteger mBufferedPercentageCurrent = new AtomicInteger(0);
+    private AtomicInteger mBufferedPercentageNext = new AtomicInteger(0);
+    private volatile float mVolume = 1.0f;
 
     // Modular DRM
-    private UUID mDrmUUID;
     private final Object mDrmLock = new Object();
+    //--- guarded by |mDrmLock| start
+    private UUID mDrmUUID;
     private DrmInfoImpl mDrmInfoImpl;
     private MediaDrm mDrmObj;
     private byte[] mDrmSessionId;
@@ -593,6 +132,15 @@
     private boolean mDrmProvisioningInProgress;
     private boolean mPrepareDrmInProgress;
     private ProvisioningThread mDrmProvisioningThread;
+    //--- guarded by |mDrmLock| end
+
+    private HandlerThread mHandlerThread;
+    private final Handler mTaskHandler;
+    private final Object mTaskLock = new Object();
+    @GuardedBy("mTaskLock")
+    private final List<Task> mPendingTasks = new LinkedList<>();
+    @GuardedBy("mTaskLock")
+    private Task mCurrentTask;
 
     /**
      * Default constructor.
@@ -610,6 +158,11 @@
             mEventHandler = null;
         }
 
+        mHandlerThread = new HandlerThread("MediaPlayer2TaskThread");
+        mHandlerThread.start();
+        looper = mHandlerThread.getLooper();
+        mTaskHandler = new Handler(looper);
+
         mTimeProvider = new TimeProvider(this);
         mOpenSubtitleSources = new Vector<InputStream>();
         mGuard.open("close");
@@ -620,6 +173,436 @@
         native_setup(new WeakReference<MediaPlayer2Impl>(this));
     }
 
+    /**
+     * Releases the resources held by this {@code MediaPlayer2} object.
+     *
+     * It is considered good practice to call this method when you're
+     * done using the MediaPlayer2. In particular, whenever an Activity
+     * of an application is paused (its onPause() method is called),
+     * or stopped (its onStop() method is called), this method should be
+     * invoked to release the MediaPlayer2 object, unless the application
+     * has a special need to keep the object around. In addition to
+     * unnecessary resources (such as memory and instances of codecs)
+     * being held, failure to call this method immediately if a
+     * MediaPlayer2 object is no longer needed may also lead to
+     * continuous battery consumption for mobile devices, and playback
+     * failure for other applications if no multiple instances of the
+     * same codec are supported on a device. Even if multiple instances
+     * of the same codec are supported, some performance degradation
+     * may be expected when unnecessary multiple instances are used
+     * at the same time.
+     *
+     * {@code close()} may be safely called after a prior {@code close()}.
+     * This class implements the Java {@code AutoCloseable} interface and
+     * may be used with try-with-resources.
+     */
+    @Override
+    public void close() {
+        synchronized (mGuard) {
+            release();
+        }
+    }
+
+    /**
+     * Starts or resumes playback. If playback had previously been paused,
+     * playback will continue from where it was paused. If playback had
+     * been stopped, or never started before, playback will start at the
+     * beginning.
+     *
+     * @throws IllegalStateException if it is called in an invalid state
+     */
+    @Override
+    public void play() {
+        addTask(new Task(CALL_COMPLETED_PLAY, false) {
+            @Override
+            void process() {
+                stayAwake(true);
+                _start();
+            }
+        });
+    }
+
+    private native void _start() throws IllegalStateException;
+
+    /**
+     * Prepares the player for playback, asynchronously.
+     *
+     * After setting the datasource and the display surface, you need to either
+     * call prepare(). For streams, you should call prepare(),
+     * which returns immediately, rather than blocking until enough data has been
+     * buffered.
+     *
+     * @throws IllegalStateException if it is called in an invalid state
+     */
+    @Override
+    public void prepare() {
+        addTask(new Task(CALL_COMPLETED_PREPARE, true) {
+            @Override
+            void process() {
+                _prepare();
+            }
+        });
+    }
+
+    public native void _prepare();
+
+    /**
+     * Pauses playback. Call play() to resume.
+     *
+     * @throws IllegalStateException if the internal player engine has not been
+     * initialized.
+     */
+    @Override
+    public void pause() {
+        addTask(new Task(CALL_COMPLETED_PAUSE, false) {
+            @Override
+            void process() {
+                stayAwake(false);
+                _pause();
+            }
+        });
+    }
+
+    private native void _pause() throws IllegalStateException;
+
+    /**
+     * Tries to play next data source if applicable.
+     *
+     * @throws IllegalStateException if it is called in an invalid state
+     */
+    @Override
+    public void skipToNext() {
+        addTask(new Task(CALL_COMPLETED_SKIP_TO_NEXT, false) {
+            @Override
+            void process() {
+                // TODO: switch to next data source and play
+            }
+        });
+    }
+
+    /**
+     * Gets the current playback position.
+     *
+     * @return the current position in milliseconds
+     */
+    @Override
+    public native long getCurrentPosition();
+
+    /**
+     * Gets the duration of the file.
+     *
+     * @return the duration in milliseconds, if no duration is available
+     *         (for example, if streaming live content), -1 is returned.
+     */
+    @Override
+    public native long getDuration();
+
+    /**
+     * Gets the current buffered media source position received through progressive downloading.
+     * The received buffering percentage indicates how much of the content has been buffered
+     * or played. For example a buffering update of 80 percent when half the content
+     * has already been played indicates that the next 30 percent of the
+     * content to play has been buffered.
+     *
+     * @return the current buffered media source position in milliseconds
+     */
+    @Override
+    public long getBufferedPosition() {
+        // Use cached buffered percent for now.
+        return getDuration() * mBufferedPercentageCurrent.get() / 100;
+    }
+
+    @Override
+    public @PlayerState int getPlayerState() {
+        int mediaplayer2State = getMediaPlayer2State();
+        int playerState;
+        switch (mediaplayer2State) {
+            case MEDIAPLAYER2_STATE_IDLE:
+                playerState = PLAYER_STATE_IDLE;
+                break;
+            case MEDIAPLAYER2_STATE_PREPARED:
+            case MEDIAPLAYER2_STATE_PAUSED:
+                playerState = PLAYER_STATE_PAUSED;
+                break;
+            case MEDIAPLAYER2_STATE_PLAYING:
+                playerState = PLAYER_STATE_PLAYING;
+                break;
+            case MEDIAPLAYER2_STATE_ERROR:
+            default:
+                playerState = PLAYER_STATE_ERROR;
+                break;
+        }
+
+        return playerState;
+    }
+
+    /**
+     * Gets the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     */
+    @Override
+    public @BuffState int getBufferingState() {
+        // TODO: use cached state or call native function.
+        return BUFFERING_STATE_UNKNOWN;
+    }
+
+    /**
+     * Sets the audio attributes for this MediaPlayer2.
+     * See {@link AudioAttributes} for how to build and configure an instance of this class.
+     * You must call this method before {@link #prepare()} in order
+     * for the audio attributes to become effective thereafter.
+     * @param attributes a non-null set of audio attributes
+     * @throws IllegalArgumentException if the attributes are null or invalid.
+     */
+    @Override
+    public void setAudioAttributes(@NonNull AudioAttributes attributes) {
+        addTask(new Task(CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, false) {
+            @Override
+            void process() {
+                if (attributes == null) {
+                    final String msg = "Cannot set AudioAttributes to null";
+                    throw new IllegalArgumentException(msg);
+                }
+                Parcel pattributes = Parcel.obtain();
+                attributes.writeToParcel(pattributes, AudioAttributes.FLATTEN_TAGS);
+                setParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES, pattributes);
+                pattributes.recycle();
+            }
+        });
+    }
+
+    @Override
+    public @NonNull AudioAttributes getAudioAttributes() {
+        Parcel pattributes = getParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES);
+        AudioAttributes attributes = AudioAttributes.CREATOR.createFromParcel(pattributes);
+        pattributes.recycle();
+        return attributes;
+    }
+
+    /**
+     * Sets the data source as described by a DataSourceDesc.
+     *
+     * @param dsd the descriptor of data source you want to play
+     * @throws IllegalStateException if it is called in an invalid state
+     * @throws NullPointerException if dsd is null
+     */
+    @Override
+    public void setDataSource(@NonNull DataSourceDesc dsd) {
+        addTask(new Task(CALL_COMPLETED_SET_DATA_SOURCE, false) {
+            @Override
+            void process() {
+                Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+                // TODO: setDataSource could update exist data source
+                synchronized (mSrcLock) {
+                    mCurrentDSD = dsd;
+                    mCurrentSrcId = mSrcIdGenerator++;
+                    try {
+                        handleDataSource(true /* isCurrent */, dsd, mCurrentSrcId);
+                    } catch (IOException e) {
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Sets a single data source as described by a DataSourceDesc which will be played
+     * after current data source is finished.
+     *
+     * @param dsd the descriptor of data source you want to play after current one
+     * @throws IllegalStateException if it is called in an invalid state
+     * @throws NullPointerException if dsd is null
+     */
+    @Override
+    public void setNextDataSource(@NonNull DataSourceDesc dsd) {
+        addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCE, false) {
+            @Override
+            void process() {
+                Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+                synchronized (mSrcLock) {
+                    mNextDSDs = new ArrayList<DataSourceDesc>(1);
+                    mNextDSDs.add(dsd);
+                    mNextSrcId = mSrcIdGenerator++;
+                    mNextSourceState = NEXT_SOURCE_STATE_INIT;
+                    mNextSourcePlayPending = false;
+                }
+                int state = getMediaPlayer2State();
+                if (state != MEDIAPLAYER2_STATE_IDLE) {
+                    synchronized (mSrcLock) {
+                        prepareNextDataSource_l();
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Sets a list of data sources to be played sequentially after current data source is done.
+     *
+     * @param dsds the list of data sources you want to play after current one
+     * @throws IllegalStateException if it is called in an invalid state
+     * @throws IllegalArgumentException if dsds is null or empty, or contains null DataSourceDesc
+     */
+    @Override
+    public void setNextDataSources(@NonNull List<DataSourceDesc> dsds) {
+        addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCES, false) {
+            @Override
+            void process() {
+                if (dsds == null || dsds.size() == 0) {
+                    throw new IllegalArgumentException("data source list cannot be null or empty.");
+                }
+                for (DataSourceDesc dsd : dsds) {
+                    if (dsd == null) {
+                        throw new IllegalArgumentException(
+                                "DataSourceDesc in the source list cannot be null.");
+                    }
+                }
+
+                synchronized (mSrcLock) {
+                    mNextDSDs = new ArrayList(dsds);
+                    mNextSrcId = mSrcIdGenerator++;
+                    mNextSourceState = NEXT_SOURCE_STATE_INIT;
+                    mNextSourcePlayPending = false;
+                }
+                int state = getMediaPlayer2State();
+                if (state != MEDIAPLAYER2_STATE_IDLE) {
+                    synchronized (mSrcLock) {
+                        prepareNextDataSource_l();
+                    }
+                }
+            }
+        });
+    }
+
+    @Override
+    public @NonNull DataSourceDesc getCurrentDataSource() {
+        synchronized (mSrcLock) {
+            return mCurrentDSD;
+        }
+    }
+
+    /**
+     * Configures the player to loop on the current data source.
+     * @param loop true if the current data source is meant to loop.
+     */
+    @Override
+    public void loopCurrent(boolean loop) {
+        addTask(new Task(CALL_COMPLETED_LOOP_CURRENT, false) {
+            @Override
+            void process() {
+                // TODO: set the looping mode, send notification
+                setLooping(loop);
+            }
+        });
+    }
+
+    private native void setLooping(boolean looping);
+
+    /**
+     * Sets the playback speed.
+     * A value of 1.0f is the default playback value.
+     * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+     * before using negative values.<br>
+     * After changing the playback speed, it is recommended to query the actual speed supported
+     * by the player, see {@link #getPlaybackSpeed()}.
+     * @param speed the desired playback speed
+     */
+    @Override
+    public void setPlaybackSpeed(float speed) {
+        addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_SPEED, false) {
+            @Override
+            void process() {
+                _setPlaybackParams(getPlaybackParams().setSpeed(speed));
+            }
+        });
+    }
+
+    /**
+     * Returns the actual playback speed to be used by the player when playing.
+     * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+     * @return the actual playback speed
+     */
+    @Override
+    public float getPlaybackSpeed() {
+        return getPlaybackParams().getSpeed();
+    }
+
+    /**
+     * Indicates whether reverse playback is supported.
+     * Reverse playback is indicated by negative playback speeds, see
+     * {@link #setPlaybackSpeed(float)}.
+     * @return true if reverse playback is supported.
+     */
+    @Override
+    public boolean isReversePlaybackSupported() {
+        return false;
+    }
+
+    /**
+     * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+     * on the audio samples.
+     * Note that this volume is specific to the player, and is separate from stream volume
+     * used across the platform.<br>
+     * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+     * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+     * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+     */
+    @Override
+    public void setPlayerVolume(float volume) {
+        addTask(new Task(CALL_COMPLETED_SET_PLAYER_VOLUME, false) {
+            @Override
+            void process() {
+                mVolume = volume;
+                _setVolume(volume, volume);
+            }
+        });
+    }
+
+    private native void _setVolume(float leftVolume, float rightVolume);
+
+    /**
+     * Returns the current volume of this player to this player.
+     * Note that it does not take into account the associated stream volume.
+     * @return the player volume.
+     */
+    @Override
+    public float getPlayerVolume() {
+        return mVolume;
+    }
+
+    /**
+     * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+     */
+    @Override
+    public float getMaxPlayerVolume() {
+        return 1.0f;
+    }
+
+    /**
+     * Adds a callback to be notified of events for this player.
+     * @param e the {@link Executor} to be used for the events.
+     * @param cb the callback to receive the events.
+     */
+    @Override
+    public void registerPlayerEventCallback(@NonNull Executor e,
+            @NonNull PlayerEventCallback cb) {
+    }
+
+    /**
+     * Removes a previously registered callback for player events
+     * @param cb the callback to remove
+     */
+    @Override
+    public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
+    }
+
+
+    private static final int NEXT_SOURCE_STATE_ERROR = -1;
+    private static final int NEXT_SOURCE_STATE_INIT = 0;
+    private static final int NEXT_SOURCE_STATE_PREPARING = 1;
+    private static final int NEXT_SOURCE_STATE_PREPARED = 2;
+
     /*
      * Update the MediaPlayer2Impl SurfaceTexture.
      * Call after setting a new display surface.
@@ -677,6 +660,21 @@
         }
     }
 
+    @Override
+    public void notifyWhenCommandLabelReached(Object label) {
+        addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
+            @Override
+            void process() {
+                synchronized (mEventCbLock) {
+                    for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                        cb.first.execute(() -> cb.second.onCommandLabelReached(
+                                MediaPlayer2Impl.this, label));
+                    }
+                }
+            }
+        });
+    }
+
     /**
      * Sets the {@link SurfaceHolder} to use for displaying the video
      * portion of the media.
@@ -727,12 +725,17 @@
      */
     @Override
     public void setSurface(Surface surface) {
-        if (mScreenOnWhilePlaying && surface != null) {
-            Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective for Surface");
-        }
-        mSurfaceHolder = null;
-        _setVideoSurface(surface);
-        updateSurfaceScreenOn();
+        addTask(new Task(CALL_COMPLETED_SET_SURFACE, false) {
+            @Override
+            void process() {
+                if (mScreenOnWhilePlaying && surface != null) {
+                    Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective for Surface");
+                }
+                mSurfaceHolder = null;
+                _setVideoSurface(surface);
+                updateSurfaceScreenOn();
+            }
+        });
     }
 
     /**
@@ -756,20 +759,25 @@
      */
     @Override
     public void setVideoScalingMode(int mode) {
-        if (!isVideoScalingModeSupported(mode)) {
-            final String msg = "Scaling mode " + mode + " is not supported";
-            throw new IllegalArgumentException(msg);
-        }
-        Parcel request = Parcel.obtain();
-        Parcel reply = Parcel.obtain();
-        try {
-            request.writeInt(INVOKE_ID_SET_VIDEO_SCALE_MODE);
-            request.writeInt(mode);
-            invoke(request, reply);
-        } finally {
-            request.recycle();
-            reply.recycle();
-        }
+        addTask(new Task(CALL_COMPLETED_SET_VIDEO_SCALING_MODE, false) {
+            @Override
+            void process() {
+                if (!isVideoScalingModeSupported(mode)) {
+                    final String msg = "Scaling mode " + mode + " is not supported";
+                    throw new IllegalArgumentException(msg);
+                }
+                Parcel request = Parcel.obtain();
+                Parcel reply = Parcel.obtain();
+                try {
+                    request.writeInt(INVOKE_ID_SET_VIDEO_SCALE_MODE);
+                    request.writeInt(mode);
+                    invoke(request, reply);
+                } finally {
+                    request.recycle();
+                    reply.recycle();
+                }
+            }
+        });
     }
 
     /**
@@ -779,314 +787,51 @@
     public void clearPendingCommands() {
     }
 
-    /**
-     * Sets the data source as described by a DataSourceDesc.
-     *
-     * @param dsd the descriptor of data source you want to play
-     * @throws IllegalStateException if it is called in an invalid state
-     * @throws NullPointerException if dsd is null
-     */
-    @Override
-    public void setDataSource(@NonNull DataSourceDesc dsd) throws IOException {
-        Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
-        mPlaylist = Collections.synchronizedList(new ArrayList<DataSourceDesc>(1));
-        mPlaylist.add(dsd);
-        mPLCurrentIndex = 0;
-        setDataSourcePriv(dsd);
-    }
-
-    /**
-     * Gets the current data source as described by a DataSourceDesc.
-     *
-     * @return the current DataSourceDesc
-     */
-    @Override
-    public DataSourceDesc getCurrentDataSource() {
-        if (mPlaylist == null) {
-            return null;
+    private void addTask(Task task) {
+        synchronized (mTaskLock) {
+            mPendingTasks.add(task);
+            processPendingTask_l();
         }
-        return mPlaylist.get(mPLCurrentIndex);
     }
 
-    /**
-     * Sets the play list.
-     *
-     * If startIndex falls outside play list range, it will be clamped to the nearest index
-     * in the play list.
-     *
-     * @param pl the play list of data source you want to play
-     * @param startIndex the index of the DataSourceDesc in the play list you want to play first
-     * @throws IllegalStateException if it is called in an invalid state
-     * @throws IllegalArgumentException if pl is null or empty, or pl contains null DataSourceDesc
-     */
-    @Override
-    public void setPlaylist(@NonNull List<DataSourceDesc> pl, int startIndex)
+    @GuardedBy("mTaskLock")
+    private void processPendingTask_l() {
+        if (mCurrentTask != null) {
+            return;
+        }
+        if (!mPendingTasks.isEmpty()) {
+            Task task = mPendingTasks.remove(0);
+            mCurrentTask = task;
+            mTaskHandler.post(task);
+        }
+    }
+
+    private void handleDataSource(boolean isCurrent, @NonNull DataSourceDesc dsd, long srcId)
             throws IOException {
-        if (pl == null || pl.size() == 0) {
-            throw new IllegalArgumentException("play list cannot be null or empty.");
-        }
-        HashSet ids = new HashSet(pl.size());
-        for (DataSourceDesc dsd : pl) {
-            if (dsd == null) {
-                throw new IllegalArgumentException("DataSourceDesc in play list cannot be null.");
-            }
-            if (ids.add(dsd.getId()) == false) {
-                throw new IllegalArgumentException("DataSourceDesc Id in play list should be unique.");
-            }
-        }
-
-        if (startIndex < 0) {
-            startIndex = 0;
-        } else if (startIndex >= pl.size()) {
-            startIndex = pl.size() - 1;
-        }
-
-        mPlaylist = Collections.synchronizedList(new ArrayList(pl));
-        mPLCurrentIndex = startIndex;
-        setDataSourcePriv(mPlaylist.get(startIndex));
-        // TODO: handle the preparation of next source in the play list.
-        // It should be processed after current source is prepared.
-    }
-
-    /**
-     * Gets a copy of the play list.
-     *
-     * @return a copy of the play list used by {@link MediaPlayer2}
-     */
-    @Override
-    public List<DataSourceDesc> getPlaylist() {
-        if (mPlaylist == null) {
-            return null;
-        }
-        return new ArrayList(mPlaylist);
-    }
-
-    /**
-     * Sets the index of current DataSourceDesc in the play list to be played.
-     *
-     * @param index the index of DataSourceDesc in the play list you want to play
-     * @throws IllegalArgumentException if the play list is null
-     * @throws NullPointerException if index is outside play list range
-     */
-    @Override
-    public void setCurrentPlaylistItem(int index) {
-        if (mPlaylist == null) {
-            throw new IllegalArgumentException("play list has not been set yet.");
-        }
-        if (index < 0 || index >= mPlaylist.size()) {
-            throw new IndexOutOfBoundsException("index is out of play list range.");
-        }
-
-        if (index == mPLCurrentIndex) {
-            return;
-        }
-
-        // TODO: in playing state, stop current source and start to play source of index.
-        mPLCurrentIndex = index;
-    }
-
-    /**
-     * Sets the index of next-to-be-played DataSourceDesc in the play list.
-     *
-     * @param index the index of next-to-be-played DataSourceDesc in the play list
-     * @throws IllegalArgumentException if the play list is null
-     * @throws NullPointerException if index is outside play list range
-     */
-    @Override
-    public void setNextPlaylistItem(int index) {
-        if (mPlaylist == null) {
-            throw new IllegalArgumentException("play list has not been set yet.");
-        }
-        if (index < 0 || index >= mPlaylist.size()) {
-            throw new IndexOutOfBoundsException("index is out of play list range.");
-        }
-
-        if (index == mPLNextIndex) {
-            return;
-        }
-
-        // TODO: prepare the new next-to-be-played DataSourceDesc
-        mPLNextIndex = index;
-    }
-
-    /**
-     * Gets the current index of play list.
-     *
-     * @return the index of the current DataSourceDesc in the play list
-     */
-    @Override
-    public int getCurrentPlaylistItemIndex() {
-        return mPLCurrentIndex;
-    }
-
-    /**
-     * Sets the looping mode of the play list.
-     * The mode shall be one of {@link #LOOPING_MODE_NONE}, {@link #LOOPING_MODE_FULL},
-     * {@link #LOOPING_MODE_SINGLE}, {@link #LOOPING_MODE_SHUFFLE}.
-     *
-     * @param mode the mode in which the play list will be played
-     * @throws IllegalArgumentException if mode is not supported
-     */
-    @Override
-    public void setLoopingMode(@LoopingMode int mode) {
-        if (mode != LOOPING_MODE_NONE
-            && mode != LOOPING_MODE_FULL
-            && mode != LOOPING_MODE_SINGLE
-            && mode != LOOPING_MODE_SHUFFLE) {
-            throw new IllegalArgumentException("mode is not supported.");
-        }
-        mLoopingMode = mode;
-        if (mPlaylist == null) {
-            return;
-        }
-
-        // TODO: handle the new mode if necessary.
-    }
-
-    /**
-     * Gets the looping mode of play list.
-     *
-     * @return the looping mode of the play list
-     */
-    @Override
-    public int getLoopingMode() {
-        return mPLCurrentIndex;
-    }
-
-    /**
-     * Moves the DataSourceDesc at indexFrom in the play list to indexTo.
-     *
-     * @throws IllegalArgumentException if the play list is null
-     * @throws IndexOutOfBoundsException if indexFrom or indexTo is outside play list range
-     */
-    @Override
-    public void movePlaylistItem(int indexFrom, int indexTo) {
-        if (mPlaylist == null) {
-            throw new IllegalArgumentException("play list has not been set yet.");
-        }
-        // TODO: move the DataSourceDesc from indexFrom to indexTo.
-    }
-
-    /**
-     * Removes the DataSourceDesc at index in the play list.
-     *
-     * If index is same as the current index of the play list, current DataSourceDesc
-     * will be stopped and playback moves to next source in the list.
-     *
-     * @return the removed DataSourceDesc at index in the play list
-     * @throws IllegalArgumentException if the play list is null
-     * @throws IndexOutOfBoundsException if index is outside play list range
-     */
-    @Override
-    public DataSourceDesc removePlaylistItem(int index) {
-        if (mPlaylist == null) {
-            throw new IllegalArgumentException("play list has not been set yet.");
-        }
-
-        DataSourceDesc oldDsd = mPlaylist.remove(index);
-        // TODO: if index == mPLCurrentIndex, stop current source and move to next one.
-        // if index == mPLNextIndex, prepare the new next-to-be-played source.
-        return oldDsd;
-    }
-
-    /**
-     * Inserts the DataSourceDesc to the play list at position index.
-     *
-     * This will not change the DataSourceDesc currently being played.
-     * If index is less than or equal to the current index of the play list,
-     * the current index of the play list will be incremented correspondingly.
-     *
-     * @param index the index you want to add dsd to the play list
-     * @param dsd the descriptor of data source you want to add to the play list
-     * @throws IndexOutOfBoundsException if index is outside play list range
-     * @throws NullPointerException if dsd is null
-     */
-    @Override
-    public void addPlaylistItem(int index, DataSourceDesc dsd) {
-        Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
-
-        if (mPlaylist == null) {
-            if (index == 0) {
-                mPlaylist = Collections.synchronizedList(new ArrayList<DataSourceDesc>());
-                mPlaylist.add(dsd);
-                mPLCurrentIndex = 0;
-                return;
-            }
-            throw new IllegalArgumentException("index should be 0 for first DataSourceDesc.");
-        }
-
-        long id = dsd.getId();
-        for (DataSourceDesc pldsd : mPlaylist) {
-            if (id == pldsd.getId()) {
-                throw new IllegalArgumentException("Id of dsd already exists in the play list.");
-            }
-        }
-
-        mPlaylist.add(index, dsd);
-        if (index <= mPLCurrentIndex) {
-            ++mPLCurrentIndex;
-        }
-    }
-
-    /**
-     * replaces the DataSourceDesc at index in the play list with given dsd.
-     *
-     * When index is same as the current index of the play list, the current source
-     * will be stopped and the new source will be played, except that if new
-     * and old source only differ on end position and current media position is
-     * smaller then the new end position.
-     *
-     * This will not change the DataSourceDesc currently being played.
-     * If index is less than or equal to the current index of the play list,
-     * the current index of the play list will be incremented correspondingly.
-     *
-     * @param index the index you want to add dsd to the play list
-     * @param dsd the descriptor of data source you want to add to the play list
-     * @throws IndexOutOfBoundsException if index is outside play list range
-     * @throws NullPointerException if dsd is null
-     */
-    @Override
-    public DataSourceDesc editPlaylistItem(int index, DataSourceDesc dsd) {
-        Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
-        Preconditions.checkNotNull(mPlaylist, "the play list cannot be null");
-
-        long id = dsd.getId();
-        for (int i = 0; i < mPlaylist.size(); ++i) {
-            if (i == index) {
-                continue;
-            }
-            if (id == mPlaylist.get(i).getId()) {
-                throw new IllegalArgumentException("Id of dsd already exists in the play list.");
-            }
-        }
-
-        // TODO: if needed, stop playback of current source, and start new dsd.
-        DataSourceDesc oldDsd = mPlaylist.set(index, dsd);
-        return mPlaylist.set(index, dsd);
-    }
-
-    private void setDataSourcePriv(@NonNull DataSourceDesc dsd) throws IOException {
         Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
 
         switch (dsd.getType()) {
             case DataSourceDesc.TYPE_CALLBACK:
-                setDataSourcePriv(dsd.getId(),
-                                  dsd.getMedia2DataSource());
+                handleDataSource(isCurrent,
+                                 srcId,
+                                 dsd.getMedia2DataSource());
                 break;
 
             case DataSourceDesc.TYPE_FD:
-                setDataSourcePriv(dsd.getId(),
-                                  dsd.getFileDescriptor(),
-                                  dsd.getFileDescriptorOffset(),
-                                  dsd.getFileDescriptorLength());
+                handleDataSource(isCurrent,
+                                 srcId,
+                                 dsd.getFileDescriptor(),
+                                 dsd.getFileDescriptorOffset(),
+                                 dsd.getFileDescriptorLength());
                 break;
 
             case DataSourceDesc.TYPE_URI:
-                setDataSourcePriv(dsd.getId(),
-                                  dsd.getUriContext(),
-                                  dsd.getUri(),
-                                  dsd.getUriHeaders(),
-                                  dsd.getUriCookies());
+                handleDataSource(isCurrent,
+                                 srcId,
+                                 dsd.getUriContext(),
+                                 dsd.getUri(),
+                                 dsd.getUriHeaders(),
+                                 dsd.getUriCookies());
                 break;
 
             default:
@@ -1113,66 +858,59 @@
      * @throws NullPointerException     if context or uri is null
      * @throws IOException              if uri has a file scheme and an I/O error occurs
      */
-    private void setDataSourcePriv(long srcId, @NonNull Context context, @NonNull Uri uri,
+    private void handleDataSource(
+            boolean isCurrent, long srcId,
+            @NonNull Context context, @NonNull Uri uri,
             @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies)
             throws IOException {
-        if (context == null) {
-            throw new NullPointerException("context param can not be null.");
-        }
-
-        if (uri == null) {
-            throw new NullPointerException("uri param can not be null.");
-        }
-
-        if (cookies != null) {
-            CookieHandler cookieHandler = CookieHandler.getDefault();
-            if (cookieHandler != null && !(cookieHandler instanceof CookieManager)) {
-                throw new IllegalArgumentException("The cookie handler has to be of CookieManager "
-                        + "type when cookies are provided.");
-            }
-        }
-
         // The context and URI usually belong to the calling user. Get a resolver for that user
         // and strip out the userId from the URI if present.
         final ContentResolver resolver = context.getContentResolver();
         final String scheme = uri.getScheme();
         final String authority = ContentProvider.getAuthorityWithoutUserId(uri.getAuthority());
         if (ContentResolver.SCHEME_FILE.equals(scheme)) {
-            setDataSourcePriv(srcId, uri.getPath(), null, null);
+            handleDataSource(isCurrent, srcId, uri.getPath(), null, null);
             return;
-        } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)
+        }
+
+        if (ContentResolver.SCHEME_CONTENT.equals(scheme)
                 && Settings.AUTHORITY.equals(authority)) {
             // Try cached ringtone first since the actual provider may not be
             // encryption aware, or it may be stored on CE media storage
             final int type = RingtoneManager.getDefaultType(uri);
             final Uri cacheUri = RingtoneManager.getCacheForType(type, context.getUserId());
             final Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
-            if (attemptDataSource(srcId, resolver, cacheUri)) {
+            if (attemptDataSource(isCurrent, srcId, resolver, cacheUri)) {
                 return;
-            } else if (attemptDataSource(srcId, resolver, actualUri)) {
-                return;
-            } else {
-                setDataSourcePriv(srcId, uri.toString(), headers, cookies);
             }
+            if (attemptDataSource(isCurrent, srcId, resolver, actualUri)) {
+                return;
+            }
+            handleDataSource(isCurrent, srcId, uri.toString(), headers, cookies);
         } else {
             // Try requested Uri locally first, or fallback to media server
-            if (attemptDataSource(srcId, resolver, uri)) {
+            if (attemptDataSource(isCurrent, srcId, resolver, uri)) {
                 return;
-            } else {
-                setDataSourcePriv(srcId, uri.toString(), headers, cookies);
             }
+            handleDataSource(isCurrent, srcId, uri.toString(), headers, cookies);
         }
     }
 
-    private boolean attemptDataSource(long srcId, ContentResolver resolver, Uri uri) {
+    private boolean attemptDataSource(
+            boolean isCurrent, long srcId, ContentResolver resolver, Uri uri) {
         try (AssetFileDescriptor afd = resolver.openAssetFileDescriptor(uri, "r")) {
             if (afd.getDeclaredLength() < 0) {
-                setDataSourcePriv(srcId, afd.getFileDescriptor(), 0, DataSourceDesc.LONG_MAX);
+                handleDataSource(isCurrent,
+                                 srcId,
+                                 afd.getFileDescriptor(),
+                                 0,
+                                 DataSourceDesc.LONG_MAX);
             } else {
-                setDataSourcePriv(srcId,
-                                  afd.getFileDescriptor(),
-                                  afd.getStartOffset(),
-                                  afd.getDeclaredLength());
+                handleDataSource(isCurrent,
+                                 srcId,
+                                 afd.getFileDescriptor(),
+                                 afd.getStartOffset(),
+                                 afd.getDeclaredLength());
             }
             return true;
         } catch (NullPointerException | SecurityException | IOException ex) {
@@ -1181,10 +919,10 @@
         }
     }
 
-    private void setDataSourcePriv(
-            long srcId, String path, Map<String, String> headers, List<HttpCookie> cookies)
-            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException
-    {
+    private void handleDataSource(
+            boolean isCurrent, long srcId,
+            String path, Map<String, String> headers, List<HttpCookie> cookies)
+            throws IOException {
         String[] keys = null;
         String[] values = null;
 
@@ -1199,19 +937,21 @@
                 ++i;
             }
         }
-        setDataSourcePriv(srcId, path, keys, values, cookies);
+        handleDataSource(isCurrent, srcId, path, keys, values, cookies);
     }
 
-    private void setDataSourcePriv(long srcId, String path, String[] keys, String[] values,
-            List<HttpCookie> cookies)
-            throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+    private void handleDataSource(boolean isCurrent, long srcId,
+            String path, String[] keys, String[] values, List<HttpCookie> cookies)
+            throws IOException {
         final Uri uri = Uri.parse(path);
         final String scheme = uri.getScheme();
         if ("file".equals(scheme)) {
             path = uri.getPath();
         } else if (scheme != null) {
             // handle non-file sources
-            nativeSetDataSource(
+            nativeHandleDataSourceUrl(
+                isCurrent,
+                srcId,
                 Media2HTTPService.createHTTPService(path, cookies),
                 path,
                 keys,
@@ -1223,16 +963,17 @@
         if (file.exists()) {
             FileInputStream is = new FileInputStream(file);
             FileDescriptor fd = is.getFD();
-            setDataSourcePriv(srcId, fd, 0, DataSourceDesc.LONG_MAX);
+            handleDataSource(isCurrent, srcId, fd, 0, DataSourceDesc.LONG_MAX);
             is.close();
         } else {
-            throw new IOException("setDataSourcePriv failed.");
+            throw new IOException("handleDataSource failed.");
         }
     }
 
-    private native void nativeSetDataSource(
-        Media2HTTPService httpService, String path, String[] keys, String[] values)
-        throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;
+    private native void nativeHandleDataSourceUrl(
+            boolean isCurrent, long srcId,
+            Media2HTTPService httpService, String path, String[] keys, String[] values)
+            throws IOException;
 
     /**
      * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
@@ -1243,76 +984,91 @@
      * @throws IllegalArgumentException if fd is not a valid FileDescriptor
      * @throws IOException if fd can not be read
      */
-    private void setDataSourcePriv(long srcId, FileDescriptor fd, long offset, long length)
-            throws IOException {
-        _setDataSource(fd, offset, length);
+    private void handleDataSource(
+            boolean isCurrent, long srcId,
+            FileDescriptor fd, long offset, long length) throws IOException {
+        nativeHandleDataSourceFD(isCurrent, srcId, fd, offset, length);
     }
 
-    private native void _setDataSource(FileDescriptor fd, long offset, long length)
-            throws IOException;
+    private native void nativeHandleDataSourceFD(boolean isCurrent, long srcId,
+            FileDescriptor fd, long offset, long length) throws IOException;
 
     /**
      * @throws IllegalStateException if it is called in an invalid state
      * @throws IllegalArgumentException if dataSource is not a valid Media2DataSource
      */
-    private void setDataSourcePriv(long srcId, Media2DataSource dataSource) {
-        _setDataSource(dataSource);
+    private void handleDataSource(boolean isCurrent, long srcId, Media2DataSource dataSource) {
+        nativeHandleDataSourceCallback(isCurrent, srcId, dataSource);
     }
 
-    private native void _setDataSource(Media2DataSource dataSource);
+    private native void nativeHandleDataSourceCallback(
+            boolean isCurrent, long srcId, Media2DataSource dataSource);
 
-    /**
-     * Prepares the player for playback, synchronously.
-     *
-     * After setting the datasource and the display surface, you need to either
-     * call prepare() or prepareAsync(). For files, it is OK to call prepare(),
-     * which blocks until MediaPlayer2 is ready for playback.
-     *
-     * @throws IOException if source can not be accessed
-     * @throws IllegalStateException if it is called in an invalid state
-     * @hide
-     */
-    @Override
-    public void prepare() throws IOException {
-        _prepare();
-        scanInternalSubtitleTracks();
+    // This function shall be called with |mSrcLock| acquired.
+    private void prepareNextDataSource_l() {
+        if (mNextDSDs == null || mNextDSDs.isEmpty()
+                || mNextSourceState != NEXT_SOURCE_STATE_INIT) {
+            // There is no next source or it's in preparing or prepared state.
+            return;
+        }
 
-        // DrmInfo, if any, has been resolved by now.
-        synchronized (mDrmLock) {
-            mDrmInfoResolved = true;
+        try {
+            mNextSourceState = NEXT_SOURCE_STATE_PREPARING;
+            handleDataSource(false /* isCurrent */, mNextDSDs.get(0), mNextSrcId);
+        } catch (Exception e) {
+            Message msg2 = mEventHandler.obtainMessage(
+                    MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
+            final long nextSrcId = mNextSrcId;
+            mEventHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mEventHandler.handleMessage(msg2, nextSrcId);
+                }
+            });
         }
     }
 
-    private native void _prepare() throws IOException, IllegalStateException;
+    // This function shall be called with |mSrcLock| acquired.
+    private void playNextDataSource_l() {
+        if (mNextDSDs == null || mNextDSDs.isEmpty()) {
+            return;
+        }
 
-    /**
-     * Prepares the player for playback, asynchronously.
-     *
-     * After setting the datasource and the display surface, you need to either
-     * call prepare() or prepareAsync(). For streams, you should call prepareAsync(),
-     * which returns immediately, rather than blocking until enough data has been
-     * buffered.
-     *
-     * @throws IllegalStateException if it is called in an invalid state
-     */
-    @Override
-    public native void prepareAsync();
+        if (mNextSourceState == NEXT_SOURCE_STATE_PREPARED) {
+            // Switch to next source only when it's in prepared state.
+            mCurrentDSD = mNextDSDs.get(0);
+            mCurrentSrcId = mNextSrcId;
+            mBufferedPercentageCurrent.set(mBufferedPercentageNext.get());
+            mNextDSDs.remove(0);
+            mNextSrcId = mSrcIdGenerator++;  // make it different from mCurrentSrcId
+            mBufferedPercentageNext.set(0);
+            mNextSourceState = NEXT_SOURCE_STATE_INIT;
+            mNextSourcePlayPending = false;
 
-    /**
-     * Starts or resumes playback. If playback had previously been paused,
-     * playback will continue from where it was paused. If playback had
-     * been stopped, or never started before, playback will start at the
-     * beginning.
-     *
-     * @throws IllegalStateException if it is called in an invalid state
-     */
-    @Override
-    public void play() {
-        stayAwake(true);
-        _start();
+            long srcId = mCurrentSrcId;
+            try {
+                nativePlayNextDataSource(srcId);
+            } catch (Exception e) {
+                Message msg2 = mEventHandler.obtainMessage(
+                        MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
+                mEventHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mEventHandler.handleMessage(msg2, srcId);
+                    }
+                });
+            }
+
+            // Wait for MEDIA2_INFO_STARTED_AS_NEXT to prepare next source.
+        } else {
+            if (mNextSourceState == NEXT_SOURCE_STATE_INIT) {
+                prepareNextDataSource_l();
+            }
+            mNextSourcePlayPending = true;
+        }
     }
 
-    private native void _start() throws IllegalStateException;
+    private native void nativePlayNextDataSource(long srcId);
 
 
     private int getAudioStreamType() {
@@ -1339,20 +1095,6 @@
 
     private native void _stop() throws IllegalStateException;
 
-    /**
-     * Pauses playback. Call play() to resume.
-     *
-     * @throws IllegalStateException if the internal player engine has not been
-     * initialized.
-     */
-    @Override
-    public void pause() {
-        stayAwake(false);
-        _pause();
-    }
-
-    private native void _pause() throws IllegalStateException;
-
     //--------------------------------------------------------------------------
     // Explicit Routing
     //--------------------
@@ -1417,6 +1159,7 @@
     /*
      * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler.
      */
+    @GuardedBy("mRoutingChangeListeners")
     private void enableNativeRoutingCallbacksLocked(boolean enabled) {
         if (mRoutingChangeListeners.size() == 0) {
             native_enableDeviceCallback(enabled);
@@ -1562,9 +1305,10 @@
      *
      * @return the width of the video, or 0 if there is no video,
      * no display surface was set, or the width has not been determined
-     * yet. The {@code EventCallback} can be registered via
-     * {@link #registerEventCallback(Executor, EventCallback)} to provide a
-     * notification {@code EventCallback.onVideoSizeChanged} when the width is available.
+     * yet. The {@code MediaPlayer2EventCallback} can be registered via
+     * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+     * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the width
+     * is available.
      */
     @Override
     public native int getVideoWidth();
@@ -1574,9 +1318,10 @@
      *
      * @return the height of the video, or 0 if there is no video,
      * no display surface was set, or the height has not been determined
-     * yet. The {@code EventCallback} can be registered via
-     * {@link #registerEventCallback(Executor, EventCallback)} to provide a
-     * notification {@code EventCallback.onVideoSizeChanged} when the height is available.
+     * yet. The {@code MediaPlayer2EventCallback} can be registered via
+     * {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
+     * notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the height
+     * is available.
      */
     @Override
     public native int getVideoHeight();
@@ -1605,10 +1350,18 @@
      * @return true if currently playing, false otherwise
      * @throws IllegalStateException if the internal player engine has not been
      * initialized or has been released.
+     * @hide
      */
     @Override
     public native boolean isPlaying();
 
+    @Override
+    public @MediaPlayer2State int getMediaPlayer2State() {
+        return native_getMediaPlayer2State();
+    }
+
+    private native int native_getMediaPlayer2State();
+
     /**
      * Gets the current buffering management params used by the source component.
      * Calling it only after {@code setDataSource} has been called.
@@ -1638,7 +1391,17 @@
      * @hide
      */
     @Override
-    public native void setBufferingParams(@NonNull BufferingParams params);
+    public void setBufferingParams(@NonNull BufferingParams params) {
+        addTask(new Task(CALL_COMPLETED_SET_BUFFERING_PARAMS, false) {
+            @Override
+            void process() {
+                Preconditions.checkNotNull(params, "the BufferingParams cannot be null");
+                _setBufferingParams(params);
+            }
+        });
+    }
+
+    private native void _setBufferingParams(@NonNull BufferingParams params);
 
     /**
      * Sets playback rate and audio mode.
@@ -1692,7 +1455,17 @@
      * @throws IllegalArgumentException if params is not supported.
      */
     @Override
-    public native void setPlaybackParams(@NonNull PlaybackParams params);
+    public void setPlaybackParams(@NonNull PlaybackParams params) {
+        addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_PARAMS, false) {
+            @Override
+            void process() {
+                Preconditions.checkNotNull(params, "the PlaybackParams cannot be null");
+                _setPlaybackParams(params);
+            }
+        });
+    }
+
+    private native void _setPlaybackParams(@NonNull PlaybackParams params);
 
     /**
      * Gets the playback params, containing the current playback rate.
@@ -1715,7 +1488,17 @@
      * @throws IllegalArgumentException if params are not supported.
      */
     @Override
-    public native void setSyncParams(@NonNull SyncParams params);
+    public void setSyncParams(@NonNull SyncParams params) {
+        addTask(new Task(CALL_COMPLETED_SET_SYNC_PARAMS, false) {
+            @Override
+            void process() {
+                Preconditions.checkNotNull(params, "the SyncParams cannot be null");
+                _setSyncParams(params);
+            }
+        });
+    }
+
+    private native void _setSyncParams(@NonNull SyncParams params);
 
     /**
      * Gets the A/V sync mode.
@@ -1729,8 +1512,6 @@
     @NonNull
     public native SyncParams getSyncParams();
 
-    private native final void _seekTo(long msec, int mode);
-
     /**
      * Moves the media to specified time position by considering the given mode.
      * <p>
@@ -1762,22 +1543,32 @@
      * @throws IllegalArgumentException if the mode is invalid.
      */
     @Override
-    public void seekTo(long msec, @SeekMode int mode) {
-        if (mode < SEEK_PREVIOUS_SYNC || mode > SEEK_CLOSEST) {
-            final String msg = "Illegal seek mode: " + mode;
-            throw new IllegalArgumentException(msg);
-        }
-        // TODO: pass long to native, instead of truncating here.
-        if (msec > Integer.MAX_VALUE) {
-            Log.w(TAG, "seekTo offset " + msec + " is too large, cap to " + Integer.MAX_VALUE);
-            msec = Integer.MAX_VALUE;
-        } else if (msec < Integer.MIN_VALUE) {
-            Log.w(TAG, "seekTo offset " + msec + " is too small, cap to " + Integer.MIN_VALUE);
-            msec = Integer.MIN_VALUE;
-        }
-        _seekTo(msec, mode);
+    public void seekTo(final long msec, @SeekMode int mode) {
+        addTask(new Task(CALL_COMPLETED_SEEK_TO, true) {
+            @Override
+            void process() {
+                if (mode < SEEK_PREVIOUS_SYNC || mode > SEEK_CLOSEST) {
+                    final String msg = "Illegal seek mode: " + mode;
+                    throw new IllegalArgumentException(msg);
+                }
+                // TODO: pass long to native, instead of truncating here.
+                long posMs = msec;
+                if (posMs > Integer.MAX_VALUE) {
+                    Log.w(TAG, "seekTo offset " + posMs + " is too large, cap to "
+                            + Integer.MAX_VALUE);
+                    posMs = Integer.MAX_VALUE;
+                } else if (posMs < Integer.MIN_VALUE) {
+                    Log.w(TAG, "seekTo offset " + posMs + " is too small, cap to "
+                            + Integer.MIN_VALUE);
+                    posMs = Integer.MIN_VALUE;
+                }
+                _seekTo(posMs, mode);
+            }
+        });
     }
 
+    private native final void _seekTo(long msec, int mode);
+
     /**
      * Get current playback position as a {@link MediaTimestamp}.
      * <p>
@@ -1812,23 +1603,6 @@
     }
 
     /**
-     * Gets the current playback position.
-     *
-     * @return the current position in milliseconds
-     */
-    @Override
-    public native int getCurrentPosition();
-
-    /**
-     * Gets the duration of the file.
-     *
-     * @return the duration in milliseconds, if no duration is available
-     *         (for example, if streaming live content), -1 is returned.
-     */
-    @Override
-    public native int getDuration();
-
-    /**
      * Gets the media metadata.
      *
      * @param update_only controls whether the full set of available
@@ -1914,28 +1688,6 @@
     }
 
     /**
-     * Set the MediaPlayer2 to start when this MediaPlayer2 finishes playback
-     * (i.e. reaches the end of the stream).
-     * The media framework will attempt to transition from this player to
-     * the next as seamlessly as possible. The next player can be set at
-     * any time before completion, but shall be after setDataSource has been
-     * called successfully. The next player must be prepared by the
-     * app, and the application should not call play() on it.
-     * The next MediaPlayer2 must be different from 'this'. An exception
-     * will be thrown if next == this.
-     * The application may call setNextMediaPlayer(null) to indicate no
-     * next player should be started at the end of playback.
-     * If the current player is looping, it will keep looping and the next
-     * player will not be started.
-     *
-     * @param next the player to start after this one completes playback.
-     *
-     * @hide
-     */
-    @Override
-    public native void setNextMediaPlayer(MediaPlayer2 next);
-
-    /**
      * Resets the MediaPlayer2 to its uninitialized state. After calling
      * this method, you will have to initialize it again by setting the
      * data source and calling prepare().
@@ -1960,6 +1712,13 @@
             mTimeProvider = null;
         }
 
+        synchronized (mEventCbLock) {
+            mEventCallbackRecords.clear();
+        }
+        synchronized (mDrmEventCbLock) {
+            mDrmEventCallbackRecords.clear();
+        }
+
         stayAwake(false);
         _reset();
         // make sure none of the listeners get called anymore
@@ -1999,41 +1758,11 @@
      * @param key key indicates the parameter to be set.
      * @param value value of the parameter to be set.
      * @return true if the parameter is set successfully, false otherwise
-     * {@hide}
      */
     private native boolean setParameter(int key, Parcel value);
 
-    /**
-     * Sets the audio attributes for this MediaPlayer2.
-     * See {@link AudioAttributes} for how to build and configure an instance of this class.
-     * You must call this method before {@link #prepare()} or {@link #prepareAsync()} in order
-     * for the audio attributes to become effective thereafter.
-     * @param attributes a non-null set of audio attributes
-     * @throws IllegalArgumentException if the attributes are null or invalid.
-     */
-    @Override
-    public void setAudioAttributes(AudioAttributes attributes) {
-        if (attributes == null) {
-            final String msg = "Cannot set AudioAttributes to null";
-            throw new IllegalArgumentException(msg);
-        }
-        mUsage = attributes.getUsage();
-        mBypassInterruptionPolicy = (attributes.getAllFlags()
-                & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY) != 0;
-        Parcel pattributes = Parcel.obtain();
-        attributes.writeToParcel(pattributes, AudioAttributes.FLATTEN_TAGS);
-        setParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES, pattributes);
-        pattributes.recycle();
-    }
+    private native Parcel getParameter(int key);
 
-    /**
-     * Sets the player to be looping or non-looping.
-     *
-     * @param looping whether to loop or not
-     * @hide
-     */
-    @Override
-    public native void setLooping(boolean looping);
 
     /**
      * Checks whether the MediaPlayer2 is looping or non-looping.
@@ -2045,39 +1774,6 @@
     public native boolean isLooping();
 
     /**
-     * Sets the volume on this player.
-     * This API is recommended for balancing the output of audio streams
-     * within an application. Unless you are writing an application to
-     * control user settings, this API should be used in preference to
-     * {@link AudioManager#setStreamVolume(int, int, int)} which sets the volume of ALL streams of
-     * a particular type. Note that the passed volume values are raw scalars in range 0.0 to 1.0.
-     * UI controls should be scaled logarithmically.
-     *
-     * @param leftVolume left volume scalar
-     * @param rightVolume right volume scalar
-     */
-    /*
-     * FIXME: Merge this into javadoc comment above when setVolume(float) is not @hide.
-     * The single parameter form below is preferred if the channel volumes don't need
-     * to be set independently.
-     */
-    @Override
-    public void setVolume(float leftVolume, float rightVolume) {
-        _setVolume(leftVolume, rightVolume);
-    }
-
-    private native void _setVolume(float leftVolume, float rightVolume);
-
-    /**
-     * Similar, excepts sets volume of all channels to same value.
-     * @hide
-     */
-    @Override
-    public void setVolume(float volume) {
-        setVolume(volume, volume);
-    }
-
-    /**
      * Sets the audio session ID.
      *
      * @param sessionId the audio session ID.
@@ -2095,7 +1791,16 @@
      * @throws IllegalArgumentException if the sessionId is invalid.
      */
     @Override
-    public native void setAudioSessionId(int sessionId);
+    public void setAudioSessionId(int sessionId) {
+        addTask(new Task(CALL_COMPLETED_SET_AUDIO_SESSION_ID, false) {
+            @Override
+            void process() {
+                _setAudioSessionId(sessionId);
+            }
+        });
+    }
+
+    private native void _setAudioSessionId(int sessionId);
 
     /**
      * Returns the audio session ID.
@@ -2121,8 +1826,16 @@
      * @param effectId system wide unique id of the effect to attach
      */
     @Override
-    public native void attachAuxEffect(int effectId);
+    public void attachAuxEffect(int effectId) {
+        addTask(new Task(CALL_COMPLETED_ATTACH_AUX_EFFECT, false) {
+            @Override
+            void process() {
+                _attachAuxEffect(effectId);
+            }
+        });
+    }
 
+    private native void _attachAuxEffect(int effectId);
 
     /**
      * Sets the send level of the player to the attached auxiliary effect.
@@ -2138,7 +1851,12 @@
      */
     @Override
     public void setAuxEffectSendLevel(float level) {
-        _setAuxEffectSendLevel(level);
+        addTask(new Task(CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, false) {
+            @Override
+            void process() {
+                _setAuxEffectSendLevel(level);
+            }
+        });
     }
 
     private native void _setAuxEffectSendLevel(float level);
@@ -2181,6 +1899,13 @@
     private native final void native_setup(Object mediaplayer2_this);
     private native final void native_finalize();
 
+    private static native final void native_stream_event_onTearDown(
+            long nativeCallbackPtr, long userDataPtr);
+    private static native final void native_stream_event_onStreamPresentationEnd(
+            long nativeCallbackPtr, long userDataPtr);
+    private static native final void native_stream_event_onStreamDataRequest(
+            long jAudioTrackPtr, long nativeCallbackPtr, long userDataPtr);
+
     /**
      * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
      *
@@ -2870,7 +2595,12 @@
      */
     @Override
     public void selectTrack(int index) {
-        selectOrDeselectTrack(index, true /* select */);
+        addTask(new Task(CALL_COMPLETED_SELECT_TRACK, false) {
+            @Override
+            void process() {
+                selectOrDeselectTrack(index, true /* select */);
+            }
+        });
     }
 
     /**
@@ -2889,7 +2619,12 @@
      */
     @Override
     public void deselectTrack(int index) {
-        selectOrDeselectTrack(index, false /* select */);
+        addTask(new Task(CALL_COMPLETED_DESELECT_TRACK, false) {
+            @Override
+            void process() {
+                selectOrDeselectTrack(index, false /* select */);
+            }
+        });
     }
 
     private void selectOrDeselectTrack(int index, boolean select)
@@ -2956,83 +2691,6 @@
         }
     }
 
-    /**
-     * Sets the target UDP re-transmit endpoint for the low level player.
-     * Generally, the address portion of the endpoint is an IP multicast
-     * address, although a unicast address would be equally valid.  When a valid
-     * retransmit endpoint has been set, the media player will not decode and
-     * render the media presentation locally.  Instead, the player will attempt
-     * to re-multiplex its media data using the Android@Home RTP profile and
-     * re-transmit to the target endpoint.  Receiver devices (which may be
-     * either the same as the transmitting device or different devices) may
-     * instantiate, prepare, and start a receiver player using a setDataSource
-     * URL of the form...
-     *
-     * aahRX://&lt;multicastIP&gt;:&lt;port&gt;
-     *
-     * to receive, decode and render the re-transmitted content.
-     *
-     * setRetransmitEndpoint may only be called before setDataSource has been
-     * called; while the player is in the Idle state.
-     *
-     * @param endpoint the address and UDP port of the re-transmission target or
-     * null if no re-transmission is to be performed.
-     * @throws IllegalStateException if it is called in an invalid state
-     * @throws IllegalArgumentException if the retransmit endpoint is supplied,
-     * but invalid.
-     *
-     * {@hide} pending API council
-     */
-    @Override
-    public void setRetransmitEndpoint(InetSocketAddress endpoint)
-            throws IllegalStateException, IllegalArgumentException
-    {
-        String addrString = null;
-        int port = 0;
-
-        if (null != endpoint) {
-            addrString = endpoint.getAddress().getHostAddress();
-            port = endpoint.getPort();
-        }
-
-        int ret = native_setRetransmitEndpoint(addrString, port);
-        if (ret != 0) {
-            throw new IllegalArgumentException("Illegal re-transmit endpoint; native ret " + ret);
-        }
-    }
-
-    private native final int native_setRetransmitEndpoint(String addrString, int port);
-
-    /**
-     * Releases the resources held by this {@code MediaPlayer2} object.
-     *
-     * It is considered good practice to call this method when you're
-     * done using the MediaPlayer2. In particular, whenever an Activity
-     * of an application is paused (its onPause() method is called),
-     * or stopped (its onStop() method is called), this method should be
-     * invoked to release the MediaPlayer2 object, unless the application
-     * has a special need to keep the object around. In addition to
-     * unnecessary resources (such as memory and instances of codecs)
-     * being held, failure to call this method immediately if a
-     * MediaPlayer2 object is no longer needed may also lead to
-     * continuous battery consumption for mobile devices, and playback
-     * failure for other applications if no multiple instances of the
-     * same codec are supported on a device. Even if multiple instances
-     * of the same codec are supported, some performance degradation
-     * may be expected when unnecessary multiple instances are used
-     * at the same time.
-     *
-     * {@code close()} may be safely called after a prior {@code close()}.
-     * This class implements the Java {@code AutoCloseable} interface and
-     * may be used with try-with-resources.
-     */
-    @Override
-    public void close() {
-        synchronized (mGuard) {
-            release();
-        }
-    }
-
     // Have to declare protected for finalize() since it is protected
     // in the base class Object.
     @Override
@@ -3049,8 +2707,11 @@
         stayAwake(false);
         updateSurfaceScreenOn();
         synchronized (mEventCbLock) {
-            mEventCb = null;
-            mEventExec = null;
+            mEventCallbackRecords.clear();
+        }
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread = null;
         }
         if (mTimeProvider != null) {
             mTimeProvider.close();
@@ -3061,8 +2722,7 @@
         // Modular DRM clean up
         mOnDrmConfigHelper = null;
         synchronized (mDrmEventCbLock) {
-            mDrmEventCb = null;
-            mDrmEventExec = null;
+            mDrmEventCallbackRecords.clear();
         }
         resetDrmState();
 
@@ -3114,24 +2774,20 @@
 
         @Override
         public void handleMessage(Message msg) {
+            handleMessage(msg, 0);
+        }
+
+        public void handleMessage(Message msg, long srcId) {
             if (mMediaPlayer.mNativeContext == 0) {
                 Log.w(TAG, "mediaplayer2 went away with unhandled events");
                 return;
             }
-            final Executor eventExec;
-            final EventCallback eventCb;
-            synchronized (mEventCbLock) {
-                eventExec = mEventExec;
-                eventCb = mEventCb;
-            }
-            final Executor drmEventExec;
-            final DrmEventCallback drmEventCb;
-            synchronized (mDrmEventCbLock) {
-                drmEventExec = mDrmEventExec;
-                drmEventCb = mDrmEventCb;
-            }
+            final int what = msg.arg1;
+            final int extra = msg.arg2;
+
             switch(msg.what) {
             case MEDIA_PREPARED:
+            {
                 try {
                     scanInternalSubtitleTracks();
                 } catch (RuntimeException e) {
@@ -3143,174 +2799,273 @@
                     sendMessage(msg2);
                 }
 
-                if (eventCb != null && eventExec != null) {
-                    eventExec.execute(() -> eventCb.onInfo(
-                            mMediaPlayer, 0, MEDIA_INFO_PREPARED, 0));
+                final DataSourceDesc dsd;
+                synchronized (mSrcLock) {
+                    Log.i(TAG, "MEDIA_PREPARED: srcId=" + srcId
+                            + ", currentSrcId=" + mCurrentSrcId + ", nextSrcId=" + mNextSrcId);
+                    if (srcId == mCurrentSrcId) {
+                        dsd = mCurrentDSD;
+                        prepareNextDataSource_l();
+                    } else if (mNextDSDs != null && !mNextDSDs.isEmpty()
+                            && srcId == mNextSrcId) {
+                        dsd = mNextDSDs.get(0);
+                        mNextSourceState = NEXT_SOURCE_STATE_PREPARED;
+                        if (mNextSourcePlayPending) {
+                            playNextDataSource_l();
+                        }
+                    } else {
+                        dsd = null;
+                    }
+                }
+
+                if (dsd != null) {
+                    synchronized (mEventCbLock) {
+                        for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                            cb.first.execute(() -> cb.second.onInfo(
+                                    mMediaPlayer, dsd, MEDIA_INFO_PREPARED, 0));
+                        }
+                    }
+                }
+                synchronized (mTaskLock) {
+                    if (mCurrentTask != null
+                            && mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
+                            && mCurrentTask.mDSD == dsd
+                            && mCurrentTask.mNeedToWaitForEventToComplete) {
+                        mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
+                        mCurrentTask = null;
+                        processPendingTask_l();
+                    }
                 }
                 return;
+            }
 
             case MEDIA_DRM_INFO:
-                Log.v(TAG, "MEDIA_DRM_INFO " + mDrmEventCb);
-
+            {
                 if (msg.obj == null) {
                     Log.w(TAG, "MEDIA_DRM_INFO msg.obj=NULL");
                 } else if (msg.obj instanceof Parcel) {
-                    if (drmEventExec != null && drmEventCb != null) {
-                        // The parcel was parsed already in postEventFromNative
-                        final DrmInfoImpl drmInfo;
+                    // The parcel was parsed already in postEventFromNative
+                    final DrmInfoImpl drmInfo;
 
-                        synchronized (mDrmLock) {
-                            if (mDrmInfoImpl != null) {
-                                drmInfo = mDrmInfoImpl.makeCopy();
-                            } else {
-                                drmInfo = null;
-                            }
+                    synchronized (mDrmLock) {
+                        if (mDrmInfoImpl != null) {
+                            drmInfo = mDrmInfoImpl.makeCopy();
+                        } else {
+                            drmInfo = null;
                         }
+                    }
 
-                        // notifying the client outside the lock
-                        if (drmInfo != null) {
-                            drmEventExec.execute(() -> drmEventCb.onDrmInfo(mMediaPlayer, drmInfo));
+                    // notifying the client outside the lock
+                    if (drmInfo != null) {
+                        synchronized (mEventCbLock) {
+                            for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) {
+                                cb.first.execute(() -> cb.second.onDrmInfo(
+                                        mMediaPlayer, mCurrentDSD, drmInfo));
+                            }
                         }
                     }
                 } else {
                     Log.w(TAG, "MEDIA_DRM_INFO msg.obj of unexpected type " + msg.obj);
                 }
                 return;
+            }
 
             case MEDIA_PLAYBACK_COMPLETE:
-                if (eventCb != null && eventExec != null) {
-                    eventExec.execute(() -> eventCb.onInfo(
-                            mMediaPlayer, 0, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+            {
+                final DataSourceDesc dsd = mCurrentDSD;
+                synchronized (mSrcLock) {
+                    if (srcId == mCurrentSrcId) {
+                        Log.i(TAG, "MEDIA_PLAYBACK_COMPLETE: srcId=" + srcId
+                                + ", currentSrcId=" + mCurrentSrcId + ", nextSrcId=" + mNextSrcId);
+                        playNextDataSource_l();
+                    }
+                }
+
+                synchronized (mEventCbLock) {
+                    for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                        cb.first.execute(() -> cb.second.onInfo(
+                                mMediaPlayer, dsd, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+                    }
                 }
                 stayAwake(false);
                 return;
+            }
 
             case MEDIA_STOPPED:
-                {
-                    TimeProvider timeProvider = mTimeProvider;
-                    if (timeProvider != null) {
-                        timeProvider.onStopped();
-                    }
+            {
+                TimeProvider timeProvider = mTimeProvider;
+                if (timeProvider != null) {
+                    timeProvider.onStopped();
                 }
                 break;
+            }
 
             case MEDIA_STARTED:
             case MEDIA_PAUSED:
-                {
-                    TimeProvider timeProvider = mTimeProvider;
-                    if (timeProvider != null) {
-                        timeProvider.onPaused(msg.what == MEDIA_PAUSED);
-                    }
+            {
+                TimeProvider timeProvider = mTimeProvider;
+                if (timeProvider != null) {
+                    timeProvider.onPaused(msg.what == MEDIA_PAUSED);
                 }
                 break;
+            }
 
             case MEDIA_BUFFERING_UPDATE:
-                if (eventCb != null && eventExec != null) {
-                    final int percent = msg.arg1;
-                    eventExec.execute(() -> eventCb.onBufferingUpdate(mMediaPlayer, 0, percent));
+            {
+                final int percent = msg.arg1;
+                synchronized (mEventCbLock) {
+                    if (srcId == mCurrentSrcId) {
+                        mBufferedPercentageCurrent.set(percent);
+                        for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                            cb.first.execute(() -> cb.second.onInfo(
+                                    mMediaPlayer, mCurrentDSD, MEDIA_INFO_BUFFERING_UPDATE,
+                                    percent));
+                        }
+                    } else if (srcId == mNextSrcId && !mNextDSDs.isEmpty()) {
+                        mBufferedPercentageNext.set(percent);
+                        DataSourceDesc nextDSD = mNextDSDs.get(0);
+                        for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                            cb.first.execute(() -> cb.second.onInfo(
+                                    mMediaPlayer, nextDSD, MEDIA_INFO_BUFFERING_UPDATE,
+                                    percent));
+                        }
+                    }
                 }
                 return;
+            }
 
             case MEDIA_SEEK_COMPLETE:
-                if (eventCb != null && eventExec != null) {
-                    eventExec.execute(() -> eventCb.onInfo(
-                            mMediaPlayer, 0, MEDIA_INFO_COMPLETE_CALL_SEEK, 0));
+            {
+                synchronized (mTaskLock) {
+                    if (mCurrentTask != null
+                            && mCurrentTask.mMediaCallType == CALL_COMPLETED_SEEK_TO
+                            && mCurrentTask.mNeedToWaitForEventToComplete) {
+                        mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
+                        mCurrentTask = null;
+                        processPendingTask_l();
+                    }
                 }
+            }
                 // fall through
 
             case MEDIA_SKIPPED:
-                {
-                    TimeProvider timeProvider = mTimeProvider;
-                    if (timeProvider != null) {
-                        timeProvider.onSeekComplete(mMediaPlayer);
+            {
+                TimeProvider timeProvider = mTimeProvider;
+                if (timeProvider != null) {
+                    timeProvider.onSeekComplete(mMediaPlayer);
+                }
+                return;
+            }
+
+            case MEDIA_SET_VIDEO_SIZE:
+            {
+                final int width = msg.arg1;
+                final int height = msg.arg2;
+                synchronized (mEventCbLock) {
+                    for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                        cb.first.execute(() -> cb.second.onVideoSizeChanged(
+                                mMediaPlayer, mCurrentDSD, width, height));
                     }
                 }
                 return;
-
-            case MEDIA_SET_VIDEO_SIZE:
-                if (eventCb != null && eventExec != null) {
-                    final int width = msg.arg1;
-                    final int height = msg.arg2;
-                    eventExec.execute(() -> eventCb.onVideoSizeChanged(
-                            mMediaPlayer, 0, width, height));
-                }
-                return;
+            }
 
             case MEDIA_ERROR:
+            {
                 Log.e(TAG, "Error (" + msg.arg1 + "," + msg.arg2 + ")");
-                if (eventCb != null && eventExec != null) {
-                    final int what = msg.arg1;
-                    final int extra = msg.arg2;
-                    eventExec.execute(() -> eventCb.onError(mMediaPlayer, 0, what, extra));
-                    eventExec.execute(() -> eventCb.onInfo(
-                            mMediaPlayer, 0, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+                synchronized (mEventCbLock) {
+                    for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                        cb.first.execute(() -> cb.second.onError(
+                                mMediaPlayer, mCurrentDSD, what, extra));
+                        cb.first.execute(() -> cb.second.onInfo(
+                                mMediaPlayer, mCurrentDSD, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+                    }
                 }
                 stayAwake(false);
                 return;
+            }
 
             case MEDIA_INFO:
+            {
                 switch (msg.arg1) {
-                case MEDIA_INFO_VIDEO_TRACK_LAGGING:
-                    Log.i(TAG, "Info (" + msg.arg1 + "," + msg.arg2 + ")");
-                    break;
-                case MEDIA_INFO_METADATA_UPDATE:
-                    try {
-                        scanInternalSubtitleTracks();
-                    } catch (RuntimeException e) {
-                        Message msg2 = obtainMessage(
-                                MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
-                        sendMessage(msg2);
-                    }
-                    // fall through
+                    case MEDIA_INFO_STARTED_AS_NEXT:
+                        if (srcId == mCurrentSrcId) {
+                            prepareNextDataSource_l();
+                        }
+                        break;
 
-                case MEDIA_INFO_EXTERNAL_METADATA_UPDATE:
-                    msg.arg1 = MEDIA_INFO_METADATA_UPDATE;
-                    // update default track selection
-                    if (mSubtitleController != null) {
-                        mSubtitleController.selectDefaultTrack();
-                    }
-                    break;
-                case MEDIA_INFO_BUFFERING_START:
-                case MEDIA_INFO_BUFFERING_END:
-                    TimeProvider timeProvider = mTimeProvider;
-                    if (timeProvider != null) {
-                        timeProvider.onBuffering(msg.arg1 == MEDIA_INFO_BUFFERING_START);
-                    }
-                    break;
+                    case MEDIA_INFO_VIDEO_TRACK_LAGGING:
+                        Log.i(TAG, "Info (" + msg.arg1 + "," + msg.arg2 + ")");
+                        break;
+
+                    case MEDIA_INFO_METADATA_UPDATE:
+                        try {
+                            scanInternalSubtitleTracks();
+                        } catch (RuntimeException e) {
+                            Message msg2 = obtainMessage(
+                                    MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED,
+                                    null);
+                            sendMessage(msg2);
+                        }
+                        // fall through
+
+                    case MEDIA_INFO_EXTERNAL_METADATA_UPDATE:
+                        msg.arg1 = MEDIA_INFO_METADATA_UPDATE;
+                        // update default track selection
+                        if (mSubtitleController != null) {
+                            mSubtitleController.selectDefaultTrack();
+                        }
+                        break;
+
+                    case MEDIA_INFO_BUFFERING_START:
+                    case MEDIA_INFO_BUFFERING_END:
+                        TimeProvider timeProvider = mTimeProvider;
+                        if (timeProvider != null) {
+                            timeProvider.onBuffering(msg.arg1 == MEDIA_INFO_BUFFERING_START);
+                        }
+                        break;
                 }
 
-                if (eventCb != null && eventExec != null) {
-                    final int what = msg.arg1;
-                    final int extra = msg.arg2;
-                    eventExec.execute(() -> eventCb.onInfo(mMediaPlayer, 0, what, extra));
+                synchronized (mEventCbLock) {
+                    for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                        cb.first.execute(() -> cb.second.onInfo(
+                                mMediaPlayer, mCurrentDSD, what, extra));
+                    }
                 }
                 // No real default action so far.
                 return;
+            }
 
             case MEDIA_NOTIFY_TIME:
-                    TimeProvider timeProvider = mTimeProvider;
-                    if (timeProvider != null) {
-                        timeProvider.onNotifyTime();
-                    }
+            {
+                TimeProvider timeProvider = mTimeProvider;
+                if (timeProvider != null) {
+                    timeProvider.onNotifyTime();
+                }
                 return;
+            }
 
             case MEDIA_TIMED_TEXT:
-                if (eventCb == null || eventExec == null) {
-                    return;
-                }
-                if (msg.obj == null) {
-                    eventExec.execute(() -> eventCb.onTimedText(mMediaPlayer, 0, null));
+            {
+                final TimedText text;
+                if (msg.obj instanceof Parcel) {
+                    Parcel parcel = (Parcel)msg.obj;
+                    text = new TimedText(parcel);
+                    parcel.recycle();
                 } else {
-                    if (msg.obj instanceof Parcel) {
-                        Parcel parcel = (Parcel)msg.obj;
-                        TimedText text = new TimedText(parcel);
-                        parcel.recycle();
-                        eventExec.execute(() -> eventCb.onTimedText(mMediaPlayer, 0, text));
+                    text = null;
+                }
+
+                synchronized (mEventCbLock) {
+                    for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                        cb.first.execute(() -> cb.second.onTimedText(mMediaPlayer, mCurrentDSD, text));
                     }
                 }
                 return;
+            }
 
             case MEDIA_SUBTITLE_DATA:
+            {
                 OnSubtitleDataListener onSubtitleDataListener = mOnSubtitleDataListener;
                 if (onSubtitleDataListener == null) {
                     return;
@@ -3322,24 +3077,35 @@
                     onSubtitleDataListener.onSubtitleData(mMediaPlayer, data);
                 }
                 return;
+            }
 
             case MEDIA_META_DATA:
-                if (eventCb == null || eventExec == null) {
-                    return;
-                }
+            {
+                final TimedMetaData data;
                 if (msg.obj instanceof Parcel) {
                     Parcel parcel = (Parcel) msg.obj;
-                    TimedMetaData data = TimedMetaData.createTimedMetaDataFromParcel(parcel);
+                    data = TimedMetaData.createTimedMetaDataFromParcel(parcel);
                     parcel.recycle();
-                    eventExec.execute(() -> eventCb.onTimedMetaDataAvailable(
-                            mMediaPlayer, 0, data));
+                } else {
+                    data = null;
+                }
+
+                synchronized (mEventCbLock) {
+                    for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                        cb.first.execute(() -> cb.second.onTimedMetaDataAvailable(
+                                mMediaPlayer, mCurrentDSD, data));
+                    }
                 }
                 return;
+            }
 
             case MEDIA_NOP: // interface test message - ignore
+            {
                 break;
+            }
 
             case MEDIA_AUDIO_ROUTING_CHANGED:
+            {
                 AudioManager.resetAudioPortGeneration();
                 synchronized (mRoutingChangeListeners) {
                     for (NativeRoutingEventHandlerDelegate delegate
@@ -3348,11 +3114,14 @@
                     }
                 }
                 return;
+            }
 
             default:
+            {
                 Log.e(TAG, "Unknown message type " + msg.what);
                 return;
             }
+            }
         }
     }
 
@@ -3363,7 +3132,7 @@
      * code is safe from the object disappearing from underneath it.  (This is
      * the cookie passed to native_setup().)
      */
-    private static void postEventFromNative(Object mediaplayer2_ref,
+    private static void postEventFromNative(Object mediaplayer2_ref, long srcId,
                                             int what, int arg1, int arg2, Object obj)
     {
         final MediaPlayer2Impl mp = (MediaPlayer2Impl)((WeakReference)mediaplayer2_ref).get();
@@ -3404,7 +3173,7 @@
 
         case MEDIA_PREPARED:
             // By this time, we've learned about DrmInfo's presence or absence. This is meant
-            // mainly for prepareAsync() use case. For prepare(), this still can run to a race
+            // mainly for prepare() use case. For prepare(), this still can run to a race
             // condition b/c MediaPlayerNative releases the prepare() lock before calling notify
             // so we also set mDrmInfoResolved in prepare().
             synchronized (mp.mDrmLock) {
@@ -3416,13 +3185,19 @@
 
         if (mp.mEventHandler != null) {
             Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj);
-            mp.mEventHandler.sendMessage(m);
+
+            mp.mEventHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    mp.mEventHandler.handleMessage(m, srcId);
+                }
+            });
         }
     }
 
-    private Executor mEventExec;
-    private EventCallback mEventCb;
     private final Object mEventCbLock = new Object();
+    private ArrayList<Pair<Executor, MediaPlayer2EventCallback> > mEventCallbackRecords
+        = new ArrayList<Pair<Executor, MediaPlayer2EventCallback> >();
 
     /**
      * Register a callback to be invoked when the media source is ready
@@ -3432,33 +3207,27 @@
      * @param executor the executor through which the callback should be invoked
      */
     @Override
-    public void registerEventCallback(@NonNull @CallbackExecutor Executor executor,
-            @NonNull EventCallback eventCallback) {
+    public void setMediaPlayer2EventCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull MediaPlayer2EventCallback eventCallback) {
         if (eventCallback == null) {
-            throw new IllegalArgumentException("Illegal null EventCallback");
+            throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
         }
         if (executor == null) {
-            throw new IllegalArgumentException("Illegal null Executor for the EventCallback");
+            throw new IllegalArgumentException(
+                    "Illegal null Executor for the MediaPlayer2EventCallback");
         }
         synchronized (mEventCbLock) {
-            // TODO: support multiple callbacks.
-            mEventExec = executor;
-            mEventCb = eventCallback;
+            mEventCallbackRecords.add(new Pair(executor, eventCallback));
         }
     }
 
     /**
-     * Unregisters an {@link EventCallback}.
-     *
-     * @param callback an {@link EventCallback} to unregister
+     * Clears the {@link MediaPlayer2EventCallback}.
      */
     @Override
-    public void unregisterEventCallback(EventCallback callback) {
+    public void clearMediaPlayer2EventCallback() {
         synchronized (mEventCbLock) {
-            if (callback == mEventCb) {
-                mEventExec = null;
-                mEventCb = null;
-            }
+            mEventCallbackRecords.clear();
         }
     }
 
@@ -3497,9 +3266,9 @@
 
     private OnDrmConfigHelper mOnDrmConfigHelper;
 
-    private Executor mDrmEventExec;
-    private DrmEventCallback mDrmEventCb;
     private final Object mDrmEventCbLock = new Object();
+    private ArrayList<Pair<Executor, DrmEventCallback> > mDrmEventCallbackRecords
+        = new ArrayList<Pair<Executor, DrmEventCallback> >();
 
     /**
      * Register a callback to be invoked when the media source is ready
@@ -3509,33 +3278,27 @@
      * @param executor the executor through which the callback should be invoked
      */
     @Override
-    public void registerDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
+    public void setDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
             @NonNull DrmEventCallback eventCallback) {
         if (eventCallback == null) {
-            throw new IllegalArgumentException("Illegal null EventCallback");
+            throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
         }
         if (executor == null) {
-            throw new IllegalArgumentException("Illegal null Executor for the EventCallback");
+            throw new IllegalArgumentException(
+                    "Illegal null Executor for the MediaPlayer2EventCallback");
         }
         synchronized (mDrmEventCbLock) {
-            // TODO: support multiple callbacks.
-            mDrmEventExec = executor;
-            mDrmEventCb = eventCallback;
+            mDrmEventCallbackRecords.add(new Pair(executor, eventCallback));
         }
     }
 
     /**
-     * Unregisters a {@link DrmEventCallback}.
-     *
-     * @param callback a {@link DrmEventCallback} to unregister
+     * Clears the {@link DrmEventCallback}.
      */
     @Override
-    public void unregisterDrmEventCallback(DrmEventCallback callback) {
+    public void clearDrmEventCallback() {
         synchronized (mDrmEventCbLock) {
-            if (callback == mDrmEventCb) {
-                mDrmEventExec = null;
-                mDrmEventCb = null;
-            }
+            mDrmEventCallbackRecords.clear();
         }
     }
 
@@ -3662,7 +3425,7 @@
 
         // call the callback outside the lock
         if (mOnDrmConfigHelper != null)  {
-            mOnDrmConfigHelper.onDrmConfig(this);
+            mOnDrmConfigHelper.onDrmConfig(this, mCurrentDSD);
         }
 
         synchronized (mDrmLock) {
@@ -3733,15 +3496,11 @@
 
         // if finished successfully without provisioning, call the callback outside the lock
         if (allDoneWithoutProvisioning) {
-            final Executor drmEventExec;
-            final DrmEventCallback drmEventCb;
             synchronized (mDrmEventCbLock) {
-                drmEventExec = mDrmEventExec;
-                drmEventCb = mDrmEventCb;
-            }
-            if (drmEventExec != null && drmEventCb != null) {
-                drmEventExec.execute(() -> drmEventCb.onDrmPrepared(
-                    this, PREPARE_DRM_STATUS_SUCCESS));
+                for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) {
+                    cb.first.execute(() -> cb.second.onDrmPrepared(
+                            this, mCurrentDSD, PREPARE_DRM_STATUS_SUCCESS));
+                }
             }
         }
 
@@ -3763,32 +3522,39 @@
     public void releaseDrm()
             throws NoDrmSchemeException
     {
-        Log.v(TAG, "releaseDrm:");
+        addTask(new Task(CALL_COMPLETED_RELEASE_DRM, false) {
+            @Override
+            void process() throws NoDrmSchemeException {
+                synchronized (mDrmLock) {
+                    Log.v(TAG, "releaseDrm:");
 
-        synchronized (mDrmLock) {
-            if (!mActiveDrmScheme) {
-                Log.e(TAG, "releaseDrm(): No active DRM scheme to release.");
-                throw new NoDrmSchemeExceptionImpl("releaseDrm: No active DRM scheme to release.");
+                    if (!mActiveDrmScheme) {
+                        Log.e(TAG, "releaseDrm(): No active DRM scheme to release.");
+                        throw new NoDrmSchemeExceptionImpl(
+                                "releaseDrm: No active DRM scheme to release.");
+                    }
+
+                    try {
+                        // we don't have the player's state in this layer. The below call raises
+                        // exception if we're in a non-stopped/prepared state.
+
+                        // for cleaning native/mediaserver crypto object
+                        _releaseDrm();
+
+                        // for cleaning client-side MediaDrm object; only called if above has succeeded
+                        cleanDrmObj();
+
+                        mActiveDrmScheme = false;
+                    } catch (IllegalStateException e) {
+                        Log.w(TAG, "releaseDrm: Exception ", e);
+                        throw new IllegalStateException(
+                                "releaseDrm: The player is not in a valid state.");
+                    } catch (Exception e) {
+                        Log.e(TAG, "releaseDrm: Exception ", e);
+                    }
+                }   // synchronized
             }
-
-            try {
-                // we don't have the player's state in this layer. The below call raises
-                // exception if we're in a non-stopped/prepared state.
-
-                // for cleaning native/mediaserver crypto object
-                _releaseDrm();
-
-                // for cleaning client-side MediaDrm object; only called if above has succeeded
-                cleanDrmObj();
-
-                mActiveDrmScheme = false;
-            } catch (IllegalStateException e) {
-                Log.w(TAG, "releaseDrm: Exception ", e);
-                throw new IllegalStateException("releaseDrm: The player is not in a valid state.");
-            } catch (Exception e) {
-                Log.e(TAG, "releaseDrm: Exception ", e);
-            }
-        }   // synchronized
+        });
     }
 
 
@@ -3796,14 +3562,14 @@
      * A key request/response exchange occurs between the app and a license server
      * to obtain or release keys used to decrypt encrypted content.
      * <p>
-     * getKeyRequest() is used to obtain an opaque key request byte array that is
+     * getDrmKeyRequest() is used to obtain an opaque key request byte array that is
      * delivered to the license server.  The opaque key request byte array is returned
      * in KeyRequest.data.  The recommended URL to deliver the key request to is
      * returned in KeyRequest.defaultUrl.
      * <p>
      * After the app has received the key request response from the server,
      * it should deliver to the response to the DRM engine plugin using the method
-     * {@link #provideKeyResponse}.
+     * {@link #provideDrmKeyResponse}.
      *
      * @param keySetId is the key-set identifier of the offline keys being released when keyType is
      * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
@@ -3831,19 +3597,20 @@
      */
     @Override
     @NonNull
-    public MediaDrm.KeyRequest getKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
+    public MediaDrm.KeyRequest getDrmKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
             @Nullable String mimeType, @MediaDrm.KeyType int keyType,
             @Nullable Map<String, String> optionalParameters)
             throws NoDrmSchemeException
     {
-        Log.v(TAG, "getKeyRequest: " +
+        Log.v(TAG, "getDrmKeyRequest: " +
                 " keySetId: " + keySetId + " initData:" + initData + " mimeType: " + mimeType +
                 " keyType: " + keyType + " optionalParameters: " + optionalParameters);
 
         synchronized (mDrmLock) {
             if (!mActiveDrmScheme) {
-                Log.e(TAG, "getKeyRequest NoDrmSchemeException");
-                throw new NoDrmSchemeExceptionImpl("getKeyRequest: Has to set a DRM scheme first.");
+                Log.e(TAG, "getDrmKeyRequest NoDrmSchemeException");
+                throw new NoDrmSchemeExceptionImpl(
+                        "getDrmKeyRequest: Has to set a DRM scheme first.");
             }
 
             try {
@@ -3858,16 +3625,16 @@
 
                 MediaDrm.KeyRequest request = mDrmObj.getKeyRequest(scope, initData, mimeType,
                                                               keyType, hmapOptionalParameters);
-                Log.v(TAG, "getKeyRequest:   --> request: " + request);
+                Log.v(TAG, "getDrmKeyRequest:   --> request: " + request);
 
                 return request;
 
             } catch (NotProvisionedException e) {
-                Log.w(TAG, "getKeyRequest NotProvisionedException: " +
+                Log.w(TAG, "getDrmKeyRequest NotProvisionedException: " +
                         "Unexpected. Shouldn't have reached here.");
-                throw new IllegalStateException("getKeyRequest: Unexpected provisioning error.");
+                throw new IllegalStateException("getDrmKeyRequest: Unexpected provisioning error.");
             } catch (Exception e) {
-                Log.w(TAG, "getKeyRequest Exception " + e);
+                Log.w(TAG, "getDrmKeyRequest Exception " + e);
                 throw e;
             }
 
@@ -3877,15 +3644,15 @@
 
     /**
      * A key response is received from the license server by the app, then it is
-     * provided to the DRM engine plugin using provideKeyResponse. When the
+     * provided to the DRM engine plugin using provideDrmKeyResponse. When the
      * response is for an offline key request, a key-set identifier is returned that
      * can be used to later restore the keys to a new session with the method
-     * {@ link # restoreKeys}.
+     * {@ link # restoreDrmKeys}.
      * When the response is for a streaming or release request, null is returned.
      *
      * @param keySetId When the response is for a release request, keySetId identifies
      * the saved key associated with the release request (i.e., the same keySetId
-     * passed to the earlier {@ link # getKeyRequest} call. It MUST be null when the
+     * passed to the earlier {@ link #getDrmKeyRequest} call. It MUST be null when the
      * response is for either streaming or offline key requests.
      *
      * @param response the byte array response from the server
@@ -3895,16 +3662,17 @@
      * server rejected the request
      */
     @Override
-    public byte[] provideKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
+    public byte[] provideDrmKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
             throws NoDrmSchemeException, DeniedByServerException
     {
-        Log.v(TAG, "provideKeyResponse: keySetId: " + keySetId + " response: " + response);
+        Log.v(TAG, "provideDrmKeyResponse: keySetId: " + keySetId + " response: " + response);
 
         synchronized (mDrmLock) {
 
             if (!mActiveDrmScheme) {
-                Log.e(TAG, "getKeyRequest NoDrmSchemeException");
-                throw new NoDrmSchemeExceptionImpl("getKeyRequest: Has to set a DRM scheme first.");
+                Log.e(TAG, "getDrmKeyRequest NoDrmSchemeException");
+                throw new NoDrmSchemeExceptionImpl(
+                        "getDrmKeyRequest: Has to set a DRM scheme first.");
             }
 
             try {
@@ -3914,19 +3682,19 @@
 
                 byte[] keySetResult = mDrmObj.provideKeyResponse(scope, response);
 
-                Log.v(TAG, "provideKeyResponse: keySetId: " + keySetId + " response: " + response +
-                        " --> " + keySetResult);
+                Log.v(TAG, "provideDrmKeyResponse: keySetId: " + keySetId + " response: " + response
+                        + " --> " + keySetResult);
 
 
                 return keySetResult;
 
             } catch (NotProvisionedException e) {
-                Log.w(TAG, "provideKeyResponse NotProvisionedException: " +
+                Log.w(TAG, "provideDrmKeyResponse NotProvisionedException: " +
                         "Unexpected. Shouldn't have reached here.");
-                throw new IllegalStateException("provideKeyResponse: " +
+                throw new IllegalStateException("provideDrmKeyResponse: " +
                         "Unexpected provisioning error.");
             } catch (Exception e) {
-                Log.w(TAG, "provideKeyResponse Exception " + e);
+                Log.w(TAG, "provideDrmKeyResponse Exception " + e);
                 throw e;
             }
         }   // synchronized
@@ -3935,31 +3703,37 @@
 
     /**
      * Restore persisted offline keys into a new session.  keySetId identifies the
-     * keys to load, obtained from a prior call to {@link #provideKeyResponse}.
+     * keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
      *
      * @param keySetId identifies the saved key set to restore
      */
     @Override
-    public void restoreKeys(@NonNull byte[] keySetId)
+    public void restoreDrmKeys(@NonNull byte[] keySetId)
             throws NoDrmSchemeException
     {
-        Log.v(TAG, "restoreKeys: keySetId: " + keySetId);
+        addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
+            @Override
+            void process() throws NoDrmSchemeException {
+                Log.v(TAG, "restoreDrmKeys: keySetId: " + keySetId);
 
-        synchronized (mDrmLock) {
+                synchronized (mDrmLock) {
 
-            if (!mActiveDrmScheme) {
-                Log.w(TAG, "restoreKeys NoDrmSchemeException");
-                throw new NoDrmSchemeExceptionImpl("restoreKeys: Has to set a DRM scheme first.");
+                    if (!mActiveDrmScheme) {
+                        Log.w(TAG, "restoreDrmKeys NoDrmSchemeException");
+                        throw new NoDrmSchemeExceptionImpl(
+                                "restoreDrmKeys: Has to set a DRM scheme first.");
+                    }
+
+                    try {
+                        mDrmObj.restoreKeys(mDrmSessionId, keySetId);
+                    } catch (Exception e) {
+                        Log.w(TAG, "restoreKeys Exception " + e);
+                        throw e;
+                    }
+
+                }   // synchronized
             }
-
-            try {
-                mDrmObj.restoreKeys(mDrmSessionId, keySetId);
-            } catch (Exception e) {
-                Log.w(TAG, "restoreKeys Exception " + e);
-                throw e;
-            }
-
-        }   // synchronized
+        });
     }
 
 
@@ -3984,7 +3758,8 @@
 
             if (!mActiveDrmScheme && !mDrmConfigAllowed) {
                 Log.w(TAG, "getDrmPropertyString NoDrmSchemeException");
-                throw new NoDrmSchemeExceptionImpl("getDrmPropertyString: Has to prepareDrm() first.");
+                throw new NoDrmSchemeExceptionImpl(
+                        "getDrmPropertyString: Has to prepareDrm() first.");
             }
 
             try {
@@ -4022,7 +3797,8 @@
 
             if ( !mActiveDrmScheme && !mDrmConfigAllowed ) {
                 Log.w(TAG, "setDrmPropertyString NoDrmSchemeException");
-                throw new NoDrmSchemeExceptionImpl("setDrmPropertyString: Has to prepareDrm() first.");
+                throw new NoDrmSchemeExceptionImpl(
+                        "setDrmPropertyString: Has to prepareDrm() first.");
             }
 
             try {
@@ -4233,7 +4009,7 @@
 
         // TODO: don't need an open session for a future specialKeyReleaseDrm mode but we should do
         // it anyway so it raises provisioning error if needed. We'd rather handle provisioning
-        // at prepareDrm/openSession rather than getKeyRequest/provideKeyResponse
+        // at prepareDrm/openSession rather than getDrmKeyRequest/provideDrmKeyResponse
         try {
             mDrmSessionId = mDrmObj.openSession();
             Log.v(TAG, "prepareDrm_openSessionStep: mDrmSessionId=" + mDrmSessionId);
@@ -4250,6 +4026,65 @@
 
     }
 
+    // Called from the native side
+    @SuppressWarnings("unused")
+    private static boolean setAudioOutputDeviceById(AudioTrack track, int deviceId) {
+        if (track == null) {
+            return false;
+        }
+
+        if (deviceId == 0) {
+            // Use default routing.
+            track.setPreferredDevice(null);
+            return true;
+        }
+
+        // TODO: Unhide AudioManager.getDevicesStatic.
+        AudioDeviceInfo[] outputDevices =
+                AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS);
+
+        boolean success = false;
+        for (AudioDeviceInfo device : outputDevices) {
+            if (device.getId() == deviceId) {
+                track.setPreferredDevice(device);
+                success = true;
+                break;
+            }
+        }
+        return success;
+    }
+
+    // Instantiated from the native side
+    @SuppressWarnings("unused")
+    private static class StreamEventCallback extends AudioTrack.StreamEventCallback {
+        public long mJAudioTrackPtr;
+        public long mNativeCallbackPtr;
+        public long mUserDataPtr;
+
+        public StreamEventCallback(long jAudioTrackPtr, long nativeCallbackPtr, long userDataPtr) {
+            super();
+            mJAudioTrackPtr = jAudioTrackPtr;
+            mNativeCallbackPtr = nativeCallbackPtr;
+            mUserDataPtr = userDataPtr;
+        }
+
+        @Override
+        public void onTearDown(AudioTrack track) {
+            native_stream_event_onTearDown(mNativeCallbackPtr, mUserDataPtr);
+        }
+
+        @Override
+        public void onStreamPresentationEnd(AudioTrack track) {
+            native_stream_event_onStreamPresentationEnd(mNativeCallbackPtr, mUserDataPtr);
+        }
+
+        @Override
+        public void onStreamDataRequest(AudioTrack track) {
+            native_stream_event_onStreamDataRequest(
+                    mJAudioTrackPtr, mNativeCallbackPtr, mUserDataPtr);
+        }
+    }
+
     private class ProvisioningThread extends Thread {
         public static final int TIMEOUT_MS = 60000;
 
@@ -4324,14 +4159,12 @@
 
             boolean succeeded = false;
 
-            final Executor drmEventExec;
-            final DrmEventCallback drmEventCb;
+            boolean hasCallback = false;
             synchronized (mDrmEventCbLock) {
-                drmEventExec = mDrmEventExec;
-                drmEventCb = mDrmEventCb;
+                hasCallback = !mDrmEventCallbackRecords.isEmpty();
             }
             // non-blocking mode needs the lock
-            if (drmEventExec != null && drmEventCb != null) {
+            if (hasCallback) {
 
                 synchronized (drmLock) {
                     // continuing with prepareDrm
@@ -4349,7 +4182,12 @@
                 } // synchronized
 
                 // calling the callback outside the lock
-                drmEventExec.execute(() -> drmEventCb.onDrmPrepared(mediaPlayer, status));
+                synchronized (mDrmEventCbLock) {
+                    for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) {
+                        cb.first.execute(() -> cb.second.onDrmPrepared(
+                                mediaPlayer, mCurrentDSD, status));
+                    }
+                }
             } else {   // blocking mode already has the lock
 
                 // continuing with prepareDrm
@@ -4397,13 +4235,11 @@
         int result;
 
         // non-blocking: this is not the final result
-        final Executor drmEventExec;
-        final DrmEventCallback drmEventCb;
+        boolean hasCallback = false;
         synchronized (mDrmEventCbLock) {
-            drmEventExec = mDrmEventExec;
-            drmEventCb = mDrmEventCb;
+            hasCallback = !mDrmEventCallbackRecords.isEmpty();
         }
-        if (drmEventCb != null && drmEventExec != null) {
+        if (hasCallback) {
             result = PREPARE_DRM_STATUS_SUCCESS;
         } else {
             // if blocking mode, wait till provisioning is done
@@ -4523,7 +4359,7 @@
         // no need for log(N) search performance
         private MediaTimeProvider.OnMediaTimeListener mListeners[];
         private long mTimes[];
-        private Handler mEventHandler;
+        private EventHandler mEventHandler;
         private boolean mRefresh = false;
         private boolean mPausing = false;
         private boolean mSeeking = false;
@@ -4896,4 +4732,65 @@
             }
         }
     }
+
+    private abstract class Task implements Runnable {
+        private final int mMediaCallType;
+        private final boolean mNeedToWaitForEventToComplete;
+        private DataSourceDesc mDSD;
+
+        public Task (int mediaCallType, boolean needToWaitForEventToComplete) {
+            mMediaCallType = mediaCallType;
+            mNeedToWaitForEventToComplete = needToWaitForEventToComplete;
+        }
+
+        abstract void process() throws IOException, NoDrmSchemeException;
+
+        @Override
+        public void run() {
+            int status = CALL_STATUS_NO_ERROR;
+            try {
+                process();
+            } catch (IllegalStateException e) {
+                status = CALL_STATUS_INVALID_OPERATION;
+            } catch (IllegalArgumentException e) {
+                status = CALL_STATUS_BAD_VALUE;
+            } catch (SecurityException e) {
+                status = CALL_STATUS_PERMISSION_DENIED;
+            } catch (IOException e) {
+                status = CALL_STATUS_ERROR_IO;
+            } catch (NoDrmSchemeException e) {
+                status = CALL_STATUS_NO_DRM_SCHEME;
+            } catch (Exception e) {
+                status = CALL_STATUS_ERROR_UNKNOWN;
+            }
+            synchronized (mSrcLock) {
+                mDSD = mCurrentDSD;
+            }
+
+            // TODO: Make native implementations asynchronous and let them send notifications.
+            if (!mNeedToWaitForEventToComplete || status != CALL_STATUS_NO_ERROR) {
+
+                sendCompleteNotification(status);
+
+                synchronized (mTaskLock) {
+                    mCurrentTask = null;
+                    processPendingTask_l();
+                }
+            }
+        }
+
+        private void sendCompleteNotification(int status) {
+            // In {@link #notifyWhenCommandLabelReached} case, a separate callback
+            // {#link #onCommandLabelReached} is already called in {@code process()}.
+            if (mMediaCallType == CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED) {
+                return;
+            }
+            synchronized (mEventCbLock) {
+                for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
+                    cb.first.execute(() -> cb.second.onCallCompleted(
+                            MediaPlayer2Impl.this, mDSD, mMediaCallType, status));
+                }
+            }
+        }
+    };
 }
diff --git a/android/media/MediaPlayerBase.java b/android/media/MediaPlayerBase.java
index d638a9f..a426552 100644
--- a/android/media/MediaPlayerBase.java
+++ b/android/media/MediaPlayerBase.java
@@ -16,57 +16,316 @@
 
 package android.media;
 
-import android.media.MediaSession2.PlaylistParam;
-import android.media.session.PlaybackState;
-import android.os.Handler;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
- * Base interfaces for all media players that want media session.
- *
  * @hide
+ * Base class for all media players that want media session.
  */
-public abstract class MediaPlayerBase {
+public abstract class MediaPlayerBase implements AutoCloseable {
     /**
-     * Listens change in {@link PlaybackState2}.
+     * @hide
      */
-    public interface PlaybackListener {
+    @IntDef({
+        PLAYER_STATE_IDLE,
+        PLAYER_STATE_PAUSED,
+        PLAYER_STATE_PLAYING,
+        PLAYER_STATE_ERROR })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PlayerState {}
+
+    /**
+     * @hide
+     */
+    @IntDef({
+        BUFFERING_STATE_UNKNOWN,
+        BUFFERING_STATE_BUFFERING_AND_PLAYABLE,
+        BUFFERING_STATE_BUFFERING_AND_STARVED,
+        BUFFERING_STATE_BUFFERING_COMPLETE })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface BuffState {}
+
+    /**
+     * State when the player is idle, and needs configuration to start playback.
+     */
+    public static final int PLAYER_STATE_IDLE = 0;
+
+    /**
+     * State when the player's playback is paused
+     */
+    public static final int PLAYER_STATE_PAUSED = 1;
+
+    /**
+     * State when the player's playback is ongoing
+     */
+    public static final int PLAYER_STATE_PLAYING = 2;
+
+    /**
+     * State when the player is in error state and cannot be recovered self.
+     */
+    public static final int PLAYER_STATE_ERROR = 3;
+
+    /**
+     * Buffering state is unknown.
+     */
+    public static final int BUFFERING_STATE_UNKNOWN = 0;
+
+    /**
+     * Buffering state indicating the player is buffering but enough has been buffered
+     * for this player to be able to play the content.
+     * See {@link #getBufferedPosition()} for how far is buffered already.
+     */
+    public static final int BUFFERING_STATE_BUFFERING_AND_PLAYABLE = 1;
+
+    /**
+     * Buffering state indicating the player is buffering, but the player is currently starved
+     * for data, and cannot play.
+     */
+    public static final int BUFFERING_STATE_BUFFERING_AND_STARVED = 2;
+
+    /**
+     * Buffering state indicating the player is done buffering, and the remainder of the content is
+     * available for playback.
+     */
+    public static final int BUFFERING_STATE_BUFFERING_COMPLETE = 3;
+
+    /**
+     * Starts or resumes playback.
+     */
+    public abstract void play();
+
+    /**
+     * Prepares the player for playback.
+     * See {@link PlayerEventCallback#onMediaPrepared(MediaPlayerBase, DataSourceDesc)} for being
+     * notified when the preparation phase completed. During this time, the player may allocate
+     * resources required to play, such as audio and video decoders.
+     */
+    public abstract void prepare();
+
+    /**
+     * Pauses playback.
+     */
+    public abstract void pause();
+
+    /**
+     * Resets the MediaPlayerBase to its uninitialized state.
+     */
+    public abstract void reset();
+
+    /**
+     *
+     */
+    public abstract void skipToNext();
+
+    /**
+     * Moves the playback head to the specified position
+     * @param pos the new playback position expressed in ms.
+     */
+    public abstract void seekTo(long pos);
+
+    public static final long UNKNOWN_TIME = -1;
+
+    /**
+     * Gets the current playback head position.
+     * @return the current playback position in ms, or {@link #UNKNOWN_TIME} if unknown.
+     */
+    public long getCurrentPosition() { return UNKNOWN_TIME; }
+
+    /**
+     * Returns the duration of the current data source, or {@link #UNKNOWN_TIME} if unknown.
+     * @return the duration in ms, or {@link #UNKNOWN_TIME}.
+     */
+    public long getDuration() { return UNKNOWN_TIME; }
+
+    /**
+     * Gets the buffered position of current playback, or {@link #UNKNOWN_TIME} if unknown.
+     * @return the buffered position in ms, or {@link #UNKNOWN_TIME}.
+     */
+    public long getBufferedPosition() { return UNKNOWN_TIME; }
+
+    /**
+     * Returns the current player state.
+     * See also {@link PlayerEventCallback#onPlayerStateChanged(MediaPlayerBase, int)} for
+     * notification of changes.
+     * @return the current player state
+     */
+    public abstract @PlayerState int getPlayerState();
+
+    /**
+     * Returns the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     * @return the buffering state.
+     */
+    public abstract @BuffState int getBufferingState();
+
+    /**
+     * Sets the {@link AudioAttributes} to be used during the playback of the media.
+     *
+     * @param attributes non-null <code>AudioAttributes</code>.
+     */
+    public abstract void setAudioAttributes(@NonNull AudioAttributes attributes);
+
+    /**
+     * Returns AudioAttributes that media player has.
+     */
+    public abstract @Nullable AudioAttributes getAudioAttributes();
+
+    /**
+     * Sets the data source to be played.
+     * @param dsd
+     */
+    public abstract void setDataSource(@NonNull DataSourceDesc dsd);
+
+    /**
+     * Sets the data source that will be played immediately after the current one is done playing.
+     * @param dsd
+     */
+    public abstract void setNextDataSource(@NonNull DataSourceDesc dsd);
+
+    /**
+     * Sets the list of data sources that will be sequentially played after the current one. Each
+     * data source is played immediately after the previous one is done playing.
+     * @param dsds
+     */
+    public abstract void setNextDataSources(@NonNull List<DataSourceDesc> dsds);
+
+    /**
+     * Returns the current data source.
+     * @return the current data source, or null if none is set, or none available to play.
+     */
+    public abstract @Nullable DataSourceDesc getCurrentDataSource();
+
+    /**
+     * Configures the player to loop on the current data source.
+     * @param loop true if the current data source is meant to loop.
+     */
+    public abstract void loopCurrent(boolean loop);
+
+    /**
+     * Sets the playback speed.
+     * A value of 1.0f is the default playback value.
+     * A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
+     * before using negative values.<br>
+     * After changing the playback speed, it is recommended to query the actual speed supported
+     * by the player, see {@link #getPlaybackSpeed()}.
+     * @param speed
+     */
+    public abstract void setPlaybackSpeed(float speed);
+
+    /**
+     * Returns the actual playback speed to be used by the player when playing.
+     * Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
+     * @return the actual playback speed
+     */
+    public float getPlaybackSpeed() { return 1.0f; }
+
+    /**
+     * Indicates whether reverse playback is supported.
+     * Reverse playback is indicated by negative playback speeds, see
+     * {@link #setPlaybackSpeed(float)}.
+     * @return true if reverse playback is supported.
+     */
+    public boolean isReversePlaybackSupported() { return false; }
+
+    /**
+     * Sets the volume of the audio of the media to play, expressed as a linear multiplier
+     * on the audio samples.
+     * Note that this volume is specific to the player, and is separate from stream volume
+     * used across the platform.<br>
+     * A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
+     * gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
+     * @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
+     */
+    public abstract void setPlayerVolume(float volume);
+
+    /**
+     * Returns the current volume of this player to this player.
+     * Note that it does not take into account the associated stream volume.
+     * @return the player volume.
+     */
+    public abstract float getPlayerVolume();
+
+    /**
+     * @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
+     */
+    public float getMaxPlayerVolume() { return 1.0f; }
+
+    /**
+     * Adds a callback to be notified of events for this player.
+     * @param e the {@link Executor} to be used for the events.
+     * @param cb the callback to receive the events.
+     */
+    public abstract void registerPlayerEventCallback(@NonNull Executor e,
+            @NonNull PlayerEventCallback cb);
+
+    /**
+     * Removes a previously registered callback for player events
+     * @param cb the callback to remove
+     */
+    public abstract void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb);
+
+    /**
+     * A callback class to receive notifications for events on the media player.
+     * See {@link MediaPlayerBase#registerPlayerEventCallback(Executor, PlayerEventCallback)} to
+     * register this callback.
+     */
+    public static abstract class PlayerEventCallback {
         /**
-         * Called when {@link PlaybackState2} for this player is changed.
+         * Called when the player's current data source has changed.
+         *
+         * @param mpb the player whose data source changed.
+         * @param dsd the new current data source. null, if no more data sources available.
          */
-        void onPlaybackChanged(PlaybackState2 state);
+        public void onCurrentDataSourceChanged(@NonNull MediaPlayerBase mpb,
+                @Nullable DataSourceDesc dsd) { }
+        /**
+         * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
+         * referenced by the given data source.
+         * @param mpb the player that is prepared.
+         * @param dsd the data source that the player is prepared to play.
+         */
+        public void onMediaPrepared(@NonNull MediaPlayerBase mpb, @NonNull DataSourceDesc dsd) { }
+
+        /**
+         * Called to indicate that the state of the player has changed.
+         * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+         * @param mpb the player whose state has changed.
+         * @param state the new state of the player.
+         */
+        public void onPlayerStateChanged(@NonNull MediaPlayerBase mpb, @PlayerState int state) { }
+
+        /**
+         * Called to report buffering events for a data source.
+         * @param mpb the player that is buffering
+         * @param dsd the data source for which buffering is happening.
+         * @param state the new buffering state.
+         */
+        public void onBufferingStateChanged(@NonNull MediaPlayerBase mpb,
+                @NonNull DataSourceDesc dsd, @BuffState int state) { }
+
+        /**
+         * Called to indicate that the playback speed has changed.
+         * @param mpb the player that has changed the playback speed.
+         * @param speed the new playback speed.
+         */
+        public void onPlaybackSpeedChanged(@NonNull MediaPlayerBase mpb, float speed) { }
+
+        /**
+         * Called to indicate that {@link #seekTo(long)} is completed.
+         *
+         * @param mpb the player that has completed seeking.
+         * @param position the previous seeking request.
+         * @see #seekTo(long)
+         */
+        public void onSeekCompleted(@NonNull MediaPlayerBase mpb, long position) { }
     }
 
-    public abstract void play();
-    public abstract void prepare();
-    public abstract void pause();
-    public abstract void stop();
-    public abstract void skipToPrevious();
-    public abstract void skipToNext();
-    public abstract void seekTo(long pos);
-    public abstract void fastFoward();
-    public abstract void rewind();
-
-    public abstract PlaybackState2 getPlaybackState();
-    public abstract AudioAttributes getAudioAttributes();
-
-    public abstract void setPlaylist(List<MediaItem2> item, PlaylistParam param);
-    public abstract void setCurrentPlaylistItem(int index);
-
-    /**
-     * Add a {@link PlaybackListener} to be invoked when the playback state is changed.
-     *
-     * @param executor the Handler that will receive the listener
-     * @param listener the listener that will be run
-     */
-    public abstract void addPlaybackListener(Executor executor, PlaybackListener listener);
-
-    /**
-     * Remove previously added {@link PlaybackListener}.
-     *
-     * @param listener the listener to be removed
-     */
-    public abstract void removePlaybackListener(PlaybackListener listener);
 }
diff --git a/android/media/MediaPlaylistAgent.java b/android/media/MediaPlaylistAgent.java
new file mode 100644
index 0000000..88f37e7
--- /dev/null
+++ b/android/media/MediaPlaylistAgent.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2018 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.NonNull;
+import android.annotation.Nullable;
+import android.media.update.ApiLoader;
+import android.media.update.MediaPlaylistAgentProvider;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ * MediaPlaylistAgent is the abstract class an application needs to derive from to pass an object
+ * to a MediaSession2 that will override default playlist handling behaviors. It contains a set of
+ * notify methods to signal MediaSession2 that playlist-related state has changed.
+ * <p>
+ * Playlists are composed of one or multiple {@link MediaItem2} instances, which combine metadata
+ * and data sources (as {@link DataSourceDesc})
+ * Used by {@link MediaSession2} and {@link MediaController2}.
+ */
+// This class only includes methods that contain {@link MediaItem2}.
+public abstract class MediaPlaylistAgent {
+    /**
+     * @hide
+     */
+    @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL,
+            REPEAT_MODE_GROUP})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RepeatMode {}
+
+    /**
+     * Playback will be stopped at the end of the playing media list.
+     */
+    public static final int REPEAT_MODE_NONE = 0;
+
+    /**
+     * Playback of the current playing media item will be repeated.
+     */
+    public static final int REPEAT_MODE_ONE = 1;
+
+    /**
+     * Playing media list will be repeated.
+     */
+    public static final int REPEAT_MODE_ALL = 2;
+
+    /**
+     * Playback of the playing media group will be repeated.
+     * A group is a logical block of media items which is specified in the section 5.7 of the
+     * Bluetooth AVRCP 1.6. An example of a group is the playlist.
+     */
+    public static final int REPEAT_MODE_GROUP = 3;
+
+    /**
+     * @hide
+     */
+    @IntDef({SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ShuffleMode {}
+
+    /**
+     * Media list will be played in order.
+     */
+    public static final int SHUFFLE_MODE_NONE = 0;
+
+    /**
+     * Media list will be played in shuffled order.
+     */
+    public static final int SHUFFLE_MODE_ALL = 1;
+
+    /**
+     * Media group will be played in shuffled order.
+     * A group is a logical block of media items which is specified in the section 5.7 of the
+     * Bluetooth AVRCP 1.6. An example of a group is the playlist.
+     */
+    public static final int SHUFFLE_MODE_GROUP = 2;
+
+    private final MediaPlaylistAgentProvider mProvider;
+
+    /**
+     * A callback class to receive notifications for events on the media player. See
+     * {@link MediaPlaylistAgent#registerPlaylistEventCallback(Executor, PlaylistEventCallback)}
+     * to register this callback.
+     */
+    public static abstract class PlaylistEventCallback {
+        /**
+         * Called when a playlist is changed.
+         *
+         * @param playlistAgent playlist agent for this event
+         * @param list new playlist
+         * @param metadata new metadata
+         */
+        public void onPlaylistChanged(@NonNull MediaPlaylistAgent playlistAgent,
+                @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when a playlist metadata is changed.
+         *
+         * @param playlistAgent playlist agent for this event
+         * @param metadata new metadata
+         */
+        public void onPlaylistMetadataChanged(@NonNull MediaPlaylistAgent playlistAgent,
+                @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when the shuffle mode is changed.
+         *
+         * @param playlistAgent playlist agent for this event
+         * @param shuffleMode repeat mode
+         * @see #SHUFFLE_MODE_NONE
+         * @see #SHUFFLE_MODE_ALL
+         * @see #SHUFFLE_MODE_GROUP
+         */
+        public void onShuffleModeChanged(@NonNull MediaPlaylistAgent playlistAgent,
+                @ShuffleMode int shuffleMode) { }
+
+        /**
+         * Called when the repeat mode is changed.
+         *
+         * @param playlistAgent playlist agent for this event
+         * @param repeatMode repeat mode
+         * @see #REPEAT_MODE_NONE
+         * @see #REPEAT_MODE_ONE
+         * @see #REPEAT_MODE_ALL
+         * @see #REPEAT_MODE_GROUP
+         */
+        public void onRepeatModeChanged(@NonNull MediaPlaylistAgent playlistAgent,
+                @RepeatMode int repeatMode) { }
+    }
+
+    public MediaPlaylistAgent() {
+        mProvider = ApiLoader.getProvider().createMediaPlaylistAgent(this);
+    }
+
+    /**
+     * Register {@link PlaylistEventCallback} to listen changes in the underlying
+     * {@link MediaPlaylistAgent}.
+     *
+     * @param executor a callback Executor
+     * @param callback a PlaylistEventCallback
+     * @throws IllegalArgumentException if executor or callback is {@code null}.
+     */
+    public final void registerPlaylistEventCallback(
+            @NonNull @CallbackExecutor Executor executor, @NonNull PlaylistEventCallback callback) {
+        mProvider.registerPlaylistEventCallback_impl(executor, callback);
+    }
+
+    /**
+     * Unregister the previously registered {@link PlaylistEventCallback}.
+     *
+     * @param callback the callback to be removed
+     * @throws IllegalArgumentException if the callback is {@code null}.
+     */
+    public final void unregisterPlaylistEventCallback(@NonNull PlaylistEventCallback callback) {
+        mProvider.unregisterPlaylistEventCallback_impl(callback);
+    }
+
+    public final void notifyPlaylistChanged() {
+        mProvider.notifyPlaylistChanged_impl();
+    }
+
+    public final void notifyPlaylistMetadataChanged() {
+        mProvider.notifyPlaylistMetadataChanged_impl();
+    }
+
+    public final void notifyShuffleModeChanged() {
+        mProvider.notifyShuffleModeChanged_impl();
+    }
+
+    public final void notifyRepeatModeChanged() {
+        mProvider.notifyRepeatModeChanged_impl();
+    }
+
+    /**
+     * Returns the playlist
+     *
+     * @return playlist, or null if none is set.
+     */
+    public @Nullable List<MediaItem2> getPlaylist() {
+        return mProvider.getPlaylist_impl();
+    }
+
+    /**
+     * Sets the playlist.
+     *
+     * @param list playlist
+     * @param metadata metadata of the playlist
+     */
+    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+        mProvider.setPlaylist_impl(list, metadata);
+    }
+
+    /**
+     * Returns the playlist metadata
+     *
+     * @return metadata metadata of the playlist, or null if none is set
+     */
+    public @Nullable MediaMetadata2 getPlaylistMetadata() {
+        return mProvider.getPlaylistMetadata_impl();
+    }
+
+    /**
+     * Updates the playlist metadata
+     *
+     * @param metadata metadata of the playlist
+     */
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        mProvider.updatePlaylistMetadata_impl(metadata);
+    }
+
+    /**
+     * Adds the media item to the playlist at position index. Index equals or greater than
+     * the current playlist size will add the item at the end of the playlist.
+     * <p>
+     * This will not change the currently playing media item.
+     * If index is less than or equal to the current index of the playlist,
+     * the current index of the playlist will be incremented correspondingly.
+     *
+     * @param index the index you want to add
+     * @param item the media item you want to add
+     */
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        mProvider.addPlaylistItem_impl(index, item);
+    }
+
+    /**
+     * Removes the media item from the playlist
+     *
+     * @param item media item to remove
+     */
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
+        mProvider.removePlaylistItem_impl(item);
+    }
+
+    /**
+     * Replace the media item at index in the playlist. This can be also used to update metadata of
+     * an item.
+     *
+     * @param index the index of the item to replace
+     * @param item the new item
+     */
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        mProvider.replacePlaylistItem_impl(index, item);
+    }
+
+    /**
+     * Skips to the the media item, and plays from it.
+     *
+     * @param item media item to start playing from
+     */
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        mProvider.skipToPlaylistItem_impl(item);
+    }
+
+    /**
+     * Skips to the previous item in the playlist.
+     */
+    public void skipToPreviousItem() {
+        mProvider.skipToPreviousItem_impl();
+    }
+
+    /**
+     * Skips to the next item in the playlist.
+     */
+    public void skipToNextItem() {
+        mProvider.skipToNextItem_impl();
+    }
+
+    /**
+     * Gets the repeat mode
+     *
+     * @return repeat mode
+     * @see #REPEAT_MODE_NONE
+     * @see #REPEAT_MODE_ONE
+     * @see #REPEAT_MODE_ALL
+     * @see #REPEAT_MODE_GROUP
+     */
+    public @RepeatMode int getRepeatMode() {
+        return mProvider.getRepeatMode_impl();
+    }
+
+    /**
+     * Sets the repeat mode
+     *
+     * @param repeatMode repeat mode
+     * @see #REPEAT_MODE_NONE
+     * @see #REPEAT_MODE_ONE
+     * @see #REPEAT_MODE_ALL
+     * @see #REPEAT_MODE_GROUP
+     */
+    public void setRepeatMode(@RepeatMode int repeatMode) {
+        mProvider.setRepeatMode_impl(repeatMode);
+    }
+
+    /**
+     * Gets the shuffle mode
+     *
+     * @return The shuffle mode
+     * @see #SHUFFLE_MODE_NONE
+     * @see #SHUFFLE_MODE_ALL
+     * @see #SHUFFLE_MODE_GROUP
+     */
+    public @ShuffleMode int getShuffleMode() {
+        return mProvider.getShuffleMode_impl();
+    }
+
+    /**
+     * Sets the shuffle mode
+     *
+     * @param shuffleMode The shuffle mode
+     * @see #SHUFFLE_MODE_NONE
+     * @see #SHUFFLE_MODE_ALL
+     * @see #SHUFFLE_MODE_GROUP
+     */
+    public void setShuffleMode(@ShuffleMode int shuffleMode) {
+        mProvider.setShuffleMode_impl(shuffleMode);
+    }
+
+    /**
+     * Called by {@link MediaSession2} when it wants to translate {@link DataSourceDesc} from the
+     * {@link MediaPlayerBase.PlayerEventCallback} to the {@link MediaItem2}. Override this method
+     * if you want to create {@link DataSourceDesc}s dynamically, instead of specifying them with
+     * {@link #setPlaylist(List, MediaMetadata2)}.
+     * <p>
+     * Session would throw an exception if this returns {@code null} for {@param dsd} from the
+     * {@link MediaPlayerBase.PlayerEventCallback}.
+     * <p>
+     * Default implementation calls the {@link #getPlaylist()} and searches the {@link MediaItem2}
+     * with the {@param dsd}.
+     *
+     * @param dsd The dsd to query.
+     * @return A {@link MediaItem2} object in the playlist that matches given {@code dsd}.
+     * @throws IllegalArgumentException if {@code dsd} is null
+     */
+    public @Nullable MediaItem2 getMediaItem(@NonNull DataSourceDesc dsd) {
+        return mProvider.getMediaItem_impl(dsd);
+    }
+}
diff --git a/android/media/MediaRecorder.java b/android/media/MediaRecorder.java
index 78477f7..90b6bff 100644
--- a/android/media/MediaRecorder.java
+++ b/android/media/MediaRecorder.java
@@ -17,6 +17,7 @@
 package android.media;
 
 import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.app.ActivityThread;
 import android.hardware.Camera;
@@ -25,8 +26,10 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.PersistableBundle;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
+import android.util.Pair;
 import android.view.Surface;
 
 import java.io.File;
@@ -34,6 +37,8 @@
 import java.io.IOException;
 import java.io.RandomAccessFile;
 import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
 
 import com.android.internal.annotations.GuardedBy;
 
@@ -101,6 +106,8 @@
     private OnErrorListener mOnErrorListener;
     private OnInfoListener mOnInfoListener;
 
+    private int mChannelCount;
+
     /**
      * Default constructor.
      */
@@ -115,6 +122,7 @@
             mEventHandler = null;
         }
 
+        mChannelCount = 1;
         String packageName = ActivityThread.currentPackageName();
         /* Native setup requires a weak reference to our object.
          * It's easier to create it here than in C++.
@@ -275,6 +283,7 @@
          * third-party applications.
          * </p>
          */
+        @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT)
         public static final int REMOTE_SUBMIX = 8;
 
         /** Microphone audio source tuned for unprocessed (raw) sound if available, behaves like
@@ -300,6 +309,7 @@
          * @hide
          */
         @SystemApi
+        @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
         public static final int HOTWORD = 1999;
     }
 
@@ -749,6 +759,7 @@
         if (numChannels <= 0) {
             throw new IllegalArgumentException("Number of channels is not positive");
         }
+        mChannelCount = numChannels;
         setParameter("audio-param-number-of-channels=" + numChannels);
     }
 
@@ -1350,6 +1361,7 @@
     /*
      * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler.
      */
+    @GuardedBy("mRoutingChangeListeners")
     private void enableNativeRoutingCallbacksLocked(boolean enabled) {
         if (mRoutingChangeListeners.size() == 0) {
             native_enableDeviceCallback(enabled);
@@ -1406,6 +1418,45 @@
     private native final int native_getRoutedDeviceId();
     private native final void native_enableDeviceCallback(boolean enabled);
 
+    //--------------------------------------------------------------------------
+    // Microphone information
+    //--------------------
+    /**
+     * Return A lists of {@link MicrophoneInfo} representing the active microphones.
+     * By querying channel mapping for each active microphone, developer can know how
+     * the microphone is used by each channels or a capture stream.
+     *
+     * @return a lists of {@link MicrophoneInfo} representing the active microphones
+     * @throws IOException if an error occurs
+     */
+    public List<MicrophoneInfo> getActiveMicrophones() throws IOException {
+        ArrayList<MicrophoneInfo> activeMicrophones = new ArrayList<>();
+        int status = native_getActiveMicrophones(activeMicrophones);
+        if (status != AudioManager.SUCCESS) {
+            Log.e(TAG, "getActiveMicrophones failed:" + status);
+            return new ArrayList<MicrophoneInfo>();
+        }
+        AudioManager.setPortIdForMicrophones(activeMicrophones);
+
+        // Use routed device when there is not information returned by hal.
+        if (activeMicrophones.size() == 0) {
+            AudioDeviceInfo device = getRoutedDevice();
+            if (device != null) {
+                MicrophoneInfo microphone = AudioManager.microphoneInfoFromAudioDeviceInfo(device);
+                ArrayList<Pair<Integer, Integer>> channelMapping = new ArrayList<>();
+                for (int i = 0; i < mChannelCount; i++) {
+                    channelMapping.add(new Pair(i, MicrophoneInfo.CHANNEL_MAPPING_DIRECT));
+                }
+                microphone.setChannelMapping(channelMapping);
+                activeMicrophones.add(microphone);
+            }
+        }
+        return activeMicrophones;
+    }
+
+    private native final int native_getActiveMicrophones(
+            ArrayList<MicrophoneInfo> activeMicrophones);
+
     /**
      * Called from native code when an interesting event happens.  This method
      * just uses the EventHandler system to post the event back to the main app thread.
diff --git a/android/media/MediaScanner.java b/android/media/MediaScanner.java
index cb4e46f..f476a6c 100644
--- a/android/media/MediaScanner.java
+++ b/android/media/MediaScanner.java
@@ -158,6 +158,7 @@
     public static final String SCANNED_BUILD_PREFS_NAME = "MediaScanBuild";
     public static final String LAST_INTERNAL_SCAN_FINGERPRINT = "lastScanFingerprint";
     private static final String SYSTEM_SOUNDS_DIR = "/system/media/audio";
+    private static final String PRODUCT_SOUNDS_DIR = "/product/media/audio";
     private static String sLastInternalScanFingerprint;
 
     private static final String[] ID3_GENRES = {
@@ -323,7 +324,6 @@
     private final Uri mAudioUri;
     private final Uri mVideoUri;
     private final Uri mImagesUri;
-    private final Uri mThumbsUri;
     private final Uri mPlaylistsUri;
     private final Uri mFilesUri;
     private final Uri mFilesUriNoNotify;
@@ -419,7 +419,6 @@
         mAudioUri = Audio.Media.getContentUri(volumeName);
         mVideoUri = Video.Media.getContentUri(volumeName);
         mImagesUri = Images.Media.getContentUri(volumeName);
-        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
         mFilesUri = Files.getContentUri(volumeName);
         mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();
 
@@ -947,6 +946,7 @@
                 values.put(Audio.Media.IS_MUSIC, music);
                 values.put(Audio.Media.IS_PODCAST, podcasts);
             } else if ((mFileType == MediaFile.FILE_TYPE_JPEG
+                    || mFileType == MediaFile.FILE_TYPE_HEIF
                     || MediaFile.isRawImageFileType(mFileType)) && !mNoMedia) {
                 ExifInterface exif = null;
                 try {
@@ -1153,7 +1153,10 @@
     private static boolean isSystemSoundWithMetadata(String path) {
         if (path.startsWith(SYSTEM_SOUNDS_DIR + ALARMS_DIR)
                 || path.startsWith(SYSTEM_SOUNDS_DIR + RINGTONES_DIR)
-                || path.startsWith(SYSTEM_SOUNDS_DIR + NOTIFICATIONS_DIR)) {
+                || path.startsWith(SYSTEM_SOUNDS_DIR + NOTIFICATIONS_DIR)
+                || path.startsWith(PRODUCT_SOUNDS_DIR + ALARMS_DIR)
+                || path.startsWith(PRODUCT_SOUNDS_DIR + RINGTONES_DIR)
+                || path.startsWith(PRODUCT_SOUNDS_DIR + NOTIFICATIONS_DIR)) {
             return true;
         }
         return false;
@@ -1283,53 +1286,6 @@
         }
     }
 
-    private void pruneDeadThumbnailFiles() {
-        HashSet<String> existingFiles = new HashSet<String>();
-        String directory = "/sdcard/DCIM/.thumbnails";
-        String [] files = (new File(directory)).list();
-        Cursor c = null;
-        if (files == null)
-            files = new String[0];
-
-        for (int i = 0; i < files.length; i++) {
-            String fullPathString = directory + "/" + files[i];
-            existingFiles.add(fullPathString);
-        }
-
-        try {
-            c = mMediaProvider.query(
-                    mThumbsUri,
-                    new String [] { "_data" },
-                    null,
-                    null,
-                    null, null);
-            Log.v(TAG, "pruneDeadThumbnailFiles... " + c);
-            if (c != null && c.moveToFirst()) {
-                do {
-                    String fullPathString = c.getString(0);
-                    existingFiles.remove(fullPathString);
-                } while (c.moveToNext());
-            }
-
-            for (String fileToDelete : existingFiles) {
-                if (false)
-                    Log.v(TAG, "fileToDelete is " + fileToDelete);
-                try {
-                    (new File(fileToDelete)).delete();
-                } catch (SecurityException ex) {
-                }
-            }
-
-            Log.v(TAG, "/pruneDeadThumbnailFiles... " + c);
-        } catch (RemoteException e) {
-            // We will soon be killed...
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-    }
-
     static class MediaBulkDeleter {
         StringBuilder whereClause = new StringBuilder();
         ArrayList<String> whereArgs = new ArrayList<String>(100);
@@ -1373,9 +1329,6 @@
             processPlayLists();
         }
 
-        if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
-            pruneDeadThumbnailFiles();
-
         // allow GC to clean up
         mPlayLists.clear();
     }
diff --git a/android/media/MediaSession2.java b/android/media/MediaSession2.java
index 0e90040..2b3c2b4 100644
--- a/android/media/MediaSession2.java
+++ b/android/media/MediaSession2.java
@@ -16,36 +16,37 @@
 
 package android.media;
 
+import static android.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.Activity;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
-import android.media.MediaPlayerBase.PlaybackListener;
-import android.media.session.MediaSession;
-import android.media.session.MediaSession.Callback;
-import android.media.session.PlaybackState;
+import android.media.MediaPlayerBase.BuffState;
+import android.media.MediaPlayerBase.PlayerState;
+import android.media.MediaPlaylistAgent.RepeatMode;
+import android.media.MediaPlaylistAgent.ShuffleMode;
 import android.media.update.ApiLoader;
 import android.media.update.MediaSession2Provider;
+import android.media.update.MediaSession2Provider.BuilderBaseProvider;
+import android.media.update.MediaSession2Provider.CommandButtonProvider;
 import android.media.update.MediaSession2Provider.ControllerInfoProvider;
+import android.media.update.ProviderCreator;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Parcelable;
+import android.os.IInterface;
 import android.os.ResultReceiver;
-import android.text.TextUtils;
-import android.util.ArraySet;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
+ * @hide
  * Allows a media app to expose its transport controls and playback information in a process to
  * other processes including the Android framework and other apps. Common use cases are as follows.
  * <ul>
@@ -59,7 +60,7 @@
  * sessions can be created to provide finer grain controls of media.
  * <p>
  * If you want to support background playback, {@link MediaSessionService2} is preferred
- * instead. With it, your playback can be revived even after you've finished playback. See
+ * instead. With it, your playback can be revived even after playback is finished. See
  * {@link MediaSessionService2} for details.
  * <p>
  * A session can be obtained by {@link Builder}. The owner of the session may pass its session token
@@ -67,7 +68,8 @@
  * session.
  * <p>
  * When a session receive transport control commands, the session sends the commands directly to
- * the the underlying media player set by {@link Builder} or {@link #setPlayer(MediaPlayerBase)}.
+ * the the underlying media player set by {@link Builder} or
+ * {@link #updatePlayer}.
  * <p>
  * When an app is finished performing playback it must call {@link #close()} to clean up the session
  * and notify any controllers.
@@ -75,228 +77,110 @@
  * {@link MediaSession2} objects should be used on the thread on the looper.
  *
  * @see MediaSessionService2
- * @hide
  */
-// TODO(jaewan): Unhide
-// TODO(jaewan): Revisit comments. Currently it's borrowed from the MediaSession.
-// TODO(jaewan): Should we support thread safe? It may cause tricky issue such as b/63797089
-// TODO(jaewan): Should we make APIs for MediaSessionService2 public? It's helpful for
-//               developers that doesn't want to override from Browser, but user may not use this
-//               correctly.
 public class MediaSession2 implements AutoCloseable {
     private final MediaSession2Provider mProvider;
 
-    // Note: Do not define IntDef because subclass can add more command code on top of these.
-    // TODO(jaewan): Shouldn't we pull out?
-    public static final int COMMAND_CODE_CUSTOM = 0;
-    public static final int COMMAND_CODE_PLAYBACK_START = 1;
-    public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2;
-    public static final int COMMAND_CODE_PLAYBACK_STOP = 3;
-    public static final int COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM = 4;
-    public static final int COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM = 5;
-    public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6;
-    public static final int COMMAND_CODE_PLAYBACK_FAST_FORWARD = 7;
-    public static final int COMMAND_CODE_PLAYBACK_REWIND = 8;
-    public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9;
-    public static final int COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM = 10;
-
-    public static final int COMMAND_CODE_PLAYLIST_GET = 11;
-    public static final int COMMAND_CODE_PLAYLIST_ADD = 12;
-    public static final int COMMAND_CODE_PLAYLIST_REMOVE = 13;
-
-    public static final int COMMAND_CODE_PLAY_FROM_MEDIA_ID = 14;
-    public static final int COMMAND_CODE_PLAY_FROM_URI = 15;
-    public static final int COMMAND_CODE_PLAY_FROM_SEARCH = 16;
-
-    public static final int COMMAND_CODE_PREPARE_FROM_MEDIA_ID = 17;
-    public static final int COMMAND_CODE_PREPARE_FROM_URI = 18;
-    public static final int COMMAND_CODE_PREPARE_FROM_SEARCH = 19;
+    /**
+     * @hide
+     */
+    @IntDef({ERROR_CODE_UNKNOWN_ERROR, ERROR_CODE_APP_ERROR, ERROR_CODE_NOT_SUPPORTED,
+            ERROR_CODE_AUTHENTICATION_EXPIRED, ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED,
+            ERROR_CODE_CONCURRENT_STREAM_LIMIT, ERROR_CODE_PARENTAL_CONTROL_RESTRICTED,
+            ERROR_CODE_NOT_AVAILABLE_IN_REGION, ERROR_CODE_CONTENT_ALREADY_PLAYING,
+            ERROR_CODE_SKIP_LIMIT_REACHED, ERROR_CODE_ACTION_ABORTED, ERROR_CODE_END_OF_QUEUE,
+            ERROR_CODE_SETUP_REQUIRED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ErrorCode {}
 
     /**
-     * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
-     * <p>
-     * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command.
-     * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and
-     * {@link #getCustomCommand()} shouldn't be {@code null}.
+     * This is the default error code and indicates that none of the other error codes applies.
      */
-    // TODO(jaewan): Move this into the updatable.
-    public static final class Command {
-        private static final String KEY_COMMAND_CODE
-                = "android.media.media_session2.command.command_code";
-        private static final String KEY_COMMAND_CUSTOM_COMMAND
-                = "android.media.media_session2.command.custom_command";
-        private static final String KEY_COMMAND_EXTRA
-                = "android.media.media_session2.command.extra";
-
-        private final int mCommandCode;
-        // Nonnull if it's custom command
-        private final String mCustomCommand;
-        private final Bundle mExtra;
-
-        public Command(int commandCode) {
-            mCommandCode = commandCode;
-            mCustomCommand = null;
-            mExtra = null;
-        }
-
-        public Command(@NonNull String action, @Nullable Bundle extra) {
-            if (action == null) {
-                throw new IllegalArgumentException("action shouldn't be null");
-            }
-            mCommandCode = COMMAND_CODE_CUSTOM;
-            mCustomCommand = action;
-            mExtra = extra;
-        }
-
-        public int getCommandCode() {
-            return mCommandCode;
-        }
-
-        public @Nullable String getCustomCommand() {
-            return mCustomCommand;
-        }
-
-        public @Nullable Bundle getExtra() {
-            return mExtra;
-        }
-
-        /**
-         * @return a new Bundle instance from the Command
-         * @hide
-         */
-        public Bundle toBundle() {
-            Bundle bundle = new Bundle();
-            bundle.putInt(KEY_COMMAND_CODE, mCommandCode);
-            bundle.putString(KEY_COMMAND_CUSTOM_COMMAND, mCustomCommand);
-            bundle.putBundle(KEY_COMMAND_EXTRA, mExtra);
-            return bundle;
-        }
-
-        /**
-         * @return a new Command instance from the Bundle
-         * @hide
-         */
-        public static Command fromBundle(Bundle command) {
-            int code = command.getInt(KEY_COMMAND_CODE);
-            if (code != COMMAND_CODE_CUSTOM) {
-                return new Command(code);
-            } else {
-                String customCommand = command.getString(KEY_COMMAND_CUSTOM_COMMAND);
-                if (customCommand == null) {
-                    return null;
-                }
-                return new Command(customCommand, command.getBundle(KEY_COMMAND_EXTRA));
-            }
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (!(obj instanceof Command)) {
-                return false;
-            }
-            Command other = (Command) obj;
-            // TODO(jaewan): Should we also compare contents in bundle?
-            //               It may not be possible if the bundle contains private class.
-            return mCommandCode == other.mCommandCode
-                    && TextUtils.equals(mCustomCommand, other.mCustomCommand);
-        }
-
-        @Override
-        public int hashCode() {
-            final int prime = 31;
-            return ((mCustomCommand != null) ? mCustomCommand.hashCode() : 0) * prime + mCommandCode;
-        }
-    }
+    public static final int ERROR_CODE_UNKNOWN_ERROR = 0;
 
     /**
-     * Represent set of {@link Command}.
+     * Error code when the application state is invalid to fulfill the request.
      */
-    // TODO(jaewan): Move this to updatable
-    public static class CommandGroup {
-        private static final String KEY_COMMANDS =
-                "android.media.mediasession2.commandgroup.commands";
-        private ArraySet<Command> mCommands = new ArraySet<>();
+    public static final int ERROR_CODE_APP_ERROR = 1;
 
-        public CommandGroup() {
-        }
+    /**
+     * Error code when the request is not supported by the application.
+     */
+    public static final int ERROR_CODE_NOT_SUPPORTED = 2;
 
-        public CommandGroup(CommandGroup others) {
-            mCommands.addAll(others.mCommands);
-        }
+    /**
+     * Error code when the request cannot be performed because authentication has expired.
+     */
+    public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3;
 
-        public void addCommand(Command command) {
-            mCommands.add(command);
-        }
+    /**
+     * Error code when a premium account is required for the request to succeed.
+     */
+    public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4;
 
-        public void addAllPredefinedCommands() {
-            // TODO(jaewan): Is there any better way than this?
-            mCommands.add(new Command(COMMAND_CODE_PLAYBACK_START));
-            mCommands.add(new Command(COMMAND_CODE_PLAYBACK_PAUSE));
-            mCommands.add(new Command(COMMAND_CODE_PLAYBACK_STOP));
-            mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM));
-            mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM));
-        }
+    /**
+     * Error code when too many concurrent streams are detected.
+     */
+    public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5;
 
-        public void removeCommand(Command command) {
-            mCommands.remove(command);
-        }
+    /**
+     * Error code when the content is blocked due to parental controls.
+     */
+    public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6;
 
-        public boolean hasCommand(Command command) {
-            return mCommands.contains(command);
-        }
+    /**
+     * Error code when the content is blocked due to being regionally unavailable.
+     */
+    public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7;
 
-        public boolean hasCommand(int code) {
-            if (code == COMMAND_CODE_CUSTOM) {
-                throw new IllegalArgumentException("Use hasCommand(Command) for custom command");
-            }
-            for (int i = 0; i < mCommands.size(); i++) {
-                if (mCommands.valueAt(i).getCommandCode() == code) {
-                    return true;
-                }
-            }
-            return false;
-        }
+    /**
+     * Error code when the requested content is already playing.
+     */
+    public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8;
 
+    /**
+     * Error code when the application cannot skip any more songs because skip limit is reached.
+     */
+    public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9;
+
+    /**
+     * Error code when the action is interrupted due to some external event.
+     */
+    public static final int ERROR_CODE_ACTION_ABORTED = 10;
+
+    /**
+     * Error code when the playback navigation (previous, next) is not possible because the queue
+     * was exhausted.
+     */
+    public static final int ERROR_CODE_END_OF_QUEUE = 11;
+
+    /**
+     * Error code when the session needs user's manual intervention.
+     */
+    public static final int ERROR_CODE_SETUP_REQUIRED = 12;
+
+    /**
+     * Interface definition of a callback to be invoked when a {@link MediaItem2} in the playlist
+     * didn't have a {@link DataSourceDesc} but it's needed now for preparing or playing it.
+     *
+     * #see #setOnDataSourceMissingHelper
+     */
+    public interface OnDataSourceMissingHelper {
         /**
-         * @return new bundle from the CommandGroup
-         * @hide
+         * Called when a {@link MediaItem2} in the playlist didn't have a {@link DataSourceDesc}
+         * but it's needed now for preparing or playing it. Returned data source descriptor will be
+         * sent to the player directly to prepare or play the contents.
+         * <p>
+         * An exception may be thrown if the returned {@link DataSourceDesc} is duplicated in the
+         * playlist, so items cannot be differentiated.
+         *
+         * @param session the session for this event
+         * @param item media item from the controller
+         * @return a data source descriptor if the media item. Can be {@code null} if the content
+         *        isn't available.
          */
-        public Bundle toBundle() {
-            ArrayList<Bundle> list = new ArrayList<>();
-            for (int i = 0; i < mCommands.size(); i++) {
-                list.add(mCommands.valueAt(i).toBundle());
-            }
-            Bundle bundle = new Bundle();
-            bundle.putParcelableArrayList(KEY_COMMANDS, list);
-            return bundle;
-        }
-
-        /**
-         * @return new instance of CommandGroup from the bundle
-         * @hide
-         */
-        public static @Nullable CommandGroup fromBundle(Bundle commands) {
-            if (commands == null) {
-                return null;
-            }
-            List<Parcelable> list = commands.getParcelableArrayList(KEY_COMMANDS);
-            if (list == null) {
-                return null;
-            }
-            CommandGroup commandGroup = new CommandGroup();
-            for (int i = 0; i < list.size(); i++) {
-                Parcelable parcelable = list.get(i);
-                if (!(parcelable instanceof Bundle)) {
-                    continue;
-                }
-                Bundle commandBundle = (Bundle) parcelable;
-                Command command = Command.fromBundle(commandBundle);
-                if (command != null) {
-                    commandGroup.addCommand(command);
-                }
-            }
-            return commandGroup;
-        }
+        @Nullable DataSourceDesc onDataSourceMissing(@NonNull MediaSession2 session,
+                @NonNull MediaItem2 item);
     }
 
     /**
@@ -305,21 +189,23 @@
      * If it's not set, the session will accept all controllers and all incoming commands by
      * default.
      */
-    // TODO(jaewan): Can we move this inside of the updatable for default implementation.
-    public static class SessionCallback {
+    // TODO(jaewan): Move this to updatable for default implementation (b/74091963)
+    public static abstract class SessionCallback {
         /**
          * Called when a controller is created for this session. Return allowed commands for
          * controller. By default it allows all connection requests and commands.
          * <p>
          * You can reject the connection by return {@code null}. In that case, controller receives
-         * {@link MediaController2.ControllerCallback#onDisconnected()} and cannot be usable.
+         * {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)} and cannot
+         * be usable.
          *
+         * @param session the session for this event
          * @param controller controller information.
-         * @return allowed commands. Can be {@code null} to reject coonnection.
+         * @return allowed commands. Can be {@code null} to reject connection.
          */
-        // TODO(jaewan): Change return type. Once we do, null is for reject.
-        public @Nullable CommandGroup onConnect(@NonNull ControllerInfo controller) {
-            CommandGroup commands = new CommandGroup();
+        public @Nullable SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller) {
+            SessionCommandGroup2 commands = new SessionCommandGroup2();
             commands.addAllPredefinedCommands();
             return commands;
         }
@@ -327,213 +213,395 @@
         /**
          * Called when a controller is disconnected
          *
+         * @param session the session for this event
          * @param controller controller information
          */
-        public void onDisconnected(@NonNull ControllerInfo controller) { }
+        public void onDisconnected(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller) { }
 
         /**
-         * Called when a controller sent a command to the session, and the command will be sent to
-         * the player directly unless you reject the request by {@code false}.
+         * Called when a controller sent a command that will be sent directly to the player. Return
+         * {@code false} here to reject the request and stop sending command to the player.
          *
+         * @param session the session for this event
          * @param controller controller information.
          * @param command a command. This method will be called for every single command.
          * @return {@code true} if you want to accept incoming command. {@code false} otherwise.
+         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PLAY
+         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PAUSE
+         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_STOP
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_NEXT_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_PREV_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PREPARE
+         * @see SessionCommand2#COMMAND_CODE_SESSION_FAST_FORWARD
+         * @see SessionCommand2#COMMAND_CODE_SESSION_REWIND
+         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_SEEK_TO
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REMOVE_ITEM
+         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
+         * @see SessionCommand2#COMMAND_CODE_SET_VOLUME
          */
-        // TODO(jaewan): Add more documentations (or make it clear) which commands can be filtered
-        //               with this.
-        public boolean onCommandRequest(@NonNull ControllerInfo controller,
-                @NonNull Command command) {
+        public boolean onCommandRequest(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull SessionCommand2 command) {
             return true;
         }
 
         /**
-         * Called when a controller set rating on the currently playing contents.
+         * Called when a controller set rating of a media item through
+         * {@link MediaController2#setRating(String, Rating2)}.
+         * <p>
+         * To allow setting user rating for a {@link MediaItem2}, the media item's metadata
+         * should have {@link Rating2} with the key {@link MediaMetadata#METADATA_KEY_USER_RATING},
+         * in order to provide possible rating style for controller. Controller will follow the
+         * rating style.
          *
-         * @param
+         * @param session the session for this event
+         * @param controller controller information
+         * @param mediaId media id from the controller
+         * @param rating new rating from the controller
          */
-        public void onSetRating(@NonNull ControllerInfo controller, @NonNull Rating2 rating) { }
+        public void onSetRating(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
+                @NonNull String mediaId, @NonNull Rating2 rating) { }
 
         /**
-         * Called when a controller sent a custom command.
+         * Called when a controller sent a custom command through
+         * {@link MediaController2#sendCustomCommand(SessionCommand2, Bundle, ResultReceiver)}.
          *
+         * @param session the session for this event
          * @param controller controller information
          * @param customCommand custom command.
          * @param args optional arguments
          * @param cb optional result receiver
          */
-        public void onCustomCommand(@NonNull ControllerInfo controller,
-                @NonNull Command customCommand, @Nullable Bundle args,
-                @Nullable ResultReceiver cb) { }
+        public void onCustomCommand(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull SessionCommand2 customCommand,
+                @Nullable Bundle args, @Nullable ResultReceiver cb) { }
 
         /**
-         * Override to handle requests to prepare for playing a specific mediaId.
+         * Called when a controller requested to play a specific mediaId through
+         * {@link MediaController2#playFromMediaId(String, Bundle)}.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param mediaId media id
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID
+         */
+        public void onPlayFromMediaId(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull String mediaId,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when a controller requested to begin playback from a search query through
+         * {@link MediaController2#playFromSearch(String, Bundle)}
+         * <p>
+         * An empty query indicates that the app may play any music. The implementation should
+         * attempt to make a smart choice about what to play.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param query query string. Can be empty to indicate any suggested media
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_SEARCH
+         */
+        public void onPlayFromSearch(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull String query,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when a controller requested to play a specific media item represented by a URI
+         * through {@link MediaController2#playFromUri(Uri, Bundle)}
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param uri uri
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_URI
+         */
+        public void onPlayFromUri(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull Uri uri,
+                @Nullable Bundle extras) { }
+
+        /**
+         * Called when a controller requested to prepare for playing a specific mediaId through
+         * {@link MediaController2#prepareFromMediaId(String, Bundle)}.
+         * <p>
          * During the preparation, a session should not hold audio focus in order to allow other
          * sessions play seamlessly. The state of playback should be updated to
-         * {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+         * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
          * <p>
          * The playback of the prepared content should start in the later calls of
          * {@link MediaSession2#play()}.
          * <p>
          * Override {@link #onPlayFromMediaId} to handle requests for starting
          * playback without preparation.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param mediaId media id to prepare
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID
          */
-        public void onPlayFromMediaId(@NonNull ControllerInfo controller,
-                @NonNull String mediaId, @Nullable Bundle extras) { }
+        public void onPrepareFromMediaId(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull String mediaId,
+                @Nullable Bundle extras) { }
 
         /**
-         * Override to handle requests to prepare playback from a search query. An empty query
-         * indicates that the app may prepare any music. The implementation should attempt to make a
-         * smart choice about what to play. During the preparation, a session should not hold audio
-         * focus in order to allow other sessions play seamlessly. The state of playback should be
-         * updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+         * Called when a controller requested to prepare playback from a search query through
+         * {@link MediaController2#prepareFromSearch(String, Bundle)}.
          * <p>
-         * The playback of the prepared content should start in the later calls of
-         * {@link MediaSession2#play()}.
+         * An empty query indicates that the app may prepare any music. The implementation should
+         * attempt to make a smart choice about what to play.
+         * <p>
+         * The state of playback should be updated to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}
+         * after the preparation is done. The playback of the prepared content should start in the
+         * later calls of {@link MediaSession2#play()}.
          * <p>
          * Override {@link #onPlayFromSearch} to handle requests for starting playback without
          * preparation.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param query query string. Can be empty to indicate any suggested media
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH
          */
-        public void onPlayFromSearch(@NonNull ControllerInfo controller,
-                @NonNull String query, @Nullable Bundle extras) { }
+        public void onPrepareFromSearch(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull String query,
+                @Nullable Bundle extras) { }
 
         /**
-         * Override to handle requests to prepare a specific media item represented by a URI.
+         * Called when a controller requested to prepare a specific media item represented by a URI
+         * through {@link MediaController2#prepareFromUri(Uri, Bundle)}.
+         * <p>
          * During the preparation, a session should not hold audio focus in order to allow
          * other sessions play seamlessly. The state of playback should be updated to
-         * {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+         * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
          * <p>
          * The playback of the prepared content should start in the later calls of
          * {@link MediaSession2#play()}.
          * <p>
          * Override {@link #onPlayFromUri} to handle requests for starting playback without
          * preparation.
+         *
+         * @param session the session for this event
+         * @param controller controller information
+         * @param uri uri
+         * @param extras optional extra bundle
+         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_URI
          */
-        public void onPlayFromUri(@NonNull ControllerInfo controller,
-                @NonNull String uri, @Nullable Bundle extras) { }
+        public void onPrepareFromUri(@NonNull MediaSession2 session,
+                @NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) { }
 
         /**
-         * Override to handle requests to play a specific mediaId.
+         * Called when a controller called {@link MediaController2#fastForward()}
+         *
+         * @param session the session for this event
          */
-        public void onPrepareFromMediaId(@NonNull ControllerInfo controller,
-                @NonNull String mediaId, @Nullable Bundle extras) { }
+        public void onFastForward(@NonNull MediaSession2 session) { }
 
         /**
-         * Override to handle requests to begin playback from a search query. An
-         * empty query indicates that the app may play any music. The
-         * implementation should attempt to make a smart choice about what to
-         * play.
+         * Called when a controller called {@link MediaController2#rewind()}
+         *
+         * @param session the session for this event
          */
-        public void onPrepareFromSearch(@NonNull ControllerInfo controller,
-                @NonNull String query, @Nullable Bundle extras) { }
+        public void onRewind(@NonNull MediaSession2 session) { }
 
         /**
-         * Override to handle requests to play a specific media item represented by a URI.
-         */
-        public void prepareFromUri(@NonNull ControllerInfo controller,
-                @NonNull Uri uri, @Nullable Bundle extras) { }
-
-        /**
-         * Called when a controller wants to add a {@link MediaItem2} at the specified position
-         * in the play queue.
+         * Called when the player's current playing item is changed
          * <p>
-         * The item from the media controller wouldn't have valid data source descriptor because
-         * it would have been anonymized when it's sent to the remote process.
+         * When it's called, you should invalidate previous playback information and wait for later
+         * callbacks.
          *
-         * @param item The media item to be inserted.
-         * @param index The index at which the item is to be inserted.
+         * @param session the controller for this event
+         * @param player the player for this event
+         * @param item new item
          */
-        public void onAddPlaylistItem(@NonNull ControllerInfo controller,
-                @NonNull MediaItem2 item, int index) { }
+        // TODO(jaewan): Use this (b/74316764)
+        public void onCurrentMediaItemChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlayerBase player, @NonNull MediaItem2 item) { }
 
         /**
-         * Called when a controller wants to remove the {@link MediaItem2}
-         *
-         * @param item
+         * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
+         * referenced by the given data source.
+         * @param session the session for this event
+         * @param player the player for this event
+         * @param item the media item for which buffering is happening
          */
-        // Can we do this automatically?
-        public void onRemovePlaylistItem(@NonNull MediaItem2 item) { }
-    };
+        public void onMediaPrepared(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
+                @NonNull MediaItem2 item) { }
+
+        /**
+         * Called to indicate that the state of the player has changed.
+         * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
+         * @param session the session for this event
+         * @param player the player for this event
+         * @param state the new state of the player.
+         */
+        public void onPlayerStateChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlayerBase player, @PlayerState int state) { }
+
+        /**
+         * Called to report buffering events for a data source.
+         *
+         * @param session the session for this event
+         * @param player the player for this event
+         * @param item the media item for which buffering is happening.
+         * @param state the new buffering state.
+         */
+        public void onBufferingStateChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlayerBase player, @NonNull MediaItem2 item, @BuffState int state) { }
+
+        /**
+         * Called to indicate that the playback speed has changed.
+         * @param session the session for this event
+         * @param player the player for this event
+         * @param speed the new playback speed.
+         */
+        public void onPlaybackSpeedChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlayerBase player, float speed) { }
+
+        /**
+         * Called to indicate that {@link #seekTo(long)} is completed.
+         *
+         * @param session the session for this event.
+         * @param mpb the player that has completed seeking.
+         * @param position the previous seeking request.
+         * @see #seekTo(long)
+         */
+        public void onSeekCompleted(@NonNull MediaSession2 session, @NonNull MediaPlayerBase mpb,
+                long position) { }
+
+        /**
+         * Called when a playlist is changed from the {@link MediaPlaylistAgent}.
+         * <p>
+         * This is called when the underlying agent has called
+         * {@link MediaPlaylistAgent.PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent,
+         * List, MediaMetadata2)}.
+         *
+         * @param session the session for this event
+         * @param playlistAgent playlist agent for this event
+         * @param list new playlist
+         * @param metadata new metadata
+         */
+        public void onPlaylistChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlaylistAgent playlistAgent, @NonNull List<MediaItem2> list,
+                @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when a playlist metadata is changed.
+         *
+         * @param session the session for this event
+         * @param playlistAgent playlist agent for this event
+         * @param metadata new metadata
+         */
+        public void onPlaylistMetadataChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlaylistAgent playlistAgent, @Nullable MediaMetadata2 metadata) { }
+
+        /**
+         * Called when the shuffle mode is changed.
+         *
+         * @param session the session for this event
+         * @param playlistAgent playlist agent for this event
+         * @param shuffleMode repeat mode
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+         * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+         */
+        public void onShuffleModeChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlaylistAgent playlistAgent,
+                @MediaPlaylistAgent.ShuffleMode int shuffleMode) { }
+
+        /**
+         * Called when the repeat mode is changed.
+         *
+         * @param session the session for this event
+         * @param playlistAgent playlist agent for this event
+         * @param repeatMode repeat mode
+         * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+         * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+         * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+         * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+         */
+        public void onRepeatModeChanged(@NonNull MediaSession2 session,
+                @NonNull MediaPlaylistAgent playlistAgent,
+                @MediaPlaylistAgent.RepeatMode int repeatMode) { }
+    }
 
     /**
-     * Base builder class for MediaSession2 and its subclass.
-     *
+     * Base builder class for MediaSession2 and its subclass. Any change in this class should be
+     * also applied to the subclasses {@link MediaSession2.Builder} and
+     * {@link MediaLibraryService2.MediaLibrarySession.Builder}.
+     * <p>
+     * APIs here should be package private, but should have documentations for developers.
+     * Otherwise, javadoc will generate documentation with the generic types such as follows.
+     * <pre>U extends BuilderBase<T, U, C> setSessionCallback(Executor executor, C callback)</pre>
+     * <p>
+     * This class is hidden to prevent from generating test stub, which fails with
+     * 'unexpected bound' because it tries to auto generate stub class as follows.
+     * <pre>abstract static class BuilderBase<
+     *      T extends android.media.MediaSession2,
+     *      U extends android.media.MediaSession2.BuilderBase<
+     *              T, U, C extends android.media.MediaSession2.SessionCallback>, C></pre>
      * @hide
      */
     static abstract class BuilderBase
-            <T extends MediaSession2.BuilderBase<T, C>, C extends SessionCallback> {
-        final Context mContext;
-        final MediaPlayerBase mPlayer;
-        String mId;
-        Executor mCallbackExecutor;
-        C mCallback;
-        VolumeProvider mVolumeProvider;
-        int mRatingType;
-        PendingIntent mSessionActivity;
+            <T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> {
+        private final BuilderBaseProvider<T, C> mProvider;
 
-        /**
-         * Constructor.
-         *
-         * @param context a context
-         * @param player a player to handle incoming command from any controller.
-         * @throws IllegalArgumentException if any parameter is null, or the player is a
-         *      {@link MediaSession2} or {@link MediaController2}.
-         */
-        // TODO(jaewan): Also need executor
-        public BuilderBase(@NonNull Context context, @NonNull MediaPlayerBase player) {
-            if (context == null) {
-                throw new IllegalArgumentException("context shouldn't be null");
-            }
-            if (player == null) {
-                throw new IllegalArgumentException("player shouldn't be null");
-            }
-            mContext = context;
-            mPlayer = player;
-            // Ensure non-null
-            mId = "";
+        BuilderBase(ProviderCreator<BuilderBase<T, U, C>, BuilderBaseProvider<T, C>> creator) {
+            mProvider = creator.createProvider(this);
         }
 
         /**
-         * Set volume provider to configure this session to use remote volume handling.
-         * This must be called to receive volume button events, otherwise the system
-         * will adjust the appropriate stream volume for this session's player.
+         * Sets the underlying {@link MediaPlayerBase} for this session to dispatch incoming event
+         * to.
+         *
+         * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
+         */
+        @NonNull U setPlayer(@NonNull MediaPlayerBase player) {
+            mProvider.setPlayer_impl(player);
+            return (U) this;
+        }
+
+        /**
+         * Sets the {@link MediaPlaylistAgent} for this session to manages playlist of the
+         * underlying {@link MediaPlayerBase}. The playlist agent should manage
+         * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
          * <p>
-         * Set {@code null} to reset.
+         * If the {@link MediaPlaylistAgent} isn't set, session will create the default playlist
+         * agent.
          *
-         * @param volumeProvider The provider that will handle volume changes. Can be {@code null}
+         * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the
+         *                      {@code player}
          */
-        public T setVolumeProvider(@Nullable VolumeProvider volumeProvider) {
-            mVolumeProvider = volumeProvider;
-            return (T) this;
+        U setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+            mProvider.setPlaylistAgent_impl(playlistAgent);
+            return (U) this;
         }
 
         /**
-         * Set the style of rating used by this session. Apps trying to set the
-         * rating should use this style. Must be one of the following:
-         * <ul>
-         * <li>{@link Rating2#RATING_NONE}</li>
-         * <li>{@link Rating2#RATING_3_STARS}</li>
-         * <li>{@link Rating2#RATING_4_STARS}</li>
-         * <li>{@link Rating2#RATING_5_STARS}</li>
-         * <li>{@link Rating2#RATING_HEART}</li>
-         * <li>{@link Rating2#RATING_PERCENTAGE}</li>
-         * <li>{@link Rating2#RATING_THUMB_UP_DOWN}</li>
-         * </ul>
+         * Sets the {@link VolumeProvider2} for this session to handle volume events. If not set,
+         * system will adjust the appropriate stream volume for this session's player.
+         *
+         * @param volumeProvider The provider that will receive volume button events.
          */
-        public T setRatingType(@Rating2.Style int type) {
-            mRatingType = type;
-            return (T) this;
+        @NonNull U setVolumeProvider(@Nullable VolumeProvider2 volumeProvider) {
+            mProvider.setVolumeProvider_impl(volumeProvider);
+            return (U) this;
         }
 
         /**
          * Set an intent for launching UI for this Session. This can be used as a
          * quick link to an ongoing media screen. The intent should be for an
-         * activity that may be started using {@link Activity#startActivity(Intent)}.
+         * activity that may be started using {@link Context#startActivity(Intent)}.
          *
          * @param pi The intent to launch to show UI for this session.
          */
-        public T setSessionActivity(@Nullable PendingIntent pi) {
-            mSessionActivity = pi;
-            return (T) this;
+        @NonNull U setSessionActivity(@Nullable PendingIntent pi) {
+            mProvider.setSessionActivity_impl(pi);
+            return (U) this;
         }
 
         /**
@@ -546,32 +614,22 @@
          * @throws IllegalArgumentException if id is {@code null}
          * @return
          */
-        public T setId(@NonNull String id) {
-            if (id == null) {
-                throw new IllegalArgumentException("id shouldn't be null");
-            }
-            mId = id;
-            return (T) this;
+        @NonNull U setId(@NonNull String id) {
+            mProvider.setId_impl(id);
+            return (U) this;
         }
 
         /**
-         * Set {@link SessionCallback}.
+         * Set callback for the session.
          *
          * @param executor callback executor
          * @param callback session callback.
          * @return
          */
-        public T setSessionCallback(@NonNull @CallbackExecutor Executor executor,
+        @NonNull U setSessionCallback(@NonNull @CallbackExecutor Executor executor,
                 @NonNull C callback) {
-            if (executor == null) {
-                throw new IllegalArgumentException("executor shouldn't be null");
-            }
-            if (callback == null) {
-                throw new IllegalArgumentException("callback shouldn't be null");
-            }
-            mCallbackExecutor = executor;
-            mCallback = callback;
-            return (T) this;
+            mProvider.setSessionCallback_impl(executor, callback);
+            return (U) this;
         }
 
         /**
@@ -581,7 +639,9 @@
          * @throws IllegalStateException if the session with the same id is already exists for the
          *      package.
          */
-        public abstract MediaSession2 build() throws IllegalStateException;
+        @NonNull T build() {
+            return mProvider.build_impl();
+        }
     }
 
     /**
@@ -590,47 +650,70 @@
      * Any incoming event from the {@link MediaController2} will be handled on the thread
      * that created session with the {@link Builder#build()}.
      */
-    // TODO(jaewan): Move this to updatable
-    // TODO(jaewan): Add setRatingType()
-    // TODO(jaewan): Add setSessionActivity()
-    public static final class Builder extends BuilderBase<Builder, SessionCallback> {
-        public Builder(Context context, @NonNull MediaPlayerBase player) {
-            super(context, player);
+    // Override all methods just to show them with the type instead of generics in Javadoc.
+    // This workarounds javadoc issue described in the MediaSession2.BuilderBase.
+    public static final class Builder extends BuilderBase<MediaSession2, Builder, SessionCallback> {
+        public Builder(Context context) {
+            super((instance) -> ApiLoader.getProvider().createMediaSession2Builder(
+                    context, (Builder) instance));
         }
 
         @Override
-        public MediaSession2 build() throws IllegalStateException {
-            if (mCallback == null) {
-                mCallback = new SessionCallback();
-            }
-            return new MediaSession2(mContext, mPlayer, mId, mCallbackExecutor, mCallback,
-                    mVolumeProvider, mRatingType, mSessionActivity);
+        public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
+            return super.setPlayer(player);
+        }
+
+        @Override
+        public Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
+            return super.setPlaylistAgent(playlistAgent);
+        }
+
+        @Override
+        public @NonNull Builder setVolumeProvider(@Nullable VolumeProvider2 volumeProvider) {
+            return super.setVolumeProvider(volumeProvider);
+        }
+
+        @Override
+        public @NonNull Builder setSessionActivity(@Nullable PendingIntent pi) {
+            return super.setSessionActivity(pi);
+        }
+
+        @Override
+        public @NonNull Builder setId(@NonNull String id) {
+            return super.setId(id);
+        }
+
+        @Override
+        public @NonNull Builder setSessionCallback(@NonNull Executor executor,
+                @Nullable SessionCallback callback) {
+            return super.setSessionCallback(executor, callback);
+        }
+
+        @Override
+        public @NonNull MediaSession2 build() {
+            return super.build();
         }
     }
 
     /**
      * Information of a controller.
      */
-    // TODO(jaewan): Move implementation to the updatable.
     public static final class ControllerInfo {
         private final ControllerInfoProvider mProvider;
 
         /**
          * @hide
          */
-        // TODO(jaewan): SystemApi
-        // TODO(jaewan): Also accept componentName to check notificaiton listener.
-        public ControllerInfo(Context context, int uid, int pid, String packageName,
-                IMediaSession2Callback callback) {
-            mProvider = ApiLoader.getProvider(context)
-                    .createMediaSession2ControllerInfoProvider(
-                            this, context, uid, pid, packageName, callback);
+        public ControllerInfo(@NonNull Context context, int uid, int pid,
+                @NonNull String packageName, @NonNull IInterface callback) {
+            mProvider = ApiLoader.getProvider().createMediaSession2ControllerInfo(
+                    context, this, uid, pid, packageName, callback);
         }
 
         /**
          * @return package name of the controller
          */
-        public String getPackageName() {
+        public @NonNull String getPackageName() {
             return mProvider.getPackageName_impl();
         }
 
@@ -654,10 +737,8 @@
 
         /**
          * @hide
-         * @return
          */
-        // TODO(jaewan): SystemApi
-        public ControllerInfoProvider getProvider() {
+        public @NonNull ControllerInfoProvider getProvider() {
             return mProvider;
         }
 
@@ -668,52 +749,28 @@
 
         @Override
         public boolean equals(Object obj) {
-            if (!(obj instanceof ControllerInfo)) {
-                return false;
-            }
-            ControllerInfo other = (ControllerInfo) obj;
-            return mProvider.equals_impl(other.mProvider);
+            return mProvider.equals_impl(obj);
         }
 
         @Override
         public String toString() {
-            // TODO(jaewan): Move this to updatable.
-            return "ControllerInfo {pkg=" + getPackageName() + ", uid=" + getUid() + ", trusted="
-                    + isTrusted() + "}";
+            return mProvider.toString_impl();
         }
     }
 
     /**
-     * Button for a {@link Command} that will be shown by the controller.
+     * Button for a {@link SessionCommand2} that will be shown by the controller.
      * <p>
      * It's up to the controller's decision to respect or ignore this customization request.
      */
-    // TODO(jaewan): Move this to updatable.
-    public static class CommandButton {
-        private static final String KEY_COMMAND
-                = "android.media.media_session2.command_button.command";
-        private static final String KEY_ICON_RES_ID
-                = "android.media.media_session2.command_button.icon_res_id";
-        private static final String KEY_DISPLAY_NAME
-                = "android.media.media_session2.command_button.display_name";
-        private static final String KEY_EXTRA
-                = "android.media.media_session2.command_button.extra";
-        private static final String KEY_ENABLED
-                = "android.media.media_session2.command_button.enabled";
+    public static final class CommandButton {
+        private final CommandButtonProvider mProvider;
 
-        private Command mCommand;
-        private int mIconResId;
-        private String mDisplayName;
-        private Bundle mExtra;
-        private boolean mEnabled;
-
-        private CommandButton(@Nullable Command command, int iconResId,
-                @Nullable String displayName, Bundle extra, boolean enabled) {
-            mCommand = command;
-            mIconResId = iconResId;
-            mDisplayName = displayName;
-            mExtra = extra;
-            mEnabled = enabled;
+        /**
+         * @hide
+         */
+        public CommandButton(CommandButtonProvider provider) {
+            mProvider = provider;
         }
 
         /**
@@ -722,8 +779,9 @@
          *
          * @return command or {@code null}
          */
-        public @Nullable Command getCommand() {
-            return mCommand;
+        public @Nullable
+        SessionCommand2 getCommand() {
+            return mProvider.getCommand_impl();
         }
 
         /**
@@ -733,7 +791,7 @@
          * @return resource id of the icon. Can be {@code 0}.
          */
         public int getIconResId() {
-            return mIconResId;
+            return mProvider.getIconResId_impl();
         }
 
         /**
@@ -743,7 +801,7 @@
          * @return custom display name. Can be {@code null} or empty.
          */
         public @Nullable String getDisplayName() {
-            return mDisplayName;
+            return mProvider.getDisplayName_impl();
         }
 
         /**
@@ -751,8 +809,8 @@
          *
          * @return
          */
-        public @Nullable Bundle getExtra() {
-            return mExtra;
+        public @Nullable Bundle getExtras() {
+            return mProvider.getExtras_impl();
         }
 
         /**
@@ -761,181 +819,53 @@
          * @return {@code true} if enabled. {@code false} otherwise.
          */
         public boolean isEnabled() {
-            return mEnabled;
+            return mProvider.isEnabled_impl();
         }
 
         /**
          * @hide
          */
-        // TODO(jaewan): @SystemApi
-        public @NonNull Bundle toBundle() {
-            Bundle bundle = new Bundle();
-            bundle.putBundle(KEY_COMMAND, mCommand.toBundle());
-            bundle.putInt(KEY_ICON_RES_ID, mIconResId);
-            bundle.putString(KEY_DISPLAY_NAME, mDisplayName);
-            bundle.putBundle(KEY_EXTRA, mExtra);
-            bundle.putBoolean(KEY_ENABLED, mEnabled);
-            return bundle;
-        }
-
-        /**
-         * @hide
-         */
-        // TODO(jaewan): @SystemApi
-        public static @Nullable CommandButton fromBundle(Bundle bundle) {
-            Builder builder = new Builder();
-            builder.setCommand(Command.fromBundle(bundle.getBundle(KEY_COMMAND)));
-            builder.setIconResId(bundle.getInt(KEY_ICON_RES_ID, 0));
-            builder.setDisplayName(bundle.getString(KEY_DISPLAY_NAME));
-            builder.setExtra(bundle.getBundle(KEY_EXTRA));
-            builder.setEnabled(bundle.getBoolean(KEY_ENABLED));
-            try {
-                return builder.build();
-            } catch (IllegalStateException e) {
-                // Malformed or version mismatch. Return null for now.
-                return null;
-            }
+        public @NonNull CommandButtonProvider getProvider() {
+            return mProvider;
         }
 
         /**
          * Builder for {@link CommandButton}.
          */
-        public static class Builder {
-            private Command mCommand;
-            private int mIconResId;
-            private String mDisplayName;
-            private Bundle mExtra;
-            private boolean mEnabled;
+        public static final class Builder {
+            private final CommandButtonProvider.BuilderProvider mProvider;
 
             public Builder() {
-                mEnabled = true;
+                mProvider = ApiLoader.getProvider().createMediaSession2CommandButtonBuilder(this);
             }
 
-            public Builder setCommand(Command command) {
-                mCommand = command;
-                return this;
+            public @NonNull Builder setCommand(@Nullable SessionCommand2 command) {
+                return mProvider.setCommand_impl(command);
             }
 
-            public Builder setIconResId(int resId) {
-                mIconResId = resId;
-                return this;
+            public @NonNull Builder setIconResId(int resId) {
+                return mProvider.setIconResId_impl(resId);
             }
 
-            public Builder setDisplayName(String displayName) {
-                mDisplayName = displayName;
-                return this;
+            public @NonNull Builder setDisplayName(@Nullable String displayName) {
+                return mProvider.setDisplayName_impl(displayName);
             }
 
-            public Builder setEnabled(boolean enabled) {
-                mEnabled = enabled;
-                return this;
+            public @NonNull Builder setEnabled(boolean enabled) {
+                return mProvider.setEnabled_impl(enabled);
             }
 
-            public Builder setExtra(Bundle extra) {
-                mExtra = extra;
-                return this;
+            public @NonNull Builder setExtras(@Nullable Bundle extras) {
+                return mProvider.setExtras_impl(extras);
             }
 
-            public CommandButton build() {
-                if (mEnabled && mCommand == null) {
-                    throw new IllegalStateException("Enabled button needs Command"
-                            + " for controller to invoke the command");
-                }
-                if (mCommand != null && mCommand.getCommandCode() == COMMAND_CODE_CUSTOM
-                        && (mIconResId == 0 || TextUtils.isEmpty(mDisplayName))) {
-                    throw new IllegalStateException("Custom commands needs icon and"
-                            + " and name to display");
-                }
-                return new CommandButton(mCommand, mIconResId, mDisplayName, mExtra, mEnabled);
+            public @NonNull CommandButton build() {
+                return mProvider.build_impl();
             }
         }
     }
 
     /**
-     * Parameter for the playlist.
-     */
-    // TODO(jaewan): add fromBundle()/toBundle()
-    public static class PlaylistParam {
-        /**
-         * @hide
-         */
-        @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL,
-                REPEAT_MODE_GROUP})
-        @Retention(RetentionPolicy.SOURCE)
-        public @interface RepeatMode {}
-
-        /**
-         * Playback will be stopped at the end of the playing media list.
-         */
-        public static final int REPEAT_MODE_NONE = 0;
-
-        /**
-         * Playback of the current playing media item will be repeated.
-         */
-        public static final int REPEAT_MODE_ONE = 1;
-
-        /**
-         * Playing media list will be repeated.
-         */
-        public static final int REPEAT_MODE_ALL = 2;
-
-        /**
-         * Playback of the playing media group will be repeated.
-         * A group is a logical block of media items which is specified in the section 5.7 of the
-         * Bluetooth AVRCP 1.6.
-         */
-        public static final int REPEAT_MODE_GROUP = 3;
-
-        /**
-         * @hide
-         */
-        @IntDef({SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP})
-        @Retention(RetentionPolicy.SOURCE)
-        public @interface ShuffleMode {}
-
-        /**
-         * Media list will be played in order.
-         */
-        public static final int SHUFFLE_MODE_NONE = 0;
-
-        /**
-         * Media list will be played in shuffled order.
-         */
-        public static final int SHUFFLE_MODE_ALL = 1;
-
-        /**
-         * Media group will be played in shuffled order.
-         * A group is a logical block of media items which is specified in the section 5.7 of the
-         * Bluetooth AVRCP 1.6.
-         */
-        public static final int SHUFFLE_MODE_GROUP = 2;
-
-        private @RepeatMode int mRepeatMode;
-        private @ShuffleMode int mShuffleMode;
-
-        private MediaMetadata2 mPlaylistMetadata;
-
-        public PlaylistParam(@RepeatMode int repeatMode, @ShuffleMode int shuffleMode,
-                @Nullable MediaMetadata2 playlistMetadata) {
-            mRepeatMode = repeatMode;
-            mShuffleMode = shuffleMode;
-            mPlaylistMetadata = playlistMetadata;
-        }
-
-        public @RepeatMode int getRepeatMode() {
-            return mRepeatMode;
-        }
-
-        public @ShuffleMode int getShuffleMode() {
-            return mShuffleMode;
-        }
-
-        public MediaMetadata2 getPlaylistMetadata() {
-            return mPlaylistMetadata;
-        }
-    }
-
-    /**
      * Constructor is hidden and apps can only instantiate indirectly through {@link Builder}.
      * <p>
      * This intended behavior and here's the reasons.
@@ -943,66 +873,45 @@
      *       Whenever it happens only one session was properly setup and others were all dummies.
      *       Android framework couldn't find the right session to dispatch media key event.
      *    2. Simplify session's lifecycle.
-     *       {@link MediaSession} can be available after all of {@link MediaSession#setFlags(int)},
-     *       {@link MediaSession#setCallback(Callback)}, and
-     *       {@link MediaSession#setActive(boolean)}. It was common for an app to omit one, so
-     *       framework had to add heuristics to figure out if an app is
+     *       {@link android.media.session.MediaSession} is available after all of
+     *       {@link android.media.session.MediaSession#setFlags(int)},
+     *       {@link android.media.session.MediaSession#setCallback(
+     *              android.media.session.MediaSession.Callback)},
+     *       and {@link android.media.session.MediaSession#setActive(boolean)}.
+     *       It was common for an app to omit one, so framework had to add heuristics to figure out
+     *       which should be the highest priority for handling media key event.
      * @hide
      */
-    MediaSession2(Context context, MediaPlayerBase player, String id, Executor callbackExecutor,
-            SessionCallback callback, VolumeProvider volumeProvider, int ratingType,
-            PendingIntent sessionActivity) {
+    public MediaSession2(MediaSession2Provider provider) {
         super();
-        mProvider = createProvider(context, player, id, callbackExecutor, callback,
-                volumeProvider, ratingType, sessionActivity);
-    }
-
-    MediaSession2Provider createProvider(Context context, MediaPlayerBase player, String id,
-            Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
-            int ratingType, PendingIntent sessionActivity) {
-        return ApiLoader.getProvider(context)
-                .createMediaSession2(this, context, player, id, callbackExecutor,
-                        callback, volumeProvider, ratingType, sessionActivity);
+        mProvider = provider;
     }
 
     /**
      * @hide
      */
-    // TODO(jaewan): SystemApi
-    public MediaSession2Provider getProvider() {
+    public @NonNull MediaSession2Provider getProvider() {
         return mProvider;
     }
 
     /**
-     * Set the underlying {@link MediaPlayerBase} for this session to dispatch incoming event to.
-     * Events from the {@link MediaController2} will be sent directly to the underlying
-     * player on the {@link Handler} where the session is created on.
+     * Sets the underlying {@link MediaPlayerBase} and {@link MediaPlaylistAgent} for this session
+     * to dispatch incoming event to.
      * <p>
-     * If the new player is successfully set, {@link PlaybackListener}
-     * will be called to tell the current playback state of the new player.
+     * When a {@link MediaPlaylistAgent} is specified here, the playlist agent should manage
+     * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
      * <p>
-     * You can also specify a volume provider. If so, playback in the player is considered as
-     * remote playback.
+     * If the {@link MediaPlaylistAgent} isn't set, session will recreate the default playlist
+     * agent.
      *
-     * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
-     * @throws IllegalArgumentException if the player is {@code null}.
+     * @param player a {@link MediaPlayerBase} that handles actual media playback in your app
+     * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the {@code player}
+     * @param volumeProvider a {@link VolumeProvider2}. If {@code null}, system will adjust the
+     *                       appropriate stream volume for this session's player.
      */
-    public void setPlayer(@NonNull MediaPlayerBase player) {
-        mProvider.setPlayer_impl(player);
-    }
-
-    /**
-     * Set the underlying {@link MediaPlayerBase} with the volume provider for remote playback.
-     *
-     * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
-     * @param volumeProvider a volume provider
-     * @see #setPlayer(MediaPlayerBase)
-     * @see Builder#setVolumeProvider(VolumeProvider)
-     * @throws IllegalArgumentException if a parameter is {@code null}.
-     */
-    public void setPlayer(@NonNull MediaPlayerBase player, @NonNull VolumeProvider volumeProvider)
-            throws IllegalArgumentException {
-        mProvider.setPlayer_impl(player, volumeProvider);
+    public void updatePlayer(@NonNull MediaPlayerBase player,
+            @Nullable MediaPlaylistAgent playlistAgent, @Nullable VolumeProvider2 volumeProvider) {
+        mProvider.updatePlayer_impl(player, playlistAgent, volumeProvider);
     }
 
     @Override
@@ -1013,11 +922,25 @@
     /**
      * @return player
      */
-    public @Nullable MediaPlayerBase getPlayer() {
+    public @NonNull MediaPlayerBase getPlayer() {
         return mProvider.getPlayer_impl();
     }
 
     /**
+     * @return playlist agent
+     */
+    public @NonNull MediaPlaylistAgent getPlaylistAgent() {
+        return mProvider.getPlaylistAgent_impl();
+    }
+
+    /**
+     * @return volume provider
+     */
+    public @Nullable VolumeProvider2 getVolumeProvider() {
+        return mProvider.getVolumeProvider_impl();
+    }
+
+    /**
      * Returns the {@link SessionToken2} for creating {@link MediaController2}.
      */
     public @NonNull
@@ -1030,31 +953,13 @@
     }
 
     /**
-     * Sets the {@link AudioAttributes} to be used during the playback of the video.
+     * Set the {@link AudioFocusRequest} to obtain the audio focus
      *
-     * @param attributes non-null <code>AudioAttributes</code>.
+     * @param afr the full request parameters
      */
-    public void setAudioAttributes(@NonNull AudioAttributes attributes) {
-        mProvider.setAudioAttributes_impl(attributes);
-    }
-
-    /**
-     * Sets which type of audio focus will be requested during the playback, or configures playback
-     * to not request audio focus. Valid values for focus requests are
-     * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
-     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
-     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
-     * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
-     * requested when playback starts. You can for instance use this when playing a silent animation
-     * through this class, and you don't want to affect other audio applications playing in the
-     * background.
-     *
-     * @param focusGain the type of audio focus gain that will be requested, or
-     *                  {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
-     *                  playback.
-     */
-    public void setAudioFocusRequest(int focusGain) {
-        mProvider.setAudioFocusRequest_impl(focusGain);
+    public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
+        // TODO(jaewan): implement this (b/72529899)
+        // mProvider.setAudioFocusRequest_impl(focusGain);
     }
 
     /**
@@ -1071,10 +976,11 @@
      *      expanded row:   layout[5] layout[6] layout[7] layout[8] layout[9]
      *      main row:       layout[3] layout[1] layout[0] layout[2] layout[4]
      * <p>
-     * This API can be called in the {@link SessionCallback#onConnect(ControllerInfo)}.
+     * This API can be called in the {@link SessionCallback#onConnect(
+     * MediaSession2, ControllerInfo)}.
      *
      * @param controller controller to specify layout.
-     * @param layout oredered list of layout.
+     * @param layout ordered list of layout.
      */
     public void setCustomLayout(@NonNull ControllerInfo controller,
             @NonNull List<CommandButton> layout) {
@@ -1088,25 +994,17 @@
      * @param commands new allowed commands
      */
     public void setAllowedCommands(@NonNull ControllerInfo controller,
-            @NonNull CommandGroup commands) {
+            @NonNull SessionCommandGroup2 commands) {
         mProvider.setAllowedCommands_impl(controller, commands);
     }
 
     /**
-     * Notify changes in metadata of previously set playlist. Controller will get the whole set of
-     * playlist again.
-     */
-    public void notifyMetadataChanged() {
-        mProvider.notifyMetadataChanged_impl();
-    }
-
-    /**
      * Send custom command to all connected controllers.
      *
      * @param command a command
      * @param args optional argument
      */
-    public void sendCustomCommand(@NonNull Command command, @Nullable Bundle args) {
+    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
         mProvider.sendCustomCommand_impl(command, args);
     }
 
@@ -1117,73 +1015,54 @@
      * @param args optional argument
      * @param receiver result receiver for the session
      */
-    public void sendCustomCommand(@NonNull ControllerInfo controller, @NonNull Command command,
-            @Nullable Bundle args, @Nullable ResultReceiver receiver) {
+    public void sendCustomCommand(@NonNull ControllerInfo controller,
+            @NonNull SessionCommand2 command, @Nullable Bundle args,
+            @Nullable ResultReceiver receiver) {
         // Equivalent to the MediaController.sendCustomCommand(Action action, ResultReceiver r);
         mProvider.sendCustomCommand_impl(controller, command, args, receiver);
     }
 
     /**
      * Play playback
+     * <p>
+     * This calls {@link MediaPlayerBase#play()}.
      */
     public void play() {
         mProvider.play_impl();
     }
 
     /**
-     * Pause playback
+     * Pause playback.
+     * <p>
+     * This calls {@link MediaPlayerBase#pause()}.
      */
     public void pause() {
         mProvider.pause_impl();
     }
 
     /**
-     * Stop playback
+     * Stop playback, and reset the player to the initial state.
+     * <p>
+     * This calls {@link MediaPlayerBase#reset()}.
      */
     public void stop() {
         mProvider.stop_impl();
     }
 
     /**
-     * Rewind playback
-     */
-    public void skipToPrevious() {
-        mProvider.skipToPrevious_impl();
-    }
-
-    /**
-     * Rewind playback
-     */
-    public void skipToNext() {
-        mProvider.skipToNext_impl();
-    }
-
-    /**
      * Request that the player prepare its playback. In other words, other sessions can continue
      * to play during the preparation of this session. This method can be used to speed up the
      * start of the playback. Once the preparation is done, the session will change its playback
-     * state to {@link PlaybackState#STATE_PAUSED}. Afterwards, {@link #play} can be called to
-     * start playback.
+     * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
+     * to start playback.
+     * <p>
+     * This calls {@link MediaPlayerBase#reset()}.
      */
     public void prepare() {
         mProvider.prepare_impl();
     }
 
     /**
-     * Start fast forwarding. If playback is already fast forwarding this may increase the rate.
-     */
-    public void fastForward() {
-        mProvider.fastForward_impl();
-    }
-
-    /**
-     * Start rewinding. If playback is already rewinding this may increase the rate.
-     */
-    public void rewind() {
-        mProvider.rewind_impl();
-    }
-
-    /**
      * Move to a new location in the media stream.
      *
      * @param pos Position to move to, in milliseconds.
@@ -1193,17 +1072,6 @@
     }
 
     /**
-     * Sets the index of current DataSourceDesc in the play list to be played.
-     *
-     * @param index the index of DataSourceDesc in the play list you want to play
-     * @throws IllegalArgumentException if the play list is null
-     * @throws NullPointerException if index is outside play list range
-     */
-    public void setCurrentPlaylistItem(int index) {
-        mProvider.setCurrentPlaylistItem_impl(index);
-    }
-
-    /**
      * @hide
      */
     public void skipForward() {
@@ -1217,7 +1085,310 @@
         // To match with KEYCODE_MEDIA_SKIP_BACKWARD
     }
 
-    public void setPlaylist(@NonNull List<MediaItem2> playlist, @NonNull PlaylistParam param) {
-        mProvider.setPlaylist_impl(playlist, param);
+    /**
+     * Notify errors to the connected controllers
+     *
+     * @param errorCode error code
+     * @param extras extras
+     */
+    public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
+        mProvider.notifyError_impl(errorCode, extras);
+    }
+
+    /**
+     * Gets the current player state.
+     *
+     * @return the current player state
+     */
+    public @PlayerState int getPlayerState() {
+        return mProvider.getPlayerState_impl();
+    }
+
+    /**
+     * Gets the current position.
+     *
+     * @return the current playback position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME} if
+     *         unknown.
+     */
+    public long getCurrentPosition() {
+        return mProvider.getCurrentPosition_impl();
+    }
+
+    /**
+     * Gets the buffered position, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
+     *
+     * @return the buffered position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
+     */
+    public long getBufferedPosition() {
+        return mProvider.getBufferedPosition_impl();
+    }
+
+    /**
+     * Gets the current buffering state of the player.
+     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
+     * buffered.
+     *
+     * @return the buffering state.
+     */
+    public @BuffState int getBufferingState() {
+        // TODO(jaewan): Implement this
+        return BUFFERING_STATE_UNKNOWN;
+    }
+
+    /**
+     * Get the playback speed.
+     *
+     * @return speed
+     */
+    public float getPlaybackSpeed() {
+        // TODO(jaewan): implement this (b/74093080)
+        return -1;
+    }
+
+    /**
+     * Set the playback speed.
+     */
+    public void setPlaybackSpeed(float speed) {
+        // TODO(jaewan): implement this (b/74093080)
+    }
+
+    /**
+     * Sets the data source missing helper. Helper will be used to provide default implementation of
+     * {@link MediaPlaylistAgent} when it isn't set by developer.
+     * <p>
+     * Default implementation of the {@link MediaPlaylistAgent} will call helper when a
+     * {@link MediaItem2} in the playlist doesn't have a {@link DataSourceDesc}. This may happen
+     * when
+     * <ul>
+     *      <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
+     *          have {@link DataSourceDesc}</li>
+     *      <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
+     *          by {@link SessionCallback#onCommandRequest(
+     *          MediaSession2, ControllerInfo, SessionCommand2)}.
+     *          In that case, an item would be added automatically without the data source.</li>
+     * </ul>
+     * <p>
+     * If it's not set, playback wouldn't happen for the item without data source descriptor.
+     * <p>
+     * The helper will be run on the executor that was specified by
+     * {@link Builder#setSessionCallback(Executor, SessionCallback)}.
+     *
+     * @param helper a data source missing helper.
+     * @throws IllegalStateException when the helper is set when the playlist agent is set
+     * @see #setPlaylist(List, MediaMetadata2)
+     * @see SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)
+     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
+     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
+     */
+    public void setOnDataSourceMissingHelper(@NonNull OnDataSourceMissingHelper helper) {
+        mProvider.setOnDataSourceMissingHelper_impl(helper);
+    }
+
+    /**
+     * Clears the data source missing helper.
+     *
+     * @see #setOnDataSourceMissingHelper(OnDataSourceMissingHelper)
+     */
+    public void clearOnDataSourceMissingHelper() {
+        mProvider.clearOnDataSourceMissingHelper_impl();
+    }
+
+    /**
+     * Returns the playlist from the {@link MediaPlaylistAgent}.
+     * <p>
+     * This list may differ with the list that was specified with
+     * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
+     * implementation. Use media items returned here for other playlist agent APIs such as
+     * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
+     *
+     * @return playlist
+     * @see MediaPlaylistAgent#getPlaylist()
+     * @see SessionCallback#onPlaylistChanged(
+     *          MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+     */
+    public List<MediaItem2> getPlaylist() {
+        return mProvider.getPlaylist_impl();
+    }
+
+    /**
+     * Sets a list of {@link MediaItem2} to the {@link MediaPlaylistAgent}. Ensure uniqueness of
+     * each {@link MediaItem2} in the playlist so the session can uniquely identity individual
+     * items.
+     * <p>
+     * This may be an asynchronous call, and {@link MediaPlaylistAgent} may keep the copy of the
+     * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
+     * List, MediaMetadata2)} to know the operation finishes.
+     * <p>
+     * You may specify a {@link MediaItem2} without {@link DataSourceDesc}. In that case,
+     * {@link MediaPlaylistAgent} has responsibility to dynamically query {@link DataSourceDesc}
+     * when such media item is ready for preparation or play. Default implementation needs
+     * {@link OnDataSourceMissingHelper} for such case.
+     *
+     * @param list A list of {@link MediaItem2} objects to set as a play list.
+     * @throws IllegalArgumentException if given list is {@code null}, or has duplicated media
+     * items.
+     * @see MediaPlaylistAgent#setPlaylist(List, MediaMetadata2)
+     * @see SessionCallback#onPlaylistChanged(
+     *          MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
+     * @see #setOnDataSourceMissingHelper
+     */
+    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
+        mProvider.setPlaylist_impl(list, metadata);
+    }
+
+    /**
+     * Skips to the item in the playlist.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)} and the behavior depends
+     * on the playlist agent implementation, especially with the shuffle/repeat mode.
+     *
+     * @param item The item in the playlist you want to play
+     * @see #getShuffleMode()
+     * @see #getRepeatMode()
+     */
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        mProvider.skipToPlaylistItem_impl(item);
+    }
+
+    /**
+     * Skips to the previous item.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToPreviousItem()} and the behavior depends on the
+     * playlist agent implementation, especially with the shuffle/repeat mode.
+     *
+     * @see #getShuffleMode()
+     * @see #getRepeatMode()
+     **/
+    public void skipToPreviousItem() {
+        mProvider.skipToPreviousItem_impl();
+    }
+
+    /**
+     * Skips to the next item.
+     * <p>
+     * This calls {@link MediaPlaylistAgent#skipToNextItem()} and the behavior depends on the
+     * playlist agent implementation, especially with the shuffle/repeat mode.
+     *
+     * @see #getShuffleMode()
+     * @see #getRepeatMode()
+     */
+    public void skipToNextItem() {
+        mProvider.skipToNextItem_impl();
+    }
+
+    /**
+     * Gets the playlist metadata from the {@link MediaPlaylistAgent}.
+     *
+     * @return the playlist metadata
+     */
+    public MediaMetadata2 getPlaylistMetadata() {
+        return mProvider.getPlaylistMetadata_impl();
+    }
+
+    /**
+     * Adds the media item to the playlist at position index. Index equals or greater than
+     * the current playlist size will add the item at the end of the playlist.
+     * <p>
+     * This will not change the currently playing media item.
+     * If index is less than or equal to the current index of the play list,
+     * the current index of the play list will be incremented correspondingly.
+     *
+     * @param index the index you want to add
+     * @param item the media item you want to add
+     */
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        mProvider.addPlaylistItem_impl(index, item);
+    }
+
+    /**
+     * Removes the media item in the playlist.
+     * <p>
+     * If the item is the currently playing item of the playlist, current playback
+     * will be stopped and playback moves to next source in the list.
+     *
+     * @param item the media item you want to add
+     */
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
+        mProvider.removePlaylistItem_impl(item);
+    }
+
+    /**
+     * Replaces the media item at index in the playlist. This can be also used to update metadata of
+     * an item.
+     *
+     * @param index the index of the item to replace
+     * @param item the new item
+     */
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        mProvider.replacePlaylistItem_impl(index, item);
+    }
+
+    /**
+     * Return currently playing media item.
+     *
+     * @return currently playing media item
+     */
+    public MediaItem2 getCurrentMediaItem() {
+        // TODO(jaewan): Rename provider, and implement (b/74316764)
+        return mProvider.getCurrentPlaylistItem_impl();
+    }
+
+    /**
+     * Updates the playlist metadata to the {@link MediaPlaylistAgent}.
+     *
+     * @param metadata metadata of the playlist
+     */
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        mProvider.updatePlaylistMetadata_impl(metadata);
+    }
+
+    /**
+     * Gets the repeat mode from the {@link MediaPlaylistAgent}.
+     *
+     * @return repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    public @RepeatMode int getRepeatMode() {
+        return mProvider.getRepeatMode_impl();
+    }
+
+    /**
+     * Sets the repeat mode to the {@link MediaPlaylistAgent}.
+     *
+     * @param repeatMode repeat mode
+     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
+     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
+     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
+     */
+    public void setRepeatMode(@RepeatMode int repeatMode) {
+        mProvider.setRepeatMode_impl(repeatMode);
+    }
+
+    /**
+     * Gets the shuffle mode from the {@link MediaPlaylistAgent}.
+     *
+     * @return The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    public @ShuffleMode int getShuffleMode() {
+        return mProvider.getShuffleMode_impl();
+    }
+
+    /**
+     * Sets the shuffle mode to the {@link MediaPlaylistAgent}.
+     *
+     * @param shuffleMode The shuffle mode
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
+     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
+     */
+    public void setShuffleMode(@ShuffleMode int shuffleMode) {
+        mProvider.setShuffleMode_impl(shuffleMode);
     }
 }
diff --git a/android/media/MediaSession2Test.java b/android/media/MediaSession2Test.java
deleted file mode 100644
index 045dcd5..0000000
--- a/android/media/MediaSession2Test.java
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright 2018 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.media.MediaPlayerBase.PlaybackListener;
-import android.media.MediaSession2.Builder;
-import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
-import android.media.session.PlaybackState;
-import android.os.Process;
-import android.os.Looper;
-import android.support.annotation.NonNull;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import java.util.ArrayList;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static android.media.TestUtils.createPlaybackState;
-import static org.junit.Assert.*;
-
-/**
- * Tests {@link MediaSession2}.
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class MediaSession2Test extends MediaSession2TestBase {
-    private static final String TAG = "MediaSession2Test";
-
-    private MediaSession2 mSession;
-    private MockPlayer mPlayer;
-
-    @Before
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        sHandler.postAndSync(() -> {
-            mPlayer = new MockPlayer(0);
-            mSession = new MediaSession2.Builder(mContext, mPlayer).build();
-        });
-    }
-
-    @After
-    @Override
-    public void cleanUp() throws Exception {
-        super.cleanUp();
-        sHandler.postAndSync(() -> {
-            mSession.close();
-        });
-    }
-
-    @Test
-    public void testBuilder() throws Exception {
-        try {
-            MediaSession2.Builder builder = new Builder(mContext, null);
-            fail("null player shouldn't be allowed");
-        } catch (IllegalArgumentException e) {
-            // expected. pass-through
-        }
-        MediaSession2.Builder builder = new Builder(mContext, mPlayer);
-        try {
-            builder.setId(null);
-            fail("null id shouldn't be allowed");
-        } catch (IllegalArgumentException e) {
-            // expected. pass-through
-        }
-    }
-
-    @Test
-    public void testSetPlayer() throws Exception {
-        sHandler.postAndSync(() -> {
-            MockPlayer player = new MockPlayer(0);
-            // Test if setPlayer doesn't crash with various situations.
-            mSession.setPlayer(mPlayer);
-            mSession.setPlayer(player);
-            mSession.close();
-        });
-    }
-
-    @Test
-    public void testPlay() throws Exception {
-        sHandler.postAndSync(() -> {
-            mSession.play();
-            assertTrue(mPlayer.mPlayCalled);
-        });
-    }
-
-    @Test
-    public void testPause() throws Exception {
-        sHandler.postAndSync(() -> {
-            mSession.pause();
-            assertTrue(mPlayer.mPauseCalled);
-        });
-    }
-
-    @Test
-    public void testStop() throws Exception {
-        sHandler.postAndSync(() -> {
-            mSession.stop();
-            assertTrue(mPlayer.mStopCalled);
-        });
-    }
-
-    @Test
-    public void testSkipToNext() throws Exception {
-        sHandler.postAndSync(() -> {
-            mSession.skipToNext();
-            assertTrue(mPlayer.mSkipToNextCalled);
-        });
-    }
-
-    @Test
-    public void testSkipToPrevious() throws Exception {
-        sHandler.postAndSync(() -> {
-            mSession.skipToPrevious();
-            assertTrue(mPlayer.mSkipToPreviousCalled);
-        });
-    }
-
-    @Test
-    public void testPlaybackStateChangedListener() throws InterruptedException {
-        // TODO(jaewan): Add equivalent tests again
-        /*
-        final CountDownLatch latch = new CountDownLatch(2);
-        final MockPlayer player = new MockPlayer(0);
-        final PlaybackListener listener = (state) -> {
-            assertEquals(sHandler.getLooper(), Looper.myLooper());
-            assertNotNull(state);
-            switch ((int) latch.getCount()) {
-                case 2:
-                    assertEquals(PlaybackState.STATE_PLAYING, state.getState());
-                    break;
-                case 1:
-                    assertEquals(PlaybackState.STATE_PAUSED, state.getState());
-                    break;
-                case 0:
-                    fail();
-            }
-            latch.countDown();
-        };
-        player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
-        sHandler.postAndSync(() -> {
-            mSession.addPlaybackListener(listener, sHandler);
-            // When the player is set, listeners will be notified about the player's current state.
-            mSession.setPlayer(player);
-        });
-        player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
-        assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-        */
-    }
-
-    @Test
-    public void testBadPlayer() throws InterruptedException {
-        // TODO(jaewan): Add equivalent tests again
-        /*
-        final CountDownLatch latch = new CountDownLatch(3); // expected call + 1
-        final BadPlayer player = new BadPlayer(0);
-        sHandler.postAndSync(() -> {
-            mSession.addPlaybackListener((state) -> {
-                // This will be called for every setPlayer() calls, but no more.
-                assertNull(state);
-                latch.countDown();
-            }, sHandler);
-            mSession.setPlayer(player);
-            mSession.setPlayer(mPlayer);
-        });
-        player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
-        assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        */
-    }
-
-    private static class BadPlayer extends MockPlayer {
-        public BadPlayer(int count) {
-            super(count);
-        }
-
-        @Override
-        public void removePlaybackListener(@NonNull PlaybackListener listener) {
-            // No-op. This bad player will keep push notification to the listener that is previously
-            // registered by session.setPlayer().
-        }
-    }
-
-    @Test
-    public void testOnCommandCallback() throws InterruptedException {
-        final MockOnCommandCallback callback = new MockOnCommandCallback();
-        sHandler.postAndSync(() -> {
-            mSession.close();
-            mPlayer = new MockPlayer(1);
-            mSession = new MediaSession2.Builder(mContext, mPlayer)
-                    .setSessionCallback(sHandlerExecutor, callback).build();
-        });
-        MediaController2 controller = createController(mSession.getToken());
-        controller.pause();
-        assertFalse(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        assertFalse(mPlayer.mPauseCalled);
-        assertEquals(1, callback.commands.size());
-        assertEquals(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE,
-                (long) callback.commands.get(0).getCommandCode());
-        controller.skipToNext();
-        assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        assertTrue(mPlayer.mSkipToNextCalled);
-        assertFalse(mPlayer.mPauseCalled);
-        assertEquals(2, callback.commands.size());
-        assertEquals(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM,
-                (long) callback.commands.get(1).getCommandCode());
-    }
-
-    @Test
-    public void testOnConnectCallback() throws InterruptedException {
-        final MockOnConnectCallback sessionCallback = new MockOnConnectCallback();
-        sHandler.postAndSync(() -> {
-            mSession.close();
-            mSession = new MediaSession2.Builder(mContext, mPlayer)
-                    .setSessionCallback(sHandlerExecutor, sessionCallback).build();
-        });
-        MediaController2 controller =
-                createController(mSession.getToken(), false, null);
-        assertNotNull(controller);
-        waitForConnect(controller, false);
-        waitForDisconnect(controller, true);
-    }
-
-    public class MockOnConnectCallback extends SessionCallback {
-        @Override
-        public MediaSession2.CommandGroup onConnect(ControllerInfo controllerInfo) {
-            if (Process.myUid() != controllerInfo.getUid()) {
-                return null;
-            }
-            assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
-            assertEquals(Process.myUid(), controllerInfo.getUid());
-            assertFalse(controllerInfo.isTrusted());
-            // Reject all
-            return null;
-        }
-    }
-
-    public class MockOnCommandCallback extends SessionCallback {
-        public final ArrayList<MediaSession2.Command> commands = new ArrayList<>();
-
-        @Override
-        public boolean onCommandRequest(ControllerInfo controllerInfo, MediaSession2.Command command) {
-            assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
-            assertEquals(Process.myUid(), controllerInfo.getUid());
-            assertFalse(controllerInfo.isTrusted());
-            commands.add(command);
-            if (command.getCommandCode() == MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE) {
-                return false;
-            }
-            return true;
-        }
-    }
-}
diff --git a/android/media/MediaSession2TestBase.java b/android/media/MediaSession2TestBase.java
deleted file mode 100644
index 96afcb9..0000000
--- a/android/media/MediaSession2TestBase.java
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright 2018 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 static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import android.content.Context;
-import android.media.MediaController2.ControllerCallback;
-import android.media.MediaSession2.CommandGroup;
-import android.os.Bundle;
-import android.os.HandlerThread;
-import android.support.annotation.CallSuper;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.test.InstrumentationRegistry;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-/**
- * Base class for session test.
- */
-abstract class MediaSession2TestBase {
-    // Expected success
-    static final int WAIT_TIME_MS = 1000;
-
-    // Expected timeout
-    static final int TIMEOUT_MS = 500;
-
-    static TestUtils.SyncHandler sHandler;
-    static Executor sHandlerExecutor;
-
-    Context mContext;
-    private List<MediaController2> mControllers = new ArrayList<>();
-
-    interface TestControllerInterface {
-        ControllerCallback getCallback();
-    }
-
-    interface TestControllerCallbackInterface {
-        // Currently empty. Add methods in ControllerCallback/BrowserCallback that you want to test.
-
-        // Browser specific callbacks
-        default void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {}
-    }
-
-    interface WaitForConnectionInterface {
-        void waitForConnect(boolean expect) throws InterruptedException;
-        void waitForDisconnect(boolean expect) throws InterruptedException;
-    }
-
-    @BeforeClass
-    public static void setUpThread() {
-        if (sHandler == null) {
-            HandlerThread handlerThread = new HandlerThread("MediaSession2TestBase");
-            handlerThread.start();
-            sHandler = new TestUtils.SyncHandler(handlerThread.getLooper());
-            sHandlerExecutor = (runnable) -> {
-                sHandler.post(runnable);
-            };
-        }
-    }
-
-    @AfterClass
-    public static void cleanUpThread() {
-        if (sHandler != null) {
-            sHandler.getLooper().quitSafely();
-            sHandler = null;
-            sHandlerExecutor = null;
-        }
-    }
-
-    @CallSuper
-    public void setUp() throws Exception {
-        mContext = InstrumentationRegistry.getTargetContext();
-    }
-
-    @CallSuper
-    public void cleanUp() throws Exception {
-        for (int i = 0; i < mControllers.size(); i++) {
-            mControllers.get(i).close();
-        }
-    }
-
-    final MediaController2 createController(SessionToken2 token) throws InterruptedException {
-        return createController(token, true, null);
-    }
-
-    final MediaController2 createController(@NonNull SessionToken2 token,
-            boolean waitForConnect, @Nullable TestControllerCallbackInterface callback)
-            throws InterruptedException {
-        TestControllerInterface instance = onCreateController(token, callback);
-        if (!(instance instanceof MediaController2)) {
-            throw new RuntimeException("Test has a bug. Expected MediaController2 but returned "
-                    + instance);
-        }
-        MediaController2 controller = (MediaController2) instance;
-        mControllers.add(controller);
-        if (waitForConnect) {
-            waitForConnect(controller, true);
-        }
-        return controller;
-    }
-
-    private static WaitForConnectionInterface getWaitForConnectionInterface(
-            MediaController2 controller) {
-        if (!(controller instanceof TestControllerInterface)) {
-            throw new RuntimeException("Test has a bug. Expected controller implemented"
-                    + " TestControllerInterface but got " + controller);
-        }
-        ControllerCallback callback = ((TestControllerInterface) controller).getCallback();
-        if (!(callback instanceof WaitForConnectionInterface)) {
-            throw new RuntimeException("Test has a bug. Expected controller with callback "
-                    + " implemented WaitForConnectionInterface but got " + controller);
-        }
-        return (WaitForConnectionInterface) callback;
-    }
-
-    public static void waitForConnect(MediaController2 controller, boolean expected)
-            throws InterruptedException {
-        getWaitForConnectionInterface(controller).waitForConnect(expected);
-    }
-
-    public static void waitForDisconnect(MediaController2 controller, boolean expected)
-            throws InterruptedException {
-        getWaitForConnectionInterface(controller).waitForDisconnect(expected);
-    }
-
-    TestControllerInterface onCreateController(@NonNull SessionToken2 token,
-            @NonNull TestControllerCallbackInterface callback) {
-        return new TestMediaController(mContext, token, new TestControllerCallback(callback));
-    }
-
-    public static class TestControllerCallback extends MediaController2.ControllerCallback
-            implements WaitForConnectionInterface {
-        public final TestControllerCallbackInterface mCallbackProxy;
-        public final CountDownLatch connectLatch = new CountDownLatch(1);
-        public final CountDownLatch disconnectLatch = new CountDownLatch(1);
-
-        TestControllerCallback(TestControllerCallbackInterface callbackProxy) {
-            mCallbackProxy = callbackProxy;
-        }
-
-        @CallSuper
-        @Override
-        public void onConnected(CommandGroup commands) {
-            super.onConnected(commands);
-            connectLatch.countDown();
-        }
-
-        @CallSuper
-        @Override
-        public void onDisconnected() {
-            super.onDisconnected();
-            disconnectLatch.countDown();
-        }
-
-        @Override
-        public void waitForConnect(boolean expect) throws InterruptedException {
-            if (expect) {
-                assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-            } else {
-                assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-            }
-        }
-
-        @Override
-        public void waitForDisconnect(boolean expect) throws InterruptedException {
-            if (expect) {
-                assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-            } else {
-                assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-            }
-        }
-    }
-
-    public class TestMediaController extends MediaController2 implements TestControllerInterface {
-        private final ControllerCallback mCallback;
-
-        public TestMediaController(@NonNull Context context, @NonNull SessionToken2 token,
-                @NonNull ControllerCallback callback) {
-            super(context, token, callback, sHandlerExecutor);
-            mCallback = callback;
-        }
-
-        @Override
-        public ControllerCallback getCallback() {
-            return mCallback;
-        }
-    }
-}
diff --git a/android/media/MediaSessionManager_MediaSession2.java b/android/media/MediaSessionManager_MediaSession2.java
deleted file mode 100644
index 192cbc2..0000000
--- a/android/media/MediaSessionManager_MediaSession2.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright 2018 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.content.Context;
-import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
-import android.media.session.MediaSessionManager;
-import android.media.session.PlaybackState;
-import android.support.test.filters.SmallTest;
-import android.support.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import static android.media.TestUtils.createPlaybackState;
-import static org.junit.Assert.*;
-
-/**
- * Tests {@link MediaSessionManager} with {@link MediaSession2} specific APIs.
- */
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-@Ignore
-// TODO(jaewan): Reenable test when the media session service detects newly installed sesison
-//               service app.
-public class MediaSessionManager_MediaSession2 extends MediaSession2TestBase {
-    private static final String TAG = "MediaSessionManager_MediaSession2";
-
-    private MediaSessionManager mManager;
-    private MediaSession2 mSession;
-
-    @Before
-    @Override
-    public void setUp() throws Exception {
-        super.setUp();
-        mManager = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
-
-        // Specify TAG here so {@link MediaSession2.getInstance()} doesn't complaint about
-        // per test thread differs across the {@link MediaSession2} with the same TAG.
-        final MockPlayer player = new MockPlayer(1);
-        sHandler.postAndSync(() -> {
-            mSession = new MediaSession2.Builder(mContext, player).setId(TAG).build();
-        });
-        ensureChangeInSession();
-    }
-
-    @After
-    @Override
-    public void cleanUp() throws Exception {
-        super.cleanUp();
-        sHandler.removeCallbacksAndMessages(null);
-        sHandler.postAndSync(() -> {
-            mSession.close();
-        });
-    }
-
-    // TODO(jaewan): Make this host-side test to see per-user behavior.
-    @Test
-    public void testGetMediaSession2Tokens_hasMediaController() throws InterruptedException {
-        final MockPlayer player = (MockPlayer) mSession.getPlayer();
-        player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_STOPPED));
-
-        MediaController2 controller = null;
-        List<SessionToken2> tokens = mManager.getActiveSessionTokens();
-        assertNotNull(tokens);
-        for (int i = 0; i < tokens.size(); i++) {
-            SessionToken2 token = tokens.get(i);
-            if (mContext.getPackageName().equals(token.getPackageName())
-                    && TAG.equals(token.getId())) {
-                assertNotNull(token.getSessionBinder());
-                assertNull(controller);
-                controller = createController(token);
-            }
-        }
-        assertNotNull(controller);
-
-        // Test if the found controller is correct one.
-        assertEquals(PlaybackState.STATE_STOPPED, controller.getPlaybackState().getState());
-        controller.play();
-
-        assertTrue(player.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-        assertTrue(player.mPlayCalled);
-    }
-
-    /**
-     * Test if server recognizes session even if session refuses the connection from server.
-     *
-     * @throws InterruptedException
-     */
-    @Test
-    public void testGetSessionTokens_sessionRejected() throws InterruptedException {
-        sHandler.postAndSync(() -> {
-            mSession.close();
-            mSession = new MediaSession2.Builder(mContext, new MockPlayer(0)).setId(TAG)
-                    .setSessionCallback(sHandlerExecutor, new SessionCallback() {
-                        @Override
-                        public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
-                            // Reject all connection request.
-                            return null;
-                        }
-                    }).build();
-        });
-        ensureChangeInSession();
-
-        boolean foundSession = false;
-        List<SessionToken2> tokens = mManager.getActiveSessionTokens();
-        assertNotNull(tokens);
-        for (int i = 0; i < tokens.size(); i++) {
-            SessionToken2 token = tokens.get(i);
-            if (mContext.getPackageName().equals(token.getPackageName())
-                    && TAG.equals(token.getId())) {
-                assertFalse(foundSession);
-                foundSession = true;
-            }
-        }
-        assertTrue(foundSession);
-    }
-
-    @Test
-    public void testGetMediaSession2Tokens_playerRemoved() throws InterruptedException {
-        // Release
-        sHandler.postAndSync(() -> {
-            mSession.close();
-        });
-        ensureChangeInSession();
-
-        // When the mSession's player becomes null, it should lose binder connection between server.
-        // So server will forget the session.
-        List<SessionToken2> tokens = mManager.getActiveSessionTokens();
-        for (int i = 0; i < tokens.size(); i++) {
-            SessionToken2 token = tokens.get(i);
-            assertFalse(mContext.getPackageName().equals(token.getPackageName())
-                    && TAG.equals(token.getId()));
-        }
-    }
-
-    @Test
-    public void testGetMediaSessionService2Token() throws InterruptedException {
-        boolean foundTestSessionService = false;
-        boolean foundTestLibraryService = false;
-        List<SessionToken2> tokens = mManager.getSessionServiceTokens();
-        for (int i = 0; i < tokens.size(); i++) {
-            SessionToken2 token = tokens.get(i);
-            if (mContext.getPackageName().equals(token.getPackageName())
-                    && MockMediaSessionService2.ID.equals(token.getId())) {
-                assertFalse(foundTestSessionService);
-                assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
-                assertNull(token.getSessionBinder());
-                foundTestSessionService = true;
-            } else if (mContext.getPackageName().equals(token.getPackageName())
-                    && MockMediaLibraryService2.ID.equals(token.getId())) {
-                assertFalse(foundTestLibraryService);
-                assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
-                assertNull(token.getSessionBinder());
-                foundTestLibraryService = true;
-            }
-        }
-        assertTrue(foundTestSessionService);
-        assertTrue(foundTestLibraryService);
-    }
-
-    @Test
-    public void testGetAllSessionTokens() throws InterruptedException {
-        boolean foundTestSession = false;
-        boolean foundTestSessionService = false;
-        boolean foundTestLibraryService = false;
-        List<SessionToken2> tokens = mManager.getAllSessionTokens();
-        for (int i = 0; i < tokens.size(); i++) {
-            SessionToken2 token = tokens.get(i);
-            if (!mContext.getPackageName().equals(token.getPackageName())) {
-                continue;
-            }
-            switch (token.getId()) {
-                case TAG:
-                    assertFalse(foundTestSession);
-                    foundTestSession = true;
-                    break;
-                case MockMediaSessionService2.ID:
-                    assertFalse(foundTestSessionService);
-                    foundTestSessionService = true;
-                    assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
-                    break;
-                case MockMediaLibraryService2.ID:
-                    assertFalse(foundTestLibraryService);
-                    assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
-                    foundTestLibraryService = true;
-                    break;
-                default:
-                    fail("Unexpected session " + token + " exists in the package");
-            }
-        }
-        assertTrue(foundTestSession);
-        assertTrue(foundTestSessionService);
-        assertTrue(foundTestLibraryService);
-    }
-
-    // Ensures if the session creation/release is notified to the server.
-    private void ensureChangeInSession() throws InterruptedException {
-        // TODO(jaewan): Wait by listener.
-        Thread.sleep(WAIT_TIME_MS);
-    }
-}
diff --git a/android/media/MediaSessionService2.java b/android/media/MediaSessionService2.java
index 19814f0..6c3a4bf 100644
--- a/android/media/MediaSessionService2.java
+++ b/android/media/MediaSessionService2.java
@@ -23,12 +23,13 @@
 import android.app.Service;
 import android.content.Intent;
 import android.media.MediaSession2.ControllerInfo;
-import android.media.session.PlaybackState;
 import android.media.update.ApiLoader;
 import android.media.update.MediaSessionService2Provider;
+import android.media.update.MediaSessionService2Provider.MediaNotificationProvider;
 import android.os.IBinder;
 
 /**
+ * @hide
  * Base class for media session services, which is the service version of the {@link MediaSession2}.
  * <p>
  * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants
@@ -84,12 +85,13 @@
  * session service, the controller binds to the session service. {@link #onCreateSession(String)}
  * may be called after the {@link #onCreate} if the service hasn't created yet.
  * <p>
- * After the binding, session's {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)}
+ * After the binding, session's {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}
+ *
  * will be called to accept or reject connection request from a controller. If the connection is
  * rejected, the controller will unbind. If it's accepted, the controller will be available to use
  * and keep binding.
  * <p>
- * When playback is started for this session service, {@link #onUpdateNotification(PlaybackState)}
+ * When playback is started for this session service, {@link #onUpdateNotification()}
  * is called and service would become a foreground service. It's needed to keep playback after the
  * controller is destroyed. The session service becomes background service when the playback is
  * stopped.
@@ -98,21 +100,8 @@
  * <p>
  * Any app can bind to the session service with controller, but the controller can be used only if
  * the session service accepted the connection request through
- * {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)}.
- *
- * @hide
+ * {@link MediaSession2.SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
  */
-// TODO(jaewan): Unhide
-// TODO(jaewan): Can we clean up sessions in onDestroy() automatically instead?
-//               What about currently running SessionCallback when the onDestroy() is called?
-// TODO(jaewan): Protect this with system|privilleged permission - Q.
-// TODO(jaewan): Add permission check for the service to know incoming connection request.
-//               Follow-up questions: What about asking a XML for list of white/black packages for
-//                                    allowing enumeration?
-//                                    We can read the information even when the service is started,
-//                                    so SessionManager.getXXXXService() can only return apps
-//                                    TODO(jaewan): Will be the black/white listing persistent?
-//                                                  In other words, can we cache the rejection?
 public abstract class MediaSessionService2 extends Service {
     private final MediaSessionService2Provider mProvider;
 
@@ -134,7 +123,7 @@
     }
 
     MediaSessionService2Provider createProvider() {
-        return ApiLoader.getProvider(this).createMediaSessionService2(this);
+        return ApiLoader.getProvider().createMediaSessionService2(this);
     }
 
     /**
@@ -169,27 +158,27 @@
     public @NonNull abstract MediaSession2 onCreateSession(String sessionId);
 
     /**
-     * Called when the playback state of this session is changed, and notification needs update.
-     * Override this method to show your own notification UI.
+     * Called when the playback state of this session is changed so notification needs update.
+     * Override this method to show or cancel your own notification UI.
      * <p>
      * With the notification returned here, the service become foreground service when the playback
      * is started. It becomes background service after the playback is stopped.
      *
-     * @param state playback state
      * @return a {@link MediaNotification}. If it's {@code null}, notification wouldn't be shown.
      */
-    // TODO(jaewan): Also add metadata
-    public MediaNotification onUpdateNotification(PlaybackState2 state) {
-        return mProvider.onUpdateNotification_impl(state);
+    public @Nullable MediaNotification onUpdateNotification() {
+        return mProvider.onUpdateNotification_impl();
     }
 
     /**
      * Get instance of the {@link MediaSession2} that you've previously created with the
      * {@link #onCreateSession} for this service.
+     * <p>
+     * This may be {@code null} before the {@link #onCreate()} is finished.
      *
      * @return created session
      */
-    public final MediaSession2 getSession() {
+    public final @Nullable MediaSession2 getSession() {
         return mProvider.getSession_impl();
     }
 
@@ -213,35 +202,32 @@
     }
 
     /**
-     * Returned by {@link #onUpdateNotification(PlaybackState)} for making session service
-     * foreground service to keep playback running in the background. It's highly recommended to
-     * show media style notification here.
+     * Returned by {@link #onUpdateNotification()} for making session service foreground service
+     * to keep playback running in the background. It's highly recommended to show media style
+     * notification here.
      */
-    // TODO(jaewan): Should we also move this to updatable?
     public static class MediaNotification {
-        public final int id;
-        public final Notification notification;
-
-        private MediaNotification(int id, @NonNull Notification notification) {
-            this.id = id;
-            this.notification = notification;
-        }
+        private final MediaNotificationProvider mProvider;
 
         /**
-         * Create a {@link MediaNotification}.
+         * Default constructor
          *
          * @param notificationId notification id to be used for
          *      {@link android.app.NotificationManager#notify(int, Notification)}.
          * @param notification a notification to make session service foreground service. Media
          *      style notification is recommended here.
-         * @return
          */
-        public static MediaNotification create(int notificationId,
-                @NonNull Notification notification) {
-            if (notification == null) {
-                throw new IllegalArgumentException("Notification cannot be null");
-            }
-            return new MediaNotification(notificationId, notification);
+        public MediaNotification(int notificationId, @NonNull Notification notification) {
+            mProvider = ApiLoader.getProvider().createMediaSessionService2MediaNotification(
+                    this, notificationId, notification);
+        }
+
+        public int getNotificationId() {
+            return mProvider.getNotificationId_impl();
+        }
+
+        public @NonNull Notification getNotification() {
+            return mProvider.getNotification_impl();
         }
     }
 }
diff --git a/android/media/MediaTimestamp.java b/android/media/MediaTimestamp.java
index 5ea6bbe..938dd14 100644
--- a/android/media/MediaTimestamp.java
+++ b/android/media/MediaTimestamp.java
@@ -37,6 +37,11 @@
 public final class MediaTimestamp
 {
     /**
+     * An unknown media timestamp value
+     */
+    public static final MediaTimestamp TIMESTAMP_UNKNOWN = new MediaTimestamp(-1, -1, 0.0f);
+
+    /**
      * Get the media time of the anchor in microseconds.
      */
     public long getAnchorMediaTimeUs() {
@@ -82,4 +87,15 @@
         nanoTime = 0;
         clockRate = 1.0f;
     }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null || getClass() != obj.getClass()) return false;
+
+        final MediaTimestamp that = (MediaTimestamp) obj;
+        return (this.mediaTimeUs == that.mediaTimeUs)
+                && (this.nanoTime == that.nanoTime)
+                && (this.clockRate == that.clockRate);
+    }
 }
diff --git a/android/media/MicrophoneInfo.java b/android/media/MicrophoneInfo.java
new file mode 100644
index 0000000..d6399a4
--- /dev/null
+++ b/android/media/MicrophoneInfo.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2018 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.IntDef;
+import android.annotation.NonNull;
+import android.util.Pair;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Class providing information on a microphone. It indicates the location and orientation of the
+ * microphone on the device as well as useful information like frequency response and sensitivity.
+ * It can be used by applications implementing special pre processing effects like noise suppression
+ * of beam forming that need to know about precise microphone characteristics in order to adapt
+ * their algorithms.
+ */
+public final class MicrophoneInfo {
+
+    /**
+     * A microphone that the location is unknown.
+     */
+    public static final int LOCATION_UNKNOWN = 0;
+
+    /**
+     * A microphone that locate on main body of the device.
+     */
+    public static final int LOCATION_MAINBODY = 1;
+
+    /**
+     * A microphone that locate on a movable main body of the device.
+     */
+    public static final int LOCATION_MAINBODY_MOVABLE = 2;
+
+    /**
+     * A microphone that locate on a peripheral.
+     */
+    public static final int LOCATION_PERIPHERAL = 3;
+
+    /**
+     * Unknown microphone directionality.
+     */
+    public static final int DIRECTIONALITY_UNKNOWN = 0;
+
+    /**
+     * Microphone directionality type: omni.
+     */
+    public static final int DIRECTIONALITY_OMNI = 1;
+
+    /**
+     * Microphone directionality type: bi-directional.
+     */
+    public static final int DIRECTIONALITY_BI_DIRECTIONAL = 2;
+
+    /**
+     * Microphone directionality type: cardioid.
+     */
+    public static final int DIRECTIONALITY_CARDIOID = 3;
+
+    /**
+     * Microphone directionality type: hyper cardioid.
+     */
+    public static final int DIRECTIONALITY_HYPER_CARDIOID = 4;
+
+    /**
+     * Microphone directionality type: super cardioid.
+     */
+    public static final int DIRECTIONALITY_SUPER_CARDIOID = 5;
+
+    /**
+     * The channel contains raw audio from this microphone.
+     */
+    public static final int CHANNEL_MAPPING_DIRECT = 1;
+
+    /**
+     * The channel contains processed audio from this microphone and possibly another microphone.
+     */
+    public static final int CHANNEL_MAPPING_PROCESSED = 2;
+
+    /**
+     * Value used for when the group of the microphone is unknown.
+     */
+    public static final int GROUP_UNKNOWN = -1;
+
+    /**
+     * Value used for when the index in the group of the microphone is unknown.
+     */
+    public static final int INDEX_IN_THE_GROUP_UNKNOWN = -1;
+
+    /**
+     * Value used for when the position of the microphone is unknown.
+     */
+    public static final Coordinate3F POSITION_UNKNOWN = new Coordinate3F(
+            -Float.MAX_VALUE, -Float.MAX_VALUE, -Float.MAX_VALUE);
+
+    /**
+     * Value used for when the orientation of the microphone is unknown.
+     */
+    public static final Coordinate3F ORIENTATION_UNKNOWN = new Coordinate3F(0.0f, 0.0f, 0.0f);
+
+    /**
+     * Value used for when the sensitivity of the microphone is unknown.
+     */
+    public static final float SENSITIVITY_UNKNOWN = -Float.MAX_VALUE;
+
+    /**
+     * Value used for when the SPL of the microphone is unknown. This value could be used when
+     * maximum SPL or minimum SPL is unknown.
+     */
+    public static final float SPL_UNKNOWN = -Float.MAX_VALUE;
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "LOCATION_" }, value = {
+            LOCATION_UNKNOWN,
+            LOCATION_MAINBODY,
+            LOCATION_MAINBODY_MOVABLE,
+            LOCATION_PERIPHERAL,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MicrophoneLocation {}
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "DIRECTIONALITY_" }, value = {
+            DIRECTIONALITY_UNKNOWN,
+            DIRECTIONALITY_OMNI,
+            DIRECTIONALITY_BI_DIRECTIONAL,
+            DIRECTIONALITY_CARDIOID,
+            DIRECTIONALITY_HYPER_CARDIOID,
+            DIRECTIONALITY_SUPER_CARDIOID,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MicrophoneDirectionality {}
+
+    private Coordinate3F mPosition;
+    private Coordinate3F mOrientation;
+    private String mDeviceId;
+    private String mAddress;
+    private List<Pair<Float, Float>> mFrequencyResponse;
+    private List<Pair<Integer, Integer>> mChannelMapping;
+    private float mMaxSpl;
+    private float mMinSpl;
+    private float mSensitivity;
+    private int mLocation;
+    private int mGroup; /* Usually 0 will be used for main body. */
+    private int mIndexInTheGroup;
+    private int mPortId; /* mPortId will correspond to the id in AudioPort */
+    private int mType;
+    private int mDirectionality;
+
+    MicrophoneInfo(String deviceId, int type, String address, int location,
+            int group, int indexInTheGroup, Coordinate3F position,
+            Coordinate3F orientation, List<Pair<Float, Float>> frequencyResponse,
+            List<Pair<Integer, Integer>> channelMapping, float sensitivity, float maxSpl,
+            float minSpl, int directionality) {
+        mDeviceId = deviceId;
+        mType = type;
+        mAddress = address;
+        mLocation = location;
+        mGroup = group;
+        mIndexInTheGroup = indexInTheGroup;
+        mPosition = position;
+        mOrientation = orientation;
+        mFrequencyResponse = frequencyResponse;
+        mChannelMapping = channelMapping;
+        mSensitivity = sensitivity;
+        mMaxSpl = maxSpl;
+        mMinSpl = minSpl;
+        mDirectionality = directionality;
+    }
+
+    /**
+     * Returns alphanumeric code that uniquely identifies the device.
+     *
+     * @return the description of the microphone
+     */
+    public String getDescription() {
+        return mDeviceId;
+    }
+
+    /**
+     * Returns The system unique device ID that corresponds to the id
+     * returned by {@link AudioDeviceInfo#getId()}.
+     *
+     * @return the microphone's id
+     */
+    public int getId() {
+        return mPortId;
+    }
+
+    /**
+     * @hide
+     * Returns the internal device type (e.g AudioSystem.DEVICE_IN_BUILTIN_MIC).
+     * The internal device type could be used when getting microphone's port id
+     * by matching type and address.
+     *
+     * @return the internal device type
+     */
+    public int getInternalDeviceType() {
+        return mType;
+    }
+
+    /**
+     * Returns the device type identifier of the microphone (e.g AudioDeviceInfo.TYPE_BUILTIN_MIC).
+     *
+     * @return the device type of the microphone
+     */
+    public int getType() {
+        return AudioDeviceInfo.convertInternalDeviceToDeviceType(mType);
+    }
+
+    /**
+     * Returns The "address" string of the microphone that corresponds to the
+     * address returned by {@link AudioDeviceInfo#getAddress()}
+     * @return the address of the microphone
+     */
+    public @NonNull String getAddress() {
+        return mAddress;
+    }
+
+    /**
+     * Returns the location of the microphone. The return value is
+     * one of {@link #LOCATION_UNKNOWN}, {@link #LOCATION_MAINBODY},
+     * {@link #LOCATION_MAINBODY_MOVABLE}, or {@link #LOCATION_PERIPHERAL}.
+     *
+     * @return the location of the microphone
+     */
+    public @MicrophoneLocation int getLocation() {
+        return mLocation;
+    }
+
+    /**
+     * Returns A device group id that can be used to group together microphones on the same
+     * peripheral, attachments or logical groups. Main body is usually group 0.
+     *
+     * @return the group of the microphone or {@link #GROUP_UNKNOWN} if the group is unknown
+     */
+    public int getGroup() {
+        return mGroup;
+    }
+
+    /**
+     * Returns unique index for device within its group.
+     *
+     * @return the microphone's index in its group or {@link #INDEX_IN_THE_GROUP_UNKNOWN} if the
+     * index in the group is unknown
+     */
+    public int getIndexInTheGroup() {
+        return mIndexInTheGroup;
+    }
+
+    /**
+     * Returns A {@link Coordinate3F} object that represents the geometric location of microphone
+     * in meters, from bottom-left-back corner of appliance. X-axis, Y-axis and Z-axis show
+     * as the x, y, z values.
+     *
+     * @return the geometric location of the microphone or {@link #POSITION_UNKNOWN} if the
+     * geometric location is unknown
+     */
+    public Coordinate3F getPosition() {
+        return mPosition;
+    }
+
+    /**
+     * Returns A {@link Coordinate3F} object that represents the orientation of microphone.
+     * X-axis, Y-axis and Z-axis show as the x, y, z value. The orientation will be normalized
+     * such as sqrt(x^2 + y^2 + z^2) equals 1.
+     *
+     * @return the orientation of the microphone or {@link #ORIENTATION_UNKNOWN} if orientation
+     * is unknown
+     */
+    public Coordinate3F getOrientation() {
+        return mOrientation;
+    }
+
+    /**
+     * Returns a {@link android.util.Pair} list of frequency responses.
+     * For every {@link android.util.Pair} in the list, the first value represents frequency in Hz,
+     * and the second value represents response in dB.
+     *
+     * @return the frequency response of the microphone
+     */
+    public List<Pair<Float, Float>> getFrequencyResponse() {
+        return mFrequencyResponse;
+    }
+
+    /**
+     * Returns a {@link android.util.Pair} list for channel mapping, which indicating how this
+     * microphone is used by each channels or a capture stream. For each {@link android.util.Pair},
+     * the first value is channel index, the second value is channel mapping type, which could be
+     * either {@link #CHANNEL_MAPPING_DIRECT} or {@link #CHANNEL_MAPPING_PROCESSED}.
+     * If a channel has contributions from more than one microphone, it is likely the HAL
+     * did some extra processing to combine the sources, but this is to be inferred by the user.
+     * Empty list when the MicrophoneInfo is returned by AudioManager.getMicrophones().
+     * At least one entry when the MicrophoneInfo is returned by AudioRecord.getActiveMicrophones().
+     *
+     * @return a {@link android.util.Pair} list for channel mapping
+     */
+    public List<Pair<Integer, Integer>> getChannelMapping() {
+        return mChannelMapping;
+    }
+
+    /**
+     * Returns the level in dBFS produced by a 1000Hz tone at 94 dB SPL.
+     *
+     * @return the sensitivity of the microphone or {@link #SENSITIVITY_UNKNOWN} if the sensitivity
+     * is unknown
+     */
+    public float getSensitivity() {
+        return mSensitivity;
+    }
+
+    /**
+     * Returns the level in dB of the maximum SPL supported by the device at 1000Hz.
+     *
+     * @return the maximum level in dB or {@link #SPL_UNKNOWN} if maximum SPL is unknown
+     */
+    public float getMaxSpl() {
+        return mMaxSpl;
+    }
+
+    /**
+     * Returns the level in dB of the minimum SPL that can be registered by the device at 1000Hz.
+     *
+     * @return the minimum level in dB or {@link #SPL_UNKNOWN} if minimum SPL is unknown
+     */
+    public float getMinSpl() {
+        return mMinSpl;
+    }
+
+    /**
+     * Returns the directionality of microphone. The return value is one of
+     * {@link #DIRECTIONALITY_UNKNOWN}, {@link #DIRECTIONALITY_OMNI},
+     * {@link #DIRECTIONALITY_BI_DIRECTIONAL}, {@link #DIRECTIONALITY_CARDIOID},
+     * {@link #DIRECTIONALITY_HYPER_CARDIOID}, or {@link #DIRECTIONALITY_SUPER_CARDIOID}.
+     *
+     * @return the directionality of microphone
+     */
+    public @MicrophoneDirectionality int getDirectionality() {
+        return mDirectionality;
+    }
+
+    /**
+     * Set the port id for the device.
+     * @hide
+     */
+    public void setId(int portId) {
+        mPortId = portId;
+    }
+
+    /**
+     * Set the channel mapping for the device.
+     * @hide
+     */
+    public void setChannelMapping(List<Pair<Integer, Integer>> channelMapping) {
+        mChannelMapping = channelMapping;
+    }
+
+    /* A class containing three float value to represent a 3D coordinate */
+    public static final class Coordinate3F {
+        public final float x;
+        public final float y;
+        public final float z;
+
+        Coordinate3F(float x, float y, float z) {
+            this.x = x;
+            this.y = y;
+            this.z = z;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (!(obj instanceof Coordinate3F)) {
+                return false;
+            }
+            Coordinate3F other = (Coordinate3F) obj;
+            return this.x == other.x && this.y == other.y && this.z == other.z;
+        }
+    }
+}
diff --git a/android/media/MiniThumbFile.java b/android/media/MiniThumbFile.java
index 664308c..9899367 100644
--- a/android/media/MiniThumbFile.java
+++ b/android/media/MiniThumbFile.java
@@ -44,13 +44,14 @@
  */
 public class MiniThumbFile {
     private static final String TAG = "MiniThumbFile";
-    private static final int MINI_THUMB_DATA_FILE_VERSION = 3;
+    private static final int MINI_THUMB_DATA_FILE_VERSION = 4;
     public static final int BYTES_PER_MINTHUMB = 10000;
     private static final int HEADER_SIZE = 1 + 8 + 4;
     private Uri mUri;
     private RandomAccessFile mMiniThumbFile;
     private FileChannel mChannel;
     private ByteBuffer mBuffer;
+    private ByteBuffer mEmptyBuffer;
     private static final Hashtable<String, MiniThumbFile> sThumbFiles =
         new Hashtable<String, MiniThumbFile>();
 
@@ -127,9 +128,10 @@
         return mMiniThumbFile;
     }
 
-    public MiniThumbFile(Uri uri) {
+    private MiniThumbFile(Uri uri) {
         mUri = uri;
         mBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
+        mEmptyBuffer = ByteBuffer.allocateDirect(BYTES_PER_MINTHUMB);
     }
 
     public synchronized void deactivate() {
@@ -184,6 +186,54 @@
         return 0;
     }
 
+    public synchronized void eraseMiniThumb(long id) {
+        RandomAccessFile r = miniThumbDataFile();
+        if (r != null) {
+            long pos = id * BYTES_PER_MINTHUMB;
+            FileLock lock = null;
+            try {
+                mBuffer.clear();
+                mBuffer.limit(1 + 8);
+
+                lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false);
+                // check that we can read the following 9 bytes
+                // (1 for the "status" and 8 for the long)
+                if (mChannel.read(mBuffer, pos) == 9) {
+                    mBuffer.position(0);
+                    if (mBuffer.get() == 1) {
+                        long currentMagic = mBuffer.getLong();
+                        if (currentMagic == 0) {
+                            // there is no thumbnail stored here
+                            Log.i(TAG, "no thumbnail for id " + id);
+                            return;
+                        }
+                        // zero out the thumbnail slot
+                        // Log.v(TAG, "clearing slot " + id + ", magic " + currentMagic
+                        //         + " at offset " + pos);
+                        mChannel.write(mEmptyBuffer, pos);
+                    }
+                } else {
+                    // Log.v(TAG, "No slot");
+                }
+            } catch (IOException ex) {
+                Log.v(TAG, "Got exception checking file magic: ", ex);
+            } catch (RuntimeException ex) {
+                // Other NIO related exception like disk full, read only channel..etc
+                Log.e(TAG, "Got exception when reading magic, id = " + id +
+                        ", disk full or mount read-only? " + ex.getClass());
+            } finally {
+                try {
+                    if (lock != null) lock.release();
+                }
+                catch (IOException ex) {
+                    // ignore it.
+                }
+            }
+        } else {
+            // Log.v(TAG, "No data file");
+        }
+    }
+
     public synchronized void saveMiniThumbToFile(byte[] data, long id, long magic)
             throws IOException {
         RandomAccessFile r = miniThumbDataFile();
diff --git a/android/media/MockMediaLibraryService2.java b/android/media/MockMediaLibraryService2.java
deleted file mode 100644
index 14cf257..0000000
--- a/android/media/MockMediaLibraryService2.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
-* Copyright 2018 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 static junit.framework.Assert.fail;
-
-import android.content.Context;
-import android.media.MediaSession2.CommandGroup;
-import android.media.MediaSession2.ControllerInfo;
-import android.media.TestUtils.SyncHandler;
-import android.os.Bundle;
-import android.os.Process;
-
-import javax.annotation.concurrent.GuardedBy;
-
-/**
- * Mock implementation of {@link MediaLibraryService2} for testing.
- */
-public class MockMediaLibraryService2 extends MediaLibraryService2 {
-    // Keep in sync with the AndroidManifest.xml
-    public static final String ID = "TestLibrary";
-
-    public static final String ROOT_ID = "rootId";
-    public static final Bundle EXTRA = new Bundle();
-    static {
-        EXTRA.putString(ROOT_ID, ROOT_ID);
-    }
-    @GuardedBy("MockMediaLibraryService2.class")
-    private static SessionToken2 sToken;
-
-    private MediaLibrarySession mSession;
-
-    @Override
-    public MediaLibrarySession onCreateSession(String sessionId) {
-        final MockPlayer player = new MockPlayer(1);
-        final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
-        try {
-            handler.postAndSync(() -> {
-                TestLibrarySessionCallback callback = new TestLibrarySessionCallback();
-                mSession = new MediaLibrarySessionBuilder(MockMediaLibraryService2.this,
-                        player, (runnable) -> handler.post(runnable), callback)
-                        .setId(sessionId).build();
-            });
-        } catch (InterruptedException e) {
-            fail(e.toString());
-        }
-        return mSession;
-    }
-
-    @Override
-    public void onDestroy() {
-        TestServiceRegistry.getInstance().cleanUp();
-        super.onDestroy();
-    }
-
-    public static SessionToken2 getToken(Context context) {
-        synchronized (MockMediaLibraryService2.class) {
-            if (sToken == null) {
-                sToken = new SessionToken2(SessionToken2.TYPE_LIBRARY_SERVICE,
-                        context.getPackageName(), ID,
-                        MockMediaLibraryService2.class.getName(), null);
-            }
-            return sToken;
-        }
-    }
-
-    private class TestLibrarySessionCallback extends MediaLibrarySessionCallback {
-        @Override
-        public CommandGroup onConnect(ControllerInfo controller) {
-            if (Process.myUid() != controller.getUid()) {
-                // It's system app wants to listen changes. Ignore.
-                return super.onConnect(controller);
-            }
-            TestServiceRegistry.getInstance().setServiceInstance(
-                    MockMediaLibraryService2.this, controller);
-            return super.onConnect(controller);
-        }
-
-        @Override
-        public BrowserRoot onGetRoot(ControllerInfo controller, Bundle rootHints) {
-            return new BrowserRoot(ROOT_ID, EXTRA);
-        }
-    }
-}
\ No newline at end of file
diff --git a/android/media/MockMediaSessionService2.java b/android/media/MockMediaSessionService2.java
deleted file mode 100644
index b058117..0000000
--- a/android/media/MockMediaSessionService2.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright 2018 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 static junit.framework.Assert.fail;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.media.MediaSession2.ControllerInfo;
-import android.media.MediaSession2.SessionCallback;
-import android.media.TestUtils.SyncHandler;
-import android.media.session.PlaybackState;
-import android.os.Process;
-
-/**
- * Mock implementation of {@link android.media.MediaSessionService2} for testing.
- */
-public class MockMediaSessionService2 extends MediaSessionService2 {
-    // Keep in sync with the AndroidManifest.xml
-    public static final String ID = "TestSession";
-
-    private static final String DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID = "media_session_service";
-    private static final int DEFAULT_MEDIA_NOTIFICATION_ID = 1001;
-
-    private NotificationChannel mDefaultNotificationChannel;
-    private MediaSession2 mSession;
-    private NotificationManager mNotificationManager;
-
-    @Override
-    public MediaSession2 onCreateSession(String sessionId) {
-        final MockPlayer player = new MockPlayer(1);
-        final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
-        try {
-            handler.postAndSync(() -> {
-                mSession = new MediaSession2.Builder(MockMediaSessionService2.this, player)
-                        .setId(sessionId).setSessionCallback((runnable)->handler.post(runnable),
-                                new MySessionCallback()).build();
-            });
-        } catch (InterruptedException e) {
-            fail(e.toString());
-        }
-        return mSession;
-    }
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-    }
-
-    @Override
-    public void onDestroy() {
-        TestServiceRegistry.getInstance().cleanUp();
-        super.onDestroy();
-    }
-
-    @Override
-    public MediaNotification onUpdateNotification(PlaybackState2 state) {
-        if (mDefaultNotificationChannel == null) {
-            mDefaultNotificationChannel = new NotificationChannel(
-                    DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
-                    DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
-                    NotificationManager.IMPORTANCE_DEFAULT);
-            mNotificationManager.createNotificationChannel(mDefaultNotificationChannel);
-        }
-        Notification notification = new Notification.Builder(
-                this, DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID)
-                .setContentTitle(getPackageName())
-                .setContentText("Playback state: " + state.getState())
-                .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
-        return MediaNotification.create(DEFAULT_MEDIA_NOTIFICATION_ID, notification);
-    }
-
-    private class MySessionCallback extends SessionCallback {
-        @Override
-        public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
-            if (Process.myUid() != controller.getUid()) {
-                // It's system app wants to listen changes. Ignore.
-                return super.onConnect(controller);
-            }
-            TestServiceRegistry.getInstance().setServiceInstance(
-                    MockMediaSessionService2.this, controller);
-            return super.onConnect(controller);
-        }
-    }
-}
diff --git a/android/media/MockPlayer.java b/android/media/MockPlayer.java
deleted file mode 100644
index fd69309..0000000
--- a/android/media/MockPlayer.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright 2018 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.media.MediaSession2.PlaylistParam;
-import android.media.session.PlaybackState;
-import android.os.Handler;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executor;
-
-/**
- * A mock implementation of {@link MediaPlayerBase} for testing.
- */
-public class MockPlayer extends MediaPlayerBase {
-    public final CountDownLatch mCountDownLatch;
-
-    public boolean mPlayCalled;
-    public boolean mPauseCalled;
-    public boolean mStopCalled;
-    public boolean mSkipToPreviousCalled;
-    public boolean mSkipToNextCalled;
-    public List<PlaybackListenerHolder> mListeners = new ArrayList<>();
-    private PlaybackState2 mLastPlaybackState;
-
-    public MockPlayer(int count) {
-        mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
-    }
-
-    @Override
-    public void play() {
-        mPlayCalled = true;
-        if (mCountDownLatch != null) {
-            mCountDownLatch.countDown();
-        }
-    }
-
-    @Override
-    public void pause() {
-        mPauseCalled = true;
-        if (mCountDownLatch != null) {
-            mCountDownLatch.countDown();
-        }
-    }
-
-    @Override
-    public void stop() {
-        mStopCalled = true;
-        if (mCountDownLatch != null) {
-            mCountDownLatch.countDown();
-        }
-    }
-
-    @Override
-    public void skipToPrevious() {
-        mSkipToPreviousCalled = true;
-        if (mCountDownLatch != null) {
-            mCountDownLatch.countDown();
-        }
-    }
-
-    @Override
-    public void skipToNext() {
-        mSkipToNextCalled = true;
-        if (mCountDownLatch != null) {
-            mCountDownLatch.countDown();
-        }
-    }
-
-
-
-    @Nullable
-    @Override
-    public PlaybackState2 getPlaybackState() {
-        return mLastPlaybackState;
-    }
-
-    @Override
-    public void addPlaybackListener(@NonNull Executor executor,
-            @NonNull PlaybackListener listener) {
-        mListeners.add(new PlaybackListenerHolder(executor, listener));
-    }
-
-    @Override
-    public void removePlaybackListener(@NonNull PlaybackListener listener) {
-        int index = PlaybackListenerHolder.indexOf(mListeners, listener);
-        if (index >= 0) {
-            mListeners.remove(index);
-        }
-    }
-
-    public void notifyPlaybackState(final PlaybackState2 state) {
-        mLastPlaybackState = state;
-        for (int i = 0; i < mListeners.size(); i++) {
-            mListeners.get(i).postPlaybackChange(state);
-        }
-    }
-
-    // No-op. Should be added for test later.
-    @Override
-    public void prepare() {
-    }
-
-    @Override
-    public void seekTo(long pos) {
-    }
-
-    @Override
-    public void fastFoward() {
-    }
-
-    @Override
-    public void rewind() {
-    }
-
-    @Override
-    public AudioAttributes getAudioAttributes() {
-        return null;
-    }
-
-    @Override
-    public void setPlaylist(List<MediaItem2> item, PlaylistParam param) {
-    }
-
-    @Override
-    public void setCurrentPlaylistItem(int index) {
-    }
-}
diff --git a/android/media/PlaybackListenerHolder.java b/android/media/PlaybackListenerHolder.java
deleted file mode 100644
index 4e19d4d..0000000
--- a/android/media/PlaybackListenerHolder.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2018 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.media.MediaPlayerBase.PlaybackListener;
-import android.media.session.PlaybackState;
-import android.os.Handler;
-import android.os.Message;
-import android.support.annotation.NonNull;
-
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * Holds {@link PlaybackListener} with the {@link Handler}.
- */
-public class PlaybackListenerHolder {
-    public final Executor executor;
-    public final PlaybackListener listener;
-
-    public PlaybackListenerHolder(Executor executor, @NonNull PlaybackListener listener) {
-        this.executor = executor;
-        this.listener = listener;
-    }
-
-    public void postPlaybackChange(final PlaybackState2 state) {
-        executor.execute(() -> listener.onPlaybackChanged(state));
-    }
-
-    /**
-     * Returns {@code true} if the given list contains a {@link PlaybackListenerHolder} that holds
-     * the given listener.
-     *
-     * @param list list to check
-     * @param listener listener to check
-     * @return {@code true} if the given list contains listener. {@code false} otherwise.
-     */
-    public static <Holder extends PlaybackListenerHolder> boolean contains(
-            @NonNull List<Holder> list, PlaybackListener listener) {
-        return indexOf(list, listener) >= 0;
-    }
-
-    /**
-     * Returns the index of the {@link PlaybackListenerHolder} that contains the given listener.
-     *
-     * @param list list to check
-     * @param listener listener to check
-     * @return {@code index} of item if the given list contains listener. {@code -1} otherwise.
-     */
-    public static <Holder extends PlaybackListenerHolder> int indexOf(
-            @NonNull List<Holder> list, PlaybackListener listener) {
-        for (int i = 0; i < list.size(); i++) {
-            if (list.get(i).listener == listener) {
-                return i;
-            }
-        }
-        return -1;
-    }
-}
diff --git a/android/media/PlaybackState2.java b/android/media/PlaybackState2.java
deleted file mode 100644
index 46d6f45..0000000
--- a/android/media/PlaybackState2.java
+++ /dev/null
@@ -1,216 +0,0 @@
-/*
- * Copyright 2018 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.IntDef;
-import android.os.Bundle;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * Playback state for a {@link MediaPlayerBase}, to be shared between {@link MediaSession2} and
- * {@link MediaController2}. This includes a playback state {@link #STATE_PLAYING},
- * the current playback position and extra.
- * @hide
- */
-// TODO(jaewan): Move to updatable
-public final class PlaybackState2 {
-    private static final String TAG = "PlaybackState2";
-
-    private static final String KEY_STATE = "android.media.playbackstate2.state";
-
-    // TODO(jaewan): Replace states from MediaPlayer2
-    /**
-     * @hide
-     */
-    @IntDef({STATE_NONE, STATE_STOPPED, STATE_PREPARED, STATE_PAUSED, STATE_PLAYING,
-            STATE_FINISH, STATE_BUFFERING, STATE_ERROR})
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface State {}
-
-    /**
-     * This is the default playback state and indicates that no media has been
-     * added yet, or the performer has been reset and has no content to play.
-     */
-    public final static int STATE_NONE = 0;
-
-    /**
-     * State indicating this item is currently stopped.
-     */
-    public final static int STATE_STOPPED = 1;
-
-    /**
-     * State indicating this item is currently prepared
-     */
-    public final static int STATE_PREPARED = 2;
-
-    /**
-     * State indicating this item is currently paused.
-     */
-    public final static int STATE_PAUSED = 3;
-
-    /**
-     * State indicating this item is currently playing.
-     */
-    public final static int STATE_PLAYING = 4;
-
-    /**
-     * State indicating the playback reaches the end of the item.
-     */
-    public final static int STATE_FINISH = 5;
-
-    /**
-     * State indicating this item is currently buffering and will begin playing
-     * when enough data has buffered.
-     */
-    public final static int STATE_BUFFERING = 6;
-
-    /**
-     * State indicating this item is currently in an error state. The error
-     * message should also be set when entering this state.
-     */
-    public final static int STATE_ERROR = 7;
-
-    /**
-     * Use this value for the position to indicate the position is not known.
-     */
-    public final static long PLAYBACK_POSITION_UNKNOWN = -1;
-
-    private final int mState;
-    private final long mPosition;
-    private final long mBufferedPosition;
-    private final float mSpeed;
-    private final CharSequence mErrorMessage;
-    private final long mUpdateTime;
-    private final long mActiveItemId;
-
-    public PlaybackState2(int state, long position, long updateTime, float speed,
-            long bufferedPosition, long activeItemId, CharSequence error) {
-        mState = state;
-        mPosition = position;
-        mSpeed = speed;
-        mUpdateTime = updateTime;
-        mBufferedPosition = bufferedPosition;
-        mActiveItemId = activeItemId;
-        mErrorMessage = error;
-    }
-
-    @Override
-    public String toString() {
-        StringBuilder bob = new StringBuilder("PlaybackState {");
-        bob.append("state=").append(mState);
-        bob.append(", position=").append(mPosition);
-        bob.append(", buffered position=").append(mBufferedPosition);
-        bob.append(", speed=").append(mSpeed);
-        bob.append(", updated=").append(mUpdateTime);
-        bob.append(", active item id=").append(mActiveItemId);
-        bob.append(", error=").append(mErrorMessage);
-        bob.append("}");
-        return bob.toString();
-    }
-
-    /**
-     * Get the current state of playback. One of the following:
-     * <ul>
-     * <li> {@link PlaybackState2#STATE_NONE}</li>
-     * <li> {@link PlaybackState2#STATE_STOPPED}</li>
-     * <li> {@link PlaybackState2#STATE_PLAYING}</li>
-     * <li> {@link PlaybackState2#STATE_PAUSED}</li>
-     * <li> {@link PlaybackState2#STATE_BUFFERING}</li>
-     * <li> {@link PlaybackState2#STATE_ERROR}</li>
-     * </ul>
-     */
-    @State
-    public int getState() {
-        return mState;
-    }
-
-    /**
-     * Get the current playback position in ms.
-     */
-    public long getPosition() {
-        return mPosition;
-    }
-
-    /**
-     * Get the current buffered position in ms. This is the farthest playback
-     * point that can be reached from the current position using only buffered
-     * content.
-     */
-    public long getBufferedPosition() {
-        return mBufferedPosition;
-    }
-
-    /**
-     * Get the current playback speed as a multiple of normal playback. This
-     * should be negative when rewinding. A value of 1 means normal playback and
-     * 0 means paused.
-     *
-     * @return The current speed of playback.
-     */
-    public float getPlaybackSpeed() {
-        return mSpeed;
-    }
-
-    /**
-     * Get a user readable error message. This should be set when the state is
-     * {@link PlaybackState2#STATE_ERROR}.
-     */
-    public CharSequence getErrorMessage() {
-        return mErrorMessage;
-    }
-
-    /**
-     * Get the elapsed real time at which position was last updated. If the
-     * position has never been set this will return 0;
-     *
-     * @return The last time the position was updated.
-     */
-    public long getLastPositionUpdateTime() {
-        return mUpdateTime;
-    }
-
-    /**
-     * Get the id of the currently active item in the playlist.
-     *
-     * @return The id of the currently active item in the queue
-     */
-    public long getCurrentPlaylistItemIndex() {
-        return mActiveItemId;
-    }
-
-    /**
-     * @return Bundle object for this to share between processes.
-     */
-    public Bundle toBundle() {
-        // TODO(jaewan): Include other variables.
-        Bundle bundle = new Bundle();
-        bundle.putInt(KEY_STATE, mState);
-        return bundle;
-    }
-
-    /**
-     * @param bundle input
-     * @return
-     */
-    public static PlaybackState2 fromBundle(Bundle bundle) {
-        // TODO(jaewan): Include other variables.
-        final int state = bundle.getInt(KEY_STATE);
-        return new PlaybackState2(state, 0, 0, 0, 0, 0, null);
-    }
-}
\ No newline at end of file
diff --git a/android/media/PlayerBase.java b/android/media/PlayerBase.java
index 09449a1..80049ba 100644
--- a/android/media/PlayerBase.java
+++ b/android/media/PlayerBase.java
@@ -47,10 +47,10 @@
 public abstract class PlayerBase {
 
     private static final String TAG = "PlayerBase";
-    private static final boolean DEBUG = false;
-    private static IAudioService sService; //lazy initialization, use getService()
     /** Debug app ops */
     private static final boolean DEBUG_APP_OPS = false;
+    private static final boolean DEBUG = DEBUG_APP_OPS || false;
+    private static IAudioService sService; //lazy initialization, use getService()
 
     // parameters of the player that affect AppOps
     protected AudioAttributes mAttributes;
@@ -102,6 +102,7 @@
             mAppOps.startWatchingMode(AppOpsManager.OP_PLAY_AUDIO,
                     ActivityThread.currentPackageName(), mAppOpsCallback);
         } catch (RemoteException e) {
+            Log.e(TAG, "Error registering appOps callback", e);
             mHasAppOpsPlayAudio = false;
         }
         try {
diff --git a/android/media/Rating2.java b/android/media/Rating2.java
index 67e5e72..9213190 100644
--- a/android/media/Rating2.java
+++ b/android/media/Rating2.java
@@ -16,7 +16,11 @@
 
 package android.media;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.IntDef;
+import android.media.update.ApiLoader;
+import android.media.update.Rating2Provider;
 import android.os.Bundle;
 import android.util.Log;
 
@@ -24,21 +28,17 @@
 import java.lang.annotation.RetentionPolicy;
 
 /**
+ * @hide
  * A class to encapsulate rating information used as content metadata.
  * A rating is defined by its rating style (see {@link #RATING_HEART},
  * {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
  * {@link #RATING_5_STARS} or {@link #RATING_PERCENTAGE}) and the actual rating value (which may
  * be defined as "unrated"), both of which are defined when the rating instance is constructed
  * through one of the factory methods.
- * @hide
  */
-// TODO(jaewan): Move this to updatable
+// New version of Rating with following change
+//   - Don't implement Parcelable for updatable support.
 public final class Rating2 {
-    private static final String TAG = "Rating2";
-
-    private static final String KEY_STYLE = "android.media.rating2.style";
-    private static final String KEY_VALUE = "android.media.rating2.value";
-
     /**
      * @hide
      */
@@ -92,31 +92,45 @@
      */
     public final static int RATING_PERCENTAGE = 6;
 
-    private final static float RATING_NOT_RATED = -1.0f;
+    private final Rating2Provider mProvider;
 
-    private final int mRatingStyle;
-
-    private final float mRatingValue;
-
-    private Rating2(@Style int ratingStyle, float rating) {
-        mRatingStyle = ratingStyle;
-        mRatingValue = rating;
+    /**
+     * @hide
+     */
+    public Rating2(@NonNull Rating2Provider provider) {
+        mProvider = provider;
     }
 
     @Override
     public String toString() {
-        return "Rating2:style=" + mRatingStyle + " rating="
-                + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue));
+        return mProvider.toString_impl();
+    }
+
+    /**
+     * @hide
+     */
+    public Rating2Provider getProvider() {
+        return mProvider;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return mProvider.equals_impl(obj);
+    }
+
+    @Override
+    public int hashCode() {
+        return mProvider.hashCode_impl();
     }
 
     /**
      * Create an instance from bundle object, previoulsy created by {@link #toBundle()}
      *
      * @param bundle bundle
-     * @return new Rating2 instance
+     * @return new Rating2 instance or {@code null} for error
      */
-    public static Rating2 fromBundle(Bundle bundle) {
-        return new Rating2(bundle.getInt(KEY_STYLE), bundle.getFloat(KEY_VALUE));
+    public static Rating2 fromBundle(@Nullable Bundle bundle) {
+        return ApiLoader.getProvider().fromBundle_Rating2(bundle);
     }
 
     /**
@@ -124,10 +138,7 @@
      * @return bundle of this object
      */
     public Bundle toBundle() {
-        Bundle bundle = new Bundle();
-        bundle.putInt(KEY_STYLE, mRatingStyle);
-        bundle.putFloat(KEY_VALUE, mRatingValue);
-        return bundle;
+        return mProvider.toBundle_impl();
     }
 
     /**
@@ -139,18 +150,8 @@
      *    or {@link #RATING_PERCENTAGE}.
      * @return null if an invalid rating style is passed, a new Rating2 instance otherwise.
      */
-    public static Rating2 newUnratedRating(@Style int ratingStyle) {
-        switch(ratingStyle) {
-            case RATING_HEART:
-            case RATING_THUMB_UP_DOWN:
-            case RATING_3_STARS:
-            case RATING_4_STARS:
-            case RATING_5_STARS:
-            case RATING_PERCENTAGE:
-                return new Rating2(ratingStyle, RATING_NOT_RATED);
-            default:
-                return null;
-        }
+    public static @Nullable Rating2 newUnratedRating(@Style int ratingStyle) {
+        return ApiLoader.getProvider().newUnratedRating_Rating2(ratingStyle);
     }
 
     /**
@@ -160,8 +161,8 @@
      * @param hasHeart true for a "heart selected" rating, false for "heart unselected".
      * @return a new Rating2 instance.
      */
-    public static Rating2 newHeartRating(boolean hasHeart) {
-        return new Rating2(RATING_HEART, hasHeart ? 1.0f : 0.0f);
+    public static @Nullable Rating2 newHeartRating(boolean hasHeart) {
+        return ApiLoader.getProvider().newHeartRating_Rating2(hasHeart);
     }
 
     /**
@@ -171,8 +172,8 @@
      * @param thumbIsUp true for a "thumb up" rating, false for "thumb down".
      * @return a new Rating2 instance.
      */
-    public static Rating2 newThumbRating(boolean thumbIsUp) {
-        return new Rating2(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f);
+    public static @Nullable Rating2 newThumbRating(boolean thumbIsUp) {
+        return ApiLoader.getProvider().newThumbRating_Rating2(thumbIsUp);
     }
 
     /**
@@ -187,27 +188,9 @@
      * @return null if the rating style is invalid, or the rating is out of range,
      *     a new Rating2 instance otherwise.
      */
-    public static Rating2 newStarRating(@StarStyle int starRatingStyle, float starRating) {
-        float maxRating = -1.0f;
-        switch(starRatingStyle) {
-            case RATING_3_STARS:
-                maxRating = 3.0f;
-                break;
-            case RATING_4_STARS:
-                maxRating = 4.0f;
-                break;
-            case RATING_5_STARS:
-                maxRating = 5.0f;
-                break;
-            default:
-                Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating");
-                        return null;
-        }
-        if ((starRating < 0.0f) || (starRating > maxRating)) {
-            Log.e(TAG, "Trying to set out of range star-based rating");
-            return null;
-        }
-        return new Rating2(starRatingStyle, starRating);
+    public static @Nullable Rating2 newStarRating(
+            @StarStyle int starRatingStyle, float starRating) {
+        return ApiLoader.getProvider().newStarRating_Rating2(starRatingStyle, starRating);
     }
 
     /**
@@ -217,13 +200,8 @@
      * @param percent the value of the rating
      * @return null if the rating is out of range, a new Rating2 instance otherwise.
      */
-    public static Rating2 newPercentageRating(float percent) {
-        if ((percent < 0.0f) || (percent > 100.0f)) {
-            Log.e(TAG, "Invalid percentage-based rating value");
-            return null;
-        } else {
-            return new Rating2(RATING_PERCENTAGE, percent);
-        }
+    public static @Nullable Rating2 newPercentageRating(float percent) {
+        return ApiLoader.getProvider().newPercentageRating_Rating2(percent);
     }
 
     /**
@@ -231,7 +209,7 @@
      * @return true if the instance was not created with {@link #newUnratedRating(int)}.
      */
     public boolean isRated() {
-        return mRatingValue >= 0.0f;
+        return mProvider.isRated_impl();
     }
 
     /**
@@ -240,9 +218,8 @@
      *    {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
      *    or {@link #RATING_PERCENTAGE}.
      */
-    @Style
-    public int getRatingStyle() {
-        return mRatingStyle;
+    public @Style int getRatingStyle() {
+        return mProvider.getRatingStyle_impl();
     }
 
     /**
@@ -251,11 +228,7 @@
      *    if the rating style is not {@link #RATING_HEART} or if it is unrated.
      */
     public boolean hasHeart() {
-        if (mRatingStyle != RATING_HEART) {
-            return false;
-        } else {
-            return (mRatingValue == 1.0f);
-        }
+        return mProvider.hasHeart_impl();
     }
 
     /**
@@ -264,11 +237,7 @@
      *    if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated.
      */
     public boolean isThumbUp() {
-        if (mRatingStyle != RATING_THUMB_UP_DOWN) {
-            return false;
-        } else {
-            return (mRatingValue == 1.0f);
-        }
+        return mProvider.isThumbUp_impl();
     }
 
     /**
@@ -277,16 +246,7 @@
      *    not star-based, or if it is unrated.
      */
     public float getStarRating() {
-        switch (mRatingStyle) {
-            case RATING_3_STARS:
-            case RATING_4_STARS:
-            case RATING_5_STARS:
-                if (isRated()) {
-                    return mRatingValue;
-                }
-            default:
-                return -1.0f;
-        }
+        return mProvider.getStarRating_impl();
     }
 
     /**
@@ -295,10 +255,6 @@
      *    not percentage-based, or if it is unrated.
      */
     public float getPercentRating() {
-        if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) {
-            return -1.0f;
-        } else {
-            return mRatingValue;
-        }
+        return mProvider.getPercentRating_impl();
     }
 }
diff --git a/android/media/Ringtone.java b/android/media/Ringtone.java
index 209ec42..c0468dc 100644
--- a/android/media/Ringtone.java
+++ b/android/media/Ringtone.java
@@ -40,7 +40,7 @@
  * <p>
  * For ways of retrieving {@link Ringtone} objects or to show a ringtone
  * picker, see {@link RingtoneManager}.
- * 
+ *
  * @see RingtoneManager
  */
 public class Ringtone {
@@ -97,7 +97,7 @@
 
     /**
      * Sets the stream type where this ringtone will be played.
-     * 
+     *
      * @param streamType The stream, see {@link AudioManager}.
      * @deprecated use {@link #setAudioAttributes(AudioAttributes)}
      */
@@ -111,7 +111,7 @@
 
     /**
      * Gets the stream type where this ringtone will be played.
-     * 
+     *
      * @return The stream type, see {@link AudioManager}.
      * @deprecated use of stream types is deprecated, see
      *     {@link #setAudioAttributes(AudioAttributes)}
@@ -146,9 +146,8 @@
     }
 
     /**
-     * @hide
      * Sets the player to be looping or non-looping.
-     * @param looping whether to loop or not
+     * @param looping whether to loop or not.
      */
     public void setLooping(boolean looping) {
         synchronized (mPlaybackSettingsLock) {
@@ -158,7 +157,16 @@
     }
 
     /**
-     * @hide
+     * Returns whether the looping mode was enabled on this player.
+     * @return true if this player loops when playing.
+     */
+    public boolean isLooping() {
+        synchronized (mPlaybackSettingsLock) {
+            return mIsLooping;
+        }
+    }
+
+    /**
      * Sets the volume on this player.
      * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
      *   corresponds to no attenuation being applied.
@@ -173,6 +181,16 @@
     }
 
     /**
+     * Returns the volume scalar set on this player.
+     * @return a value between 0.0f and 1.0f.
+     */
+    public float getVolume() {
+        synchronized (mPlaybackSettingsLock) {
+            return mVolume;
+        }
+    }
+
+    /**
      * Must be called synchronized on mPlaybackSettingsLock
      */
     private void applyPlaybackProperties_sync() {
@@ -194,8 +212,8 @@
     /**
      * Returns a human-presentable title for ringtone. Looks in media
      * content provider. If not in either, uses the filename
-     * 
-     * @param context A context used for querying. 
+     *
+     * @param context A context used for querying.
      */
     public String getTitle(Context context) {
         if (mTitle != null) return mTitle;
@@ -265,12 +283,11 @@
 
         if (title == null) {
             title = context.getString(com.android.internal.R.string.ringtone_unknown);
-            
             if (title == null) {
                 title = "";
             }
         }
-        
+
         return title;
     }
 
@@ -395,7 +412,7 @@
 
     /**
      * Whether this ringtone is currently playing.
-     * 
+     *
      * @return True if playing, false otherwise.
      */
     public boolean isPlaying() {
diff --git a/android/media/RingtoneManager.java b/android/media/RingtoneManager.java
index 3eb9d52..fefa1ed 100644
--- a/android/media/RingtoneManager.java
+++ b/android/media/RingtoneManager.java
@@ -28,11 +28,13 @@
 import android.content.ContentUris;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.UserInfo;
 import android.database.Cursor;
 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
 import android.net.Uri;
 import android.os.Environment;
+import android.os.FileUtils;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
@@ -47,22 +49,17 @@
 
 import com.android.internal.database.SortCursor;
 
-import libcore.io.Streams;
-
 import java.io.Closeable;
 import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.LinkedBlockingQueue;
 
-import static android.content.ContentProvider.maybeAddUserId;
-import static android.content.pm.PackageManager.NameNotFoundException;
-
 /**
  * RingtoneManager provides access to ringtones, notification, and other types
  * of sounds. It manages querying the different media providers and combines the
@@ -855,7 +852,7 @@
             final Uri cacheUri = getCacheForType(type, context.getUserId());
             try (InputStream in = openRingtone(context, ringtoneUri);
                     OutputStream out = resolver.openOutputStream(cacheUri)) {
-                Streams.copy(in, out);
+                FileUtils.copy(in, out);
             } catch (IOException e) {
                 Log.w(TAG, "Failed to cache ringtone: " + e);
             }
@@ -960,7 +957,7 @@
         // Copy contents to external ringtone storage. Throws IOException if the copy fails.
         try (final InputStream input = mContext.getContentResolver().openInputStream(fileUri);
                 final OutputStream output = new FileOutputStream(outFile)) {
-            Streams.copy(input, output);
+            FileUtils.copy(input, output);
         }
 
         // Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}.
diff --git a/android/media/SessionCommand2.java b/android/media/SessionCommand2.java
new file mode 100644
index 0000000..fe86a3a
--- /dev/null
+++ b/android/media/SessionCommand2.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2018 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.Nullable;
+import android.content.Context;
+import android.media.update.ApiLoader;
+import android.media.update.MediaSession2Provider;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.util.List;
+
+/**
+ * @hide
+ * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
+ * <p>
+ * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command.
+ * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and
+ * {@link #getCustomCommand()} shouldn't be {@code null}.
+ */
+public final class SessionCommand2 {
+    /**
+     * Command code for the custom command which can be defined by string action in the
+     * {@link SessionCommand2}.
+     */
+    public static final int COMMAND_CODE_CUSTOM = 0;
+
+    /**
+     * Command code for {@link MediaController2#play()}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_PLAY = 1;
+
+    /**
+     * Command code for {@link MediaController2#pause()}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2;
+
+    /**
+     * Command code for {@link MediaController2#stop()}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_STOP = 3;
+
+    /**
+     * Command code for {@link MediaController2#skipToNextItem()}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the {@link SessionCallback#onCommandRequest(
+     * MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SKIP_NEXT_ITEM = 4;
+
+    /**
+     * Command code for {@link MediaController2#skipToPreviousItem()}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the {@link SessionCallback#onCommandRequest(
+     * MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SKIP_PREV_ITEM = 5;
+
+    /**
+     * Command code for {@link MediaController2#prepare()}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6;
+
+    /**
+     * Command code for {@link MediaController2#fastForward()}.
+     */
+    public static final int COMMAND_CODE_SESSION_FAST_FORWARD = 7;
+
+    /**
+     * Command code for {@link MediaController2#rewind()}.
+     */
+    public static final int COMMAND_CODE_SESSION_REWIND = 8;
+
+    /**
+     * Command code for {@link MediaController2#seekTo(long)}.
+     * <p>
+     * Command would be sent directly to the player if the session doesn't reject the request
+     * through the {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo,
+     * SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9;
+
+    /**
+     * Command code for both {@link MediaController2#setVolumeTo(int, int)}.
+     * <p>
+     * Command would set the device volume or send to the volume provider directly if the session
+     * doesn't reject the request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_SET_VOLUME = 10;
+
+    /**
+     * Command code for both {@link MediaController2#adjustVolume(int, int)}.
+     * <p>
+     * Command would adjust the device volume or send to the volume provider directly if the session
+     * doesn't reject the request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_ADJUST_VOLUME = 11;
+
+    /**
+     * Command code for {@link MediaController2#skipToPlaylistItem(MediaItem2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM = 12;
+
+    /**
+     * Command code for {@link MediaController2#setShuffleMode(int)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE = 13;
+
+    /**
+     * Command code for {@link MediaController2#setRepeatMode(int)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE = 14;
+
+    /**
+     * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_ADD_ITEM = 15;
+
+    /**
+     * Command code for {@link MediaController2#addPlaylistItem(int, MediaItem2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_REMOVE_ITEM = 16;
+
+    /**
+     * Command code for {@link MediaController2#replacePlaylistItem(int, MediaItem2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_REPLACE_ITEM = 17;
+
+    /**
+     * Command code for {@link MediaController2#getPlaylist()}. This will expose metadata
+     * information to the controller.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_GET_LIST = 18;
+
+    /**
+     * Command code for {@link MediaController2#setPlaylist(List, MediaMetadata2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SET_LIST = 19;
+
+    /**
+     * Command code for {@link MediaController2#getPlaylistMetadata()}. This will expose
+     * metadata information to the controller.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_GET_LIST_METADATA = 20;
+
+    /**
+     * Command code for {@link MediaController2#updatePlaylistMetadata(MediaMetadata2)}.
+     * <p>
+     * Command would be sent directly to the playlist agent if the session doesn't reject the
+     * request through the
+     * {@link SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)}.
+     */
+    public static final int COMMAND_CODE_PLAYLIST_SET_LIST_METADATA = 21;
+
+    /**
+     * Command code for {@link MediaController2#playFromMediaId(String, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID = 22;
+
+    /**
+     * Command code for {@link MediaController2#playFromUri(Uri, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PLAY_FROM_URI = 23;
+
+    /**
+     * Command code for {@link MediaController2#playFromSearch(String, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PLAY_FROM_SEARCH = 24;
+
+    /**
+     * Command code for {@link MediaController2#prepareFromMediaId(String, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID = 25;
+
+    /**
+     * Command code for {@link MediaController2#prepareFromUri(Uri, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PREPARE_FROM_URI = 26;
+
+    /**
+     * Command code for {@link MediaController2#prepareFromSearch(String, Bundle)}.
+     */
+    public static final int COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH = 27;
+
+    /**
+     * Command code for {@link MediaController2#setRating(String, Rating2)}.
+     */
+    public static final int COMMAND_CODE_SESSION_SET_RATING = 28;
+
+    // TODO(jaewan): Add javadoc
+    public static final int COMMAND_CODE_LIBRARY_GET_CHILDREN = 29;
+    public static final int COMMAND_CODE_LIBRARY_GET_ITEM = 30;
+    public static final int COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT = 31;
+    public static final int COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT = 32;
+    public static final int COMMAND_CODE_LIBRARY_SEARCH = 33;
+    public static final int COMMAND_CODE_LIBRARY_SUBSCRIBE = 34;
+    public static final int COMMAND_CODE_LIBRARY_UNSUBSCRIBE = 35;
+
+    // TODO(jaewan): Rename and move provider
+    private final MediaSession2Provider.CommandProvider mProvider;
+
+    public SessionCommand2(int commandCode) {
+        mProvider = ApiLoader.getProvider().createMediaSession2Command(
+                this, commandCode, null, null);
+    }
+
+    public SessionCommand2(@NonNull String action, @Nullable Bundle extras) {
+        if (action == null) {
+            throw new IllegalArgumentException("action shouldn't be null");
+        }
+        mProvider = ApiLoader.getProvider().createMediaSession2Command(
+                this, COMMAND_CODE_CUSTOM, action, extras);
+    }
+
+    /**
+     * @hide
+     */
+    public MediaSession2Provider.CommandProvider getProvider() {
+        return mProvider;
+    }
+
+    public int getCommandCode() {
+        return mProvider.getCommandCode_impl();
+    }
+
+    public @Nullable String getCustomCommand() {
+        return mProvider.getCustomCommand_impl();
+    }
+
+    public @Nullable Bundle getExtras() {
+        return mProvider.getExtras_impl();
+    }
+
+    /**
+     * @return a new Bundle instance from the Command
+     * @hide
+     */
+    public Bundle toBundle() {
+        return mProvider.toBundle_impl();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof SessionCommand2)) {
+            return false;
+        }
+        return mProvider.equals_impl(((SessionCommand2) obj).mProvider);
+    }
+
+    @Override
+    public int hashCode() {
+        return mProvider.hashCode_impl();
+    }
+
+    /**
+     * @return a new Command instance from the Bundle
+     * @hide
+     */
+    public static SessionCommand2 fromBundle(@NonNull Bundle command) {
+        return ApiLoader.getProvider().fromBundle_MediaSession2Command(command);
+    }
+}
diff --git a/android/media/SessionCommandGroup2.java b/android/media/SessionCommandGroup2.java
new file mode 100644
index 0000000..399765e
--- /dev/null
+++ b/android/media/SessionCommandGroup2.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2018 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.Nullable;
+import android.content.Context;
+import android.media.update.ApiLoader;
+import android.media.update.MediaSession2Provider;
+import android.os.Bundle;
+
+import java.util.Set;
+
+/**
+ * @hide
+ * Represent set of {@link SessionCommand2}.
+ */
+public final class SessionCommandGroup2 {
+    // TODO(jaewan): Rename and move provider
+    private final MediaSession2Provider.CommandGroupProvider mProvider;
+
+    public SessionCommandGroup2() {
+        mProvider = ApiLoader.getProvider().createMediaSession2CommandGroup(this, null);
+    }
+
+    public SessionCommandGroup2(@Nullable SessionCommandGroup2 others) {
+        mProvider = ApiLoader.getProvider().createMediaSession2CommandGroup(this, others);
+    }
+
+    /**
+     * @hide
+     */
+    public SessionCommandGroup2(@NonNull MediaSession2Provider.CommandGroupProvider provider) {
+        mProvider = provider;
+    }
+
+    public void addCommand(@NonNull SessionCommand2 command) {
+        mProvider.addCommand_impl(command);
+    }
+
+    public void addCommand(int commandCode) {
+        // TODO(jaewna): Implement
+    }
+
+    public void addAllPredefinedCommands() {
+        mProvider.addAllPredefinedCommands_impl();
+    }
+
+    public void removeCommand(@NonNull SessionCommand2 command) {
+        mProvider.removeCommand_impl(command);
+    }
+
+    public void removeCommand(int commandCode) {
+        // TODO(jaewan): Implement.
+    }
+
+    public boolean hasCommand(@NonNull SessionCommand2 command) {
+        return mProvider.hasCommand_impl(command);
+    }
+
+    public boolean hasCommand(int code) {
+        return mProvider.hasCommand_impl(code);
+    }
+
+    public @NonNull
+    Set<SessionCommand2> getCommands() {
+        return mProvider.getCommands_impl();
+    }
+
+    /**
+     * @hide
+     */
+    public @NonNull MediaSession2Provider.CommandGroupProvider getProvider() {
+        return mProvider;
+    }
+
+    /**
+     * @return new bundle from the CommandGroup
+     * @hide
+     */
+    public @NonNull Bundle toBundle() {
+        return mProvider.toBundle_impl();
+    }
+
+    /**
+     * @return new instance of CommandGroup from the bundle
+     * @hide
+     */
+    public static @Nullable SessionCommandGroup2 fromBundle(Bundle commands) {
+        return ApiLoader.getProvider().fromBundle_MediaSession2CommandGroup(commands);
+    }
+}
diff --git a/android/media/SessionToken2.java b/android/media/SessionToken2.java
index 697a5a8..bf2d445 100644
--- a/android/media/SessionToken2.java
+++ b/android/media/SessionToken2.java
@@ -18,16 +18,17 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.annotation.Nullable;
+import android.content.Context;
 import android.media.session.MediaSessionManager;
+import android.media.update.ApiLoader;
+import android.media.update.SessionToken2Provider;
 import android.os.Bundle;
-import android.os.IBinder;
-import android.text.TextUtils;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 /**
+ * @hide
  * Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}.
  * If it's representing a session service, it may not be ongoing.
  * <p>
@@ -35,11 +36,11 @@
  * {@link MediaController2} to communicate with the session.
  * <p>
  * It can be also obtained by {@link MediaSessionManager}.
- * @hide
  */
-// TODO(jaewan): Unhide. SessionToken2?
-// TODO(jaewan): Move Token to updatable!
-// TODO(jaewan): Find better name for this (SessionToken or Session2Token)
+// New version of MediaSession.Token for following reasons
+//   - Stop implementing Parcelable for updatable support
+//   - Represent session and library service (formerly browser service) in one class.
+//     Previously MediaSession.Token was for session and ComponentName was for service.
 public final class SessionToken2 {
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
@@ -50,91 +51,89 @@
     public static final int TYPE_SESSION_SERVICE = 1;
     public static final int TYPE_LIBRARY_SERVICE = 2;
 
-    private static final String KEY_TYPE = "android.media.token.type";
-    private static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
-    private static final String KEY_SERVICE_NAME = "android.media.token.service_name";
-    private static final String KEY_ID = "android.media.token.id";
-    private static final String KEY_SESSION_BINDER = "android.media.token.session_binder";
+    private final SessionToken2Provider mProvider;
 
-    private final @TokenType int mType;
-    private final String mPackageName;
-    private final String mServiceName;
-    private final String mId;
-    private final IMediaSession2 mSessionBinder;
+    // From the return value of android.os.Process.getUidForName(String) when error
+    private static final int UID_UNKNOWN = -1;
+
+    /**
+     * Constructor for the token. You can only create token for session service or library service
+     * to use by {@link MediaController2} or {@link MediaBrowser2}.
+     *
+     * @param context context
+     * @param packageName package name
+     * @param serviceName name of service. Can be {@code null} if it's not an service.
+     */
+    public SessionToken2(@NonNull Context context, @NonNull String packageName,
+            @NonNull String serviceName) {
+        this(context, packageName, serviceName, UID_UNKNOWN);
+    }
+
+    /**
+     * Constructor for the token. You can only create token for session service or library service
+     * to use by {@link MediaController2} or {@link MediaBrowser2}.
+     *
+     * @param context context
+     * @param packageName package name
+     * @param serviceName name of service. Can be {@code null} if it's not an service.
+     * @param uid uid of the app.
+     * @hide
+     */
+    public SessionToken2(@NonNull Context context, @NonNull String packageName,
+            @NonNull String serviceName, int uid) {
+        mProvider = ApiLoader.getProvider().createSessionToken2(
+                context, this, packageName, serviceName, uid);
+    }
 
     /**
      * Constructor for the token.
-     *
-     * @hide
-     * @param type type
-     * @param packageName package name
-     * @param id id
-     * @param serviceName name of service. Can be {@code null} if it's not an service.
-     * @param sessionBinder binder for this session. Can be {@code null} if it's service.
      * @hide
      */
-    // TODO(jaewan): UID is also needed.
-    // TODO(jaewan): Unhide
-    public SessionToken2(@TokenType int type, @NonNull String packageName, @NonNull String id,
-            @Nullable String serviceName, @Nullable IMediaSession2 sessionBinder) {
-        // TODO(jaewan): Add sanity check.
-        mType = type;
-        mPackageName = packageName;
-        mId = id;
-        mServiceName = serviceName;
-        mSessionBinder = sessionBinder;
+    public SessionToken2(@NonNull SessionToken2Provider provider) {
+        mProvider = provider;
     }
 
+    @Override
     public int hashCode() {
-        final int prime = 31;
-        return mType
-                + prime * (mPackageName.hashCode()
-                + prime * (mId.hashCode()
-                + prime * ((mServiceName != null ? mServiceName.hashCode() : 0)
-                + prime * (mSessionBinder != null ? mSessionBinder.asBinder().hashCode() : 0))));
+        return mProvider.hashCode_impl();
     }
 
     @Override
     public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
-        SessionToken2 other = (SessionToken2) obj;
-        if (!mPackageName.equals(other.getPackageName())
-                || !mServiceName.equals(other.getServiceName())
-                || !mId.equals(other.getId())
-                || mType != other.getType()) {
-            return false;
-        }
-        if (mSessionBinder == other.getSessionBinder()) {
-            return true;
-        } else if (mSessionBinder == null || other.getSessionBinder() == null) {
-            return false;
-        }
-        return mSessionBinder.asBinder().equals(other.getSessionBinder().asBinder());
+        return mProvider.equals_impl(obj);
     }
 
     @Override
     public String toString() {
-        return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType
-                + " service=" + mServiceName + " binder=" + mSessionBinder + "}";
+        return mProvider.toString_impl();
+    }
+
+    /**
+     * @hide
+     */
+    public SessionToken2Provider getProvider() {
+        return mProvider;
+    }
+
+    /**
+     * @return uid of the session
+     */
+    public int getUid() {
+        return mProvider.getUid_impl();
     }
 
     /**
      * @return package name
      */
     public String getPackageName() {
-        return mPackageName;
+        return mProvider.getPackageName_impl();
     }
 
     /**
      * @return id
      */
     public String getId() {
-        return mId;
+        return mProvider.getId_imp();
     }
 
     /**
@@ -143,83 +142,23 @@
      * @see #TYPE_SESSION_SERVICE
      */
     public @TokenType int getType() {
-        return mType;
-    }
-
-    /**
-     * @return session binder.
-     * @hide
-     */
-    public @Nullable IMediaSession2 getSessionBinder() {
-        return mSessionBinder;
-    }
-
-    /**
-     * @return service name if it's session service.
-     * @hide
-     */
-    public @Nullable String getServiceName() {
-        return mServiceName;
+        return mProvider.getType_impl();
     }
 
     /**
      * Create a token from the bundle, exported by {@link #toBundle()}.
-     *
      * @param bundle
      * @return
      */
     public static SessionToken2 fromBundle(@NonNull Bundle bundle) {
-        if (bundle == null) {
-            return null;
-        }
-        final @TokenType int type = bundle.getInt(KEY_TYPE, -1);
-        final String packageName = bundle.getString(KEY_PACKAGE_NAME);
-        final String serviceName = bundle.getString(KEY_SERVICE_NAME);
-        final String id = bundle.getString(KEY_ID);
-        final IBinder sessionBinder = bundle.getBinder(KEY_SESSION_BINDER);
-
-        // Sanity check.
-        switch (type) {
-            case TYPE_SESSION:
-                if (!(sessionBinder instanceof IMediaSession2)) {
-                    throw new IllegalArgumentException("Session needs sessionBinder");
-                }
-                break;
-            case TYPE_SESSION_SERVICE:
-                if (TextUtils.isEmpty(serviceName)) {
-                    throw new IllegalArgumentException("Session service needs service name");
-                }
-                if (sessionBinder != null && !(sessionBinder instanceof IMediaSession2)) {
-                    throw new IllegalArgumentException("Invalid session binder");
-                }
-                break;
-            default:
-                throw new IllegalArgumentException("Invalid type");
-        }
-        if (TextUtils.isEmpty(packageName) || id == null) {
-            throw new IllegalArgumentException("Package name nor ID cannot be null.");
-        }
-        // TODO(jaewan): Revisit here when we add connection callback to the session for individual
-        //               controller's permission check. With it, sessionBinder should be available
-        //               if and only if for session, not session service.
-        return new SessionToken2(type, packageName, id, serviceName,
-                sessionBinder != null ? IMediaSession2.Stub.asInterface(sessionBinder) : null);
+        return ApiLoader.getProvider().fromBundle_SessionToken2(bundle);
     }
 
     /**
      * Create a {@link Bundle} from this token to share it across processes.
-     *
      * @return Bundle
-     * @hide
      */
     public Bundle toBundle() {
-        Bundle bundle = new Bundle();
-        bundle.putString(KEY_PACKAGE_NAME, mPackageName);
-        bundle.putString(KEY_SERVICE_NAME, mServiceName);
-        bundle.putString(KEY_ID, mId);
-        bundle.putInt(KEY_TYPE, mType);
-        bundle.putBinder(KEY_SESSION_BINDER,
-                mSessionBinder != null ? mSessionBinder.asBinder() : null);
-        return bundle;
+        return mProvider.toBundle_impl();
     }
 }
diff --git a/android/media/SubtitleData.java b/android/media/SubtitleData.java
index 3e6f6f9..9797828 100644
--- a/android/media/SubtitleData.java
+++ b/android/media/SubtitleData.java
@@ -16,26 +16,50 @@
 
 package android.media;
 
+import android.annotation.NonNull;
 import android.os.Parcel;
 
 /**
- * @hide
- *
- * Class to hold the subtitle track's data, including:
+ * Class encapsulating subtitle data, as received through the
+ * {@link MediaPlayer.OnSubtitleDataListener} interface.
+ * The subtitle data includes:
  * <ul>
- * <li> Track index</li>
- * <li> Start time (in microseconds) of the data</li>
- * <li> Duration (in microseconds) of the data</li>
- * <li> A byte-array of the data</li>
+ * <li> the track index</li>
+ * <li> the start time (in microseconds) of the data</li>
+ * <li> the duration (in microseconds) of the data</li>
+ * <li> the actual data.</li>
  * </ul>
- *
- * <p> To receive the subtitle data, applications need to do the following:
- *
- * <ul>
- * <li> Select a track of type MEDIA_TRACK_TYPE_SUBTITLE with {@link MediaPlayer.selectTrack(int)</li>
- * <li> Implement the {@link MediaPlayer.OnSubtitleDataListener} interface</li>
- * <li> Register the {@link MediaPlayer.OnSubtitleDataListener} callback on a MediaPlayer object</li>
- * </ul>
+ * The data is stored in a byte-array, and is encoded in one of the supported in-band
+ * subtitle formats. The subtitle encoding is determined by the MIME type of the
+ * {@link MediaPlayer.TrackInfo} of the subtitle track, one of
+ * {@link MediaFormat#MIMETYPE_TEXT_CEA_608}, {@link MediaFormat#MIMETYPE_TEXT_CEA_708},
+ * {@link MediaFormat#MIMETYPE_TEXT_VTT}.
+ * <p>
+ * Here is an example of iterating over the tracks of a {@link MediaPlayer}, and checking which
+ * encoding is used for the subtitle tracks:
+ * <p>
+ * <pre class="prettyprint">
+ * MediaPlayer mp = new MediaPlayer();
+ * mp.setDataSource(myContentLocation);
+ * mp.prepare(); // synchronous prepare, ready to use when method returns
+ * final TrackInfo[] trackInfos = mp.getTrackInfo();
+ * for (TrackInfo info : trackInfo) {
+ *     if (info.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ *         final String mime = info.getFormat().getString(MediaFormat.KEY_MIME);
+ *         if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mime) {
+ *             // subtitle encoding is CEA 608
+ *         } else if (MediaFormat.MIMETYPE_TEXT_CEA_708.equals(mime) {
+ *             // subtitle encoding is CEA 708
+ *         } else if (MediaFormat.MIMETYPE_TEXT_VTT.equals(mime) {
+ *             // subtitle encoding is WebVTT
+ *         }
+ *     }
+ * }
+ * </pre>
+ * <p>
+ * See
+ * {@link MediaPlayer#setOnSubtitleDataListener(android.media.MediaPlayer.OnSubtitleDataListener, android.os.Handler)}
+ * to receive subtitle data from a MediaPlayer object.
  *
  * @see android.media.MediaPlayer
  */
@@ -48,25 +72,47 @@
     private long mDurationUs;
     private byte[] mData;
 
+    /** @hide */
     public SubtitleData(Parcel parcel) {
         if (!parseParcel(parcel)) {
             throw new IllegalArgumentException("parseParcel() fails");
         }
     }
 
+    /**
+     * Returns the index of the MediaPlayer track which contains this subtitle data.
+     * @return an index in the array returned by {@link MediaPlayer#getTrackInfo()}.
+     */
     public int getTrackIndex() {
         return mTrackIndex;
     }
 
+    /**
+     * Returns the media time at which the subtitle should be displayed, expressed in microseconds.
+     * @return the display start time for the subtitle
+     */
     public long getStartTimeUs() {
         return mStartTimeUs;
     }
 
+    /**
+     * Returns the duration in microsecond during which the subtitle should be displayed.
+     * @return the display duration for the subtitle
+     */
     public long getDurationUs() {
         return mDurationUs;
     }
 
-    public byte[] getData() {
+    /**
+     * Returns the encoded data for the subtitle content.
+     * Encoding format depends on the subtitle type, refer to
+     * <a href="https://en.wikipedia.org/wiki/CEA-708">CEA 708</a>,
+     * <a href="https://en.wikipedia.org/wiki/EIA-608">CEA/EIA 608</a> and
+     * <a href="https://www.w3.org/TR/webvtt1/">WebVTT</a>, defined by the MIME type
+     * of the subtitle track.
+     * @return the encoded subtitle data
+     */
+    public @NonNull byte[] getData() {
         return mData;
     }
 
diff --git a/android/media/TestServiceRegistry.java b/android/media/TestServiceRegistry.java
deleted file mode 100644
index 6f5512e..0000000
--- a/android/media/TestServiceRegistry.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright 2018 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 static org.junit.Assert.fail;
-
-import android.media.MediaSession2.ControllerInfo;
-import android.media.TestUtils.SyncHandler;
-import android.os.Handler;
-import android.os.Looper;
-import android.support.annotation.GuardedBy;
-
-/**
- * Keeps the instance of currently running {@link MockMediaSessionService2}. And also provides
- * a way to control them in one place.
- * <p>
- * It only support only one service at a time.
- */
-public class TestServiceRegistry {
-    public interface ServiceInstanceChangedCallback {
-        void OnServiceInstanceChanged(MediaSessionService2 service);
-    }
-
-    @GuardedBy("TestServiceRegistry.class")
-    private static TestServiceRegistry sInstance;
-    @GuardedBy("TestServiceRegistry.class")
-    private MediaSessionService2 mService;
-    @GuardedBy("TestServiceRegistry.class")
-    private SyncHandler mHandler;
-    @GuardedBy("TestServiceRegistry.class")
-    private ControllerInfo mOnConnectControllerInfo;
-    @GuardedBy("TestServiceRegistry.class")
-    private ServiceInstanceChangedCallback mCallback;
-
-    public static TestServiceRegistry getInstance() {
-        synchronized (TestServiceRegistry.class) {
-            if (sInstance == null) {
-                sInstance = new TestServiceRegistry();
-            }
-            return sInstance;
-        }
-    }
-
-    public void setHandler(Handler handler) {
-        synchronized (TestServiceRegistry.class) {
-            mHandler = new SyncHandler(handler.getLooper());
-        }
-    }
-
-    public void setServiceInstanceChangedCallback(ServiceInstanceChangedCallback callback) {
-        synchronized (TestServiceRegistry.class) {
-            mCallback = callback;
-        }
-    }
-
-    public Handler getHandler() {
-        synchronized (TestServiceRegistry.class) {
-            return mHandler;
-        }
-    }
-
-    public void setServiceInstance(MediaSessionService2 service, ControllerInfo controller) {
-        synchronized (TestServiceRegistry.class) {
-            if (mService != null) {
-                fail("Previous service instance is still running. Clean up manually to ensure"
-                        + " previoulsy running service doesn't break current test");
-            }
-            mService = service;
-            mOnConnectControllerInfo = controller;
-            if (mCallback != null) {
-                mCallback.OnServiceInstanceChanged(service);
-            }
-        }
-    }
-
-    public MediaSessionService2 getServiceInstance() {
-        synchronized (TestServiceRegistry.class) {
-            return mService;
-        }
-    }
-
-    public ControllerInfo getOnConnectControllerInfo() {
-        synchronized (TestServiceRegistry.class) {
-            return mOnConnectControllerInfo;
-        }
-    }
-
-
-    public void cleanUp() {
-        synchronized (TestServiceRegistry.class) {
-            final ServiceInstanceChangedCallback callback = mCallback;
-            if (mService != null) {
-                try {
-                    if (mHandler.getLooper() == Looper.myLooper()) {
-                        mService.getSession().close();
-                    } else {
-                        mHandler.postAndSync(() -> {
-                            mService.getSession().close();
-                        });
-                    }
-                } catch (InterruptedException e) {
-                    // No-op. Service containing session will die, but shouldn't be a huge issue.
-                }
-                // stopSelf() would not kill service while the binder connection established by
-                // bindService() exists, and close() above will do the job instead.
-                // So stopSelf() isn't really needed, but just for sure.
-                mService.stopSelf();
-                mService = null;
-            }
-            if (mHandler != null) {
-                mHandler.removeCallbacksAndMessages(null);
-            }
-            mCallback = null;
-            mOnConnectControllerInfo = null;
-
-            if (callback != null) {
-                callback.OnServiceInstanceChanged(null);
-            }
-        }
-    }
-}
diff --git a/android/media/TestUtils.java b/android/media/TestUtils.java
deleted file mode 100644
index 9a1fa10..0000000
--- a/android/media/TestUtils.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2018 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.content.Context;
-import android.media.session.MediaSessionManager;
-import android.media.session.PlaybackState;
-import android.os.Bundle;
-import android.os.Handler;
-
-import android.os.Looper;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-/**
- * Utilities for tests.
- */
-public final class TestUtils {
-    private static final int WAIT_TIME_MS = 1000;
-    private static final int WAIT_SERVICE_TIME_MS = 5000;
-
-    /**
-     * Creates a {@link android.media.session.PlaybackState} with the given state.
-     *
-     * @param state one of the PlaybackState.STATE_xxx.
-     * @return a PlaybackState
-     */
-    public static PlaybackState2 createPlaybackState(int state) {
-        return new PlaybackState2(state, 0, 0, 1.0f,
-                0, 0, null);
-    }
-
-    /**
-     * Finds the session with id in this test package.
-     *
-     * @param context
-     * @param id
-     * @return
-     */
-    // TODO(jaewan): Currently not working.
-    public static SessionToken2 getServiceToken(Context context, String id) {
-        MediaSessionManager manager =
-                (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
-        List<SessionToken2> tokens = manager.getSessionServiceTokens();
-        for (int i = 0; i < tokens.size(); i++) {
-            SessionToken2 token = tokens.get(i);
-            if (context.getPackageName().equals(token.getPackageName())
-                    && id.equals(token.getId())) {
-                return token;
-            }
-        }
-        fail("Failed to find service");
-        return null;
-    }
-
-    /**
-     * Compares contents of two bundles.
-     *
-     * @param a a bundle
-     * @param b another bundle
-     * @return {@code true} if two bundles are the same. {@code false} otherwise. This may be
-     *     incorrect if any bundle contains a bundle.
-     */
-    public static boolean equals(Bundle a, Bundle b) {
-        if (a == b) {
-            return true;
-        }
-        if (a == null || b == null) {
-            return false;
-        }
-        if (!a.keySet().containsAll(b.keySet())
-                || !b.keySet().containsAll(a.keySet())) {
-            return false;
-        }
-        for (String key : a.keySet()) {
-            if (!Objects.equals(a.get(key), b.get(key))) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Handler that always waits until the Runnable finishes.
-     */
-    public static class SyncHandler extends Handler {
-        public SyncHandler(Looper looper) {
-            super(looper);
-        }
-
-        public void postAndSync(Runnable runnable) throws InterruptedException {
-            final CountDownLatch latch = new CountDownLatch(1);
-            if (getLooper() == Looper.myLooper()) {
-                runnable.run();
-            } else {
-                post(()->{
-                    runnable.run();
-                    latch.countDown();
-                });
-                assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
-            }
-        }
-    }
-}
diff --git a/android/media/VolumePolicy.java b/android/media/VolumePolicy.java
index bbcce82..bd6667f 100644
--- a/android/media/VolumePolicy.java
+++ b/android/media/VolumePolicy.java
@@ -23,7 +23,7 @@
 
 /** @hide */
 public final class VolumePolicy implements Parcelable {
-    public static final VolumePolicy DEFAULT = new VolumePolicy(false, false, true, 400);
+    public static final VolumePolicy DEFAULT = new VolumePolicy(false, false, false, 400);
 
     /**
      * Accessibility volume policy where the STREAM_MUSIC volume (i.e. media volume) affects
diff --git a/android/media/VolumeProvider2.java b/android/media/VolumeProvider2.java
new file mode 100644
index 0000000..1a4608f
--- /dev/null
+++ b/android/media/VolumeProvider2.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2018 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.IntDef;
+import android.annotation.NonNull;
+import android.media.update.ApiLoader;
+import android.media.update.VolumeProvider2Provider;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @hide
+ * Handles requests to adjust or set the volume on a session. This is also used
+ * to push volume updates back to the session. The provider must call
+ * {@link #setCurrentVolume(int)} each time the volume being provided changes.
+ * <p>
+ * You can set a volume provider on a session by calling
+ * {@link MediaSession2#updatePlayer}.
+ */
+// New version of VolumeProvider with following changes
+//   - Don't implement Parcelable for updatable support.
+public abstract class VolumeProvider2 {
+    /**
+     * @hide
+     */
+    @IntDef({VOLUME_CONTROL_FIXED, VOLUME_CONTROL_RELATIVE, VOLUME_CONTROL_ABSOLUTE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ControlType {}
+
+    /**
+     * The volume is fixed and can not be modified. Requests to change volume
+     * should be ignored.
+     */
+    public static final int VOLUME_CONTROL_FIXED = 0;
+
+    /**
+     * The volume control uses relative adjustment via
+     * {@link #onAdjustVolume(int)}. Attempts to set the volume to a specific
+     * value should be ignored.
+     */
+    public static final int VOLUME_CONTROL_RELATIVE = 1;
+
+    /**
+     * The volume control uses an absolute value. It may be adjusted using
+     * {@link #onAdjustVolume(int)} or set directly using
+     * {@link #onSetVolumeTo(int)}.
+     */
+    public static final int VOLUME_CONTROL_ABSOLUTE = 2;
+
+    private final VolumeProvider2Provider mProvider;
+
+    /**
+     * Create a new volume provider for handling volume events. You must specify
+     * the type of volume control, the maximum volume that can be used, and the
+     * current volume on the output.
+     *
+     * @param controlType The method for controlling volume that is used by this provider.
+     * @param maxVolume The maximum allowed volume.
+     * @param currentVolume The current volume on the output.
+     */
+    public VolumeProvider2(@ControlType int controlType, int maxVolume, int currentVolume) {
+        mProvider = ApiLoader.getProvider().createVolumeProvider2(
+                this, controlType, maxVolume, currentVolume);
+    }
+
+    /**
+     * @hide
+     */
+    public VolumeProvider2Provider getProvider() {
+        return mProvider;
+    }
+
+    /**
+     * Get the volume control type that this volume provider uses.
+     *
+     * @return The volume control type for this volume provider
+     */
+    @ControlType
+    public final int getControlType() {
+        return mProvider.getControlType_impl();
+    }
+
+    /**
+     * Get the maximum volume this provider allows.
+     *
+     * @return The max allowed volume.
+     */
+    public final int getMaxVolume() {
+        return mProvider.getMaxVolume_impl();
+    }
+
+    /**
+     * Gets the current volume. This will be the last value set by
+     * {@link #setCurrentVolume(int)}.
+     *
+     * @return The current volume.
+     */
+    public final int getCurrentVolume() {
+        return mProvider.getCurrentVolume_impl();
+    }
+
+    /**
+     * Notify the system that the current volume has been changed. This must be
+     * called every time the volume changes to ensure it is displayed properly.
+     *
+     * @param currentVolume The current volume on the output.
+     */
+    public final void setCurrentVolume(int currentVolume) {
+        mProvider.setCurrentVolume_impl(currentVolume);
+    }
+
+    /**
+     * Override to handle requests to set the volume of the current output.
+     * After the volume has been modified {@link #setCurrentVolume} must be
+     * called to notify the system.
+     *
+     * @param volume The volume to set the output to.
+     */
+    public void onSetVolumeTo(int volume) { }
+
+    /**
+     * Override to handle requests to adjust the volume of the current output.
+     * Direction will be one of {@link AudioManager#ADJUST_LOWER},
+     * {@link AudioManager#ADJUST_RAISE}, {@link AudioManager#ADJUST_SAME}.
+     * After the volume has been modified {@link #setCurrentVolume} must be
+     * called to notify the system.
+     *
+     * @param direction The direction to change the volume in.
+     */
+    public void onAdjustVolume(int direction) { }
+}
diff --git a/android/media/audiofx/AudioEffect.java b/android/media/audiofx/AudioEffect.java
index 7dbca3b..21d6873 100644
--- a/android/media/audiofx/AudioEffect.java
+++ b/android/media/audiofx/AudioEffect.java
@@ -39,6 +39,7 @@
  *   <li> {@link android.media.audiofx.BassBoost}</li>
  *   <li> {@link android.media.audiofx.PresetReverb}</li>
  *   <li> {@link android.media.audiofx.EnvironmentalReverb}</li>
+ *   <li> {@link android.media.audiofx.DynamicsProcessing}</li>
  * </ul>
  * <p>To apply the audio effect to a specific AudioTrack or MediaPlayer instance,
  * the application must specify the audio session ID of that instance when creating the AudioEffect.
@@ -126,6 +127,12 @@
               .fromString("fe3199be-aed0-413f-87bb-11260eb63cf1");
 
     /**
+     * UUID for Dynamics Processing
+     */
+    public static final UUID EFFECT_TYPE_DYNAMICS_PROCESSING = UUID
+              .fromString("7261676f-6d75-7369-6364-28e2fd3ac39e");
+
+    /**
      * Null effect UUID. Used when the UUID for effect type of
      * @hide
      */
@@ -203,7 +210,8 @@
      * {@link AudioEffect#EFFECT_TYPE_AEC}, {@link AudioEffect#EFFECT_TYPE_AGC},
      * {@link AudioEffect#EFFECT_TYPE_BASS_BOOST}, {@link AudioEffect#EFFECT_TYPE_ENV_REVERB},
      * {@link AudioEffect#EFFECT_TYPE_EQUALIZER}, {@link AudioEffect#EFFECT_TYPE_NS},
-     * {@link AudioEffect#EFFECT_TYPE_PRESET_REVERB}, {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER}.
+     * {@link AudioEffect#EFFECT_TYPE_PRESET_REVERB}, {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER},
+     * {@link AudioEffect#EFFECT_TYPE_DYNAMICS_PROCESSING}.
      *  </li>
      *  <li>uuid: UUID for this particular implementation</li>
      *  <li>connectMode: {@link #EFFECT_INSERT} or {@link #EFFECT_AUXILIARY}</li>
@@ -224,7 +232,8 @@
          * {@link AudioEffect#EFFECT_TYPE_BASS_BOOST}, {@link AudioEffect#EFFECT_TYPE_ENV_REVERB},
          * {@link AudioEffect#EFFECT_TYPE_EQUALIZER}, {@link AudioEffect#EFFECT_TYPE_NS},
          * {@link AudioEffect#EFFECT_TYPE_PRESET_REVERB},
-         * {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER}.
+         * {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER},
+         * {@link AudioEffect#EFFECT_TYPE_DYNAMICS_PROCESSING}.
          * @param uuid         UUID for this particular implementation
          * @param connectMode  {@link #EFFECT_INSERT} or {@link #EFFECT_AUXILIARY}
          * @param name         human readable effect name
@@ -246,7 +255,8 @@
          *  {@link AudioEffect#EFFECT_TYPE_AGC}, {@link AudioEffect#EFFECT_TYPE_BASS_BOOST},
          *  {@link AudioEffect#EFFECT_TYPE_ENV_REVERB}, {@link AudioEffect#EFFECT_TYPE_EQUALIZER},
          *  {@link AudioEffect#EFFECT_TYPE_NS}, {@link AudioEffect#EFFECT_TYPE_PRESET_REVERB}
-         *   or {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER}.<br>
+         *  {@link AudioEffect#EFFECT_TYPE_VIRTUALIZER}
+         *   or {@link AudioEffect#EFFECT_TYPE_DYNAMICS_PROCESSING}.<br>
          *  For reverberation, bass boost, EQ and virtualizer, the UUID
          *  corresponds to the OpenSL ES Interface ID.
          */
@@ -1344,6 +1354,34 @@
     /**
      * @hide
      */
+    public static float byteArrayToFloat(byte[] valueBuf) {
+        return byteArrayToFloat(valueBuf, 0);
+
+    }
+
+    /**
+     * @hide
+     */
+    public static float byteArrayToFloat(byte[] valueBuf, int offset) {
+        ByteBuffer converter = ByteBuffer.wrap(valueBuf);
+        converter.order(ByteOrder.nativeOrder());
+        return converter.getFloat(offset);
+
+    }
+
+    /**
+     * @hide
+     */
+    public static byte[] floatToByteArray(float value) {
+        ByteBuffer converter = ByteBuffer.allocate(4);
+        converter.order(ByteOrder.nativeOrder());
+        converter.putFloat(value);
+        return converter.array();
+    }
+
+    /**
+     * @hide
+     */
     public static byte[] concatArrays(byte[]... arrays) {
         int len = 0;
         for (byte[] a : arrays) {
diff --git a/android/media/audiofx/DynamicsProcessing.java b/android/media/audiofx/DynamicsProcessing.java
new file mode 100644
index 0000000..4c17ae1
--- /dev/null
+++ b/android/media/audiofx/DynamicsProcessing.java
@@ -0,0 +1,2402 @@
+/*
+ * Copyright (C) 2018 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.audiofx;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.AudioTrack;
+import android.media.MediaPlayer;
+import android.media.audiofx.AudioEffect;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.StringTokenizer;
+
+/**
+ * DynamicsProcessing is an audio effect for equalizing and changing dynamic range properties of the
+ * sound. It is composed of multiple stages including equalization, multi-band compression and
+ * limiter.
+ * <p>The number of bands and active stages is configurable, and most parameters can be controlled
+ * in realtime, such as gains, attack/release times, thresholds, etc.
+ * <p>The effect is instantiated and controlled by channels. Each channel has the same basic
+ * architecture, but all of their parameters are independent from other channels.
+ * <p>The basic channel configuration is:
+ * <pre>
+ *
+ *    Channel 0          Channel 1       ....       Channel N-1
+ *      Input              Input                       Input
+ *        |                  |                           |
+ *   +----v----+        +----v----+                 +----v----+
+ *   |inputGain|        |inputGain|                 |inputGain|
+ *   +---------+        +---------+                 +---------+
+ *        |                  |                           |
+ *  +-----v-----+      +-----v-----+               +-----v-----+
+ *  |   PreEQ   |      |   PreEQ   |               |   PreEQ   |
+ *  +-----------+      +-----------+               +-----------+
+ *        |                  |                           |
+ *  +-----v-----+      +-----v-----+               +-----v-----+
+ *  |    MBC    |      |    MBC    |               |    MBC    |
+ *  +-----------+      +-----------+               +-----------+
+ *        |                  |                           |
+ *  +-----v-----+      +-----v-----+               +-----v-----+
+ *  |  PostEQ   |      |  PostEQ   |               |  PostEQ   |
+ *  +-----------+      +-----------+               +-----------+
+ *        |                  |                           |
+ *  +-----v-----+      +-----v-----+               +-----v-----+
+ *  |  Limiter  |      |  Limiter  |               |  Limiter  |
+ *  +-----------+      +-----------+               +-----------+
+ *        |                  |                           |
+ *     Output             Output                      Output
+ * </pre>
+ *
+ * <p>Where the stages are:
+ * inputGain: input gain factor in decibels (dB). 0 dB means no change in level.
+ * PreEQ:  Multi-band Equalizer.
+ * MBC:    Multi-band Compressor
+ * PostEQ: Multi-band Equalizer
+ * Limiter: Single band compressor/limiter.
+ *
+ * <p>An application creates a DynamicsProcessing object to instantiate and control this audio
+ * effect in the audio framework. A DynamicsProcessor.Config and DynamicsProcessor.Config.Builder
+ * are available to help configure the multiple stages and each band parameters if desired.
+ * <p>See each stage documentation for further details.
+ * <p>If no Config is specified during creation, a default configuration is chosen.
+ * <p>To attach the DynamicsProcessing to a particular AudioTrack or MediaPlayer,
+ * specify the audio session ID of this AudioTrack or MediaPlayer when constructing the effect
+ * (see {@link AudioTrack#getAudioSessionId()} and {@link MediaPlayer#getAudioSessionId()}).
+ *
+ * <p>To attach the DynamicsProcessing to a particular AudioTrack or MediaPlayer, specify the audio
+ * session ID of this AudioTrack or MediaPlayer when constructing the DynamicsProcessing.
+ * <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions.
+ * <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling audio
+ * effects.
+ */
+
+public final class DynamicsProcessing extends AudioEffect {
+
+    private final static String TAG = "DynamicsProcessing";
+
+    // These parameter constants must be synchronized with those in
+    // /system/media/audio_effects/include/audio_effects/effect_dynamicsprocessing.h
+    private static final int PARAM_GET_CHANNEL_COUNT = 0x10;
+    private static final int PARAM_INPUT_GAIN = 0x20;
+    private static final int PARAM_ENGINE_ARCHITECTURE = 0x30;
+    private static final int PARAM_PRE_EQ = 0x40;
+    private static final int PARAM_PRE_EQ_BAND = 0x45;
+    private static final int PARAM_MBC = 0x50;
+    private static final int PARAM_MBC_BAND = 0x55;
+    private static final int PARAM_POST_EQ = 0x60;
+    private static final int PARAM_POST_EQ_BAND = 0x65;
+    private static final int PARAM_LIMITER = 0x70;
+
+    /**
+     * Index of variant that favors frequency resolution. Frequency domain based implementation.
+     */
+    public static final int VARIANT_FAVOR_FREQUENCY_RESOLUTION  = 0;
+
+    /**
+     * Index of variant that favors time resolution resolution. Time domain based implementation.
+     */
+    public static final int VARIANT_FAVOR_TIME_RESOLUTION       = 1;
+
+    /**
+     * Maximum expected channels to be reported by effect
+     */
+    private static final int CHANNEL_COUNT_MAX = 32;
+
+    /**
+     * Number of channels in effect architecture
+     */
+    private int mChannelCount = 0;
+
+    /**
+     * Registered listener for parameter changes.
+     */
+    private OnParameterChangeListener mParamListener = null;
+
+    /**
+     * Listener used internally to to receive raw parameter change events
+     * from AudioEffect super class
+     */
+    private BaseParameterListener mBaseParamListener = null;
+
+    /**
+     * Lock for access to mParamListener
+     */
+    private final Object mParamListenerLock = new Object();
+
+    /**
+     * Class constructor.
+     * @param audioSession system-wide unique audio session identifier. The DynamicsProcessing
+     * will be attached to the MediaPlayer or AudioTrack in the same audio session.
+     */
+    public DynamicsProcessing(int audioSession) {
+        this(0 /*priority*/, audioSession);
+    }
+
+    /**
+     * @hide
+     * Class constructor for the DynamicsProcessing audio effect.
+     * @param priority the priority level requested by the application for controlling the
+     * DynamicsProcessing engine. As the same engine can be shared by several applications,
+     * this parameter indicates how much the requesting application needs control of effect
+     * parameters. The normal priority is 0, above normal is a positive number, below normal a
+     * negative number.
+     * @param audioSession system-wide unique audio session identifier. The DynamicsProcessing
+     * will be attached to the MediaPlayer or AudioTrack in the same audio session.
+     */
+    public DynamicsProcessing(int priority, int audioSession) {
+        this(priority, audioSession, null);
+    }
+
+    /**
+     * Class constructor for the DynamicsProcessing audio effect
+     * @param priority the priority level requested by the application for controlling the
+     * DynamicsProcessing engine. As the same engine can be shared by several applications,
+     * this parameter indicates how much the requesting application needs control of effect
+     * parameters. The normal priority is 0, above normal is a positive number, below normal a
+     * negative number.
+     * @param audioSession system-wide unique audio session identifier. The DynamicsProcessing
+     * will be attached to the MediaPlayer or AudioTrack in the same audio session.
+     * @param cfg Config object used to setup the audio effect, including bands per stage, and
+     * specific parameters for each stage/band. Use
+     * {@link android.media.audiofx.DynamicsProcessing.Config.Builder} to create a
+     * Config object that suits your needs. A null cfg parameter will create and use a default
+     * configuration for the effect
+     */
+    public DynamicsProcessing(int priority, int audioSession, @Nullable Config cfg) {
+        super(EFFECT_TYPE_DYNAMICS_PROCESSING, EFFECT_TYPE_NULL, priority, audioSession);
+        if (audioSession == 0) {
+            Log.w(TAG, "WARNING: attaching a DynamicsProcessing to global output mix is"
+                    + "deprecated!");
+        }
+        final Config config;
+        mChannelCount = getChannelCount();
+        if (cfg == null) {
+            //create a default configuration and effect, with the number of channels this effect has
+            DynamicsProcessing.Config.Builder builder =
+                    new DynamicsProcessing.Config.Builder(
+                            CONFIG_DEFAULT_VARIANT,
+                            mChannelCount,
+                            CONFIG_DEFAULT_USE_PREEQ,
+                            CONFIG_DEFAULT_PREEQ_BANDS,
+                            CONFIG_DEFAULT_USE_MBC,
+                            CONFIG_DEFAULT_MBC_BANDS,
+                            CONFIG_DEFAULT_USE_POSTEQ,
+                            CONFIG_DEFAULT_POSTEQ_BANDS,
+                            CONFIG_DEFAULT_USE_LIMITER);
+            config = builder.build();
+        } else {
+            //validate channels are ok. decide what to do: replicate channels if more
+            config = new DynamicsProcessing.Config(mChannelCount, cfg);
+        }
+
+        //configure engine
+        setEngineArchitecture(config.getVariant(),
+                config.getPreferredFrameDuration(),
+                config.isPreEqInUse(),
+                config.getPreEqBandCount(),
+                config.isMbcInUse(),
+                config.getMbcBandCount(),
+                config.isPostEqInUse(),
+                config.getPostEqBandCount(),
+                config.isLimiterInUse());
+        //update all the parameters
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            updateEngineChannelByChannelIndex(ch, config.getChannelByChannelIndex(ch));
+        }
+    }
+
+    /**
+     * Returns the Config object used to setup this effect.
+     * @return Config Current Config object used to setup this DynamicsProcessing effect.
+     */
+    public Config getConfig() {
+        //Query engine architecture to create config object
+        Number[] params = { PARAM_ENGINE_ARCHITECTURE };
+        Number[] values = { 0 /*0 variant */,
+                0.0f /* 1 preferredFrameDuration */,
+                0 /*2 preEqInUse */,
+                0 /*3 preEqBandCount */,
+                0 /*4 mbcInUse */,
+                0 /*5 mbcBandCount*/,
+                0 /*6 postEqInUse */,
+                0 /*7 postEqBandCount */,
+                0 /*8 limiterInUse */};
+        byte[] paramBytes = numberArrayToByteArray(params);
+        byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+        getParameter(paramBytes, valueBytes);
+        byteArrayToNumberArray(valueBytes, values);
+        DynamicsProcessing.Config.Builder builder =
+                new DynamicsProcessing.Config.Builder(
+                        values[0].intValue(),
+                        mChannelCount,
+                        values[2].intValue() > 0 /*use preEQ*/,
+                        values[3].intValue() /*pre eq bands*/,
+                        values[4].intValue() > 0 /*use mbc*/,
+                        values[5].intValue() /*mbc bands*/,
+                        values[6].intValue() > 0 /*use postEQ*/,
+                        values[7].intValue()/*postEq bands*/,
+                        values[8].intValue() > 0 /*use Limiter*/).
+                setPreferredFrameDuration(values[1].floatValue());
+        Config config = builder.build();
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            Channel channel = queryEngineByChannelIndex(ch);
+            config.setChannelTo(ch, channel);
+        }
+        return config;
+    }
+
+
+    private static final int CONFIG_DEFAULT_VARIANT = VARIANT_FAVOR_FREQUENCY_RESOLUTION;
+    private static final boolean CONFIG_DEFAULT_USE_PREEQ = true;
+    private static final int CONFIG_DEFAULT_PREEQ_BANDS = 6;
+    private static final boolean CONFIG_DEFAULT_USE_MBC = true;
+    private static final int CONFIG_DEFAULT_MBC_BANDS = 6;
+    private static final boolean CONFIG_DEFAULT_USE_POSTEQ = true;
+    private static final int CONFIG_DEFAULT_POSTEQ_BANDS = 6;
+    private static final boolean CONFIG_DEFAULT_USE_LIMITER = true;
+
+    private static final float CHANNEL_DEFAULT_INPUT_GAIN = 0; // dB
+    private static final float CONFIG_PREFERRED_FRAME_DURATION_MS = 10.0f; //milliseconds
+
+    private static final float EQ_DEFAULT_GAIN = 0; // dB
+    private static final boolean PREEQ_DEFAULT_ENABLED = true;
+    private static final boolean POSTEQ_DEFAULT_ENABLED = true;
+
+
+    private static final boolean MBC_DEFAULT_ENABLED = true;
+    private static final float MBC_DEFAULT_ATTACK_TIME = 50; // ms
+    private static final float MBC_DEFAULT_RELEASE_TIME = 120; // ms
+    private static final float MBC_DEFAULT_RATIO = 2; // 1:N
+    private static final float MBC_DEFAULT_THRESHOLD = -30; // dB
+    private static final float MBC_DEFAULT_KNEE_WIDTH = 0; // dB
+    private static final float MBC_DEFAULT_NOISE_GATE_THRESHOLD = -80; // dB
+    private static final float MBC_DEFAULT_EXPANDER_RATIO = 1.5f; // N:1
+    private static final float MBC_DEFAULT_PRE_GAIN = 0; // dB
+    private static final float MBC_DEFAULT_POST_GAIN = 10; // dB
+
+    private static final boolean LIMITER_DEFAULT_ENABLED = true;
+    private static final int LIMITER_DEFAULT_LINK_GROUP = 0;//;
+    private static final float LIMITER_DEFAULT_ATTACK_TIME = 50; // ms
+    private static final float LIMITER_DEFAULT_RELEASE_TIME = 120; // ms
+    private static final float LIMITER_DEFAULT_RATIO = 2; // 1:N
+    private static final float LIMITER_DEFAULT_THRESHOLD = -30; // dB
+    private static final float LIMITER_DEFAULT_POST_GAIN = 10; // dB
+
+    private static final float DEFAULT_MIN_FREQUENCY = 220; // Hz
+    private static final float DEFAULT_MAX_FREQUENCY = 20000; // Hz
+    private static final float mMinFreqLog = (float)Math.log10(DEFAULT_MIN_FREQUENCY);
+    private static final float mMaxFreqLog = (float)Math.log10(DEFAULT_MAX_FREQUENCY);
+
+    /**
+     * base class for the different stages.
+     */
+    public static class Stage {
+        private boolean mInUse;
+        private boolean mEnabled;
+        /**
+         * Class constructor for stage
+         * @param inUse true if this stage is set to be used. False otherwise. Stages that are not
+         * set "inUse" at initialization time are not available to be used at any time.
+         * @param enabled true if this stage is currently used to process sound. When disabled,
+         * the stage is bypassed and the sound is copied unaltered from input to output.
+         */
+        public Stage(boolean inUse, boolean enabled) {
+            mInUse = inUse;
+            mEnabled = enabled;
+        }
+
+        /**
+         * returns enabled state of the stage
+         * @return true if stage is enabled for processing, false otherwise
+         */
+        public boolean isEnabled() {
+            return mEnabled;
+        }
+        /**
+         * sets enabled state of the stage
+         * @param enabled true for enabled, false otherwise
+         */
+        public void setEnabled(boolean enabled) {
+            mEnabled = enabled;
+        }
+
+        /**
+         * returns inUse state of the stage.
+         * @return inUse state of the stage. True if this stage is currently used to process sound.
+         * When false, the stage is bypassed and the sound is copied unaltered from input to output.
+         */
+        public boolean isInUse() {
+            return mInUse;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(String.format(" Stage InUse: %b\n", isInUse()));
+            if (isInUse()) {
+                sb.append(String.format(" Stage Enabled: %b\n", mEnabled));
+            }
+            return sb.toString();
+        }
+    }
+
+    /**
+     * Base class for stages that hold bands
+     */
+    public static class BandStage extends Stage{
+        private int mBandCount;
+        /**
+         * Class constructor for BandStage
+         * @param inUse true if this stage is set to be used. False otherwise. Stages that are not
+         * set "inUse" at initialization time are not available to be used at any time.
+         * @param enabled true if this stage is currently used to process sound. When disabled,
+         * the stage is bypassed and the sound is copied unaltered from input to output.
+         * @param bandCount number of bands this stage will handle. If stage is not inUse, bandcount
+         * is set to 0
+         */
+        public BandStage(boolean inUse, boolean enabled, int bandCount) {
+            super(inUse, enabled);
+            mBandCount = isInUse() ? bandCount : 0;
+        }
+
+        /**
+         * gets number of bands held in this stage
+         * @return number of bands held in this stage
+         */
+        public int getBandCount() {
+            return mBandCount;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(super.toString());
+            if (isInUse()) {
+                sb.append(String.format(" Band Count: %d\n", mBandCount));
+            }
+            return sb.toString();
+        }
+    }
+
+    /**
+     * Base class for bands
+     */
+    public static class BandBase {
+        private boolean mEnabled;
+        private float mCutoffFrequency;
+        /**
+         * Class constructor for BandBase
+         * @param enabled true if this band is currently used to process sound. When false,
+         * the band is effectively muted and sound set to zero.
+         * @param cutoffFrequency topmost frequency number (in Hz) this band will process. The
+         * effective bandwidth for the band is then computed using this and the previous band
+         * topmost frequency (or 0 Hz for band number 0). Frequencies are expected to increase with
+         * band number, thus band 0 cutoffFrequency <= band 1 cutoffFrequency, and so on.
+         */
+        public BandBase(boolean enabled, float cutoffFrequency) {
+            mEnabled = enabled;
+            mCutoffFrequency = cutoffFrequency;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(String.format(" Enabled: %b\n", mEnabled));
+            sb.append(String.format(" CutoffFrequency: %f\n", mCutoffFrequency));
+            return sb.toString();
+        }
+
+        /**
+         * returns enabled state of the band
+         * @return true if bands is enabled for processing, false otherwise
+         */
+        public boolean isEnabled() {
+            return mEnabled;
+        }
+        /**
+         * sets enabled state of the band
+         * @param enabled true for enabled, false otherwise
+         */
+        public void setEnabled(boolean enabled) {
+            mEnabled = enabled;
+        }
+
+        /**
+         * gets cutoffFrequency for this band in Hertz (Hz)
+         * @return cutoffFrequency for this band in Hertz (Hz)
+         */
+        public float getCutoffFrequency() {
+            return mCutoffFrequency;
+        }
+
+        /**
+         * sets topmost frequency number (in Hz) this band will process. The
+         * effective bandwidth for the band is then computed using this and the previous band
+         * topmost frequency (or 0 Hz for band number 0). Frequencies are expected to increase with
+         * band number, thus band 0 cutoffFrequency <= band 1 cutoffFrequency, and so on.
+         * @param frequency
+         */
+        public void setCutoffFrequency(float frequency) {
+            mCutoffFrequency = frequency;
+        }
+    }
+
+    /**
+     * Class for Equalizer Bands
+     * Equalizer bands have three controllable parameters: enabled/disabled, cutoffFrequency and
+     * gain
+     */
+    public final static class EqBand extends BandBase {
+        private float mGain;
+        /**
+         * Class constructor for EqBand
+         * @param enabled true if this band is currently used to process sound. When false,
+         * the band is effectively muted and sound set to zero.
+         * @param cutoffFrequency topmost frequency number (in Hz) this band will process. The
+         * effective bandwidth for the band is then computed using this and the previous band
+         * topmost frequency (or 0 Hz for band number 0). Frequencies are expected to increase with
+         * band number, thus band 0 cutoffFrequency <= band 1 cutoffFrequency, and so on.
+         * @param gain of equalizer band in decibels (dB). A gain of 0 dB means no change in level.
+         */
+        public EqBand(boolean enabled, float cutoffFrequency, float gain) {
+            super(enabled, cutoffFrequency);
+            mGain = gain;
+        }
+
+        /**
+         * Class constructor for EqBand
+         * @param cfg copy constructor
+         */
+        public EqBand(EqBand cfg) {
+            super(cfg.isEnabled(), cfg.getCutoffFrequency());
+            mGain = cfg.mGain;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(super.toString());
+            sb.append(String.format(" Gain: %f\n", mGain));
+            return sb.toString();
+        }
+
+        /**
+         * gets current gain of band in decibels (dB)
+         * @return current gain of band in decibels (dB)
+         */
+        public float getGain() {
+            return mGain;
+        }
+
+        /**
+         * sets current gain of band in decibels (dB)
+         * @param gain desired in decibels (db)
+         */
+        public void setGain(float gain) {
+            mGain = gain;
+        }
+    }
+
+    /**
+     * Class for Multi-Band compressor bands
+     * MBC bands have multiple controllable parameters: enabled/disabled, cutoffFrequency,
+     * attackTime, releaseTime, ratio, threshold, kneeWidth, noiseGateThreshold, expanderRatio,
+     * preGain and postGain.
+     */
+    public final static class MbcBand extends BandBase{
+        private float mAttackTime;
+        private float mReleaseTime;
+        private float mRatio;
+        private float mThreshold;
+        private float mKneeWidth;
+        private float mNoiseGateThreshold;
+        private float mExpanderRatio;
+        private float mPreGain;
+        private float mPostGain;
+        /**
+         * Class constructor for MbcBand
+         * @param enabled true if this band is currently used to process sound. When false,
+         * the band is effectively muted and sound set to zero.
+         * @param cutoffFrequency topmost frequency number (in Hz) this band will process. The
+         * effective bandwidth for the band is then computed using this and the previous band
+         * topmost frequency (or 0 Hz for band number 0). Frequencies are expected to increase with
+         * band number, thus band 0 cutoffFrequency <= band 1 cutoffFrequency, and so on.
+         * @param attackTime Attack Time for compressor in milliseconds (ms)
+         * @param releaseTime Release Time for compressor in milliseconds (ms)
+         * @param ratio Compressor ratio (1:N)
+         * @param threshold Compressor threshold measured in decibels (dB) from 0 dB Full Scale
+         * (dBFS).
+         * @param kneeWidth Width in decibels (dB) around compressor threshold point.
+         * @param noiseGateThreshold Noise gate threshold in decibels (dB) from 0 dB Full Scale
+         * (dBFS).
+         * @param expanderRatio Expander ratio (N:1) for signals below the Noise Gate Threshold.
+         * @param preGain Gain applied to the signal BEFORE the compression.
+         * @param postGain Gain applied to the signal AFTER compression.
+         */
+        public MbcBand(boolean enabled, float cutoffFrequency, float attackTime, float releaseTime,
+                float ratio, float threshold, float kneeWidth, float noiseGateThreshold,
+                float expanderRatio, float preGain, float postGain) {
+            super(enabled, cutoffFrequency);
+            mAttackTime = attackTime;
+            mReleaseTime = releaseTime;
+            mRatio = ratio;
+            mThreshold = threshold;
+            mKneeWidth = kneeWidth;
+            mNoiseGateThreshold = noiseGateThreshold;
+            mExpanderRatio = expanderRatio;
+            mPreGain = preGain;
+            mPostGain = postGain;
+        }
+
+        /**
+         * Class constructor for MbcBand
+         * @param cfg copy constructor
+         */
+        public MbcBand(MbcBand cfg) {
+            super(cfg.isEnabled(), cfg.getCutoffFrequency());
+            mAttackTime = cfg.mAttackTime;
+            mReleaseTime = cfg.mReleaseTime;
+            mRatio = cfg.mRatio;
+            mThreshold = cfg.mThreshold;
+            mKneeWidth = cfg.mKneeWidth;
+            mNoiseGateThreshold = cfg.mNoiseGateThreshold;
+            mExpanderRatio = cfg.mExpanderRatio;
+            mPreGain = cfg.mPreGain;
+            mPostGain = cfg.mPostGain;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(super.toString());
+            sb.append(String.format(" AttackTime: %f (ms)\n", mAttackTime));
+            sb.append(String.format(" ReleaseTime: %f (ms)\n", mReleaseTime));
+            sb.append(String.format(" Ratio: 1:%f\n", mRatio));
+            sb.append(String.format(" Threshold: %f (dB)\n", mThreshold));
+            sb.append(String.format(" NoiseGateThreshold: %f(dB)\n", mNoiseGateThreshold));
+            sb.append(String.format(" ExpanderRatio: %f:1\n", mExpanderRatio));
+            sb.append(String.format(" PreGain: %f (dB)\n", mPreGain));
+            sb.append(String.format(" PostGain: %f (dB)\n", mPostGain));
+            return sb.toString();
+        }
+
+        /**
+         * gets attack time for compressor in milliseconds (ms)
+         * @return attack time for compressor in milliseconds (ms)
+         */
+        public float getAttackTime() { return mAttackTime; }
+        /**
+         * sets attack time for compressor in milliseconds (ms)
+         * @param attackTime desired for compressor in milliseconds (ms)
+         */
+        public void setAttackTime(float attackTime) { mAttackTime = attackTime; }
+        /**
+         * gets release time for compressor in milliseconds (ms)
+         * @return release time for compressor in milliseconds (ms)
+         */
+        public float getReleaseTime() { return mReleaseTime; }
+        /**
+         * sets release time for compressor in milliseconds (ms)
+         * @param releaseTime desired for compressor in milliseconds (ms)
+         */
+        public void setReleaseTime(float releaseTime) { mReleaseTime = releaseTime; }
+        /**
+         * gets the compressor ratio (1:N)
+         * @return compressor ratio (1:N)
+         */
+        public float getRatio() { return mRatio; }
+        /**
+         * sets compressor ratio (1:N)
+         * @param ratio desired for the compressor (1:N)
+         */
+        public void setRatio(float ratio) { mRatio = ratio; }
+        /**
+         * gets the compressor threshold measured in decibels (dB) from 0 dB Full Scale (dBFS).
+         * Thresholds are negative. A threshold of 0 dB means no compression will take place.
+         * @return compressor threshold in decibels (dB)
+         */
+        public float getThreshold() { return mThreshold; }
+        /**
+         * sets the compressor threshold measured in decibels (dB) from 0 dB Full Scale (dBFS).
+         * Thresholds are negative. A threshold of 0 dB means no compression will take place.
+         * @param threshold desired for compressor in decibels(dB)
+         */
+        public void setThreshold(float threshold) { mThreshold = threshold; }
+        /**
+         * get Knee Width in decibels (dB) around compressor threshold point. Widths are always
+         * positive, with higher values representing a wider area of transition from the linear zone
+         * to the compression zone. A knee of 0 dB means a more abrupt transition.
+         * @return Knee Width in decibels (dB)
+         */
+        public float getKneeWidth() { return mKneeWidth; }
+        /**
+         * sets knee width in decibels (dB). See
+         * {@link android.media.audiofx.DynamicsProcessing.MbcBand#getKneeWidth} for more
+         * information.
+         * @param kneeWidth desired in decibels (dB)
+         */
+        public void setKneeWidth(float kneeWidth) { mKneeWidth = kneeWidth; }
+        /**
+         * gets the noise gate threshold in decibels (dB) from 0 dB Full Scale (dBFS). Noise gate
+         * thresholds are negative. Signals below this level will be expanded according the
+         * expanderRatio parameter. A Noise Gate Threshold of -75 dB means very quiet signals might
+         * be effectively removed from the signal.
+         * @return Noise Gate Threshold in decibels (dB)
+         */
+        public float getNoiseGateThreshold() { return mNoiseGateThreshold; }
+        /**
+         * sets noise gate threshod in decibels (dB). See
+         * {@link android.media.audiofx.DynamicsProcessing.MbcBand#getNoiseGateThreshold} for more
+         * information.
+         * @param noiseGateThreshold desired in decibels (dB)
+         */
+        public void setNoiseGateThreshold(float noiseGateThreshold) {
+            mNoiseGateThreshold = noiseGateThreshold; }
+        /**
+         * gets Expander ratio (N:1) for signals below the Noise Gate Threshold.
+         * @return Expander ratio (N:1)
+         */
+        public float getExpanderRatio() { return mExpanderRatio; }
+        /**
+         * sets Expander ratio (N:1) for signals below the Noise Gate Threshold.
+         * @param expanderRatio desired expander ratio (N:1)
+         */
+        public void setExpanderRatio(float expanderRatio) { mExpanderRatio = expanderRatio; }
+        /**
+         * gets the gain applied to the signal BEFORE the compression. Measured in decibels (dB)
+         * where 0 dB means no level change.
+         * @return preGain value in decibels (dB)
+         */
+        public float getPreGain() { return mPreGain; }
+        /**
+         * sets the gain to be applied to the signal BEFORE the compression, measured in decibels
+         * (dB), where 0 dB means no level change.
+         * @param preGain desired in decibels (dB)
+         */
+        public void setPreGain(float preGain) { mPreGain = preGain; }
+        /**
+         * gets the gain applied to the signal AFTER compression. Measured in decibels (dB) where 0
+         * dB means no level change
+         * @return postGain value in decibels (dB)
+         */
+        public float getPostGain() { return mPostGain; }
+        /**
+         * sets the gain to be applied to the siganl AFTER the compression. Measured in decibels
+         * (dB), where 0 dB means no level change.
+         * @param postGain desired value in decibels (dB)
+         */
+        public void setPostGain(float postGain) { mPostGain = postGain; }
+    }
+
+    /**
+     * Class for Equalizer stage
+     */
+    public final static class Eq extends BandStage {
+        private final EqBand[] mBands;
+        /**
+         * Class constructor for Equalizer (Eq) stage
+         * @param inUse true if Eq stage will be used, false otherwise.
+         * @param enabled true if Eq stage is enabled/disabled. This can be changed while effect is
+         * running
+         * @param bandCount number of bands for this Equalizer stage. Can't be changed while effect
+         * is running
+         */
+        public Eq(boolean inUse, boolean enabled, int bandCount) {
+            super(inUse, enabled, bandCount);
+            if (isInUse()) {
+                mBands = new EqBand[bandCount];
+                for (int b = 0; b < bandCount; b++) {
+                    float freq = DEFAULT_MAX_FREQUENCY;
+                    if (bandCount > 1) {
+                        freq = (float)Math.pow(10, mMinFreqLog +
+                                b * (mMaxFreqLog - mMinFreqLog)/(bandCount -1));
+                    }
+                    mBands[b] = new EqBand(true, freq, EQ_DEFAULT_GAIN);
+                }
+            } else {
+                mBands = null;
+            }
+        }
+        /**
+         * Class constructor for Eq stage
+         * @param cfg copy constructor
+         */
+        public Eq(Eq cfg) {
+            super(cfg.isInUse(), cfg.isEnabled(), cfg.getBandCount());
+            if (isInUse()) {
+                mBands = new EqBand[cfg.mBands.length];
+                for (int b = 0; b < mBands.length; b++) {
+                    mBands[b] = new EqBand(cfg.mBands[b]);
+                }
+            } else {
+                mBands = null;
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(super.toString());
+            if (isInUse()) {
+                sb.append("--->EqBands: " + mBands.length + "\n");
+                for (int b = 0; b < mBands.length; b++) {
+                    sb.append(String.format("  Band %d\n", b));
+                    sb.append(mBands[b].toString());
+                }
+            }
+            return sb.toString();
+        }
+        /**
+         * Helper function to check if band index is within range
+         * @param band index to check
+         */
+        private void checkBand(int band) {
+            if (mBands == null || band < 0 || band >= mBands.length) {
+                throw new IllegalArgumentException("band index " + band +" out of bounds");
+            }
+        }
+        /**
+         * Sets EqBand object for given band index
+         * @param band index of band to be modified
+         * @param bandCfg EqBand object.
+         */
+        public void setBand(int band, EqBand bandCfg) {
+            checkBand(band);
+            mBands[band] = new EqBand(bandCfg);
+        }
+        /**
+         * Gets EqBand object for band of interest.
+         * @param band index of band of interest
+         * @return EqBand Object
+         */
+        public EqBand getBand(int band) {
+            checkBand(band);
+            return mBands[band];
+        }
+    }
+
+    /**
+     * Class for Multi-Band Compressor (MBC) stage
+     */
+    public final static class Mbc extends BandStage {
+        private final MbcBand[] mBands;
+        /**
+         * Constructor for Multi-Band Compressor (MBC) stage
+         * @param inUse true if MBC stage will be used, false otherwise.
+         * @param enabled true if MBC stage is enabled/disabled. This can be changed while effect
+         * is running
+         * @param bandCount number of bands for this MBC stage. Can't be changed while effect is
+         * running
+         */
+        public Mbc(boolean inUse, boolean enabled, int bandCount) {
+            super(inUse, enabled, bandCount);
+            if (isInUse()) {
+                mBands = new MbcBand[bandCount];
+                for (int b = 0; b < bandCount; b++) {
+                    float freq = DEFAULT_MAX_FREQUENCY;
+                    if (bandCount > 1) {
+                        freq = (float)Math.pow(10, mMinFreqLog +
+                                b * (mMaxFreqLog - mMinFreqLog)/(bandCount -1));
+                    }
+                    mBands[b] = new MbcBand(true, freq, MBC_DEFAULT_ATTACK_TIME,
+                            MBC_DEFAULT_RELEASE_TIME, MBC_DEFAULT_RATIO,
+                            MBC_DEFAULT_THRESHOLD, MBC_DEFAULT_KNEE_WIDTH,
+                            MBC_DEFAULT_NOISE_GATE_THRESHOLD, MBC_DEFAULT_EXPANDER_RATIO,
+                            MBC_DEFAULT_PRE_GAIN, MBC_DEFAULT_POST_GAIN);
+                }
+            } else {
+                mBands = null;
+            }
+        }
+        /**
+         * Class constructor for MBC stage
+         * @param cfg copy constructor
+         */
+        public Mbc(Mbc cfg) {
+            super(cfg.isInUse(), cfg.isEnabled(), cfg.getBandCount());
+            if (isInUse()) {
+                mBands = new MbcBand[cfg.mBands.length];
+                for (int b = 0; b < mBands.length; b++) {
+                    mBands[b] = new MbcBand(cfg.mBands[b]);
+                }
+            } else {
+                mBands = null;
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(super.toString());
+            if (isInUse()) {
+                sb.append("--->MbcBands: " + mBands.length + "\n");
+                for (int b = 0; b < mBands.length; b++) {
+                    sb.append(String.format("  Band %d\n", b));
+                    sb.append(mBands[b].toString());
+                }
+            }
+            return sb.toString();
+        }
+        /**
+         * Helper function to check if band index is within range
+         * @param band index to check
+         */
+        private void checkBand(int band) {
+            if (mBands == null || band < 0 || band >= mBands.length) {
+                throw new IllegalArgumentException("band index " + band +" out of bounds");
+            }
+        }
+        /**
+         * Sets MbcBand object for given band index
+         * @param band index of band to be modified
+         * @param bandCfg MbcBand object.
+         */
+        public void setBand(int band, MbcBand bandCfg) {
+            checkBand(band);
+            mBands[band] = new MbcBand(bandCfg);
+        }
+        /**
+         * Gets MbcBand object for band of interest.
+         * @param band index of band of interest
+         * @return MbcBand Object
+         */
+        public MbcBand getBand(int band) {
+            checkBand(band);
+            return mBands[band];
+        }
+    }
+
+    /**
+     * Class for Limiter Stage
+     * Limiter is a single band compressor at the end of the processing chain, commonly used to
+     * protect the signal from overloading and distortion. Limiters have multiple controllable
+     * parameters: enabled/disabled, linkGroup, attackTime, releaseTime, ratio, threshold, and
+     * postGain.
+     * <p>Limiters can be linked in groups across multiple channels. Linked limiters will trigger
+     * the same limiting if any of the linked limiters starts compressing.
+     */
+    public final static class Limiter extends Stage {
+        private int mLinkGroup;
+        private float mAttackTime;
+        private float mReleaseTime;
+        private float mRatio;
+        private float mThreshold;
+        private float mPostGain;
+
+        /**
+         * Class constructor for Limiter Stage
+         * @param inUse true if MBC stage will be used, false otherwise.
+         * @param enabled true if MBC stage is enabled/disabled. This can be changed while effect
+         * is running
+         * @param linkGroup index of group assigned to this Limiter. Only limiters that share the
+         * same linkGroup index will react together.
+         * @param attackTime Attack Time for limiter compressor in milliseconds (ms)
+         * @param releaseTime Release Time for limiter compressor in milliseconds (ms)
+         * @param ratio Limiter Compressor ratio (1:N)
+         * @param threshold Limiter Compressor threshold measured in decibels (dB) from 0 dB Full
+         * Scale (dBFS).
+         * @param postGain Gain applied to the signal AFTER compression.
+         */
+        public Limiter(boolean inUse, boolean enabled, int linkGroup, float attackTime,
+                float releaseTime, float ratio, float threshold, float postGain) {
+            super(inUse, enabled);
+            mLinkGroup = linkGroup;
+            mAttackTime = attackTime;
+            mReleaseTime = releaseTime;
+            mRatio = ratio;
+            mThreshold = threshold;
+            mPostGain = postGain;
+        }
+
+        /**
+         * Class Constructor for Limiter
+         * @param cfg copy constructor
+         */
+        public Limiter(Limiter cfg) {
+            super(cfg.isInUse(), cfg.isEnabled());
+            mLinkGroup = cfg.mLinkGroup;
+            mAttackTime = cfg.mAttackTime;
+            mReleaseTime = cfg.mReleaseTime;
+            mRatio = cfg.mRatio;
+            mThreshold = cfg.mThreshold;
+            mPostGain = cfg.mPostGain;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(super.toString());
+            if (isInUse()) {
+                sb.append(String.format(" LinkGroup: %d (group)\n", mLinkGroup));
+                sb.append(String.format(" AttackTime: %f (ms)\n", mAttackTime));
+                sb.append(String.format(" ReleaseTime: %f (ms)\n", mReleaseTime));
+                sb.append(String.format(" Ratio: 1:%f\n", mRatio));
+                sb.append(String.format(" Threshold: %f (dB)\n", mThreshold));
+                sb.append(String.format(" PostGain: %f (dB)\n", mPostGain));
+            }
+            return sb.toString();
+        }
+        /**
+         * Gets the linkGroup index for this Limiter Stage. Only limiters that share the same
+         * linkGroup index will react together.
+         * @return linkGroup index.
+         */
+        public int getLinkGroup() { return mLinkGroup; }
+        /**
+         * Sets the linkGroup index for this limiter Stage.
+         * @param linkGroup desired linkGroup index
+         */
+        public void setLinkGroup(int linkGroup) { mLinkGroup = linkGroup; }
+        /**
+         * gets attack time for limiter compressor in milliseconds (ms)
+         * @return attack time for limiter compressor in milliseconds (ms)
+         */
+        public float getAttackTime() { return mAttackTime; }
+        /**
+         * sets attack time for limiter compressor in milliseconds (ms)
+         * @param attackTime desired for limiter compressor in milliseconds (ms)
+         */
+        public void setAttackTime(float attackTime) { mAttackTime = attackTime; }
+        /**
+         * gets release time for limiter compressor in milliseconds (ms)
+         * @return release time for limiter compressor in milliseconds (ms)
+         */
+        public float getReleaseTime() { return mReleaseTime; }
+        /**
+         * sets release time for limiter compressor in milliseconds (ms)
+         * @param releaseTime desired for limiter compressor in milliseconds (ms)
+         */
+        public void setReleaseTime(float releaseTime) { mReleaseTime = releaseTime; }
+        /**
+         * gets the limiter compressor ratio (1:N)
+         * @return limiter compressor ratio (1:N)
+         */
+        public float getRatio() { return mRatio; }
+        /**
+         * sets limiter compressor ratio (1:N)
+         * @param ratio desired for the limiter compressor (1:N)
+         */
+        public void setRatio(float ratio) { mRatio = ratio; }
+        /**
+         * gets the limiter compressor threshold measured in decibels (dB) from 0 dB Full Scale
+         * (dBFS). Thresholds are negative. A threshold of 0 dB means no limiting will take place.
+         * @return limiter compressor threshold in decibels (dB)
+         */
+        public float getThreshold() { return mThreshold; }
+        /**
+         * sets the limiter compressor threshold measured in decibels (dB) from 0 dB Full Scale
+         * (dBFS). Thresholds are negative. A threshold of 0 dB means no limiting will take place.
+         * @param threshold desired for limiter compressor in decibels(dB)
+         */
+        public void setThreshold(float threshold) { mThreshold = threshold; }
+        /**
+         * gets the gain applied to the signal AFTER limiting. Measured in decibels (dB) where 0
+         * dB means no level change
+         * @return postGain value in decibels (dB)
+         */
+        public float getPostGain() { return mPostGain; }
+        /**
+         * sets the gain to be applied to the siganl AFTER the limiter. Measured in decibels
+         * (dB), where 0 dB means no level change.
+         * @param postGain desired value in decibels (dB)
+         */
+        public void setPostGain(float postGain) { mPostGain = postGain; }
+    }
+
+    /**
+     * Class for Channel configuration parameters. It is composed of multiple stages, which can be
+     * used/enabled independently. Stages not used or disabled will be bypassed and the sound would
+     * be unaffected by them.
+     */
+    public final static class Channel {
+        private float   mInputGain;
+        private Eq      mPreEq;
+        private Mbc     mMbc;
+        private Eq      mPostEq;
+        private Limiter mLimiter;
+
+        /**
+         * Class constructor for Channel configuration.
+         * @param inputGain value in decibels (dB) of level change applied to the audio before
+         * processing. A value of 0 dB means no change.
+         * @param preEqInUse true if PreEq stage will be used, false otherwise. This can't be
+         * changed later.
+         * @param preEqBandCount number of bands for PreEq stage. This can't be changed later.
+         * @param mbcInUse true if Mbc stage will be used, false otherwise. This can't be changed
+         * later.
+         * @param mbcBandCount number of bands for Mbc stage. This can't be changed later.
+         * @param postEqInUse true if PostEq stage will be used, false otherwise. This can't be
+         * changed later.
+         * @param postEqBandCount number of bands for PostEq stage. This can't be changed later.
+         * @param limiterInUse true if Limiter stage will be used, false otherwise. This can't be
+         * changed later.
+         */
+        public Channel (float inputGain,
+                boolean preEqInUse, int preEqBandCount,
+                boolean mbcInUse, int mbcBandCount,
+                boolean postEqInUse, int postEqBandCount,
+                boolean limiterInUse) {
+            mInputGain = inputGain;
+            mPreEq = new Eq(preEqInUse, PREEQ_DEFAULT_ENABLED, preEqBandCount);
+            mMbc = new Mbc(mbcInUse, MBC_DEFAULT_ENABLED, mbcBandCount);
+            mPostEq = new Eq(postEqInUse, POSTEQ_DEFAULT_ENABLED,
+                    postEqBandCount);
+            mLimiter = new Limiter(limiterInUse,
+                    LIMITER_DEFAULT_ENABLED, LIMITER_DEFAULT_LINK_GROUP,
+                    LIMITER_DEFAULT_ATTACK_TIME, LIMITER_DEFAULT_RELEASE_TIME,
+                    LIMITER_DEFAULT_RATIO, LIMITER_DEFAULT_THRESHOLD, LIMITER_DEFAULT_POST_GAIN);
+        }
+
+        /**
+         * Class constructor for Channel configuration
+         * @param cfg copy constructor
+         */
+        public Channel(Channel cfg) {
+            mInputGain = cfg.mInputGain;
+            mPreEq = new Eq(cfg.mPreEq);
+            mMbc = new Mbc(cfg.mMbc);
+            mPostEq = new Eq(cfg.mPostEq);
+            mLimiter = new Limiter(cfg.mLimiter);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(String.format(" InputGain: %f\n", mInputGain));
+            sb.append("-->PreEq\n");
+            sb.append(mPreEq.toString());
+            sb.append("-->MBC\n");
+            sb.append(mMbc.toString());
+            sb.append("-->PostEq\n");
+            sb.append(mPostEq.toString());
+            sb.append("-->Limiter\n");
+            sb.append(mLimiter.toString());
+            return sb.toString();
+        }
+        /**
+         * Gets inputGain value in decibels (dB). 0 dB means no change;
+         * @return gain value in decibels (dB)
+         */
+        public float getInputGain() {
+            return mInputGain;
+        }
+        /**
+         * Sets inputGain value in decibels (dB). 0 dB means no change;
+         * @param inputGain desired gain value in decibels (dB)
+         */
+        public void setInputGain(float inputGain) {
+            mInputGain = inputGain;
+        }
+
+        /**
+         * Gets PreEq configuration stage
+         * @return PreEq configuration stage
+         */
+        public Eq getPreEq() {
+            return mPreEq;
+        }
+        /**
+         * Sets PreEq configuration stage. New PreEq stage must have the same number of bands than
+         * original PreEq stage.
+         * @param preEq configuration
+         */
+        public void setPreEq(Eq preEq) {
+            if (preEq.getBandCount() != mPreEq.getBandCount()) {
+                throw new IllegalArgumentException("PreEqBandCount changed from " +
+                        mPreEq.getBandCount() + " to " + preEq.getBandCount());
+            }
+            mPreEq = new Eq(preEq);
+        }
+        /**
+         * Gets EqBand for PreEq stage for given band index.
+         * @param band index of band of interest from PreEq stage
+         * @return EqBand configuration
+         */
+        public EqBand getPreEqBand(int band) {
+            return mPreEq.getBand(band);
+        }
+        /**
+         * Sets EqBand for PreEq stage for given band index
+         * @param band index of band of interest from PreEq stage
+         * @param preEqBand configuration to be set.
+         */
+        public void setPreEqBand(int band, EqBand preEqBand) {
+            mPreEq.setBand(band, preEqBand);
+        }
+
+        /**
+         * Gets Mbc configuration stage
+         * @return Mbc configuration stage
+         */
+        public Mbc getMbc() {
+            return mMbc;
+        }
+        /**
+         * Sets Mbc configuration stage. New Mbc stage must have the same number of bands than
+         * original Mbc stage.
+         * @param mbc
+         */
+        public void setMbc(Mbc mbc) {
+            if (mbc.getBandCount() != mMbc.getBandCount()) {
+                throw new IllegalArgumentException("MbcBandCount changed from " +
+                        mMbc.getBandCount() + " to " + mbc.getBandCount());
+            }
+            mMbc = new Mbc(mbc);
+        }
+        /**
+         * Gets MbcBand configuration for Mbc stage, for given band index.
+         * @param band index of band of interest from Mbc stage
+         * @return MbcBand configuration
+         */
+        public MbcBand getMbcBand(int band) {
+            return mMbc.getBand(band);
+        }
+        /**
+         * Sets MbcBand for Mbc stage for given band index
+         * @param band index of band of interest from Mbc Stage
+         * @param mbcBand configuration to be set
+         */
+        public void setMbcBand(int band, MbcBand mbcBand) {
+            mMbc.setBand(band, mbcBand);
+        }
+
+        /**
+         * Gets PostEq configuration stage
+         * @return PostEq configuration stage
+         */
+        public Eq getPostEq() {
+            return mPostEq;
+        }
+        /**
+         * Sets PostEq configuration stage. New PostEq stage must have the same number of bands than
+         * original PostEq stage.
+         * @param postEq configuration
+         */
+        public void setPostEq(Eq postEq) {
+            if (postEq.getBandCount() != mPostEq.getBandCount()) {
+                throw new IllegalArgumentException("PostEqBandCount changed from " +
+                        mPostEq.getBandCount() + " to " + postEq.getBandCount());
+            }
+            mPostEq = new Eq(postEq);
+        }
+        /**
+         * Gets EqBand for PostEq stage for given band index.
+         * @param band index of band of interest from PostEq stage
+         * @return EqBand configuration
+         */
+        public EqBand getPostEqBand(int band) {
+            return mPostEq.getBand(band);
+        }
+        /**
+         * Sets EqBand for PostEq stage for given band index
+         * @param band index of band of interest from PostEq stage
+         * @param postEqBand configuration to be set.
+         */
+        public void setPostEqBand(int band, EqBand postEqBand) {
+            mPostEq.setBand(band, postEqBand);
+        }
+
+        /**
+         * Gets Limiter configuration stage
+         * @return Limiter configuration stage
+         */
+        public Limiter getLimiter() {
+            return mLimiter;
+        }
+        /**
+         * Sets Limiter configuration stage.
+         * @param limiter configuration stage.
+         */
+        public void setLimiter(Limiter limiter) {
+            mLimiter = new Limiter(limiter);
+        }
+    }
+
+    /**
+     * Class for Config object, used by DynamicsProcessing to configure and update the audio effect.
+     * use Builder to instantiate objects of this type.
+     */
+    public final static class Config {
+        private final int mVariant;
+        private final int mChannelCount;
+        private final boolean mPreEqInUse;
+        private final int mPreEqBandCount;
+        private final boolean mMbcInUse;
+        private final int mMbcBandCount;
+        private final boolean mPostEqInUse;
+        private final int mPostEqBandCount;
+        private final boolean mLimiterInUse;
+        private final float mPreferredFrameDuration;
+        private final Channel[] mChannel;
+
+        /**
+         * @hide
+         * Class constructor for config. None of these parameters can be changed later.
+         * @param variant index of variant used for effect engine. See
+         * {@link #VARIANT_FAVOR_FREQUENCY_RESOLUTION} and {@link #VARIANT_FAVOR_TIME_RESOLUTION}.
+         * @param frameDurationMs preferred frame duration in milliseconds (ms).
+         * @param channelCount Number of channels to be configured.
+         * @param preEqInUse true if PreEq stage will be used, false otherwise.
+         * @param preEqBandCount number of bands for PreEq stage.
+         * @param mbcInUse true if Mbc stage will be used, false otherwise.
+         * @param mbcBandCount number of bands for Mbc stage.
+         * @param postEqInUse true if PostEq stage will be used, false otherwise.
+         * @param postEqBandCount number of bands for PostEq stage.
+         * @param limiterInUse true if Limiter stage will be used, false otherwise.
+         * @param channel array of Channel objects to be used for this configuration.
+         */
+        public Config(int variant, float frameDurationMs, int channelCount,
+                boolean preEqInUse, int preEqBandCount,
+                boolean mbcInUse, int mbcBandCount,
+                boolean postEqInUse, int postEqBandCount,
+                boolean limiterInUse,
+                Channel[] channel) {
+            mVariant = variant;
+            mPreferredFrameDuration = frameDurationMs;
+            mChannelCount = channelCount;
+            mPreEqInUse = preEqInUse;
+            mPreEqBandCount = preEqBandCount;
+            mMbcInUse = mbcInUse;
+            mMbcBandCount = mbcBandCount;
+            mPostEqInUse = postEqInUse;
+            mPostEqBandCount = postEqBandCount;
+            mLimiterInUse = limiterInUse;
+
+            mChannel = new Channel[mChannelCount];
+            //check if channelconfig is null or has less channels than channel count.
+            //options: fill the missing with default options.
+            // or fail?
+            for (int ch = 0; ch < mChannelCount; ch++) {
+                if (ch < channel.length) {
+                    mChannel[ch] = new Channel(channel[ch]); //copy create
+                } else {
+                    //create a new one from scratch? //fail?
+                }
+            }
+        }
+        //a version that will scale to necessary number of channels
+        /**
+         * @hide
+         * Class constructor for Configuration.
+         * @param channelCount limit configuration to this number of channels. if channelCount is
+         * greater than number of channels in cfg, the constructor will duplicate the last channel
+         * found as many times as necessary to create a Config with channelCount number of channels.
+         * If channelCount is less than channels in cfg, the extra channels in cfg will be ignored.
+         * @param cfg copy constructor paremter.
+         */
+        public Config(int channelCount, Config cfg) {
+            mVariant = cfg.mVariant;
+            mPreferredFrameDuration = cfg.mPreferredFrameDuration;
+            mChannelCount = cfg.mChannelCount;
+            mPreEqInUse = cfg.mPreEqInUse;
+            mPreEqBandCount = cfg.mPreEqBandCount;
+            mMbcInUse = cfg.mMbcInUse;
+            mMbcBandCount = cfg.mMbcBandCount;
+            mPostEqInUse = cfg.mPostEqInUse;
+            mPostEqBandCount = cfg.mPostEqBandCount;
+            mLimiterInUse = cfg.mLimiterInUse;
+
+            if (mChannelCount != cfg.mChannel.length) {
+                throw new IllegalArgumentException("configuration channel counts differ " +
+                        mChannelCount + " !=" + cfg.mChannel.length);
+            }
+            if (channelCount < 1) {
+                throw new IllegalArgumentException("channel resizing less than 1 not allowed");
+            }
+
+            mChannel = new Channel[channelCount];
+            for (int ch = 0; ch < channelCount; ch++) {
+                if (ch < mChannelCount) {
+                    mChannel[ch] = new Channel(cfg.mChannel[ch]);
+                } else {
+                    //duplicate last
+                    mChannel[ch] = new Channel(cfg.mChannel[mChannelCount-1]);
+                }
+            }
+        }
+
+        /**
+         * @hide
+         * Class constructor for Config
+         * @param cfg Configuration object copy constructor
+         */
+        public Config(@NonNull Config cfg) {
+            this(cfg.mChannelCount, cfg);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append(String.format("Variant: %d\n", mVariant));
+            sb.append(String.format("PreferredFrameDuration: %f\n", mPreferredFrameDuration));
+            sb.append(String.format("ChannelCount: %d\n", mChannelCount));
+            sb.append(String.format("PreEq inUse: %b, bandCount:%d\n",mPreEqInUse,
+                    mPreEqBandCount));
+            sb.append(String.format("Mbc inUse: %b, bandCount: %d\n",mMbcInUse, mMbcBandCount));
+            sb.append(String.format("PostEq inUse: %b, bandCount: %d\n", mPostEqInUse,
+                    mPostEqBandCount));
+            sb.append(String.format("Limiter inUse: %b\n", mLimiterInUse));
+            for (int ch = 0; ch < mChannel.length; ch++) {
+                sb.append(String.format("==Channel %d\n", ch));
+                sb.append(mChannel[ch].toString());
+            }
+            return sb.toString();
+        }
+        private void checkChannel(int channelIndex) {
+            if (channelIndex < 0 || channelIndex >= mChannel.length) {
+                throw new IllegalArgumentException("ChannelIndex out of bounds");
+            }
+        }
+
+        //getters and setters
+        /**
+         * Gets variant for effect engine See {@link #VARIANT_FAVOR_FREQUENCY_RESOLUTION} and
+         * {@link #VARIANT_FAVOR_TIME_RESOLUTION}.
+         * @return variant of effect engine
+         */
+        public int getVariant() {
+            return mVariant;
+        }
+        /**
+         * Gets preferred frame duration in milliseconds (ms).
+         * @return preferred frame duration in milliseconds (ms)
+         */
+        public float getPreferredFrameDuration() {
+            return mPreferredFrameDuration;
+        }
+        /**
+         * Gets if preEq stage is in use
+         * @return true if preEq stage is in use;
+         */
+        public boolean isPreEqInUse() {
+            return mPreEqInUse;
+        }
+        /**
+         * Gets number of bands configured for the PreEq stage.
+         * @return number of bands configured for the PreEq stage.
+         */
+        public int getPreEqBandCount() {
+            return mPreEqBandCount;
+        }
+        /**
+         * Gets if Mbc stage is in use
+         * @return true if Mbc stage is in use;
+         */
+        public boolean isMbcInUse() {
+            return mMbcInUse;
+        }
+        /**
+         * Gets number of bands configured for the Mbc stage.
+         * @return number of bands configured for the Mbc stage.
+         */
+        public int getMbcBandCount() {
+            return mMbcBandCount;
+        }
+        /**
+         * Gets if PostEq stage is in use
+         * @return true if PostEq stage is in use;
+         */
+        public boolean isPostEqInUse() {
+            return mPostEqInUse;
+        }
+        /**
+         * Gets number of bands configured for the PostEq stage.
+         * @return number of bands configured for the PostEq stage.
+         */
+        public int getPostEqBandCount() {
+            return mPostEqBandCount;
+        }
+        /**
+         * Gets if Limiter stage is in use
+         * @return true if Limiter stage is in use;
+         */
+        public boolean isLimiterInUse() {
+            return mLimiterInUse;
+        }
+
+        //channel
+        /**
+         * Gets the Channel configuration object by using the channel index
+         * @param channelIndex of desired Channel object
+         * @return Channel configuration object
+         */
+        public Channel getChannelByChannelIndex(int channelIndex) {
+            checkChannel(channelIndex);
+            return mChannel[channelIndex];
+        }
+
+        /**
+         * Sets the chosen Channel object in the selected channelIndex
+         * Note that all the stages should have the same number of bands than the existing Channel
+         * object.
+         * @param channelIndex index of channel to be replaced
+         * @param channel Channel configuration object to be set
+         */
+        public void setChannelTo(int channelIndex, Channel channel) {
+            checkChannel(channelIndex);
+            //check all things are compatible
+            if (mMbcBandCount != channel.getMbc().getBandCount()) {
+                throw new IllegalArgumentException("MbcBandCount changed from " +
+                        mMbcBandCount + " to " + channel.getPreEq().getBandCount());
+            }
+            if (mPreEqBandCount != channel.getPreEq().getBandCount()) {
+                throw new IllegalArgumentException("PreEqBandCount changed from " +
+                        mPreEqBandCount + " to " + channel.getPreEq().getBandCount());
+            }
+            if (mPostEqBandCount != channel.getPostEq().getBandCount()) {
+                throw new IllegalArgumentException("PostEqBandCount changed from " +
+                        mPostEqBandCount + " to " + channel.getPostEq().getBandCount());
+            }
+            mChannel[channelIndex] = new Channel(channel);
+        }
+
+        /**
+         * Sets ALL channels to the chosen Channel object. Note that all the stages should have the
+         * same number of bands than the existing ones.
+         * @param channel Channel configuration object to be set.
+         */
+        public void setAllChannelsTo(Channel channel) {
+            for (int ch = 0; ch < mChannel.length; ch++) {
+                setChannelTo(ch, channel);
+            }
+        }
+
+        //===channel params
+        /**
+         * Gets inputGain value in decibels (dB) for channel indicated by channelIndex
+         * @param channelIndex index of channel of interest
+         * @return inputGain value in decibels (dB). 0 dB means no change.
+         */
+        public float getInputGainByChannelIndex(int channelIndex) {
+            checkChannel(channelIndex);
+            return mChannel[channelIndex].getInputGain();
+        }
+        /**
+         * Sets the inputGain value in decibels (dB) for the channel indicated by channelIndex.
+         * @param channelIndex index of channel of interest
+         * @param inputGain desired value in decibels (dB).
+         */
+        public void setInputGainByChannelIndex(int channelIndex, float inputGain) {
+            checkChannel(channelIndex);
+            mChannel[channelIndex].setInputGain(inputGain);
+        }
+        /**
+         * Sets the inputGain value in decibels (dB) for ALL channels
+         * @param inputGain desired value in decibels (dB)
+         */
+        public void setInputGainAllChannelsTo(float inputGain) {
+            for (int ch = 0; ch < mChannel.length; ch++) {
+                mChannel[ch].setInputGain(inputGain);
+            }
+        }
+
+        //=== PreEQ
+        /**
+         * Gets PreEq stage from channel indicated by channelIndex
+         * @param channelIndex index of channel of interest
+         * @return PreEq stage configuration object
+         */
+        public Eq getPreEqByChannelIndex(int channelIndex) {
+            checkChannel(channelIndex);
+            return mChannel[channelIndex].getPreEq();
+        }
+        /**
+         * Sets the PreEq stage configuration for the channel indicated by channelIndex. Note that
+         * new preEq stage must have the same number of bands than original preEq stage
+         * @param channelIndex index of channel to be set
+         * @param preEq desired PreEq configuration to be set
+         */
+        public void setPreEqByChannelIndex(int channelIndex, Eq preEq) {
+            checkChannel(channelIndex);
+            mChannel[channelIndex].setPreEq(preEq);
+        }
+        /**
+         * Sets the PreEq stage configuration for ALL channels. Note that new preEq stage must have
+         * the same number of bands than original preEq stages.
+         * @param preEq desired PreEq configuration to be set
+         */
+        public void setPreEqAllChannelsTo(Eq preEq) {
+            for (int ch = 0; ch < mChannel.length; ch++) {
+                mChannel[ch].setPreEq(preEq);
+            }
+        }
+        public EqBand getPreEqBandByChannelIndex(int channelIndex, int band) {
+            checkChannel(channelIndex);
+            return mChannel[channelIndex].getPreEqBand(band);
+        }
+        public void setPreEqBandByChannelIndex(int channelIndex, int band, EqBand preEqBand) {
+            checkChannel(channelIndex);
+            mChannel[channelIndex].setPreEqBand(band, preEqBand);
+        }
+        public void setPreEqBandAllChannelsTo(int band, EqBand preEqBand) {
+            for (int ch = 0; ch < mChannel.length; ch++) {
+                mChannel[ch].setPreEqBand(band, preEqBand);
+            }
+        }
+
+        //=== MBC
+        public Mbc getMbcByChannelIndex(int channelIndex) {
+            checkChannel(channelIndex);
+            return mChannel[channelIndex].getMbc();
+        }
+        public void setMbcByChannelIndex(int channelIndex, Mbc mbc) {
+            checkChannel(channelIndex);
+            mChannel[channelIndex].setMbc(mbc);
+        }
+        public void setMbcAllChannelsTo(Mbc mbc) {
+            for (int ch = 0; ch < mChannel.length; ch++) {
+                mChannel[ch].setMbc(mbc);
+            }
+        }
+        public MbcBand getMbcBandByChannelIndex(int channelIndex, int band) {
+            checkChannel(channelIndex);
+            return mChannel[channelIndex].getMbcBand(band);
+        }
+        public void setMbcBandByChannelIndex(int channelIndex, int band, MbcBand mbcBand) {
+            checkChannel(channelIndex);
+            mChannel[channelIndex].setMbcBand(band, mbcBand);
+        }
+        public void setMbcBandAllChannelsTo(int band, MbcBand mbcBand) {
+            for (int ch = 0; ch < mChannel.length; ch++) {
+                mChannel[ch].setMbcBand(band, mbcBand);
+            }
+        }
+
+        //=== PostEQ
+        public Eq getPostEqByChannelIndex(int channelIndex) {
+            checkChannel(channelIndex);
+            return mChannel[channelIndex].getPostEq();
+        }
+        public void setPostEqByChannelIndex(int channelIndex, Eq postEq) {
+            checkChannel(channelIndex);
+            mChannel[channelIndex].setPostEq(postEq);
+        }
+        public void setPostEqAllChannelsTo(Eq postEq) {
+            for (int ch = 0; ch < mChannel.length; ch++) {
+                mChannel[ch].setPostEq(postEq);
+            }
+        }
+        public EqBand getPostEqBandByChannelIndex(int channelIndex, int band) {
+            checkChannel(channelIndex);
+            return mChannel[channelIndex].getPostEqBand(band);
+        }
+        public void setPostEqBandByChannelIndex(int channelIndex, int band, EqBand postEqBand) {
+            checkChannel(channelIndex);
+            mChannel[channelIndex].setPostEqBand(band, postEqBand);
+        }
+        public void setPostEqBandAllChannelsTo(int band, EqBand postEqBand) {
+            for (int ch = 0; ch < mChannel.length; ch++) {
+                mChannel[ch].setPostEqBand(band, postEqBand);
+            }
+        }
+
+        //Limiter
+        public Limiter getLimiterByChannelIndex(int channelIndex) {
+            checkChannel(channelIndex);
+            return mChannel[channelIndex].getLimiter();
+        }
+        public void setLimiterByChannelIndex(int channelIndex, Limiter limiter) {
+            checkChannel(channelIndex);
+            mChannel[channelIndex].setLimiter(limiter);
+        }
+        public void setLimiterAllChannelsTo(Limiter limiter) {
+            for (int ch = 0; ch < mChannel.length; ch++) {
+                mChannel[ch].setLimiter(limiter);
+            }
+        }
+
+        public final static class Builder {
+            private int mVariant;
+            private int mChannelCount;
+            private boolean mPreEqInUse;
+            private int mPreEqBandCount;
+            private boolean mMbcInUse;
+            private int mMbcBandCount;
+            private boolean mPostEqInUse;
+            private int mPostEqBandCount;
+            private boolean mLimiterInUse;
+            private float mPreferredFrameDuration = CONFIG_PREFERRED_FRAME_DURATION_MS;
+            private Channel[] mChannel;
+
+            public Builder(int variant, int channelCount,
+                    boolean preEqInUse, int preEqBandCount,
+                    boolean mbcInUse, int mbcBandCount,
+                    boolean postEqInUse, int postEqBandCount,
+                    boolean limiterInUse) {
+                mVariant = variant;
+                mChannelCount = channelCount;
+                mPreEqInUse = preEqInUse;
+                mPreEqBandCount = preEqBandCount;
+                mMbcInUse = mbcInUse;
+                mMbcBandCount = mbcBandCount;
+                mPostEqInUse = postEqInUse;
+                mPostEqBandCount = postEqBandCount;
+                mLimiterInUse = limiterInUse;
+                mChannel = new Channel[mChannelCount];
+                for (int ch = 0; ch < mChannelCount; ch++) {
+                    this.mChannel[ch] = new Channel(CHANNEL_DEFAULT_INPUT_GAIN,
+                            this.mPreEqInUse, this.mPreEqBandCount,
+                            this.mMbcInUse, this.mMbcBandCount,
+                            this.mPostEqInUse, this.mPostEqBandCount,
+                            this.mLimiterInUse);
+                }
+            }
+
+            private void checkChannel(int channelIndex) {
+                if (channelIndex < 0 || channelIndex >= mChannel.length) {
+                    throw new IllegalArgumentException("ChannelIndex out of bounds");
+                }
+            }
+
+            public Builder setPreferredFrameDuration(float frameDuration) {
+                if (frameDuration < 0) {
+                    throw new IllegalArgumentException("Expected positive frameDuration");
+                }
+                mPreferredFrameDuration = frameDuration;
+                return this;
+            }
+
+            public Builder setInputGainByChannelIndex(int channelIndex, float inputGain) {
+                checkChannel(channelIndex);
+                mChannel[channelIndex].setInputGain(inputGain);
+                return this;
+            }
+            public Builder setInputGainAllChannelsTo(float inputGain) {
+                for (int ch = 0; ch < mChannel.length; ch++) {
+                    mChannel[ch].setInputGain(inputGain);
+                }
+                return this;
+            }
+
+            public Builder setChannelTo(int channelIndex, Channel channel) {
+                checkChannel(channelIndex);
+                //check all things are compatible
+                if (mMbcBandCount != channel.getMbc().getBandCount()) {
+                    throw new IllegalArgumentException("MbcBandCount changed from " +
+                            mMbcBandCount + " to " + channel.getPreEq().getBandCount());
+                }
+                if (mPreEqBandCount != channel.getPreEq().getBandCount()) {
+                    throw new IllegalArgumentException("PreEqBandCount changed from " +
+                            mPreEqBandCount + " to " + channel.getPreEq().getBandCount());
+                }
+                if (mPostEqBandCount != channel.getPostEq().getBandCount()) {
+                    throw new IllegalArgumentException("PostEqBandCount changed from " +
+                            mPostEqBandCount + " to " + channel.getPostEq().getBandCount());
+                }
+                mChannel[channelIndex] = new Channel(channel);
+                return this;
+            }
+            public Builder setAllChannelsTo(Channel channel) {
+                for (int ch = 0; ch < mChannel.length; ch++) {
+                    setChannelTo(ch, channel);
+                }
+                return this;
+            }
+
+            public Builder setPreEqByChannelIndex(int channelIndex, Eq preEq) {
+                checkChannel(channelIndex);
+                mChannel[channelIndex].setPreEq(preEq);
+                return this;
+            }
+            public Builder setPreEqAllChannelsTo(Eq preEq) {
+                for (int ch = 0; ch < mChannel.length; ch++) {
+                    setPreEqByChannelIndex(ch, preEq);
+                }
+                return this;
+            }
+
+            public Builder setMbcByChannelIndex(int channelIndex, Mbc mbc) {
+                checkChannel(channelIndex);
+                mChannel[channelIndex].setMbc(mbc);
+                return this;
+            }
+            public Builder setMbcAllChannelsTo(Mbc mbc) {
+                for (int ch = 0; ch < mChannel.length; ch++) {
+                    setMbcByChannelIndex(ch, mbc);
+                }
+                return this;
+            }
+
+            public Builder setPostEqByChannelIndex(int channelIndex, Eq postEq) {
+                checkChannel(channelIndex);
+                mChannel[channelIndex].setPostEq(postEq);
+                return this;
+            }
+            public Builder setPostEqAllChannelsTo(Eq postEq) {
+                for (int ch = 0; ch < mChannel.length; ch++) {
+                    setPostEqByChannelIndex(ch, postEq);
+                }
+                return this;
+            }
+
+            public Builder setLimiterByChannelIndex(int channelIndex, Limiter limiter) {
+                checkChannel(channelIndex);
+                mChannel[channelIndex].setLimiter(limiter);
+                return this;
+            }
+            public Builder setLimiterAllChannelsTo(Limiter limiter) {
+                for (int ch = 0; ch < mChannel.length; ch++) {
+                    setLimiterByChannelIndex(ch, limiter);
+                }
+                return this;
+            }
+
+            public Config build() {
+                return new Config(mVariant, mPreferredFrameDuration, mChannelCount,
+                        mPreEqInUse, mPreEqBandCount,
+                        mMbcInUse, mMbcBandCount,
+                        mPostEqInUse, mPostEqBandCount,
+                        mLimiterInUse, mChannel);
+            }
+        }
+    }
+    //=== CHANNEL
+    public Channel getChannelByChannelIndex(int channelIndex) {
+        return queryEngineByChannelIndex(channelIndex);
+    }
+
+    public void setChannelTo(int channelIndex, Channel channel) {
+        updateEngineChannelByChannelIndex(channelIndex, channel);
+    }
+
+    public void setAllChannelsTo(Channel channel) {
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            setChannelTo(ch, channel);
+        }
+    }
+
+    //=== channel params
+    public float getInputGainByChannelIndex(int channelIndex) {
+        return getTwoFloat(PARAM_INPUT_GAIN, channelIndex);
+    }
+    public void setInputGainbyChannel(int channelIndex, float inputGain) {
+        setTwoFloat(PARAM_INPUT_GAIN, channelIndex, inputGain);
+    }
+    public void setInputGainAllChannelsTo(float inputGain) {
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            setInputGainbyChannel(ch, inputGain);
+        }
+    }
+
+    //=== PreEQ
+    public Eq getPreEqByChannelIndex(int channelIndex) {
+        return queryEngineEqByChannelIndex(PARAM_PRE_EQ, channelIndex);
+    }
+    public void setPreEqByChannelIndex(int channelIndex, Eq preEq) {
+        updateEngineEqByChannelIndex(PARAM_PRE_EQ, channelIndex, preEq);
+    }
+    public void setPreEqAllChannelsTo(Eq preEq) {
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            setPreEqByChannelIndex(ch, preEq);
+        }
+    }
+    public EqBand getPreEqBandByChannelIndex(int channelIndex, int band) {
+        return queryEngineEqBandByChannelIndex(PARAM_PRE_EQ_BAND, channelIndex, band);
+    }
+    public void setPreEqBandByChannelIndex(int channelIndex, int band, EqBand preEqBand) {
+        updateEngineEqBandByChannelIndex(PARAM_PRE_EQ_BAND, channelIndex, band, preEqBand);
+    }
+    public void setPreEqBandAllChannelsTo(int band, EqBand preEqBand) {
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            setPreEqBandByChannelIndex(ch, band, preEqBand);
+        }
+    }
+
+    //=== MBC
+    public Mbc getMbcByChannelIndex(int channelIndex) {
+        return queryEngineMbcByChannelIndex(channelIndex);
+    }
+    public void setMbcByChannelIndex(int channelIndex, Mbc mbc) {
+        updateEngineMbcByChannelIndex(channelIndex, mbc);
+    }
+    public void setMbcAllChannelsTo(Mbc mbc) {
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            setMbcByChannelIndex(ch, mbc);
+        }
+    }
+    public MbcBand getMbcBandByChannelIndex(int channelIndex, int band) {
+        return queryEngineMbcBandByChannelIndex(channelIndex, band);
+    }
+    public void setMbcBandByChannelIndex(int channelIndex, int band, MbcBand mbcBand) {
+        updateEngineMbcBandByChannelIndex(channelIndex, band, mbcBand);
+    }
+    public void setMbcBandAllChannelsTo(int band, MbcBand mbcBand) {
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            setMbcBandByChannelIndex(ch, band, mbcBand);
+        }
+    }
+
+    //== PostEq
+    public Eq getPostEqByChannelIndex(int channelIndex) {
+        return queryEngineEqByChannelIndex(PARAM_POST_EQ, channelIndex);
+    }
+    public void setPostEqByChannelIndex(int channelIndex, Eq postEq) {
+        updateEngineEqByChannelIndex(PARAM_POST_EQ, channelIndex, postEq);
+    }
+    public void setPostEqAllChannelsTo(Eq postEq) {
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            setPostEqByChannelIndex(ch, postEq);
+        }
+    }
+    public EqBand getPostEqBandByChannelIndex(int channelIndex, int band) {
+        return queryEngineEqBandByChannelIndex(PARAM_POST_EQ_BAND, channelIndex, band);
+    }
+    public void setPostEqBandByChannelIndex(int channelIndex, int band, EqBand postEqBand) {
+        updateEngineEqBandByChannelIndex(PARAM_POST_EQ_BAND, channelIndex, band, postEqBand);
+    }
+    public void setPostEqBandAllChannelsTo(int band, EqBand postEqBand) {
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            setPostEqBandByChannelIndex(ch, band, postEqBand);
+        }
+    }
+
+    //==== Limiter
+    public Limiter getLimiterByChannelIndex(int channelIndex) {
+        return queryEngineLimiterByChannelIndex(channelIndex);
+    }
+    public void setLimiterByChannelIndex(int channelIndex, Limiter limiter) {
+        updateEngineLimiterByChannelIndex(channelIndex, limiter);
+    }
+    public void setLimiterAllChannelsTo(Limiter limiter) {
+        for (int ch = 0; ch < mChannelCount; ch++) {
+            setLimiterByChannelIndex(ch, limiter);
+        }
+    }
+
+    /**
+     * Gets the number of channels in the effect engine
+     * @return number of channels currently in use by the effect engine
+     */
+    public int getChannelCount() {
+        return getOneInt(PARAM_GET_CHANNEL_COUNT);
+    }
+
+    //=== Engine calls
+    private void setEngineArchitecture(int variant, float preferredFrameDuration,
+            boolean preEqInUse, int preEqBandCount, boolean mbcInUse, int mbcBandCount,
+            boolean postEqInUse, int postEqBandCount, boolean limiterInUse) {
+
+        Number[] params = { PARAM_ENGINE_ARCHITECTURE };
+        Number[] values = { variant /* variant */,
+                preferredFrameDuration,
+                (preEqInUse ? 1 : 0),
+                preEqBandCount,
+                (mbcInUse ? 1 : 0),
+                mbcBandCount,
+                (postEqInUse ? 1 : 0),
+                postEqBandCount,
+                (limiterInUse ? 1 : 0)};
+        setNumberArray(params, values);
+    }
+
+    private void updateEngineEqBandByChannelIndex(int param, int channelIndex, int bandIndex,
+            @NonNull EqBand eqBand) {
+        Number[] params = {param,
+                channelIndex,
+                bandIndex};
+        Number[] values = {(eqBand.isEnabled() ? 1 : 0),
+                eqBand.getCutoffFrequency(),
+                eqBand.getGain()};
+        setNumberArray(params, values);
+    }
+    private Eq queryEngineEqByChannelIndex(int param, int channelIndex) {
+
+        Number[] params = {param == PARAM_PRE_EQ ? PARAM_PRE_EQ : PARAM_POST_EQ,
+                channelIndex};
+        Number[] values = {0 /*0 in use */,
+                            0 /*1 enabled*/,
+                            0 /*2 band count */};
+        byte[] paramBytes = numberArrayToByteArray(params);
+        byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+        getParameter(paramBytes, valueBytes);
+        byteArrayToNumberArray(valueBytes, values);
+        int bandCount = values[2].intValue();
+        Eq eq = new Eq(values[0].intValue() > 0 /* in use */,
+                values[1].intValue() > 0 /* enabled */,
+                bandCount /*band count*/);
+        for (int b = 0; b < bandCount; b++) {
+            EqBand eqBand = queryEngineEqBandByChannelIndex(param == PARAM_PRE_EQ ?
+                    PARAM_PRE_EQ_BAND : PARAM_POST_EQ_BAND, channelIndex, b);
+            eq.setBand(b, eqBand);
+        }
+        return eq;
+    }
+    private EqBand queryEngineEqBandByChannelIndex(int param, int channelIndex, int bandIndex) {
+        Number[] params = {param,
+                channelIndex,
+                bandIndex};
+        Number[] values = {0 /*0 enabled*/,
+                            0.0f /*1 cutoffFrequency */,
+                            0.0f /*2 gain */};
+
+        byte[] paramBytes = numberArrayToByteArray(params);
+        byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+        getParameter(paramBytes, valueBytes);
+
+        byteArrayToNumberArray(valueBytes, values);
+
+        return new EqBand(values[0].intValue() > 0 /* enabled */,
+                values[1].floatValue() /* cutoffFrequency */,
+                values[2].floatValue() /* gain*/);
+    }
+    private void updateEngineEqByChannelIndex(int param, int channelIndex, @NonNull Eq eq) {
+        int bandCount = eq.getBandCount();
+        Number[] params = {param,
+                channelIndex};
+        Number[] values = { (eq.isInUse() ? 1 : 0),
+                (eq.isEnabled() ? 1 : 0),
+                bandCount};
+        setNumberArray(params, values);
+        for (int b = 0; b < bandCount; b++) {
+            EqBand eqBand = eq.getBand(b);
+            updateEngineEqBandByChannelIndex(param == PARAM_PRE_EQ ?
+                    PARAM_PRE_EQ_BAND : PARAM_POST_EQ_BAND, channelIndex, b, eqBand);
+        }
+    }
+
+    private Mbc queryEngineMbcByChannelIndex(int channelIndex) {
+        Number[] params = {PARAM_MBC,
+                channelIndex};
+        Number[] values = {0 /*0 in use */,
+                            0 /*1 enabled*/,
+                            0 /*2 band count */};
+        byte[] paramBytes = numberArrayToByteArray(params);
+        byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+        getParameter(paramBytes, valueBytes);
+        byteArrayToNumberArray(valueBytes, values);
+        int bandCount = values[2].intValue();
+        Mbc mbc = new Mbc(values[0].intValue() > 0 /* in use */,
+                values[1].intValue() > 0 /* enabled */,
+                bandCount /*band count*/);
+        for (int b = 0; b < bandCount; b++) {
+            MbcBand mbcBand = queryEngineMbcBandByChannelIndex(channelIndex, b);
+            mbc.setBand(b, mbcBand);
+        }
+        return mbc;
+    }
+    private MbcBand queryEngineMbcBandByChannelIndex(int channelIndex, int bandIndex) {
+        Number[] params = {PARAM_MBC_BAND,
+                channelIndex,
+                bandIndex};
+        Number[] values = {0 /*0 enabled */,
+                0.0f /*1 cutoffFrequency */,
+                0.0f /*2 AttackTime */,
+                0.0f /*3 ReleaseTime */,
+                0.0f /*4 Ratio */,
+                0.0f /*5 Threshold */,
+                0.0f /*6 KneeWidth */,
+                0.0f /*7 NoiseGateThreshold */,
+                0.0f /*8 ExpanderRatio */,
+                0.0f /*9 PreGain */,
+                0.0f /*10 PostGain*/};
+
+        byte[] paramBytes = numberArrayToByteArray(params);
+        byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+        getParameter(paramBytes, valueBytes);
+
+        byteArrayToNumberArray(valueBytes, values);
+
+        return new MbcBand(values[0].intValue() > 0 /* enabled */,
+                values[1].floatValue() /* cutoffFrequency */,
+                values[2].floatValue()/*2 AttackTime */,
+                values[3].floatValue()/*3 ReleaseTime */,
+                values[4].floatValue()/*4 Ratio */,
+                values[5].floatValue()/*5 Threshold */,
+                values[6].floatValue()/*6 KneeWidth */,
+                values[7].floatValue()/*7 NoiseGateThreshold */,
+                values[8].floatValue()/*8 ExpanderRatio */,
+                values[9].floatValue()/*9 PreGain */,
+                values[10].floatValue()/*10 PostGain*/);
+    }
+    private void updateEngineMbcBandByChannelIndex(int channelIndex, int bandIndex,
+            @NonNull MbcBand mbcBand) {
+        Number[] params = { PARAM_MBC_BAND,
+                channelIndex,
+                bandIndex};
+        Number[] values = {(mbcBand.isEnabled() ? 1 : 0),
+                mbcBand.getCutoffFrequency(),
+                mbcBand.getAttackTime(),
+                mbcBand.getReleaseTime(),
+                mbcBand.getRatio(),
+                mbcBand.getThreshold(),
+                mbcBand.getKneeWidth(),
+                mbcBand.getNoiseGateThreshold(),
+                mbcBand.getExpanderRatio(),
+                mbcBand.getPreGain(),
+                mbcBand.getPostGain()};
+        setNumberArray(params, values);
+    }
+
+    private void updateEngineMbcByChannelIndex(int channelIndex, @NonNull Mbc mbc) {
+        int bandCount = mbc.getBandCount();
+        Number[] params = { PARAM_MBC,
+                channelIndex};
+        Number[] values = {(mbc.isInUse() ? 1 : 0),
+                (mbc.isEnabled() ? 1 : 0),
+                bandCount};
+        setNumberArray(params, values);
+        for (int b = 0; b < bandCount; b++) {
+            MbcBand mbcBand = mbc.getBand(b);
+            updateEngineMbcBandByChannelIndex(channelIndex, b, mbcBand);
+        }
+    }
+
+    private void updateEngineLimiterByChannelIndex(int channelIndex, @NonNull Limiter limiter) {
+        Number[] params = { PARAM_LIMITER,
+                channelIndex};
+        Number[] values = {(limiter.isInUse() ? 1 : 0),
+                (limiter.isEnabled() ? 1 : 0),
+                limiter.getLinkGroup(),
+                limiter.getAttackTime(),
+                limiter.getReleaseTime(),
+                limiter.getRatio(),
+                limiter.getThreshold(),
+                limiter.getPostGain()};
+        setNumberArray(params, values);
+    }
+
+    private Limiter queryEngineLimiterByChannelIndex(int channelIndex) {
+        Number[] params = {PARAM_LIMITER,
+                channelIndex};
+        Number[] values = {0 /*0 in use (int)*/,
+                0 /*1 enabled (int)*/,
+                0 /*2 link group (int)*/,
+                0.0f /*3 attack time (float)*/,
+                0.0f /*4 release time (float)*/,
+                0.0f /*5 ratio (float)*/,
+                0.0f /*6 threshold (float)*/,
+                0.0f /*7 post gain(float)*/};
+
+        byte[] paramBytes = numberArrayToByteArray(params);
+        byte[] valueBytes = numberArrayToByteArray(values); //just interest in the byte size.
+        getParameter(paramBytes, valueBytes);
+        byteArrayToNumberArray(valueBytes, values);
+
+        return new Limiter(values[0].intValue() > 0 /*in use*/,
+                values[1].intValue() > 0 /*enabled*/,
+                values[2].intValue() /*linkGroup*/,
+                values[3].floatValue() /*attackTime*/,
+                values[4].floatValue() /*releaseTime*/,
+                values[5].floatValue() /*ratio*/,
+                values[6].floatValue() /*threshold*/,
+                values[7].floatValue() /*postGain*/);
+    }
+
+    private Channel queryEngineByChannelIndex(int channelIndex) {
+        float inputGain = getTwoFloat(PARAM_INPUT_GAIN, channelIndex);
+        Eq preEq = queryEngineEqByChannelIndex(PARAM_PRE_EQ, channelIndex);
+        Mbc mbc = queryEngineMbcByChannelIndex(channelIndex);
+        Eq postEq = queryEngineEqByChannelIndex(PARAM_POST_EQ, channelIndex);
+        Limiter limiter = queryEngineLimiterByChannelIndex(channelIndex);
+
+        Channel channel = new Channel(inputGain,
+                preEq.isInUse(), preEq.getBandCount(),
+                mbc.isInUse(), mbc.getBandCount(),
+                postEq.isInUse(), postEq.getBandCount(),
+                limiter.isInUse());
+        channel.setInputGain(inputGain);
+        channel.setPreEq(preEq);
+        channel.setMbc(mbc);
+        channel.setPostEq(postEq);
+        channel.setLimiter(limiter);
+        return channel;
+    }
+
+    private void updateEngineChannelByChannelIndex(int channelIndex, @NonNull Channel channel) {
+        //send things with as few calls as possible
+        setTwoFloat(PARAM_INPUT_GAIN, channelIndex, channel.getInputGain());
+        Eq preEq = channel.getPreEq();
+        updateEngineEqByChannelIndex(PARAM_PRE_EQ, channelIndex, preEq);
+        Mbc mbc = channel.getMbc();
+        updateEngineMbcByChannelIndex(channelIndex, mbc);
+        Eq postEq = channel.getPostEq();
+        updateEngineEqByChannelIndex(PARAM_POST_EQ, channelIndex, postEq);
+        Limiter limiter = channel.getLimiter();
+        updateEngineLimiterByChannelIndex(channelIndex, limiter);
+    }
+
+    //****** convenience methods:
+    //
+    private int getOneInt(int param) {
+        final int[] params = { param };
+        final int[] result = new int[1];
+
+        checkStatus(getParameter(params, result));
+        return result[0];
+    }
+
+    private void setTwoFloat(int param, int paramA, float valueSet) {
+        final int[] params = { param, paramA };
+        final byte[] value;
+
+        value = floatToByteArray(valueSet);
+        checkStatus(setParameter(params, value));
+    }
+
+    private byte[] numberArrayToByteArray(Number[] values) {
+        int expectedBytes = 0;
+        for (int i = 0; i < values.length; i++) {
+            if (values[i] instanceof Integer) {
+                expectedBytes += Integer.BYTES;
+            } else if (values[i] instanceof Float) {
+                expectedBytes += Float.BYTES;
+            } else {
+                throw new IllegalArgumentException("unknown value type " +
+                        values[i].getClass());
+            }
+        }
+        ByteBuffer converter = ByteBuffer.allocate(expectedBytes);
+        converter.order(ByteOrder.nativeOrder());
+        for (int i = 0; i < values.length; i++) {
+            if (values[i] instanceof Integer) {
+                converter.putInt(values[i].intValue());
+            } else if (values[i] instanceof Float) {
+                converter.putFloat(values[i].floatValue());
+            }
+        }
+        return converter.array();
+    }
+
+    private void byteArrayToNumberArray(byte[] valuesIn, Number[] valuesOut) {
+        int inIndex = 0;
+        int outIndex = 0;
+        while (inIndex < valuesIn.length && outIndex < valuesOut.length) {
+            if (valuesOut[outIndex] instanceof Integer) {
+                valuesOut[outIndex++] = byteArrayToInt(valuesIn, inIndex);
+                inIndex += Integer.BYTES;
+            } else if (valuesOut[outIndex] instanceof Float) {
+                valuesOut[outIndex++] = byteArrayToFloat(valuesIn, inIndex);
+                inIndex += Float.BYTES;
+            } else {
+                throw new IllegalArgumentException("can't convert " +
+                        valuesOut[outIndex].getClass());
+            }
+        }
+        if (outIndex != valuesOut.length) {
+            throw new IllegalArgumentException("only converted " + outIndex +
+                    " values out of "+ valuesOut.length + " expected");
+        }
+    }
+
+    private void setNumberArray(Number[] params, Number[] values) {
+        byte[] paramBytes = numberArrayToByteArray(params);
+        byte[] valueBytes = numberArrayToByteArray(values);
+        checkStatus(setParameter(paramBytes, valueBytes));
+    }
+
+    private float getTwoFloat(int param, int paramA) {
+        final int[] params = { param, paramA };
+        final byte[] result = new byte[4];
+
+        checkStatus(getParameter(params, result));
+        return byteArrayToFloat(result);
+    }
+
+    /**
+     * @hide
+     * The OnParameterChangeListener interface defines a method called by the DynamicsProcessing
+     * when a parameter value has changed.
+     */
+    public interface OnParameterChangeListener {
+        /**
+         * Method called when a parameter value has changed. The method is called only if the
+         * parameter was changed by another application having the control of the same
+         * DynamicsProcessing engine.
+         * @param effect the DynamicsProcessing on which the interface is registered.
+         * @param param ID of the modified parameter. See {@link #PARAM_GENERIC_PARAM1} ...
+         * @param value the new parameter value.
+         */
+        void onParameterChange(DynamicsProcessing effect, int param, int value);
+    }
+
+    /**
+     * helper method to update effect architecture parameters
+     */
+    private void updateEffectArchitecture() {
+        mChannelCount = getChannelCount();
+    }
+
+    /**
+     * Listener used internally to receive unformatted parameter change events from AudioEffect
+     * super class.
+     */
+    private class BaseParameterListener implements AudioEffect.OnParameterChangeListener {
+        private BaseParameterListener() {
+
+        }
+        public void onParameterChange(AudioEffect effect, int status, byte[] param, byte[] value) {
+            // only notify when the parameter was successfully change
+            if (status != AudioEffect.SUCCESS) {
+                return;
+            }
+            OnParameterChangeListener l = null;
+            synchronized (mParamListenerLock) {
+                if (mParamListener != null) {
+                    l = mParamListener;
+                }
+            }
+            if (l != null) {
+                int p = -1;
+                int v = Integer.MIN_VALUE;
+
+                if (param.length == 4) {
+                    p = byteArrayToInt(param, 0);
+                }
+                if (value.length == 4) {
+                    v = byteArrayToInt(value, 0);
+                }
+                if (p != -1 && v != Integer.MIN_VALUE) {
+                    l.onParameterChange(DynamicsProcessing.this, p, v);
+                }
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Registers an OnParameterChangeListener interface.
+     * @param listener OnParameterChangeListener interface registered
+     */
+    public void setParameterListener(OnParameterChangeListener listener) {
+        synchronized (mParamListenerLock) {
+            if (mParamListener == null) {
+                mBaseParamListener = new BaseParameterListener();
+                super.setParameterListener(mBaseParamListener);
+            }
+            mParamListener = listener;
+        }
+    }
+
+    /**
+     * @hide
+     * The Settings class regroups the DynamicsProcessing parameters. It is used in
+     * conjunction with the getProperties() and setProperties() methods to backup and restore
+     * all parameters in a single call.
+     */
+
+    public static class Settings {
+        public int channelCount;
+        public float[] inputGain;
+
+        public Settings() {
+        }
+
+        /**
+         * Settings class constructor from a key=value; pairs formatted string. The string is
+         * typically returned by Settings.toString() method.
+         * @throws IllegalArgumentException if the string is not correctly formatted.
+         */
+        public Settings(String settings) {
+            StringTokenizer st = new StringTokenizer(settings, "=;");
+            //int tokens = st.countTokens();
+            if (st.countTokens() != 3) {
+                throw new IllegalArgumentException("settings: " + settings);
+            }
+            String key = st.nextToken();
+            if (!key.equals("DynamicsProcessing")) {
+                throw new IllegalArgumentException(
+                        "invalid settings for DynamicsProcessing: " + key);
+            }
+            try {
+                key = st.nextToken();
+                if (!key.equals("channelCount")) {
+                    throw new IllegalArgumentException("invalid key name: " + key);
+                }
+                channelCount = Short.parseShort(st.nextToken());
+                if (channelCount > CHANNEL_COUNT_MAX) {
+                    throw new IllegalArgumentException("too many channels Settings:" + settings);
+                }
+                if (st.countTokens() != channelCount*1) { //check expected parameters.
+                    throw new IllegalArgumentException("settings: " + settings);
+                }
+                //check to see it is ok the size
+                inputGain = new float[channelCount];
+                for (int ch = 0; ch < channelCount; ch++) {
+                    key = st.nextToken();
+                    if (!key.equals(ch +"_inputGain")) {
+                        throw new IllegalArgumentException("invalid key name: " + key);
+                    }
+                    inputGain[ch] = Float.parseFloat(st.nextToken());
+                }
+             } catch (NumberFormatException nfe) {
+                throw new IllegalArgumentException("invalid value for key: " + key);
+            }
+        }
+
+        @Override
+        public String toString() {
+            String str = new String (
+                    "DynamicsProcessing"+
+                    ";channelCount="+Integer.toString(channelCount));
+                    for (int ch = 0; ch < channelCount; ch++) {
+                        str = str.concat(";"+ch+"_inputGain="+Float.toString(inputGain[ch]));
+                    }
+            return str;
+        }
+    };
+
+
+    /**
+     * @hide
+     * Gets the DynamicsProcessing properties. This method is useful when a snapshot of current
+     * effect settings must be saved by the application.
+     * @return a DynamicsProcessing.Settings object containing all current parameters values
+     */
+    public DynamicsProcessing.Settings getProperties() {
+        Settings settings = new Settings();
+
+        //TODO: just for testing, we are calling the getters one by one, this is
+        // supposed to be done in a single (or few calls) and get all the parameters at once.
+
+        settings.channelCount = getChannelCount();
+
+        if (settings.channelCount > CHANNEL_COUNT_MAX) {
+            throw new IllegalArgumentException("too many channels Settings:" + settings);
+        }
+
+        { // get inputGainmB per channel
+            settings.inputGain = new float [settings.channelCount];
+            for (int ch = 0; ch < settings.channelCount; ch++) {
+//TODO:with config                settings.inputGain[ch] = getInputGain(ch);
+            }
+        }
+        return settings;
+    }
+
+    /**
+     * @hide
+     * Sets the DynamicsProcessing properties. This method is useful when bass boost settings
+     * have to be applied from a previous backup.
+     * @param settings a DynamicsProcessing.Settings object containing the properties to apply
+     */
+    public void setProperties(DynamicsProcessing.Settings settings) {
+
+        if (settings.channelCount != settings.inputGain.length ||
+                settings.channelCount != mChannelCount) {
+                throw new IllegalArgumentException("settings invalid channel count: "
+                + settings.channelCount);
+            }
+
+        //TODO: for now calling multiple times.
+        for (int ch = 0; ch < mChannelCount; ch++) {
+//TODO: use config            setInputGain(ch, settings.inputGain[ch]);
+        }
+    }
+}
diff --git a/android/media/audiofx/Visualizer.java b/android/media/audiofx/Visualizer.java
index 0fe7246..f2b4fe0 100644
--- a/android/media/audiofx/Visualizer.java
+++ b/android/media/audiofx/Visualizer.java
@@ -546,22 +546,39 @@
         /**
          * Method called when a new waveform capture is available.
          * <p>Data in the waveform buffer is valid only within the scope of the callback.
-         * Applications which needs access to the waveform data after returning from the callback
+         * Applications which need access to the waveform data after returning from the callback
          * should make a copy of the data instead of holding a reference.
          * @param visualizer Visualizer object on which the listener is registered.
          * @param waveform array of bytes containing the waveform representation.
-         * @param samplingRate sampling rate of the audio visualized.
+         * @param samplingRate sampling rate of the visualized audio.
          */
         void onWaveFormDataCapture(Visualizer visualizer, byte[] waveform, int samplingRate);
 
         /**
          * Method called when a new frequency capture is available.
          * <p>Data in the fft buffer is valid only within the scope of the callback.
-         * Applications which needs access to the fft data after returning from the callback
+         * Applications which need access to the fft data after returning from the callback
          * should make a copy of the data instead of holding a reference.
+         *
+         * <p>In order to obtain magnitude and phase values the following formulas can
+         * be used:
+         *    <pre class="prettyprint">
+         *       for (int i = 0; i &lt; fft.size(); i += 2) {
+         *           float magnitude = (float)Math.hypot(fft[i], fft[i + 1]);
+         *           float phase = (float)Math.atan2(fft[i + 1], fft[i]);
+         *       }</pre>
          * @param visualizer Visualizer object on which the listener is registered.
          * @param fft array of bytes containing the frequency representation.
-         * @param samplingRate sampling rate of the audio visualized.
+         *    The fft array only contains the first half of the actual
+         *    FFT spectrum (frequencies up to Nyquist frequency), exploiting
+         *    the symmetry of the spectrum. For each frequencies bin <code>i</code>:
+         *    <ul>
+         *      <li>the element at index <code>2*i</code> in the array contains
+         *          the real part of a complex number,</li>
+         *      <li>the element at index <code>2*i+1</code> contains the imaginary
+         *          part of the complex number.</li>
+         *    </ul>
+         * @param samplingRate sampling rate of the visualized audio.
          */
         void onFftDataCapture(Visualizer visualizer, byte[] fft, int samplingRate);
     }
diff --git a/android/media/audiopolicy/AudioMix.java b/android/media/audiopolicy/AudioMix.java
index adeb834..fca0cc7 100644
--- a/android/media/audiopolicy/AudioMix.java
+++ b/android/media/audiopolicy/AudioMix.java
@@ -162,6 +162,24 @@
     }
 
     /** @hide */
+    public boolean isAffectingUsage(int usage) {
+        return mRule.isAffectingUsage(usage);
+    }
+
+    /** @hide */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        final AudioMix that = (AudioMix) o;
+        return (this.mRouteFlags == that.mRouteFlags)
+                && (this.mRule == that.mRule)
+                && (this.mMixType == that.mMixType)
+                && (this.mFormat == that.mFormat);
+    }
+
+    /** @hide */
     @Override
     public int hashCode() {
         return Objects.hash(mRouteFlags, mRule, mMixType, mFormat);
diff --git a/android/media/audiopolicy/AudioMixingRule.java b/android/media/audiopolicy/AudioMixingRule.java
index 5f12742..749a45e 100644
--- a/android/media/audiopolicy/AudioMixingRule.java
+++ b/android/media/audiopolicy/AudioMixingRule.java
@@ -135,11 +135,42 @@
         }
     }
 
+    boolean isAffectingUsage(int usage) {
+        for (AudioMixMatchCriterion criterion : mCriteria) {
+            if ((criterion.mRule & RULE_MATCH_ATTRIBUTE_USAGE) != 0
+                    && criterion.mAttr != null
+                    && criterion.mAttr.getUsage() == usage) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean areCriteriaEquivalent(ArrayList<AudioMixMatchCriterion> cr1,
+            ArrayList<AudioMixMatchCriterion> cr2) {
+        if (cr1 == null || cr2 == null) return false;
+        if (cr1 == cr2) return true;
+        if (cr1.size() != cr2.size()) return false;
+        //TODO iterate over rules to check they contain the same criterion
+        return (cr1.hashCode() == cr2.hashCode());
+    }
+
     private final int mTargetMixType;
     int getTargetMixType() { return mTargetMixType; }
     private final ArrayList<AudioMixMatchCriterion> mCriteria;
     ArrayList<AudioMixMatchCriterion> getCriteria() { return mCriteria; }
 
+    /** @hide */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        final AudioMixingRule that = (AudioMixingRule) o;
+        return (this.mTargetMixType == that.mTargetMixType)
+                && (areCriteriaEquivalent(this.mCriteria, that.mCriteria));
+    }
+
     @Override
     public int hashCode() {
         return Objects.hash(mTargetMixType, mCriteria);
diff --git a/android/media/audiopolicy/AudioPolicy.java b/android/media/audiopolicy/AudioPolicy.java
index 7e88c27..11107e2 100644
--- a/android/media/audiopolicy/AudioPolicy.java
+++ b/android/media/audiopolicy/AudioPolicy.java
@@ -42,6 +42,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * @hide
@@ -89,6 +90,8 @@
 
     private AudioPolicyFocusListener mFocusListener;
 
+    private final AudioPolicyVolumeCallback mVolCb;
+
     private Context mContext;
 
     private AudioPolicyConfig mConfig;
@@ -99,12 +102,15 @@
     public boolean hasFocusListener() { return mFocusListener != null; }
     /** @hide */
     public boolean isFocusPolicy() { return mIsFocusPolicy; }
+    /** @hide */
+    public boolean isVolumeController() { return mVolCb != null; }
 
     /**
      * The parameter is guaranteed non-null through the Builder
      */
     private AudioPolicy(AudioPolicyConfig config, Context context, Looper looper,
-            AudioPolicyFocusListener fl, AudioPolicyStatusListener sl, boolean isFocusPolicy) {
+            AudioPolicyFocusListener fl, AudioPolicyStatusListener sl, boolean isFocusPolicy,
+            AudioPolicyVolumeCallback vc) {
         mConfig = config;
         mStatus = POLICY_STATUS_UNREGISTERED;
         mContext = context;
@@ -120,6 +126,7 @@
         mFocusListener = fl;
         mStatusListener = sl;
         mIsFocusPolicy = isFocusPolicy;
+        mVolCb = vc;
     }
 
     /**
@@ -134,6 +141,7 @@
         private AudioPolicyFocusListener mFocusListener;
         private AudioPolicyStatusListener mStatusListener;
         private boolean mIsFocusPolicy = false;
+        private AudioPolicyVolumeCallback mVolCb;
 
         /**
          * Constructs a new Builder with no audio mixes.
@@ -208,6 +216,22 @@
             mStatusListener = l;
         }
 
+        @SystemApi
+        /**
+         * Sets the callback to receive all volume key-related events.
+         * The callback will only be called if the device is configured to handle volume events
+         * in the PhoneWindowManager (see config_handleVolumeKeysInWindowManager)
+         * @param vc
+         * @return the same Builder instance.
+         */
+        public Builder setAudioPolicyVolumeCallback(@NonNull AudioPolicyVolumeCallback vc) {
+            if (vc == null) {
+                throw new IllegalArgumentException("Invalid null volume callback");
+            }
+            mVolCb = vc;
+            return this;
+        }
+
         /**
          * Combines all of the attributes that have been set on this {@code Builder} and returns a
          * new {@link AudioPolicy} object.
@@ -229,7 +253,90 @@
                         + "an AudioPolicyFocusListener");
             }
             return new AudioPolicy(new AudioPolicyConfig(mMixes), mContext, mLooper,
-                    mFocusListener, mStatusListener, mIsFocusPolicy);
+                    mFocusListener, mStatusListener, mIsFocusPolicy, mVolCb);
+        }
+    }
+
+    /**
+     * @hide
+     * Update the current configuration of the set of audio mixes by adding new ones, while
+     * keeping the policy registered.
+     * This method can only be called on a registered policy.
+     * @param mixes the list of {@link AudioMix} to add
+     * @return {@link AudioManager#SUCCESS} if the change was successful, {@link AudioManager#ERROR}
+     *    otherwise.
+     */
+    @SystemApi
+    public int attachMixes(@NonNull List<AudioMix> mixes) {
+        if (mixes == null) {
+            throw new IllegalArgumentException("Illegal null list of AudioMix");
+        }
+        synchronized (mLock) {
+            if (mStatus != POLICY_STATUS_REGISTERED) {
+                throw new IllegalStateException("Cannot alter unregistered AudioPolicy");
+            }
+            final ArrayList<AudioMix> zeMixes = new ArrayList<AudioMix>(mixes.size());
+            for (AudioMix mix : mixes) {
+                if (mix == null) {
+                    throw new IllegalArgumentException("Illegal null AudioMix in attachMixes");
+                } else {
+                    zeMixes.add(mix);
+                }
+            }
+            final AudioPolicyConfig cfg = new AudioPolicyConfig(zeMixes);
+            IAudioService service = getService();
+            try {
+                final int status = service.addMixForPolicy(cfg, this.cb());
+                if (status == AudioManager.SUCCESS) {
+                    mConfig.add(zeMixes);
+                }
+                return status;
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in attachMixes", e);
+                return AudioManager.ERROR;
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Update the current configuration of the set of audio mixes by removing some, while
+     * keeping the policy registered.
+     * This method can only be called on a registered policy.
+     * @param mixes the list of {@link AudioMix} to remove
+     * @return {@link AudioManager#SUCCESS} if the change was successful, {@link AudioManager#ERROR}
+     *    otherwise.
+     */
+    @SystemApi
+    public int detachMixes(@NonNull List<AudioMix> mixes) {
+        if (mixes == null) {
+            throw new IllegalArgumentException("Illegal null list of AudioMix");
+        }
+        synchronized (mLock) {
+            if (mStatus != POLICY_STATUS_REGISTERED) {
+                throw new IllegalStateException("Cannot alter unregistered AudioPolicy");
+            }
+            final ArrayList<AudioMix> zeMixes = new ArrayList<AudioMix>(mixes.size());
+            for (AudioMix mix : mixes) {
+                if (mix == null) {
+                    throw new IllegalArgumentException("Illegal null AudioMix in detachMixes");
+                    // TODO also check mix is currently contained in list of mixes
+                } else {
+                    zeMixes.add(mix);
+                }
+            }
+            final AudioPolicyConfig cfg = new AudioPolicyConfig(zeMixes);
+            IAudioService service = getService();
+            try {
+                final int status = service.removeMixForPolicy(cfg, this.cb());
+                if (status == AudioManager.SUCCESS) {
+                    mConfig.remove(zeMixes);
+                }
+                return status;
+            } catch (RemoteException e) {
+                Log.e(TAG, "Dead object in detachMixes", e);
+                return AudioManager.ERROR;
+            }
         }
     }
 
@@ -377,6 +484,7 @@
                 new AudioAttributes.Builder()
                         .setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX)
                         .addTag(addressForTag(mix))
+                        .addTag(AudioRecord.SUBMIX_FIXED_VOLUME)
                         .build(),
                 mixFormat,
                 AudioRecord.getMinBufferSize(mix.getFormat().getSampleRate(),
@@ -440,9 +548,9 @@
          * Only ever called if the {@link AudioPolicy} was built with
          * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to {@code true}.
          * @param afi information about the focus request and the requester
-         * @param requestResult the result that was returned synchronously by the framework to the
-         *     application, {@link #AUDIOFOCUS_REQUEST_FAILED},or
-         *     {@link #AUDIOFOCUS_REQUEST_DELAYED}.
+         * @param requestResult deprecated after the addition of
+         *     {@link AudioManager#setFocusRequestResult(AudioFocusInfo, int, AudioPolicy)}
+         *     in Android P, always equal to {@link #AUDIOFOCUS_REQUEST_GRANTED}.
          */
         public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {}
         /**
@@ -455,6 +563,23 @@
         public void onAudioFocusAbandon(AudioFocusInfo afi) {}
     }
 
+    @SystemApi
+    /**
+     * Callback class to receive volume change-related events.
+     * See {@link #Builder.setAudioPolicyVolumeCallback(AudioPolicyCallback)} to configure the
+     * {@link AudioPolicy} to receive those events.
+     *
+     */
+    public static abstract class AudioPolicyVolumeCallback {
+        /** @hide */
+        public AudioPolicyVolumeCallback() {}
+        /**
+         * Called when volume key-related changes are triggered, on the key down event.
+         * @param adjustment the type of volume adjustment for the key.
+         */
+        public void onVolumeAdjustment(@AudioManager.VolumeAdjustment int adjustment) {}
+    }
+
     private void onPolicyStatusChange() {
         AudioPolicyStatusListener l;
         synchronized (mLock) {
@@ -494,7 +619,7 @@
             sendMsg(MSG_FOCUS_REQUEST, afi, requestResult);
             if (DEBUG) {
                 Log.v(TAG, "notifyAudioFocusRequest: pack=" + afi.getPackageName() + " client="
-                        + afi.getClientId() + "reqRes=" + requestResult);
+                        + afi.getClientId() + " gen=" + afi.getGen());
             }
         }
 
@@ -517,6 +642,13 @@
                 }
             }
         }
+
+        public void notifyVolumeAdjust(int adjustment) {
+            sendMsg(MSG_VOL_ADJUST, null /* ignored */, adjustment);
+            if (DEBUG) {
+                Log.v(TAG, "notifyVolumeAdjust: " + adjustment);
+            }
+        }
     };
 
     //==================================================
@@ -528,6 +660,7 @@
     private final static int MSG_MIX_STATE_UPDATE = 3;
     private final static int MSG_FOCUS_REQUEST = 4;
     private final static int MSG_FOCUS_ABANDON = 5;
+    private final static int MSG_VOL_ADJUST = 6;
 
     private class EventHandler extends Handler {
         public EventHandler(AudioPolicy ap, Looper looper) {
@@ -571,6 +704,13 @@
                         Log.e(TAG, "Invalid null focus listener for focus abandon event");
                     }
                     break;
+                case MSG_VOL_ADJUST:
+                    if (mVolCb != null) {
+                        mVolCb.onVolumeAdjustment(msg.arg1);
+                    } else { // should never be null, but don't crash
+                        Log.e(TAG, "Invalid null volume event");
+                    }
+                    break;
                 default:
                     Log.e(TAG, "Unknown event " + msg.what);
             }
diff --git a/android/media/audiopolicy/AudioPolicyConfig.java b/android/media/audiopolicy/AudioPolicyConfig.java
index cafa5a8..f725cac 100644
--- a/android/media/audiopolicy/AudioPolicyConfig.java
+++ b/android/media/audiopolicy/AudioPolicyConfig.java
@@ -16,6 +16,7 @@
 
 package android.media.audiopolicy;
 
+import android.annotation.NonNull;
 import android.media.AudioFormat;
 import android.media.AudioManager;
 import android.media.AudioPatch;
@@ -24,6 +25,8 @@
 import android.os.Parcelable;
 import android.util.Log;
 
+import com.android.internal.annotations.GuardedBy;
+
 import java.util.ArrayList;
 import java.util.Objects;
 
@@ -35,11 +38,16 @@
 
     private static final String TAG = "AudioPolicyConfig";
 
-    protected ArrayList<AudioMix> mMixes;
+    protected final ArrayList<AudioMix> mMixes;
     protected int mDuckingPolicy = AudioPolicy.FOCUS_POLICY_DUCKING_IN_APP;
 
     private String mRegistrationId = null;
 
+    /** counter for the mixes that are / have been in the list of AudioMix
+     *  e.g. register 4 mixes (counter is 3), remove 1 (counter is 3), add 1 (counter is 4)
+     */
+    private int mMixCounter = 0;
+
     protected AudioPolicyConfig(AudioPolicyConfig conf) {
         mMixes = conf.mMixes;
     }
@@ -201,20 +209,39 @@
             return;
         }
         mRegistrationId = regId == null ? "" : regId;
-        int mixIndex = 0;
         for (AudioMix mix : mMixes) {
-            if (!mRegistrationId.isEmpty()) {
-                if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_LOOP_BACK) ==
-                        AudioMix.ROUTE_FLAG_LOOP_BACK) {
-                    mix.setRegistration(mRegistrationId + "mix" + mixTypeId(mix.getMixType()) + ":"
-                            + mixIndex++);
-                } else if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_RENDER) ==
-                        AudioMix.ROUTE_FLAG_RENDER) {
-                    mix.setRegistration(mix.mDeviceAddress);
-                }
-            } else {
-                mix.setRegistration("");
+            setMixRegistration(mix);
+        }
+    }
+
+    private void setMixRegistration(@NonNull final AudioMix mix) {
+        if (!mRegistrationId.isEmpty()) {
+            if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_LOOP_BACK) ==
+                    AudioMix.ROUTE_FLAG_LOOP_BACK) {
+                mix.setRegistration(mRegistrationId + "mix" + mixTypeId(mix.getMixType()) + ":"
+                        + mMixCounter);
+            } else if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_RENDER) ==
+                    AudioMix.ROUTE_FLAG_RENDER) {
+                mix.setRegistration(mix.mDeviceAddress);
             }
+        } else {
+            mix.setRegistration("");
+        }
+        mMixCounter++;
+    }
+
+    @GuardedBy("mMixes")
+    protected void add(@NonNull ArrayList<AudioMix> mixes) {
+        for (AudioMix mix : mixes) {
+            setMixRegistration(mix);
+            mMixes.add(mix);
+        }
+    }
+
+    @GuardedBy("mMixes")
+    protected void remove(@NonNull ArrayList<AudioMix> mixes) {
+        for (AudioMix mix : mixes) {
+            mMixes.remove(mix);
         }
     }
 
diff --git a/android/media/midi/MidiManager.java b/android/media/midi/MidiManager.java
index a015732..dee94c6 100644
--- a/android/media/midi/MidiManager.java
+++ b/android/media/midi/MidiManager.java
@@ -16,9 +16,11 @@
 
 package android.media.midi;
 
+import android.annotation.RequiresFeature;
 import android.annotation.SystemService;
 import android.bluetooth.BluetoothDevice;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.Bundle;
@@ -32,6 +34,7 @@
  * This class is the public application interface to the MIDI service.
  */
 @SystemService(Context.MIDI_SERVICE)
+@RequiresFeature(PackageManager.FEATURE_MIDI)
 public final class MidiManager {
     private static final String TAG = "MidiManager";
 
diff --git a/android/media/session/MediaController.java b/android/media/session/MediaController.java
index 622900f..f16804c 100644
--- a/android/media/session/MediaController.java
+++ b/android/media/session/MediaController.java
@@ -133,7 +133,7 @@
             return false;
         }
         try {
-            return mSessionBinder.sendMediaButton(keyEvent);
+            return mSessionBinder.sendMediaButton(mContext.getPackageName(), keyEvent);
         } catch (RemoteException e) {
             // System is dead. =(
         }
@@ -301,7 +301,7 @@
      */
     public void setVolumeTo(int value, int flags) {
         try {
-            mSessionBinder.setVolumeTo(value, flags, mContext.getPackageName());
+            mSessionBinder.setVolumeTo(mContext.getPackageName(), value, flags);
         } catch (RemoteException e) {
             Log.wtf(TAG, "Error calling setVolumeTo.", e);
         }
@@ -322,7 +322,7 @@
      */
     public void adjustVolume(int direction, int flags) {
         try {
-            mSessionBinder.adjustVolume(direction, flags, mContext.getPackageName());
+            mSessionBinder.adjustVolume(mContext.getPackageName(), direction, flags);
         } catch (RemoteException e) {
             Log.wtf(TAG, "Error calling adjustVolumeBy.", e);
         }
@@ -388,7 +388,7 @@
             throw new IllegalArgumentException("command cannot be null or empty");
         }
         try {
-            mSessionBinder.sendCommand(command, args, cb);
+            mSessionBinder.sendCommand(mContext.getPackageName(), command, args, cb);
         } catch (RemoteException e) {
             Log.d(TAG, "Dead object in sendCommand.", e);
         }
@@ -600,7 +600,7 @@
          */
         public void prepare() {
             try {
-                mSessionBinder.prepare();
+                mSessionBinder.prepare(mContext.getPackageName());
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling prepare.", e);
             }
@@ -624,7 +624,7 @@
                         "You must specify a non-empty String for prepareFromMediaId.");
             }
             try {
-                mSessionBinder.prepareFromMediaId(mediaId, extras);
+                mSessionBinder.prepareFromMediaId(mContext.getPackageName(), mediaId, extras);
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling prepare(" + mediaId + ").", e);
             }
@@ -650,7 +650,7 @@
                 query = "";
             }
             try {
-                mSessionBinder.prepareFromSearch(query, extras);
+                mSessionBinder.prepareFromSearch(mContext.getPackageName(), query, extras);
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling prepare(" + query + ").", e);
             }
@@ -674,7 +674,7 @@
                         "You must specify a non-empty Uri for prepareFromUri.");
             }
             try {
-                mSessionBinder.prepareFromUri(uri, extras);
+                mSessionBinder.prepareFromUri(mContext.getPackageName(), uri, extras);
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling prepare(" + uri + ").", e);
             }
@@ -685,7 +685,7 @@
          */
         public void play() {
             try {
-                mSessionBinder.play();
+                mSessionBinder.play(mContext.getPackageName());
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling play.", e);
             }
@@ -704,7 +704,7 @@
                         "You must specify a non-empty String for playFromMediaId.");
             }
             try {
-                mSessionBinder.playFromMediaId(mediaId, extras);
+                mSessionBinder.playFromMediaId(mContext.getPackageName(), mediaId, extras);
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling play(" + mediaId + ").", e);
             }
@@ -726,7 +726,7 @@
                 query = "";
             }
             try {
-                mSessionBinder.playFromSearch(query, extras);
+                mSessionBinder.playFromSearch(mContext.getPackageName(), query, extras);
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling play(" + query + ").", e);
             }
@@ -745,7 +745,7 @@
                         "You must specify a non-empty Uri for playFromUri.");
             }
             try {
-                mSessionBinder.playFromUri(uri, extras);
+                mSessionBinder.playFromUri(mContext.getPackageName(), uri, extras);
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling play(" + uri + ").", e);
             }
@@ -757,7 +757,7 @@
          */
         public void skipToQueueItem(long id) {
             try {
-                mSessionBinder.skipToQueueItem(id);
+                mSessionBinder.skipToQueueItem(mContext.getPackageName(), id);
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e);
             }
@@ -769,7 +769,7 @@
          */
         public void pause() {
             try {
-                mSessionBinder.pause();
+                mSessionBinder.pause(mContext.getPackageName());
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling pause.", e);
             }
@@ -781,7 +781,7 @@
          */
         public void stop() {
             try {
-                mSessionBinder.stop();
+                mSessionBinder.stop(mContext.getPackageName());
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling stop.", e);
             }
@@ -794,7 +794,7 @@
          */
         public void seekTo(long pos) {
             try {
-                mSessionBinder.seekTo(pos);
+                mSessionBinder.seekTo(mContext.getPackageName(), pos);
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling seekTo.", e);
             }
@@ -806,7 +806,7 @@
          */
         public void fastForward() {
             try {
-                mSessionBinder.fastForward();
+                mSessionBinder.fastForward(mContext.getPackageName());
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling fastForward.", e);
             }
@@ -817,7 +817,7 @@
          */
         public void skipToNext() {
             try {
-                mSessionBinder.next();
+                mSessionBinder.next(mContext.getPackageName());
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling next.", e);
             }
@@ -829,7 +829,7 @@
          */
         public void rewind() {
             try {
-                mSessionBinder.rewind();
+                mSessionBinder.rewind(mContext.getPackageName());
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling rewind.", e);
             }
@@ -840,7 +840,7 @@
          */
         public void skipToPrevious() {
             try {
-                mSessionBinder.previous();
+                mSessionBinder.previous(mContext.getPackageName());
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling previous.", e);
             }
@@ -855,7 +855,7 @@
          */
         public void setRating(Rating rating) {
             try {
-                mSessionBinder.rate(rating);
+                mSessionBinder.rate(mContext.getPackageName(), rating);
             } catch (RemoteException e) {
                 Log.wtf(TAG, "Error calling rate.", e);
             }
@@ -890,7 +890,7 @@
                 throw new IllegalArgumentException("CustomAction cannot be null.");
             }
             try {
-                mSessionBinder.sendCustomAction(action, args);
+                mSessionBinder.sendCustomAction(mContext.getPackageName(), action, args);
             } catch (RemoteException e) {
                 Log.d(TAG, "Dead object in sendCustomAction.", e);
             }
diff --git a/android/media/session/MediaSession.java b/android/media/session/MediaSession.java
index b8184a0..5e8b8ca 100644
--- a/android/media/session/MediaSession.java
+++ b/android/media/session/MediaSession.java
@@ -39,6 +39,7 @@
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.UserHandle;
+import android.media.session.MediaSessionManager.RemoteUserInfo;
 import android.service.media.MediaBrowserService;
 import android.text.TextUtils;
 import android.util.Log;
@@ -103,6 +104,16 @@
      */
     public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16;
 
+    /**
+     * @hide
+     */
+    public static final int INVALID_UID = -1;
+
+    /**
+     * @hide
+     */
+    public static final int INVALID_PID = -1;
+
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(flag = true, value = {
@@ -119,7 +130,8 @@
     private final ISession mBinder;
     private final CallbackStub mCbStub;
 
-    private CallbackMessageHandler mCallbackHandler;
+    // Do not change the name of mCallback. Support lib accesses this by using reflection.
+    private CallbackMessageHandler mCallback;
     private VolumeProvider mVolumeProvider;
     private PlaybackState mPlaybackState;
 
@@ -194,13 +206,13 @@
      */
     public void setCallback(@Nullable Callback callback, @Nullable Handler handler) {
         synchronized (mLock) {
-            if (mCallbackHandler != null) {
+            if (mCallback != null) {
                 // We're updating the callback, clear the session from the old one.
-                mCallbackHandler.mCallback.mSession = null;
-                mCallbackHandler.removeCallbacksAndMessages(null);
+                mCallback.mCallback.mSession = null;
+                mCallback.removeCallbacksAndMessages(null);
             }
             if (callback == null) {
-                mCallbackHandler = null;
+                mCallback = null;
                 return;
             }
             if (handler == null) {
@@ -209,7 +221,7 @@
             callback.mSession = this;
             CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
                     callback);
-            mCallbackHandler = msgHandler;
+            mCallback = msgHandler;
         }
     }
 
@@ -500,6 +512,22 @@
     }
 
     /**
+     * Gets the controller information who sent the current request.
+     * <p>
+     * Note: This is only valid while in a request callback, such as {@link Callback#onPlay}.
+     *
+     * @throws IllegalStateException If this method is called outside of {@link Callback} methods.
+     * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)
+     */
+    public final @NonNull RemoteUserInfo getCurrentControllerInfo() {
+        if (mCallback == null || mCallback.mCurrentControllerInfo == null) {
+            throw new IllegalStateException(
+                    "This should be called inside of MediaSession.Callback methods");
+        }
+        return mCallback.mCurrentControllerInfo;
+    }
+
+    /**
      * Notify the system that the remote volume changed.
      *
      * @param provider The provider that is handling volume changes.
@@ -527,16 +555,14 @@
      * @hide
      */
     public String getCallingPackage() {
-        try {
-            return mBinder.getCallingPackage();
-        } catch (RemoteException e) {
-            Log.wtf(TAG, "Dead object in getCallingPackage.", e);
+        if (mCallback != null) {
+            return mCallback.mCurrentControllerInfo.getPackageName();
         }
         return null;
     }
 
-    private void dispatchPrepare() {
-        postToCallback(CallbackMessageHandler.MSG_PREPARE);
+    private void dispatchPrepare(Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_PREPARE, null, extras);
     }
 
     private void dispatchPrepareFromMediaId(String mediaId, Bundle extras) {
@@ -551,8 +577,8 @@
         postToCallback(CallbackMessageHandler.MSG_PREPARE_URI, uri, extras);
     }
 
-    private void dispatchPlay() {
-        postToCallback(CallbackMessageHandler.MSG_PLAY);
+    private void dispatchPlay(Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_PLAY, null, extras);
     }
 
     private void dispatchPlayFromMediaId(String mediaId, Bundle extras) {
@@ -567,75 +593,67 @@
         postToCallback(CallbackMessageHandler.MSG_PLAY_URI, uri, extras);
     }
 
-    private void dispatchSkipToItem(long id) {
-        postToCallback(CallbackMessageHandler.MSG_SKIP_TO_ITEM, id);
+    private void dispatchSkipToItem(long id, Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_SKIP_TO_ITEM, id, extras);
     }
 
-    private void dispatchPause() {
-        postToCallback(CallbackMessageHandler.MSG_PAUSE);
+    private void dispatchPause(Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_PAUSE, null, extras);
     }
 
-    private void dispatchStop() {
-        postToCallback(CallbackMessageHandler.MSG_STOP);
+    private void dispatchStop(Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_STOP, null, extras);
     }
 
-    private void dispatchNext() {
-        postToCallback(CallbackMessageHandler.MSG_NEXT);
+    private void dispatchNext(Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_NEXT, null, extras);
     }
 
-    private void dispatchPrevious() {
-        postToCallback(CallbackMessageHandler.MSG_PREVIOUS);
+    private void dispatchPrevious(Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_PREVIOUS, null, extras);
     }
 
-    private void dispatchFastForward() {
-        postToCallback(CallbackMessageHandler.MSG_FAST_FORWARD);
+    private void dispatchFastForward(Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_FAST_FORWARD, null, extras);
     }
 
-    private void dispatchRewind() {
-        postToCallback(CallbackMessageHandler.MSG_REWIND);
+    private void dispatchRewind(Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_REWIND, null, extras);
     }
 
-    private void dispatchSeekTo(long pos) {
-        postToCallback(CallbackMessageHandler.MSG_SEEK_TO, pos);
+    private void dispatchSeekTo(long pos, Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_SEEK_TO, pos, extras);
     }
 
-    private void dispatchRate(Rating rating) {
-        postToCallback(CallbackMessageHandler.MSG_RATE, rating);
+    private void dispatchRate(Rating rating, Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_RATE, rating, extras);
     }
 
-    private void dispatchCustomAction(String action, Bundle args) {
-        postToCallback(CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args);
+    private void dispatchCustomAction(String action, Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_CUSTOM_ACTION, action, extras);
     }
 
-    private void dispatchMediaButton(Intent mediaButtonIntent) {
-        postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent);
+    private void dispatchMediaButton(Intent mediaButtonIntent, Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent, extras);
     }
 
-    private void dispatchAdjustVolume(int direction) {
-        postToCallback(CallbackMessageHandler.MSG_ADJUST_VOLUME, direction);
+    private void dispatchAdjustVolume(int direction, Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_ADJUST_VOLUME, direction, extras);
     }
 
-    private void dispatchSetVolumeTo(int volume) {
-        postToCallback(CallbackMessageHandler.MSG_SET_VOLUME, volume);
+    private void dispatchSetVolumeTo(int volume, Bundle extras) {
+        postToCallback(CallbackMessageHandler.MSG_SET_VOLUME, volume, extras);
     }
 
-    private void postToCallback(int what) {
-        postToCallback(what, null);
-    }
-
-    private void postCommand(String command, Bundle args, ResultReceiver resultCb) {
+    private void postCommand(String command, Bundle args, ResultReceiver resultCb, Bundle extras) {
         Command cmd = new Command(command, args, resultCb);
-        postToCallback(CallbackMessageHandler.MSG_COMMAND, cmd);
-    }
-
-    private void postToCallback(int what, Object obj) {
-        postToCallback(what, obj, null);
+        postToCallback(CallbackMessageHandler.MSG_COMMAND, cmd, extras);
     }
 
     private void postToCallback(int what, Object obj, Bundle extras) {
         synchronized (mLock) {
-            if (mCallbackHandler != null) {
-                mCallbackHandler.post(what, obj, extras);
+            if (mCallback != null) {
+                mCallback.post(what, obj, extras);
             }
         }
     }
@@ -733,9 +751,13 @@
      * and the system. A callback may be set using {@link #setCallback}.
      */
     public abstract static class Callback {
+
         private MediaSession mSession;
         private CallbackMessageHandler mHandler;
         private boolean mMediaPlayPauseKeyPending;
+        private String mCallingPackage;
+        private int mCallingPid;
+        private int mCallingUid;
 
         public Callback() {
         }
@@ -1022,24 +1044,26 @@
         private WeakReference<MediaSession> mMediaSession;
 
         public CallbackStub(MediaSession session) {
-            mMediaSession = new WeakReference<MediaSession>(session);
+            mMediaSession = new WeakReference<>(session);
         }
 
         @Override
-        public void onCommand(String command, Bundle args, ResultReceiver cb) {
+        public void onCommand(String packageName, int pid, int uid, String command, Bundle args,
+                ResultReceiver cb) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.postCommand(command, args, cb);
+                session.postCommand(command, args, cb, createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber,
-                ResultReceiver cb) {
+        public void onMediaButton(String packageName, int pid, int uid, Intent mediaButtonIntent,
+                int sequenceNumber, ResultReceiver cb) {
             MediaSession session = mMediaSession.get();
             try {
                 if (session != null) {
-                    session.dispatchMediaButton(mediaButtonIntent);
+                    session.dispatchMediaButton(
+                            mediaButtonIntent, createExtraBundle(packageName, pid, uid));
                 }
             } finally {
                 if (cb != null) {
@@ -1049,165 +1073,191 @@
         }
 
         @Override
-        public void onPrepare() {
+        public void onPrepare(String packageName, int pid, int uid) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchPrepare();
+                session.dispatchPrepare(createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onPrepareFromMediaId(String mediaId, Bundle extras) {
+        public void onPrepareFromMediaId(String packageName, int pid, int uid, String mediaId,
+                Bundle extras) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchPrepareFromMediaId(mediaId, extras);
+                session.dispatchPrepareFromMediaId(
+                        mediaId, createExtraBundle(packageName, pid, uid, extras));
             }
         }
 
         @Override
-        public void onPrepareFromSearch(String query, Bundle extras) {
+        public void onPrepareFromSearch(String packageName, int pid, int uid, String query,
+                Bundle extras) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchPrepareFromSearch(query, extras);
+                session.dispatchPrepareFromSearch(
+                        query, createExtraBundle(packageName, pid, uid, extras));
             }
         }
 
         @Override
-        public void onPrepareFromUri(Uri uri, Bundle extras) {
+        public void onPrepareFromUri(String packageName, int pid, int uid, Uri uri, Bundle extras) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchPrepareFromUri(uri, extras);
+                session.dispatchPrepareFromUri(uri,
+                        createExtraBundle(packageName, pid, uid, extras));
             }
         }
 
         @Override
-        public void onPlay() {
+        public void onPlay(String packageName, int pid, int uid) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchPlay();
+                session.dispatchPlay(createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onPlayFromMediaId(String mediaId, Bundle extras) {
+        public void onPlayFromMediaId(String packageName, int pid, int uid, String mediaId,
+                Bundle extras) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchPlayFromMediaId(mediaId, extras);
+                session.dispatchPlayFromMediaId(
+                        mediaId, createExtraBundle(packageName, pid, uid, extras));
             }
         }
 
         @Override
-        public void onPlayFromSearch(String query, Bundle extras) {
+        public void onPlayFromSearch(String packageName, int pid, int uid, String query,
+                Bundle extras) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchPlayFromSearch(query, extras);
+                session.dispatchPlayFromSearch(query, createExtraBundle(packageName, pid, uid,
+                        extras));
             }
         }
 
         @Override
-        public void onPlayFromUri(Uri uri, Bundle extras) {
+        public void onPlayFromUri(String packageName, int pid, int uid, Uri uri, Bundle extras) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchPlayFromUri(uri, extras);
+                session.dispatchPlayFromUri(uri, createExtraBundle(packageName, pid, uid, extras));
             }
         }
 
         @Override
-        public void onSkipToTrack(long id) {
+        public void onSkipToTrack(String packageName, int pid, int uid, long id) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchSkipToItem(id);
+                session.dispatchSkipToItem(id, createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onPause() {
+        public void onPause(String packageName, int pid, int uid) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchPause();
+                session.dispatchPause(createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onStop() {
+        public void onStop(String packageName, int pid, int uid) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchStop();
+                session.dispatchStop(createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onNext() {
+        public void onNext(String packageName, int pid, int uid) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchNext();
+                session.dispatchNext(createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onPrevious() {
+        public void onPrevious(String packageName, int pid, int uid) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchPrevious();
+                session.dispatchPrevious(createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onFastForward() {
+        public void onFastForward(String packageName, int pid, int uid) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchFastForward();
+                session.dispatchFastForward(createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onRewind() {
+        public void onRewind(String packageName, int pid, int uid) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchRewind();
+                session.dispatchRewind(createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onSeekTo(long pos) {
+        public void onSeekTo(String packageName, int pid, int uid, long pos) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchSeekTo(pos);
+                session.dispatchSeekTo(pos, createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onRate(Rating rating) {
+        public void onRate(String packageName, int pid, int uid, Rating rating) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchRate(rating);
+                session.dispatchRate(rating, createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onCustomAction(String action, Bundle args) {
+        public void onCustomAction(String packageName, int pid, int uid, String action,
+                Bundle args) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchCustomAction(action, args);
+                session.dispatchCustomAction(
+                        action, createExtraBundle(packageName, pid, uid, args));
             }
         }
 
         @Override
-        public void onAdjustVolume(int direction) {
+        public void onAdjustVolume(String packageName, int pid, int uid, int direction) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchAdjustVolume(direction);
+                session.dispatchAdjustVolume(direction, createExtraBundle(packageName, pid, uid));
             }
         }
 
         @Override
-        public void onSetVolumeTo(int value) {
+        public void onSetVolumeTo(String packageName, int pid, int uid, int value) {
             MediaSession session = mMediaSession.get();
             if (session != null) {
-                session.dispatchSetVolumeTo(value);
+                session.dispatchSetVolumeTo(value, createExtraBundle(packageName, pid, uid));
             }
         }
 
+        private Bundle createExtraBundle(String packageName, int pid, int uid) {
+            return createExtraBundle(packageName, pid, uid, null);
+        }
+
+        private Bundle createExtraBundle(String packageName, int pid, int uid,
+                Bundle originalBundle) {
+            Bundle bundle = new Bundle();
+            bundle.putString(CallbackMessageHandler.EXTRA_KEY_CALLING_PACKAGE, packageName);
+            bundle.putInt(CallbackMessageHandler.EXTRA_KEY_CALLING_PID, pid);
+            bundle.putInt(CallbackMessageHandler.EXTRA_KEY_CALLING_UID, uid);
+            if (originalBundle != null) {
+                bundle.putBundle(CallbackMessageHandler.EXTRA_KEY_ORIGINAL_BUNDLE, originalBundle);
+            }
+            return bundle;
+        }
     }
 
     /**
@@ -1271,7 +1321,8 @@
             return 0;
         }
 
-        public static final Creator<MediaSession.QueueItem> CREATOR = new Creator<MediaSession.QueueItem>() {
+        public static final Creator<MediaSession.QueueItem> CREATOR =
+                new Creator<MediaSession.QueueItem>() {
 
             @Override
             public MediaSession.QueueItem createFromParcel(Parcel p) {
@@ -1328,6 +1379,15 @@
 
     private class CallbackMessageHandler extends Handler {
 
+        private static final String EXTRA_KEY_CALLING_PACKAGE =
+                "android.media.session.extra.CALLING_PACKAGE";
+        private static final String EXTRA_KEY_CALLING_PID =
+                "android.media.session.extra.CALLING_PID";
+        private static final String EXTRA_KEY_CALLING_UID =
+                "android.media.session.extra.CALLING_UID";
+        private static final String EXTRA_KEY_ORIGINAL_BUNDLE =
+                "android.media.session.extra.ORIGINAL_BUNDLE";
+
         private static final int MSG_COMMAND = 1;
         private static final int MSG_MEDIA_BUTTON = 2;
         private static final int MSG_PREPARE = 3;
@@ -1354,6 +1414,8 @@
 
         private MediaSession.Callback mCallback;
 
+        private RemoteUserInfo mCurrentControllerInfo;
+
         public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) {
             super(looper, null, true);
             mCallback = callback;
@@ -1366,21 +1428,17 @@
             msg.sendToTarget();
         }
 
-        public void post(int what, Object obj) {
-            obtainMessage(what, obj).sendToTarget();
-        }
-
-        public void post(int what) {
-            post(what, null);
-        }
-
-        public void post(int what, Object obj, int arg1) {
-            obtainMessage(what, arg1, 0, obj).sendToTarget();
-        }
-
         @Override
         public void handleMessage(Message msg) {
             VolumeProvider vp;
+            Bundle bundle = msg.getData();
+            Bundle originalBundle = bundle.getBundle(EXTRA_KEY_ORIGINAL_BUNDLE);
+
+            mCurrentControllerInfo = new RemoteUserInfo(
+                    bundle.getString(EXTRA_KEY_CALLING_PACKAGE),
+                    bundle.getInt(EXTRA_KEY_CALLING_PID, INVALID_PID),
+                    bundle.getInt(EXTRA_KEY_CALLING_UID, INVALID_UID));
+
             switch (msg.what) {
                 case MSG_COMMAND:
                     Command cmd = (Command) msg.obj;
@@ -1393,25 +1451,25 @@
                     mCallback.onPrepare();
                     break;
                 case MSG_PREPARE_MEDIA_ID:
-                    mCallback.onPrepareFromMediaId((String) msg.obj, msg.getData());
+                    mCallback.onPrepareFromMediaId((String) msg.obj, originalBundle);
                     break;
                 case MSG_PREPARE_SEARCH:
-                    mCallback.onPrepareFromSearch((String) msg.obj, msg.getData());
+                    mCallback.onPrepareFromSearch((String) msg.obj, originalBundle);
                     break;
                 case MSG_PREPARE_URI:
-                    mCallback.onPrepareFromUri((Uri) msg.obj, msg.getData());
+                    mCallback.onPrepareFromUri((Uri) msg.obj, originalBundle);
                     break;
                 case MSG_PLAY:
                     mCallback.onPlay();
                     break;
                 case MSG_PLAY_MEDIA_ID:
-                    mCallback.onPlayFromMediaId((String) msg.obj, msg.getData());
+                    mCallback.onPlayFromMediaId((String) msg.obj, originalBundle);
                     break;
                 case MSG_PLAY_SEARCH:
-                    mCallback.onPlayFromSearch((String) msg.obj, msg.getData());
+                    mCallback.onPlayFromSearch((String) msg.obj, originalBundle);
                     break;
                 case MSG_PLAY_URI:
-                    mCallback.onPlayFromUri((Uri) msg.obj, msg.getData());
+                    mCallback.onPlayFromUri((Uri) msg.obj, originalBundle);
                     break;
                 case MSG_SKIP_TO_ITEM:
                     mCallback.onSkipToQueueItem((Long) msg.obj);
@@ -1441,7 +1499,7 @@
                     mCallback.onSetRating((Rating) msg.obj);
                     break;
                 case MSG_CUSTOM_ACTION:
-                    mCallback.onCustomAction((String) msg.obj, msg.getData());
+                    mCallback.onCustomAction((String) msg.obj, originalBundle);
                     break;
                 case MSG_ADJUST_VOLUME:
                     synchronized (mLock) {
@@ -1463,6 +1521,7 @@
                     mCallback.handleMediaPlayPauseKeySingleTapIfPending();
                     break;
             }
+            mCurrentControllerInfo = null;
         }
     }
 }
diff --git a/android/media/session/MediaSessionManager.java b/android/media/session/MediaSessionManager.java
index 81b4603..519af1b 100644
--- a/android/media/session/MediaSessionManager.java
+++ b/android/media/session/MediaSessionManager.java
@@ -16,6 +16,7 @@
 
 package android.media.session;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
@@ -24,8 +25,8 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.media.AudioManager;
-import android.media.IMediaSession2;
 import android.media.IRemoteVolumeController;
+import android.media.ISessionTokensListener;
 import android.media.MediaSession2;
 import android.media.MediaSessionService2;
 import android.media.SessionToken2;
@@ -36,7 +37,9 @@
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
 import android.os.UserHandle;
+import android.service.media.MediaBrowserService;
 import android.service.notification.NotificationListenerService;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -44,6 +47,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * Provides support for interacting with {@link MediaSession media sessions}
@@ -71,6 +75,8 @@
 
     private final ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper> mListeners
             = new ArrayMap<OnActiveSessionsChangedListener, SessionsChangedWrapper>();
+    private final ArrayMap<OnSessionTokensChangedListener, SessionTokensChangedWrapper>
+            mSessionTokensListener = new ArrayMap<>();
     private final Object mLock = new Object();
     private final ISessionManager mService;
 
@@ -336,37 +342,74 @@
     }
 
     /**
-     * Called when a {@link MediaSession2} is created.
+     * Returns whether the app is trusted.
+     * <p>
+     * An app is trusted if the app holds the android.Manifest.permission.MEDIA_CONTENT_CONTROL
+     * permission or has an enabled notification listener.
      *
-     * @hide
+     * @param userInfo The remote user info
      */
-    // TODO(jaewan): System API
-    public SessionToken2 createSessionToken(@NonNull String callingPackage, @NonNull String id,
-            @NonNull IMediaSession2 binder) {
+    public boolean isTrustedForMediaControl(RemoteUserInfo userInfo) {
+        if (userInfo.getPackageName() == null) {
+            return false;
+        }
         try {
-            Bundle bundle = mService.createSessionToken(callingPackage, id, binder);
-            return SessionToken2.fromBundle(bundle);
+            return mService.isTrusted(
+                    userInfo.getPackageName(), userInfo.getPid(), userInfo.getUid());
         } catch (RemoteException e) {
             Log.wtf(TAG, "Cannot communicate with the service.", e);
         }
-        return null;
+        return false;
     }
 
     /**
+     * Called when a {@link MediaSession2} is created.
+     * @hide
+     */
+    public boolean createSession2(@NonNull SessionToken2 token) {
+        if (token == null) {
+            return false;
+        }
+        try {
+            return mService.createSession2(token.toBundle());
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Cannot communicate with the service.", e);
+        }
+        return false;
+    }
+
+    /**
+     * Called when a {@link MediaSession2} is destroyed.
+     * @hide
+     */
+    public void destroySession2(@NonNull SessionToken2 token) {
+        if (token == null) {
+            return;
+        }
+        try {
+            mService.destroySession2(token.toBundle());
+        } catch (RemoteException e) {
+            Log.wtf(TAG, "Cannot communicate with the service.", e);
+        }
+    }
+
+    /**
+     * @hide
      * Get {@link List} of {@link SessionToken2} whose sessions are active now. This list represents
      * active sessions regardless of whether they're {@link MediaSession2} or
      * {@link MediaSessionService2}.
+     * <p>
+     * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+     * calling app. You may also retrieve this list if your app is an enabled notification listener
+     * using the {@link NotificationListenerService} APIs.
      *
-     * @return list of Tokens
-     * @hide
+     * @return list of tokens
      */
-    // TODO(jaewan): Unhide
-    // TODO(jaewan): Protect this with permission.
-    // TODO(jaewna): Add listener for change in lists.
     public List<SessionToken2> getActiveSessionTokens() {
         try {
             List<Bundle> bundles = mService.getSessionTokens(
-                    /* activeSessionOnly */ true, /* sessionServiceOnly */ false);
+                    /* activeSessionOnly */ true, /* sessionServiceOnly */ false,
+                    mContext.getPackageName());
             return toTokenList(bundles);
         } catch (RemoteException e) {
             Log.wtf(TAG, "Cannot communicate with the service.", e);
@@ -375,18 +418,21 @@
     }
 
     /**
+     * @hide
      * Get {@link List} of {@link SessionToken2} for {@link MediaSessionService2} regardless of their
      * activeness. This list represents media apps that support background playback.
+     * <p>
+     * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+     * calling app. You may also retrieve this list if your app is an enabled notification listener
+     * using the {@link NotificationListenerService} APIs.
      *
-     * @return list of Tokens
-     * @hide
+     * @return list of tokens
      */
-    // TODO(jaewan): Unhide
-    // TODO(jaewna): Add listener for change in lists.
     public List<SessionToken2> getSessionServiceTokens() {
         try {
             List<Bundle> bundles = mService.getSessionTokens(
-                    /* activeSessionOnly */ false, /* sessionServiceOnly */ true);
+                    /* activeSessionOnly */ false, /* sessionServiceOnly */ true,
+                    mContext.getPackageName());
             return toTokenList(bundles);
         } catch (RemoteException e) {
             Log.wtf(TAG, "Cannot communicate with the service.", e);
@@ -395,21 +441,23 @@
     }
 
     /**
+     * @hide
      * Get all {@link SessionToken2}s. This is the combined list of {@link #getActiveSessionTokens()}
      * and {@link #getSessionServiceTokens}.
+     * <p>
+     * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+     * calling app. You may also retrieve this list if your app is an enabled notification listener
+     * using the {@link NotificationListenerService} APIs.
      *
-     * @return list of Tokens
+     * @return list of tokens
      * @see #getActiveSessionTokens
      * @see #getSessionServiceTokens
-     * @hide
      */
-    // TODO(jaewan): Unhide
-    // TODO(jaewan): Protect this with permission.
-    // TODO(jaewna): Add listener for change in lists.
     public List<SessionToken2> getAllSessionTokens() {
         try {
             List<Bundle> bundles = mService.getSessionTokens(
-                    /* activeSessionOnly */ false, /* sessionServiceOnly */ false);
+                    /* activeSessionOnly */ false, /* sessionServiceOnly */ false,
+                    mContext.getPackageName());
             return toTokenList(bundles);
         } catch (RemoteException e) {
             Log.wtf(TAG, "Cannot communicate with the service.", e);
@@ -417,6 +465,84 @@
         }
     }
 
+    /**
+     * @hide
+     * Add a listener to be notified when the {@link #getAllSessionTokens()} changes.
+     * <p>
+     * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+     * calling app. You may also retrieve this list if your app is an enabled notification listener
+     * using the {@link NotificationListenerService} APIs.
+     *
+     * @param executor executor to run this command
+     * @param listener The listener to add.
+     */
+    public void addOnSessionTokensChangedListener(@NonNull @CallbackExecutor Executor executor,
+            @NonNull OnSessionTokensChangedListener listener) {
+        addOnSessionTokensChangedListener(UserHandle.myUserId(), executor, listener);
+    }
+
+    /**
+     * Add a listener to be notified when the {@link #getAllSessionTokens()} changes.
+     * <p>
+     * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the
+     * calling app. You may also retrieve this list if your app is an enabled notification listener
+     * using the {@link NotificationListenerService} APIs.
+     *
+     * @param userId The userId to listen for changes on.
+     * @param executor executor to run this command
+     * @param listener The listener to add.
+     * @hide
+     */
+    public void addOnSessionTokensChangedListener(int userId,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnSessionTokensChangedListener listener) {
+        if (executor == null) {
+            throw new IllegalArgumentException("executor may not be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("listener may not be null");
+        }
+        synchronized (mLock) {
+            if (mSessionTokensListener.get(listener) != null) {
+                Log.w(TAG, "Attempted to add session listener twice, ignoring.");
+                return;
+            }
+            SessionTokensChangedWrapper wrapper = new SessionTokensChangedWrapper(
+                    mContext, executor, listener);
+            try {
+                mService.addSessionTokensListener(wrapper.mStub, userId, mContext.getPackageName());
+                mSessionTokensListener.put(listener, wrapper);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error in addSessionTokensListener.", e);
+            }
+        }
+    }
+
+    /**
+     * @hide
+     * Stop receiving session token updates on the specified listener.
+     *
+     * @param listener The listener to remove.
+     */
+    public void removeOnSessionTokensChangedListener(
+            @NonNull OnSessionTokensChangedListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener may not be null");
+        }
+        synchronized (mLock) {
+            SessionTokensChangedWrapper wrapper = mSessionTokensListener.remove(listener);
+            if (wrapper != null) {
+                try {
+                    mService.removeSessionTokensListener(wrapper.mStub, mContext.getPackageName());
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Error in removeSessionTokensListener.", e);
+                } finally {
+                    wrapper.release();
+                }
+            }
+        }
+    }
+
     private static List<SessionToken2> toTokenList(List<Bundle> bundles) {
         List<SessionToken2> tokens = new ArrayList<>();
         if (bundles != null) {
@@ -552,6 +678,15 @@
     }
 
     /**
+     * @hide
+     * Listens for changes to the {@link #getAllSessionTokens()}. This can be added
+     * using {@link #addOnActiveSessionsChangedListener}.
+     */
+    public interface OnSessionTokensChangedListener {
+        void onSessionTokensChanged(@NonNull List<SessionToken2> tokens);
+    }
+
+    /**
      * Listens the volume key long-presses.
      * @hide
      */
@@ -631,6 +766,56 @@
         public abstract void onAddressedPlayerChanged(ComponentName mediaButtonReceiver);
     }
 
+    /**
+     * Information of a remote user of {@link MediaSession} or {@link MediaBrowserService}.
+     * This can be used to decide whether the remote user is trusted app.
+     *
+     * @see #isTrustedForMediaControl(RemoteUserInfo)
+     */
+    public static final class RemoteUserInfo {
+        private String mPackageName;
+        private int mPid;
+        private int mUid;
+
+        public RemoteUserInfo(String packageName, int pid, int uid) {
+            mPackageName = packageName;
+            mPid = pid;
+            mUid = uid;
+        }
+
+        /**
+         * @return package name of the controller
+         */
+        public String getPackageName() {
+            return mPackageName;
+        }
+
+        /**
+         * @return pid of the controller
+         */
+        public int getPid() {
+            return mPid;
+        }
+
+        /**
+         * @return uid of the controller
+         */
+        public int getUid() {
+            return mUid;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof RemoteUserInfo)) {
+                return false;
+            }
+            RemoteUserInfo otherUserInfo = (RemoteUserInfo) obj;
+            return TextUtils.equals(mPackageName, otherUserInfo.mPackageName)
+                    && mPid == otherUserInfo.mPid
+                    && mUid == otherUserInfo.mUid;
+        }
+    }
+
     private static final class SessionsChangedWrapper {
         private Context mContext;
         private OnActiveSessionsChangedListener mListener;
@@ -653,8 +838,7 @@
                         public void run() {
                             final Context context = mContext;
                             if (context != null) {
-                                ArrayList<MediaController> controllers
-                                        = new ArrayList<MediaController>();
+                                ArrayList<MediaController> controllers = new ArrayList<>();
                                 int size = tokens.size();
                                 for (int i = 0; i < size; i++) {
                                     controllers.add(new MediaController(context, tokens.get(i)));
@@ -677,6 +861,41 @@
         }
     }
 
+    private static final class SessionTokensChangedWrapper {
+        private Context mContext;
+        private Executor mExecutor;
+        private OnSessionTokensChangedListener mListener;
+
+        public SessionTokensChangedWrapper(Context context, Executor executor,
+                OnSessionTokensChangedListener listener) {
+            mContext = context;
+            mExecutor = executor;
+            mListener = listener;
+        }
+
+        private final ISessionTokensListener.Stub mStub = new ISessionTokensListener.Stub() {
+            @Override
+            public void onSessionTokensChanged(final List<Bundle> bundles) {
+                final Executor executor = mExecutor;
+                if (executor != null) {
+                    executor.execute(() -> {
+                        final Context context = mContext;
+                        final OnSessionTokensChangedListener listener = mListener;
+                        if (context != null && listener != null) {
+                            listener.onSessionTokensChanged(toTokenList(bundles));
+                        }
+                    });
+                }
+            }
+        };
+
+        private void release() {
+            mListener = null;
+            mContext = null;
+            mExecutor = null;
+        }
+    }
+
     private static final class OnVolumeKeyLongPressListenerImpl
             extends IOnVolumeKeyLongPressListener.Stub {
         private OnVolumeKeyLongPressListener mListener;
diff --git a/android/media/soundtrigger/SoundTriggerDetectionService.java b/android/media/soundtrigger/SoundTriggerDetectionService.java
new file mode 100644
index 0000000..7381d97
--- /dev/null
+++ b/android/media/soundtrigger/SoundTriggerDetectionService.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2018 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.soundtrigger;
+
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.annotation.CallSuper;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.soundtrigger.SoundTrigger;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.UUID;
+
+/**
+ * A service that allows interaction with the actual sound trigger detection on the system.
+ *
+ * <p> Sound trigger detection refers to detectors that match generic sound patterns that are
+ * not voice-based. The voice-based recognition models should utilize the {@link
+ * android.service.voice.VoiceInteractionService} instead. Access to this class needs to be
+ * protected by the {@value android.Manifest.permission.BIND_SOUND_TRIGGER_DETECTION_SERVICE}
+ * permission granted only to the system.
+ *
+ * <p>This service has to be explicitly started by an app, the system does not scan for and start
+ * these services.
+ *
+ * <p>If an operation ({@link #onGenericRecognitionEvent}, {@link #onError},
+ * {@link #onRecognitionPaused}, {@link #onRecognitionResumed}) is triggered the service is
+ * considered as running in the foreground. Once the operation is processed the service should call
+ * {@link #operationFinished(UUID, int)}. If this does not happen in
+ * {@link SoundTriggerManager#getDetectionServiceOperationsTimeout()} milliseconds
+ * {@link #onStopOperation(UUID, Bundle, int)} is called and the service is unbound.
+ *
+ * <p>The total amount of operations per day might be limited.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class SoundTriggerDetectionService extends Service {
+    private static final String LOG_TAG = SoundTriggerDetectionService.class.getSimpleName();
+
+    private static final boolean DEBUG = false;
+
+    private final Object mLock = new Object();
+
+    /**
+     * Client indexed by model uuid. This is needed for the {@link #operationFinished(UUID, int)}
+     * callbacks.
+     */
+    @GuardedBy("mLock")
+    private final ArrayMap<UUID, ISoundTriggerDetectionServiceClient> mClients =
+            new ArrayMap<>();
+
+    private Handler mHandler;
+
+    /**
+     * @hide
+     */
+    @Override
+    protected final void attachBaseContext(Context base) {
+        super.attachBaseContext(base);
+        mHandler = new Handler(base.getMainLooper());
+    }
+
+    private void setClient(@NonNull UUID uuid, @Nullable Bundle params,
+            @NonNull ISoundTriggerDetectionServiceClient client) {
+        if (DEBUG) Log.i(LOG_TAG, uuid + ": handle setClient");
+
+        synchronized (mLock) {
+            mClients.put(uuid, client);
+        }
+        onConnected(uuid, params);
+    }
+
+    private void removeClient(@NonNull UUID uuid, @Nullable Bundle params) {
+        if (DEBUG) Log.i(LOG_TAG, uuid + ": handle removeClient");
+
+        synchronized (mLock) {
+            mClients.remove(uuid);
+        }
+        onDisconnected(uuid, params);
+    }
+
+    /**
+     * The system has connected to this service for the recognition registered for the model
+     * {@code uuid}.
+     *
+     * <p> This is called before any operations are delivered.
+     *
+     * @param uuid   The {@code uuid} of the model the recognitions is registered for
+     * @param params The {@code params} passed when the recognition was started
+     */
+    @MainThread
+    public void onConnected(@NonNull UUID uuid, @Nullable Bundle params) {
+        /* do nothing */
+    }
+
+    /**
+     * The system has disconnected from this service for the recognition registered for the model
+     * {@code uuid}.
+     *
+     * <p>Once this is called {@link #operationFinished} cannot be called anymore for
+     * {@code uuid}.
+     *
+     * <p> {@link #onConnected(UUID, Bundle)} is called before any further operations are delivered.
+     *
+     * @param uuid   The {@code uuid} of the model the recognitions is registered for
+     * @param params The {@code params} passed when the recognition was started
+     */
+    @MainThread
+    public void onDisconnected(@NonNull UUID uuid, @Nullable Bundle params) {
+        /* do nothing */
+    }
+
+    /**
+     * A new generic sound trigger event has been detected.
+     *
+     * @param uuid   The {@code uuid} of the model the recognition is registered for
+     * @param params The {@code params} passed when the recognition was started
+     * @param opId The id of this operation. Once the operation is done, this service needs to call
+     *             {@link #operationFinished(UUID, int)}
+     * @param event The event that has been detected
+     */
+    @MainThread
+    public void onGenericRecognitionEvent(@NonNull UUID uuid, @Nullable Bundle params, int opId,
+            @NonNull SoundTrigger.RecognitionEvent event) {
+        operationFinished(uuid, opId);
+    }
+
+    /**
+     * A error has been detected.
+     *
+     * @param uuid   The {@code uuid} of the model the recognition is registered for
+     * @param params The {@code params} passed when the recognition was started
+     * @param opId The id of this operation. Once the operation is done, this service needs to call
+     *             {@link #operationFinished(UUID, int)}
+     * @param status The error code detected
+     */
+    @MainThread
+    public void onError(@NonNull UUID uuid, @Nullable Bundle params, int opId, int status) {
+        operationFinished(uuid, opId);
+    }
+
+    /**
+     * An operation took too long and should be stopped.
+     *
+     * @param uuid   The {@code uuid} of the model the recognition is registered for
+     * @param params The {@code params} passed when the recognition was started
+     * @param opId The id of the operation that took too long
+     */
+    @MainThread
+    public abstract void onStopOperation(@NonNull UUID uuid, @Nullable Bundle params, int opId);
+
+    /**
+     * Tell that the system that an operation has been fully processed.
+     *
+     * @param uuid The {@code uuid} of the model the recognition is registered for
+     * @param opId The id of the operation that is processed
+     */
+    public final void operationFinished(@Nullable UUID uuid, int opId) {
+        try {
+            ISoundTriggerDetectionServiceClient client;
+            synchronized (mLock) {
+                client = mClients.get(uuid);
+
+                if (client == null) {
+                    throw new IllegalStateException("operationFinished called, but no client for "
+                            + uuid + ". Was this called after onDisconnected?");
+                }
+            }
+            client.onOpFinished(opId);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public final IBinder onBind(Intent intent) {
+        return new ISoundTriggerDetectionService.Stub() {
+            private final Object mBinderLock = new Object();
+
+            /** Cached params bundles indexed by the model uuid */
+            @GuardedBy("mBinderLock")
+            public final ArrayMap<UUID, Bundle> mParams = new ArrayMap<>();
+
+            @Override
+            public void setClient(ParcelUuid puuid, Bundle params,
+                    ISoundTriggerDetectionServiceClient client) {
+                UUID uuid = puuid.getUuid();
+                synchronized (mBinderLock) {
+                    mParams.put(uuid, params);
+                }
+
+                if (DEBUG) Log.i(LOG_TAG, uuid + ": setClient(" + params + ")");
+                mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::setClient,
+                        SoundTriggerDetectionService.this, uuid, params, client));
+            }
+
+            @Override
+            public void removeClient(ParcelUuid puuid) {
+                UUID uuid = puuid.getUuid();
+                Bundle params;
+                synchronized (mBinderLock) {
+                    params = mParams.remove(uuid);
+                }
+
+                if (DEBUG) Log.i(LOG_TAG, uuid + ": removeClient");
+                mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::removeClient,
+                        SoundTriggerDetectionService.this, uuid, params));
+            }
+
+            @Override
+            public void onGenericRecognitionEvent(ParcelUuid puuid, int opId,
+                    SoundTrigger.GenericRecognitionEvent event) {
+                UUID uuid = puuid.getUuid();
+                Bundle params;
+                synchronized (mBinderLock) {
+                    params = mParams.get(uuid);
+                }
+
+                if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onGenericRecognitionEvent");
+                mHandler.sendMessage(
+                        obtainMessage(SoundTriggerDetectionService::onGenericRecognitionEvent,
+                                SoundTriggerDetectionService.this, uuid, params, opId, event));
+            }
+
+            @Override
+            public void onError(ParcelUuid puuid, int opId, int status) {
+                UUID uuid = puuid.getUuid();
+                Bundle params;
+                synchronized (mBinderLock) {
+                    params = mParams.get(uuid);
+                }
+
+                if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onError(" + status + ")");
+                mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::onError,
+                        SoundTriggerDetectionService.this, uuid, params, opId, status));
+            }
+
+            @Override
+            public void onStopOperation(ParcelUuid puuid, int opId) {
+                UUID uuid = puuid.getUuid();
+                Bundle params;
+                synchronized (mBinderLock) {
+                    params = mParams.get(uuid);
+                }
+
+                if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onStopOperation");
+                mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::onStopOperation,
+                        SoundTriggerDetectionService.this, uuid, params, opId));
+            }
+        };
+    }
+
+    @CallSuper
+    @Override
+    public boolean onUnbind(Intent intent) {
+        mClients.clear();
+
+        return false;
+    }
+}
diff --git a/android/media/soundtrigger/SoundTriggerManager.java b/android/media/soundtrigger/SoundTriggerManager.java
index 92ffae0..c9ec752 100644
--- a/android/media/soundtrigger/SoundTriggerManager.java
+++ b/android/media/soundtrigger/SoundTriggerManager.java
@@ -15,26 +15,31 @@
  */
 
 package android.media.soundtrigger;
+
 import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
 
-import android.app.PendingIntent;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.Context;
 import android.hardware.soundtrigger.SoundTrigger;
-import android.hardware.soundtrigger.SoundTrigger.SoundModel;
 import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
+import android.hardware.soundtrigger.SoundTrigger.SoundModel;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.ParcelUuid;
 import android.os.RemoteException;
+import android.provider.Settings;
 import android.util.Slog;
 
 import com.android.internal.app.ISoundTriggerService;
+import com.android.internal.util.Preconditions;
 
 import java.util.HashMap;
 import java.util.UUID;
@@ -276,6 +281,40 @@
     }
 
     /**
+     * Starts recognition for the given model id. All events from the model will be sent to the
+     * service.
+     *
+     * <p>This only supports generic sound trigger events. For keyphrase events, please use
+     * {@link android.service.voice.VoiceInteractionService}.
+     *
+     * @param soundModelId Id of the sound model
+     * @param params Opaque data sent to each service call of the service as the {@code params}
+     *               argument
+     * @param detectionService The component name of the service that should receive the events.
+     *                         Needs to subclass {@link SoundTriggerDetectionService}
+     * @param config Configures the recognition
+     *
+     * @return {@link SoundTrigger#STATUS_OK} if the recognition could be started, error code
+     *         otherwise
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
+    public int startRecognition(@NonNull UUID soundModelId, @Nullable Bundle params,
+        @NonNull ComponentName detectionService, @NonNull RecognitionConfig config) {
+        Preconditions.checkNotNull(soundModelId);
+        Preconditions.checkNotNull(detectionService);
+        Preconditions.checkNotNull(config);
+
+        try {
+            return mSoundTriggerService.startRecognitionForService(new ParcelUuid(soundModelId),
+                params, detectionService, config);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Stops the given model's recognition.
      * @hide
      */
@@ -324,4 +363,19 @@
             throw e.rethrowFromSystemServer();
         }
     }
+
+    /**
+     * Get the amount of time (in milliseconds) an operation of the
+     * {@link ISoundTriggerDetectionService} is allowed to ask.
+     *
+     * @return The amount of time an sound trigger detection service operation is allowed to last
+     */
+    public int getDetectionServiceOperationsTimeout() {
+        try {
+            return Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT);
+        } catch (Settings.SettingNotFoundException e) {
+            return Integer.MAX_VALUE;
+        }
+    }
 }
diff --git a/android/media/update/ApiLoader.java b/android/media/update/ApiLoader.java
index b928e93..6f82f68 100644
--- a/android/media/update/ApiLoader.java
+++ b/android/media/update/ApiLoader.java
@@ -16,45 +16,68 @@
 
 package android.media.update;
 
-import android.content.res.Resources;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.os.Build;
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import com.android.internal.annotations.GuardedBy;
+
+import dalvik.system.PathClassLoader;
+
+import java.io.File;
 
 /**
  * @hide
  */
 public final class ApiLoader {
-    private static Object sMediaLibrary;
+    @GuardedBy("this")
+    private static StaticProvider sMediaUpdatable;
 
     private static final String UPDATE_PACKAGE = "com.android.media.update";
     private static final String UPDATE_CLASS = "com.android.media.update.ApiFactory";
     private static final String UPDATE_METHOD = "initialize";
+    private static final boolean REGISTER_UPDATE_DEPENDENCY = true;
 
     private ApiLoader() { }
 
-    public static StaticProvider getProvider(Context context) {
+    public static StaticProvider getProvider() {
+        if (sMediaUpdatable != null) return sMediaUpdatable;
+
         try {
-            return (StaticProvider) getMediaLibraryImpl(context);
-        } catch (PackageManager.NameNotFoundException | ReflectiveOperationException e) {
+            return getMediaUpdatable();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } catch (NameNotFoundException | ReflectiveOperationException e) {
             throw new RuntimeException(e);
         }
     }
 
     // TODO This method may do I/O; Ensure it does not violate (emit warnings in) strict mode.
-    private static synchronized Object getMediaLibraryImpl(Context context)
-            throws PackageManager.NameNotFoundException, ReflectiveOperationException {
-        if (sMediaLibrary != null) return sMediaLibrary;
+    private static synchronized StaticProvider getMediaUpdatable()
+            throws NameNotFoundException, ReflectiveOperationException, RemoteException {
+        if (sMediaUpdatable != null) return sMediaUpdatable;
 
         // TODO Figure out when to use which package (query media update service)
         int flags = Build.IS_DEBUGGABLE ? 0 : PackageManager.MATCH_FACTORY_ONLY;
-        Context libContext = context.createApplicationContext(
-                context.getPackageManager().getPackageInfo(UPDATE_PACKAGE, flags).applicationInfo,
-                Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
-        sMediaLibrary = libContext.getClassLoader()
-                .loadClass(UPDATE_CLASS)
-                .getMethod(UPDATE_METHOD, Resources.class, Resources.Theme.class)
-                .invoke(null, libContext.getResources(), libContext.getTheme());
-        return sMediaLibrary;
+        flags |= PackageManager.GET_SHARED_LIBRARY_FILES;
+        ApplicationInfo ai = AppGlobals.getPackageManager().getApplicationInfo(
+                UPDATE_PACKAGE, flags, UserHandle.myUserId());
+
+        if (REGISTER_UPDATE_DEPENDENCY) {
+            // Register a dependency to the updatable in order to be killed during updates
+            ActivityManager.getService().addPackageDependency(ai.packageName);
+        }
+
+        ClassLoader classLoader = new PathClassLoader(ai.sourceDir,
+                ai.nativeLibraryDir + File.pathSeparator + System.getProperty("java.library.path"),
+                ClassLoader.getSystemClassLoader().getParent());
+        return sMediaUpdatable = (StaticProvider) classLoader.loadClass(UPDATE_CLASS)
+                .getMethod(UPDATE_METHOD, ApplicationInfo.class).invoke(null, ai);
     }
 }
diff --git a/android/media/update/MediaBrowser2Provider.java b/android/media/update/MediaBrowser2Provider.java
index e48711d..a18701e 100644
--- a/android/media/update/MediaBrowser2Provider.java
+++ b/android/media/update/MediaBrowser2Provider.java
@@ -22,12 +22,13 @@
  * @hide
  */
 public interface MediaBrowser2Provider extends MediaController2Provider {
-    void getBrowserRoot_impl(Bundle rootHints);
+    void getLibraryRoot_impl(Bundle rootHints);
 
-    void subscribe_impl(String parentId, Bundle options);
-    void unsubscribe_impl(String parentId, Bundle options);
+    void subscribe_impl(String parentId, Bundle extras);
+    void unsubscribe_impl(String parentId);
 
     void getItem_impl(String mediaId);
-    void getChildren_impl(String parentId, int page, int pageSize, Bundle options);
-    void search_impl(String query, int page, int pageSize, Bundle extras);
+    void getChildren_impl(String parentId, int page, int pageSize, Bundle extras);
+    void search_impl(String query, Bundle extras);
+    void getSearchResult_impl(String query, int page, int pageSize, Bundle extras);
 }
diff --git a/android/media/update/MediaControlView2Provider.java b/android/media/update/MediaControlView2Provider.java
index 6b38c92..8e69653 100644
--- a/android/media/update/MediaControlView2Provider.java
+++ b/android/media/update/MediaControlView2Provider.java
@@ -16,9 +16,10 @@
 
 package android.media.update;
 
-import android.annotation.SystemApi;
+import android.media.SessionToken2;
 import android.media.session.MediaController;
-import android.view.View;
+import android.util.AttributeSet;
+import android.widget.MediaControlView2;
 
 /**
  * Interface for connecting the public API to an updatable implementation.
@@ -33,15 +34,19 @@
  *
  * @hide
  */
-// TODO @SystemApi
-public interface MediaControlView2Provider extends ViewProvider {
+// TODO: @SystemApi
+public interface MediaControlView2Provider extends ViewGroupProvider {
+    void initialize(AttributeSet attrs, int defStyleAttr, int defStyleRes);
+
+    void setMediaSessionToken_impl(SessionToken2 token);
+    void setOnFullScreenListener_impl(MediaControlView2.OnFullScreenListener l);
+    /**
+     * @hide TODO: remove
+     */
     void setController_impl(MediaController controller);
-    void show_impl();
-    void show_impl(int timeout);
-    boolean isShowing_impl();
-    void hide_impl();
-    void showSubtitle_impl();
-    void hideSubtitle_impl();
-    void setPrevNextListeners_impl(View.OnClickListener next, View.OnClickListener prev);
-    void setButtonVisibility_impl(int button, boolean visible);
+    /**
+     * @hide
+     */
+    void setButtonVisibility_impl(int button, int visibility);
+    void requestPlayButtonFocus_impl();
 }
diff --git a/android/media/update/MediaController2Provider.java b/android/media/update/MediaController2Provider.java
index c5f6b96..7234f7b 100644
--- a/android/media/update/MediaController2Provider.java
+++ b/android/media/update/MediaController2Provider.java
@@ -17,11 +17,11 @@
 package android.media.update;
 
 import android.app.PendingIntent;
+import android.media.AudioAttributes;
 import android.media.MediaController2.PlaybackInfo;
 import android.media.MediaItem2;
-import android.media.MediaSession2.Command;
-import android.media.MediaSession2.PlaylistParam;
-import android.media.PlaybackState2;
+import android.media.MediaMetadata2;
+import android.media.SessionCommand2;
 import android.media.Rating2;
 import android.media.SessionToken2;
 import android.net.Uri;
@@ -34,12 +34,13 @@
  * @hide
  */
 public interface MediaController2Provider extends TransportControlProvider {
+    void initialize();
+
     void close_impl();
     SessionToken2 getSessionToken_impl();
     boolean isConnected_impl();
 
     PendingIntent getSessionActivity_impl();
-    int getRatingType_impl();
 
     void setVolumeTo_impl(int value, int flags);
     void adjustVolume_impl(int direction, int flags);
@@ -47,18 +48,35 @@
 
     void prepareFromUri_impl(Uri uri, Bundle extras);
     void prepareFromSearch_impl(String query, Bundle extras);
-    void prepareMediaId_impl(String mediaId, Bundle extras);
+    void prepareFromMediaId_impl(String mediaId, Bundle extras);
     void playFromSearch_impl(String query, Bundle extras);
-    void playFromUri_impl(String uri, Bundle extras);
+    void playFromUri_impl(Uri uri, Bundle extras);
     void playFromMediaId_impl(String mediaId, Bundle extras);
+    void fastForward_impl();
+    void rewind_impl();
 
-    void setRating_impl(Rating2 rating);
-    void sendCustomCommand_impl(Command command, Bundle args, ResultReceiver cb);
+    void setRating_impl(String mediaId, Rating2 rating);
+    void sendCustomCommand_impl(SessionCommand2 command, Bundle args, ResultReceiver cb);
     List<MediaItem2> getPlaylist_impl();
+    void setPlaylist_impl(List<MediaItem2> list, MediaMetadata2 metadata);
+    MediaMetadata2 getPlaylistMetadata_impl();
+    void updatePlaylistMetadata_impl(MediaMetadata2 metadata);
 
-    void removePlaylistItem_impl(MediaItem2 index);
     void addPlaylistItem_impl(int index, MediaItem2 item);
+    void replacePlaylistItem_impl(int index, MediaItem2 item);
+    void removePlaylistItem_impl(MediaItem2 item);
 
-    PlaylistParam getPlaylistParam_impl();
-    PlaybackState2 getPlaybackState_impl();
+    int getPlayerState_impl();
+    long getCurrentPosition_impl();
+    float getPlaybackSpeed_impl();
+    long getBufferedPosition_impl();
+    MediaItem2 getCurrentMediaItem_impl();
+
+    interface PlaybackInfoProvider {
+        int getPlaybackType_impl();
+        AudioAttributes getAudioAttributes_impl();
+        int getControlType_impl();
+        int getMaxVolume_impl();
+        int getCurrentVolume_impl();
+    }
 }
diff --git a/android/media/update/MediaItem2Provider.java b/android/media/update/MediaItem2Provider.java
new file mode 100644
index 0000000..47db22f
--- /dev/null
+++ b/android/media/update/MediaItem2Provider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018 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.update;
+
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaItem2.Builder;
+import android.media.MediaMetadata2;
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+public interface MediaItem2Provider {
+    Bundle toBundle_impl();
+    String toString_impl();
+    int getFlags_impl();
+    boolean isBrowsable_impl();
+    boolean isPlayable_impl();
+    void setMetadata_impl(MediaMetadata2 metadata);
+    MediaMetadata2 getMetadata_impl();
+    String getMediaId_impl();
+    DataSourceDesc getDataSourceDesc_impl();
+    boolean equals_impl(Object obj);
+
+    interface BuilderProvider {
+        Builder setMediaId_impl(String mediaId);
+        Builder setMetadata_impl(MediaMetadata2 metadata);
+        Builder setDataSourceDesc_impl(DataSourceDesc dataSourceDesc);
+        MediaItem2 build_impl();
+    }
+}
diff --git a/android/media/update/MediaLibraryService2Provider.java b/android/media/update/MediaLibraryService2Provider.java
index dac5784..9a0d693 100644
--- a/android/media/update/MediaLibraryService2Provider.java
+++ b/android/media/update/MediaLibraryService2Provider.java
@@ -17,14 +17,24 @@
 package android.media.update;
 
 import android.media.MediaSession2.ControllerInfo;
-import android.os.Bundle; /**
+import android.os.Bundle;
+
+/**
  * @hide
  */
 public interface MediaLibraryService2Provider extends MediaSessionService2Provider {
     // Nothing new for now
 
     interface MediaLibrarySessionProvider extends MediaSession2Provider {
-        void notifyChildrenChanged_impl(ControllerInfo controller, String parentId, Bundle options);
-        void notifyChildrenChanged_impl(String parentId, Bundle options);
+        void notifyChildrenChanged_impl(ControllerInfo controller, String parentId,
+                int itemCount, Bundle extras);
+        void notifyChildrenChanged_impl(String parentId, int itemCount, Bundle extras);
+        void notifySearchResultChanged_impl(ControllerInfo controller, String query, int itemCount,
+                Bundle extras);
+    }
+
+    interface LibraryRootProvider {
+        String getRootId_impl();
+        Bundle getExtras_impl();
     }
 }
diff --git a/android/media/update/MediaMetadata2Provider.java b/android/media/update/MediaMetadata2Provider.java
new file mode 100644
index 0000000..22463e9
--- /dev/null
+++ b/android/media/update/MediaMetadata2Provider.java
@@ -0,0 +1,38 @@
+package android.media.update;
+
+import android.graphics.Bitmap;
+import android.media.MediaMetadata2;
+import android.media.MediaMetadata2.Builder;
+import android.media.Rating2;
+import android.os.Bundle;
+
+import java.util.Set;
+
+/**
+ * @hide
+ */
+public interface MediaMetadata2Provider {
+    boolean containsKey_impl(String key);
+    CharSequence getText_impl(String key);
+    String getMediaId_impl();
+    String getString_impl(String key);
+    long getLong_impl(String key);
+    Rating2 getRating_impl(String key);
+    Bundle toBundle_impl();
+    Set<String> keySet_impl();
+    int size_impl();
+    Bitmap getBitmap_impl(String key);
+    float getFloat_impl(String key);
+    Bundle getExtras_impl();
+
+    interface BuilderProvider {
+        Builder putText_impl(String key, CharSequence value);
+        Builder putString_impl(String key, String value);
+        Builder putLong_impl(String key, long value);
+        Builder putRating_impl(String key, Rating2 value);
+        Builder putBitmap_impl(String key, Bitmap value);
+        Builder putFloat_impl(String key, float value);
+        Builder setExtras_impl(Bundle bundle);
+        MediaMetadata2 build_impl();
+    }
+}
diff --git a/android/media/update/MediaPlaylistAgentProvider.java b/android/media/update/MediaPlaylistAgentProvider.java
new file mode 100644
index 0000000..e1522cf
--- /dev/null
+++ b/android/media/update/MediaPlaylistAgentProvider.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2018 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.update;
+
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.MediaPlaylistAgent.PlaylistEventCallback;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+public interface MediaPlaylistAgentProvider {
+    // final methods of MediaPlaylistAgent
+    void registerPlaylistEventCallback_impl(Executor executor, PlaylistEventCallback callback);
+    void unregisterPlaylistEventCallback_impl(PlaylistEventCallback callback);
+    void notifyPlaylistChanged_impl();
+    void notifyPlaylistMetadataChanged_impl();
+    void notifyShuffleModeChanged_impl();
+    void notifyRepeatModeChanged_impl();
+
+    // public methods of MediaPlaylistAgent
+    List<MediaItem2> getPlaylist_impl();
+    void setPlaylist_impl(List<MediaItem2> list, MediaMetadata2 metadata);
+    MediaMetadata2 getPlaylistMetadata_impl();
+    void updatePlaylistMetadata_impl(MediaMetadata2 metadata);
+    void addPlaylistItem_impl(int index, MediaItem2 item);
+    void removePlaylistItem_impl(MediaItem2 item);
+    void replacePlaylistItem_impl(int index, MediaItem2 item);
+    void skipToPlaylistItem_impl(MediaItem2 item);
+    void skipToPreviousItem_impl();
+    void skipToNextItem_impl();
+    int getRepeatMode_impl();
+    void setRepeatMode_impl(int repeatMode);
+    int getShuffleMode_impl();
+    void setShuffleMode_impl(int shuffleMode);
+    MediaItem2 getMediaItem_impl(DataSourceDesc dsd);
+}
diff --git a/android/media/update/MediaSession2Provider.java b/android/media/update/MediaSession2Provider.java
index 2a68ad1..4751348 100644
--- a/android/media/update/MediaSession2Provider.java
+++ b/android/media/update/MediaSession2Provider.java
@@ -16,50 +16,117 @@
 
 package android.media.update;
 
-import android.media.AudioAttributes;
+import android.app.PendingIntent;
+import android.media.AudioFocusRequest;
 import android.media.MediaItem2;
+import android.media.MediaMetadata2;
 import android.media.MediaPlayerBase;
+import android.media.MediaPlaylistAgent;
 import android.media.MediaSession2;
-import android.media.MediaSession2.Command;
+import android.media.SessionCommand2;
 import android.media.MediaSession2.CommandButton;
-import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.CommandButton.Builder;
+import android.media.SessionCommandGroup2;
 import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.OnDataSourceMissingHelper;
+import android.media.MediaSession2.SessionCallback;
 import android.media.SessionToken2;
-import android.media.VolumeProvider;
+import android.media.VolumeProvider2;
 import android.os.Bundle;
 import android.os.ResultReceiver;
 
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
 
 /**
  * @hide
  */
 public interface MediaSession2Provider extends TransportControlProvider {
     void close_impl();
-    void setPlayer_impl(MediaPlayerBase player);
-    void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider);
+    void updatePlayer_impl(MediaPlayerBase player, MediaPlaylistAgent playlistAgent,
+            VolumeProvider2 volumeProvider);
     MediaPlayerBase getPlayer_impl();
+    MediaMetadata2 getPlaylistMetadata_impl();
+    void updatePlaylistMetadata_impl(MediaMetadata2 metadata);
+    MediaPlaylistAgent getPlaylistAgent_impl();
+    VolumeProvider2 getVolumeProvider_impl();
     SessionToken2 getToken_impl();
     List<ControllerInfo> getConnectedControllers_impl();
     void setCustomLayout_impl(ControllerInfo controller, List<CommandButton> layout);
-    void setAudioAttributes_impl(AudioAttributes attributes);
-    void setAudioFocusRequest_impl(int focusGain);
-
-    void setAllowedCommands_impl(ControllerInfo controller, CommandGroup commands);
-    void notifyMetadataChanged_impl();
-    void sendCustomCommand_impl(ControllerInfo controller, Command command, Bundle args,
+    void setAudioFocusRequest_impl(AudioFocusRequest afr);
+    void setAllowedCommands_impl(ControllerInfo controller, SessionCommandGroup2 commands);
+    void sendCustomCommand_impl(ControllerInfo controller, SessionCommand2 command, Bundle args,
             ResultReceiver receiver);
-    void sendCustomCommand_impl(Command command, Bundle args);
-    void setPlaylist_impl(List<MediaItem2> playlist, MediaSession2.PlaylistParam param);
+    void sendCustomCommand_impl(SessionCommand2 command, Bundle args);
+    void addPlaylistItem_impl(int index, MediaItem2 item);
+    void removePlaylistItem_impl(MediaItem2 item);
+    void replacePlaylistItem_impl(int index, MediaItem2 item);
+    List<MediaItem2> getPlaylist_impl();
+    void setPlaylist_impl(List<MediaItem2> list, MediaMetadata2 metadata);
+    MediaItem2 getCurrentPlaylistItem_impl();
+    void notifyError_impl(int errorCode, Bundle extras);
+    int getPlayerState_impl();
+    long getCurrentPosition_impl();
+    long getBufferedPosition_impl();
+    void setOnDataSourceMissingHelper_impl(OnDataSourceMissingHelper helper);
+    void clearOnDataSourceMissingHelper_impl();
 
-    /**
-     * @hide
-     */
+    // TODO(jaewan): Rename and move provider
+    interface CommandProvider {
+        int getCommandCode_impl();
+        String getCustomCommand_impl();
+        Bundle getExtras_impl();
+        Bundle toBundle_impl();
+
+        boolean equals_impl(Object ob);
+        int hashCode_impl();
+    }
+
+    // TODO(jaewan): Rename and move provider
+    interface CommandGroupProvider {
+        void addCommand_impl(SessionCommand2 command);
+        void addAllPredefinedCommands_impl();
+        void removeCommand_impl(SessionCommand2 command);
+        boolean hasCommand_impl(SessionCommand2 command);
+        boolean hasCommand_impl(int code);
+        Set<SessionCommand2> getCommands_impl();
+        Bundle toBundle_impl();
+    }
+
+    interface CommandButtonProvider {
+        SessionCommand2 getCommand_impl();
+        int getIconResId_impl();
+        String getDisplayName_impl();
+        Bundle getExtras_impl();
+        boolean isEnabled_impl();
+
+        interface BuilderProvider {
+            Builder setCommand_impl(SessionCommand2 command);
+            Builder setIconResId_impl(int resId);
+            Builder setDisplayName_impl(String displayName);
+            Builder setEnabled_impl(boolean enabled);
+            Builder setExtras_impl(Bundle extras);
+            CommandButton build_impl();
+        }
+    }
+
     interface ControllerInfoProvider {
         String getPackageName_impl();
         int getUid_impl();
         boolean isTrusted_impl();
         int hashCode_impl();
-        boolean equals_impl(ControllerInfoProvider obj);
+        boolean equals_impl(Object obj);
+        String toString_impl();
+    }
+
+    interface BuilderBaseProvider<T extends MediaSession2, C extends SessionCallback> {
+        void setPlayer_impl(MediaPlayerBase player);
+        void setPlaylistAgent_impl(MediaPlaylistAgent playlistAgent);
+        void setVolumeProvider_impl(VolumeProvider2 volumeProvider);
+        void setSessionActivity_impl(PendingIntent pi);
+        void setId_impl(String id);
+        void setSessionCallback_impl(Executor executor, C callback);
+        T build_impl();
     }
 }
diff --git a/android/media/update/MediaSessionService2Provider.java b/android/media/update/MediaSessionService2Provider.java
index a6b462b..5eb6254 100644
--- a/android/media/update/MediaSessionService2Provider.java
+++ b/android/media/update/MediaSessionService2Provider.java
@@ -16,10 +16,10 @@
 
 package android.media.update;
 
+import android.app.Notification;
 import android.content.Intent;
 import android.media.MediaSession2;
 import android.media.MediaSessionService2.MediaNotification;
-import android.media.PlaybackState2;
 import android.os.IBinder;
 
 /**
@@ -27,9 +27,14 @@
  */
 public interface MediaSessionService2Provider {
     MediaSession2 getSession_impl();
-    MediaNotification onUpdateNotification_impl(PlaybackState2 state);
+    MediaNotification onUpdateNotification_impl();
 
     // Service
     void onCreate_impl();
     IBinder onBind_impl(Intent intent);
+
+    interface MediaNotificationProvider {
+        int getNotificationId_impl();
+        Notification getNotification_impl();
+    }
 }
diff --git a/android/media/update/ProviderCreator.java b/android/media/update/ProviderCreator.java
new file mode 100644
index 0000000..f5f3e47
--- /dev/null
+++ b/android/media/update/ProviderCreator.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2018 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.update;
+
+/** @hide */
+@FunctionalInterface
+public interface ProviderCreator<T, U> {
+    U createProvider(T instance);
+}
diff --git a/android/media/update/Rating2Provider.java b/android/media/update/Rating2Provider.java
new file mode 100644
index 0000000..28ad273
--- /dev/null
+++ b/android/media/update/Rating2Provider.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 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.update;
+
+import android.annotation.SystemApi;
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+public interface Rating2Provider {
+    String toString_impl();
+    boolean equals_impl(Object obj);
+    int hashCode_impl();
+    Bundle toBundle_impl();
+    boolean isRated_impl();
+    int getRatingStyle_impl();
+    boolean hasHeart_impl();
+    boolean isThumbUp_impl();
+    float getStarRating_impl();
+    float getPercentRating_impl();
+}
diff --git a/android/media/update/SessionToken2Provider.java b/android/media/update/SessionToken2Provider.java
new file mode 100644
index 0000000..95d6ce0
--- /dev/null
+++ b/android/media/update/SessionToken2Provider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018 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.update;
+
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+public interface SessionToken2Provider {
+    String getPackageName_impl();
+    String getId_imp();
+    int getType_impl();
+    int getUid_impl();
+    Bundle toBundle_impl();
+
+    int hashCode_impl();
+    boolean equals_impl(Object obj);
+    String toString_impl();
+}
diff --git a/android/media/update/StaticProvider.java b/android/media/update/StaticProvider.java
index 7c222c3..8687b80 100644
--- a/android/media/update/StaticProvider.java
+++ b/android/media/update/StaticProvider.java
@@ -17,24 +17,37 @@
 package android.media.update;
 
 import android.annotation.Nullable;
-import android.app.PendingIntent;
+import android.app.Notification;
 import android.content.Context;
-import android.media.IMediaSession2Callback;
 import android.media.MediaBrowser2;
 import android.media.MediaBrowser2.BrowserCallback;
 import android.media.MediaController2;
 import android.media.MediaController2.ControllerCallback;
+import android.media.MediaItem2;
 import android.media.MediaLibraryService2;
+import android.media.MediaLibraryService2.LibraryRoot;
 import android.media.MediaLibraryService2.MediaLibrarySession;
-import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
-import android.media.MediaPlayerBase;
+import android.media.MediaLibraryService2.MediaLibrarySession.MediaLibrarySessionCallback;
+import android.media.MediaMetadata2;
+import android.media.MediaPlaylistAgent;
 import android.media.MediaSession2;
 import android.media.MediaSession2.SessionCallback;
 import android.media.MediaSessionService2;
+import android.media.MediaSessionService2.MediaNotification;
+import android.media.Rating2;
+import android.media.SessionCommand2;
+import android.media.SessionCommandGroup2;
 import android.media.SessionToken2;
-import android.media.VolumeProvider;
-import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
+import android.media.VolumeProvider2;
+import android.media.update.MediaLibraryService2Provider.LibraryRootProvider;
+import android.media.update.MediaSession2Provider.BuilderBaseProvider;
+import android.media.update.MediaSession2Provider.CommandButtonProvider;
+import android.media.update.MediaSession2Provider.CommandGroupProvider;
+import android.media.update.MediaSession2Provider.CommandProvider;
 import android.media.update.MediaSession2Provider.ControllerInfoProvider;
+import android.media.update.MediaSessionService2Provider.MediaNotificationProvider;
+import android.os.Bundle;
+import android.os.IInterface;
 import android.util.AttributeSet;
 import android.widget.MediaControlView2;
 import android.widget.VideoView2;
@@ -46,36 +59,71 @@
  *
  * This interface provides access to constructors and static methods that are otherwise not directly
  * accessible via an implementation object.
- *
  * @hide
  */
-// TODO @SystemApi
 public interface StaticProvider {
-    MediaControlView2Provider createMediaControlView2(
-            MediaControlView2 instance, ViewProvider superProvider);
-    VideoView2Provider createVideoView2(
-            VideoView2 instance, ViewProvider superProvider,
+    MediaControlView2Provider createMediaControlView2(MediaControlView2 instance,
+            ViewGroupProvider superProvider, ViewGroupProvider privateProvider,
+            @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes);
+    VideoView2Provider createVideoView2(VideoView2 instance,
+            ViewGroupProvider superProvider, ViewGroupProvider privateProvider,
             @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes);
 
-    MediaSession2Provider createMediaSession2(MediaSession2 mediaSession2, Context context,
-            MediaPlayerBase player, String id, Executor callbackExecutor, SessionCallback callback,
-            VolumeProvider volumeProvider, int ratingType,
-            PendingIntent sessionActivity);
-    ControllerInfoProvider createMediaSession2ControllerInfoProvider(
-            MediaSession2.ControllerInfo instance, Context context, int uid, int pid,
-            String packageName, IMediaSession2Callback callback);
-    MediaController2Provider createMediaController2(
-            MediaController2 instance, Context context, SessionToken2 token,
-            ControllerCallback callback, Executor executor);
-    MediaBrowser2Provider createMediaBrowser2(
-            MediaBrowser2 instance, Context context, SessionToken2 token,
-            BrowserCallback callback, Executor executor);
-    MediaSessionService2Provider createMediaSessionService2(
-            MediaSessionService2 instance);
-    MediaSessionService2Provider createMediaLibraryService2(
-            MediaLibraryService2 instance);
-    MediaLibrarySessionProvider createMediaLibraryService2MediaLibrarySession(
-            MediaLibrarySession instance, Context context, MediaPlayerBase player, String id,
-            Executor callbackExecutor, MediaLibrarySessionCallback callback,
-            VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity);
+    CommandProvider createMediaSession2Command(SessionCommand2 instance,
+            int commandCode, String action, Bundle extra);
+    SessionCommand2 fromBundle_MediaSession2Command(Bundle bundle);
+    CommandGroupProvider createMediaSession2CommandGroup(SessionCommandGroup2 instance,
+            SessionCommandGroup2 others);
+    SessionCommandGroup2 fromBundle_MediaSession2CommandGroup(Bundle bundle);
+    ControllerInfoProvider createMediaSession2ControllerInfo(Context context,
+            MediaSession2.ControllerInfo instance, int uid, int pid,
+            String packageName, IInterface callback);
+    CommandButtonProvider.BuilderProvider createMediaSession2CommandButtonBuilder(
+            MediaSession2.CommandButton.Builder instance);
+    BuilderBaseProvider<MediaSession2, SessionCallback> createMediaSession2Builder(
+            Context context, MediaSession2.Builder instance);
+
+    MediaController2Provider createMediaController2(Context context, MediaController2 instance,
+            SessionToken2 token, Executor executor, ControllerCallback callback);
+
+    MediaBrowser2Provider createMediaBrowser2(Context context, MediaBrowser2 instance,
+            SessionToken2 token, Executor executor, BrowserCallback callback);
+
+    MediaSessionService2Provider createMediaSessionService2(MediaSessionService2 instance);
+    MediaNotificationProvider createMediaSessionService2MediaNotification(
+            MediaNotification mediaNotification, int notificationId, Notification notification);
+
+    MediaSessionService2Provider createMediaLibraryService2(MediaLibraryService2 instance);
+    BuilderBaseProvider<MediaLibrarySession, MediaLibrarySessionCallback>
+        createMediaLibraryService2Builder(
+            MediaLibraryService2 service, MediaLibrarySession.Builder instance,
+            Executor callbackExecutor, MediaLibrarySessionCallback callback);
+    LibraryRootProvider createMediaLibraryService2LibraryRoot(LibraryRoot instance, String rootId,
+            Bundle extras);
+
+    SessionToken2Provider createSessionToken2(Context context, SessionToken2 instance,
+            String packageName, String serviceName, int uid);
+    SessionToken2 fromBundle_SessionToken2(Bundle bundle);
+
+    MediaItem2Provider.BuilderProvider createMediaItem2Builder(MediaItem2.Builder instance,
+            int flags);
+    MediaItem2 fromBundle_MediaItem2(Bundle bundle);
+
+    VolumeProvider2Provider createVolumeProvider2(VolumeProvider2 instance, int controlType,
+            int maxVolume, int currentVolume);
+
+    MediaMetadata2 fromBundle_MediaMetadata2(Bundle bundle);
+    MediaMetadata2Provider.BuilderProvider createMediaMetadata2Builder(
+            MediaMetadata2.Builder instance);
+    MediaMetadata2Provider.BuilderProvider createMediaMetadata2Builder(
+            MediaMetadata2.Builder instance, MediaMetadata2 source);
+
+    Rating2 newUnratedRating_Rating2(int ratingStyle);
+    Rating2 fromBundle_Rating2(Bundle bundle);
+    Rating2 newHeartRating_Rating2(boolean hasHeart);
+    Rating2 newThumbRating_Rating2(boolean thumbIsUp);
+    Rating2 newStarRating_Rating2(int starRatingStyle, float starRating);
+    Rating2 newPercentageRating_Rating2(float percent);
+
+    MediaPlaylistAgentProvider createMediaPlaylistAgent(MediaPlaylistAgent instance);
 }
diff --git a/android/media/update/TransportControlProvider.java b/android/media/update/TransportControlProvider.java
index 5217a9d..d89a88a 100644
--- a/android/media/update/TransportControlProvider.java
+++ b/android/media/update/TransportControlProvider.java
@@ -16,24 +16,24 @@
 
 package android.media.update;
 
-import android.media.MediaPlayerBase;
-import android.media.session.PlaybackState;
-import android.os.Handler;
+import android.media.MediaItem2;
 
 /**
  * @hide
  */
-// TODO(jaewan): SystemApi
 public interface TransportControlProvider {
     void play_impl();
     void pause_impl();
     void stop_impl();
-    void skipToPrevious_impl();
-    void skipToNext_impl();
+    void skipToPreviousItem_impl();
+    void skipToNextItem_impl();
 
     void prepare_impl();
-    void fastForward_impl();
-    void rewind_impl();
     void seekTo_impl(long pos);
-    void setCurrentPlaylistItem_impl(int index);
+    void skipToPlaylistItem_impl(MediaItem2 item);
+
+    int getRepeatMode_impl();
+    void setRepeatMode_impl(int repeatMode);
+    int getShuffleMode_impl();
+    void setShuffleMode_impl(int shuffleMode);
 }
diff --git a/android/media/update/VideoView2Provider.java b/android/media/update/VideoView2Provider.java
index 416ea98..27b436f 100644
--- a/android/media/update/VideoView2Provider.java
+++ b/android/media/update/VideoView2Provider.java
@@ -16,14 +16,26 @@
 
 package android.media.update;
 
+import android.annotation.SystemApi;
 import android.media.AudioAttributes;
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
 import android.media.MediaPlayerBase;
+import android.media.SessionToken2;
+import android.media.session.MediaController;
+import android.media.session.PlaybackState;
+import android.media.session.MediaSession;
 import android.net.Uri;
+import android.util.AttributeSet;
 import android.widget.MediaControlView2;
 import android.widget.VideoView2;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executor;
 
 /**
  * Interface for connecting the public API to an updatable implementation.
@@ -39,35 +51,49 @@
  * @hide
  */
 // TODO @SystemApi
-public interface VideoView2Provider extends ViewProvider {
-    void setMediaControlView2_impl(MediaControlView2 mediaControlView);
+public interface VideoView2Provider extends ViewGroupProvider {
+    void initialize(AttributeSet attrs, int defStyleAttr, int defStyleRes);
+
+    void setMediaControlView2_impl(MediaControlView2 mediaControlView, long intervalMs);
+    void setMediaMetadata_impl(MediaMetadata2 metadata);
+    /**
+     * @hide TODO: remove
+     */
+    MediaController getMediaController_impl();
+    SessionToken2 getMediaSessionToken_impl();
     MediaControlView2 getMediaControlView2_impl();
-    void start_impl();
-    void pause_impl();
-    int getDuration_impl();
-    int getCurrentPosition_impl();
-    void seekTo_impl(int msec);
-    boolean isPlaying_impl();
-    int getBufferPercentage_impl();
-    int getAudioSessionId_impl();
-    void showSubtitle_impl();
-    void hideSubtitle_impl();
-    void setFullScreen_impl(boolean fullScreen);
+    MediaMetadata2 getMediaMetadata_impl();
+    void setSubtitleEnabled_impl(boolean enable);
+    boolean isSubtitleEnabled_impl();
+    // TODO: remove setSpeed_impl once MediaController2 is ready.
     void setSpeed_impl(float speed);
-    float getSpeed_impl();
     void setAudioFocusRequest_impl(int focusGain);
     void setAudioAttributes_impl(AudioAttributes attributes);
-    void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerBase player);
     void setVideoPath_impl(String path);
-    void setVideoURI_impl(Uri uri);
-    void setVideoURI_impl(Uri uri, Map<String, String> headers);
+    /**
+     * @hide TODO: remove
+     */
+    void setVideoUri_impl(Uri uri);
+    /**
+     * @hide TODO: remove
+     */
+    void setVideoUri_impl(Uri uri, Map<String, String> headers);
+    void setMediaItem_impl(MediaItem2 mediaItem);
+    void setDataSource_impl(DataSourceDesc dsd);
     void setViewType_impl(int viewType);
     int getViewType_impl();
-    void stopPlayback_impl();
-    void setOnPreparedListener_impl(VideoView2.OnPreparedListener l);
-    void setOnCompletionListener_impl(VideoView2.OnCompletionListener l);
-    void setOnErrorListener_impl(VideoView2.OnErrorListener l);
-    void setOnInfoListener_impl(VideoView2.OnInfoListener l);
+    /**
+     * @hide TODO: remove
+     */
+    void setCustomActions_impl(List<PlaybackState.CustomAction> actionList,
+            Executor executor, VideoView2.OnCustomActionListener listener);
+    /**
+     * @hide
+     */
+    @VisibleForTesting
     void setOnViewTypeChangedListener_impl(VideoView2.OnViewTypeChangedListener l);
-    void setFullScreenChangedListener_impl(VideoView2.OnFullScreenChangedListener l);
+    /**
+     * @hide TODO: remove
+     */
+    void setFullScreenRequestListener_impl(VideoView2.OnFullScreenRequestListener l);
 }
diff --git a/android/media/update/ViewGroupHelper.java b/android/media/update/ViewGroupHelper.java
new file mode 100644
index 0000000..6b4f15d
--- /dev/null
+++ b/android/media/update/ViewGroupHelper.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright 2018 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.update;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Helper class for connecting the public API to an updatable implementation.
+ *
+ * @see ViewGroupProvider
+ *
+ * @hide
+ */
+public abstract class ViewGroupHelper<T extends ViewGroupProvider> extends ViewGroup {
+    /** @hide */
+    final public T mProvider;
+
+    /** @hide */
+    public ViewGroupHelper(ProviderCreator<T> creator,
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        mProvider = creator.createProvider(this, new SuperProvider(),
+                new PrivateProvider());
+    }
+
+    /** @hide */
+    // TODO @SystemApi
+    public T getProvider() {
+        return mProvider;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        mProvider.onAttachedToWindow_impl();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        mProvider.onDetachedFromWindow_impl();
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return mProvider.getAccessibilityClassName_impl();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        return mProvider.onTouchEvent_impl(ev);
+    }
+
+    @Override
+    public boolean onTrackballEvent(MotionEvent ev) {
+        return mProvider.onTrackballEvent_impl(ev);
+    }
+
+    @Override
+    public void onFinishInflate() {
+        mProvider.onFinishInflate_impl();
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        mProvider.setEnabled_impl(enabled);
+    }
+
+    @Override
+    public void onVisibilityAggregated(boolean isVisible) {
+        mProvider.onVisibilityAggregated_impl(isVisible);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        mProvider.onLayout_impl(changed, left, top, right, bottom);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        mProvider.onMeasure_impl(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    protected int getSuggestedMinimumWidth() {
+        return mProvider.getSuggestedMinimumWidth_impl();
+    }
+
+    @Override
+    protected int getSuggestedMinimumHeight() {
+        return mProvider.getSuggestedMinimumHeight_impl();
+    }
+
+    // setMeasuredDimension is final
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        return mProvider.dispatchTouchEvent_impl(ev);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(LayoutParams p) {
+        return mProvider.checkLayoutParams_impl(p);
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return mProvider.generateDefaultLayoutParams_impl();
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return mProvider.generateLayoutParams_impl(attrs);
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(LayoutParams lp) {
+        return mProvider.generateLayoutParams_impl(lp);
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return mProvider.shouldDelayChildPressedState_impl();
+    }
+
+    @Override
+    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
+            int parentHeightMeasureSpec, int heightUsed) {
+        mProvider.measureChildWithMargins_impl(child,
+                parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
+    }
+
+    /** @hide */
+    public class SuperProvider implements ViewGroupProvider {
+        @Override
+        public CharSequence getAccessibilityClassName_impl() {
+            return ViewGroupHelper.super.getAccessibilityClassName();
+        }
+
+        @Override
+        public boolean onTouchEvent_impl(MotionEvent ev) {
+            return ViewGroupHelper.super.onTouchEvent(ev);
+        }
+
+        @Override
+        public boolean onTrackballEvent_impl(MotionEvent ev) {
+            return ViewGroupHelper.super.onTrackballEvent(ev);
+        }
+
+        @Override
+        public void onFinishInflate_impl() {
+            ViewGroupHelper.super.onFinishInflate();
+        }
+
+        @Override
+        public void setEnabled_impl(boolean enabled) {
+            ViewGroupHelper.super.setEnabled(enabled);
+        }
+
+        @Override
+        public void onAttachedToWindow_impl() {
+            ViewGroupHelper.super.onAttachedToWindow();
+        }
+
+        @Override
+        public void onDetachedFromWindow_impl() {
+            ViewGroupHelper.super.onDetachedFromWindow();
+        }
+
+        @Override
+        public void onVisibilityAggregated_impl(boolean isVisible) {
+            ViewGroupHelper.super.onVisibilityAggregated(isVisible);
+        }
+
+        @Override
+        public void onLayout_impl(boolean changed, int left, int top, int right, int bottom) {
+            // abstract method; no super
+        }
+
+        @Override
+        public void onMeasure_impl(int widthMeasureSpec, int heightMeasureSpec) {
+            ViewGroupHelper.super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+
+        @Override
+        public int getSuggestedMinimumWidth_impl() {
+            return ViewGroupHelper.super.getSuggestedMinimumWidth();
+        }
+
+        @Override
+        public int getSuggestedMinimumHeight_impl() {
+            return ViewGroupHelper.super.getSuggestedMinimumHeight();
+        }
+
+        @Override
+        public void setMeasuredDimension_impl(int measuredWidth, int measuredHeight) {
+            ViewGroupHelper.super.setMeasuredDimension(measuredWidth, measuredHeight);
+        }
+
+        @Override
+        public boolean dispatchTouchEvent_impl(MotionEvent ev) {
+            return ViewGroupHelper.super.dispatchTouchEvent(ev);
+        }
+
+        @Override
+        public boolean checkLayoutParams_impl(LayoutParams p) {
+            return ViewGroupHelper.super.checkLayoutParams(p);
+        }
+
+        @Override
+        public LayoutParams generateDefaultLayoutParams_impl() {
+            return ViewGroupHelper.super.generateDefaultLayoutParams();
+        }
+
+        @Override
+        public LayoutParams generateLayoutParams_impl(AttributeSet attrs) {
+            return ViewGroupHelper.super.generateLayoutParams(attrs);
+        }
+
+        @Override
+        public LayoutParams generateLayoutParams_impl(LayoutParams lp) {
+            return ViewGroupHelper.super.generateLayoutParams(lp);
+        }
+
+        @Override
+        public boolean shouldDelayChildPressedState_impl() {
+            return ViewGroupHelper.super.shouldDelayChildPressedState();
+        }
+
+        @Override
+        public void measureChildWithMargins_impl(View child,
+                int parentWidthMeasureSpec, int widthUsed,
+                int parentHeightMeasureSpec, int heightUsed) {
+            ViewGroupHelper.super.measureChildWithMargins(child,
+                    parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
+        }
+    }
+
+    /** @hide */
+    public class PrivateProvider implements ViewGroupProvider {
+        @Override
+        public CharSequence getAccessibilityClassName_impl() {
+            return ViewGroupHelper.this.getAccessibilityClassName();
+        }
+
+        @Override
+        public boolean onTouchEvent_impl(MotionEvent ev) {
+            return ViewGroupHelper.this.onTouchEvent(ev);
+        }
+
+        @Override
+        public boolean onTrackballEvent_impl(MotionEvent ev) {
+            return ViewGroupHelper.this.onTrackballEvent(ev);
+        }
+
+        @Override
+        public void onFinishInflate_impl() {
+            ViewGroupHelper.this.onFinishInflate();
+        }
+
+        @Override
+        public void setEnabled_impl(boolean enabled) {
+            ViewGroupHelper.this.setEnabled(enabled);
+        }
+
+        @Override
+        public void onAttachedToWindow_impl() {
+            ViewGroupHelper.this.onAttachedToWindow();
+        }
+
+        @Override
+        public void onDetachedFromWindow_impl() {
+            ViewGroupHelper.this.onDetachedFromWindow();
+        }
+
+        @Override
+        public void onVisibilityAggregated_impl(boolean isVisible) {
+            ViewGroupHelper.this.onVisibilityAggregated(isVisible);
+        }
+
+        @Override
+        public void onLayout_impl(boolean changed, int left, int top, int right, int bottom) {
+            ViewGroupHelper.this.onLayout(changed, left, top, right, bottom);
+        }
+
+        @Override
+        public void onMeasure_impl(int widthMeasureSpec, int heightMeasureSpec) {
+            ViewGroupHelper.this.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+
+        @Override
+        public int getSuggestedMinimumWidth_impl() {
+            return ViewGroupHelper.this.getSuggestedMinimumWidth();
+        }
+
+        @Override
+        public int getSuggestedMinimumHeight_impl() {
+            return ViewGroupHelper.this.getSuggestedMinimumHeight();
+        }
+
+        @Override
+        public void setMeasuredDimension_impl(int measuredWidth, int measuredHeight) {
+            ViewGroupHelper.this.setMeasuredDimension(measuredWidth, measuredHeight);
+        }
+
+        @Override
+        public boolean dispatchTouchEvent_impl(MotionEvent ev) {
+            return ViewGroupHelper.this.dispatchTouchEvent(ev);
+        }
+
+        @Override
+        public boolean checkLayoutParams_impl(LayoutParams p) {
+            return ViewGroupHelper.this.checkLayoutParams(p);
+        }
+
+        @Override
+        public LayoutParams generateDefaultLayoutParams_impl() {
+            return ViewGroupHelper.this.generateDefaultLayoutParams();
+        }
+
+        @Override
+        public LayoutParams generateLayoutParams_impl(AttributeSet attrs) {
+            return ViewGroupHelper.this.generateLayoutParams(attrs);
+        }
+
+        @Override
+        public LayoutParams generateLayoutParams_impl(LayoutParams lp) {
+            return ViewGroupHelper.this.generateLayoutParams(lp);
+        }
+
+        @Override
+        public boolean shouldDelayChildPressedState_impl() {
+            return ViewGroupHelper.this.shouldDelayChildPressedState();
+        }
+
+        @Override
+        public void measureChildWithMargins_impl(View child,
+                int parentWidthMeasureSpec, int widthUsed,
+                int parentHeightMeasureSpec, int heightUsed) {
+            ViewGroupHelper.this.measureChildWithMargins(child,
+                    parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed);
+        }
+    }
+
+        /** @hide */
+    @FunctionalInterface
+    public interface ProviderCreator<T extends ViewGroupProvider> {
+        T createProvider(ViewGroupHelper<T> instance, ViewGroupProvider superProvider,
+                ViewGroupProvider privateProvider);
+    }
+}
diff --git a/android/media/update/ViewGroupProvider.java b/android/media/update/ViewGroupProvider.java
new file mode 100644
index 0000000..67e8cea
--- /dev/null
+++ b/android/media/update/ViewGroupProvider.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 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.update;
+
+import android.annotation.SystemApi;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+
+/**
+ * Interface for connecting the public API to an updatable implementation.
+ *
+ * Each instance object is connected to one corresponding updatable object which implements the
+ * runtime behavior of that class. There should a corresponding provider method for all public
+ * methods.
+ *
+ * All methods behave as per their namesake in the public API.
+ *
+ * @see android.view.View
+ *
+ * @hide
+ */
+// TODO @SystemApi
+public interface ViewGroupProvider {
+    // View methods
+    void onAttachedToWindow_impl();
+    void onDetachedFromWindow_impl();
+    CharSequence getAccessibilityClassName_impl();
+    boolean onTouchEvent_impl(MotionEvent ev);
+    boolean onTrackballEvent_impl(MotionEvent ev);
+    void onFinishInflate_impl();
+    void setEnabled_impl(boolean enabled);
+    void onVisibilityAggregated_impl(boolean isVisible);
+    void onLayout_impl(boolean changed, int left, int top, int right, int bottom);
+    void onMeasure_impl(int widthMeasureSpec, int heightMeasureSpec);
+    int getSuggestedMinimumWidth_impl();
+    int getSuggestedMinimumHeight_impl();
+    void setMeasuredDimension_impl(int measuredWidth, int measuredHeight);
+    boolean dispatchTouchEvent_impl(MotionEvent ev);
+
+    // ViewGroup methods
+    boolean checkLayoutParams_impl(LayoutParams p);
+    LayoutParams generateDefaultLayoutParams_impl();
+    LayoutParams generateLayoutParams_impl(AttributeSet attrs);
+    LayoutParams generateLayoutParams_impl(LayoutParams lp);
+    boolean shouldDelayChildPressedState_impl();
+    void measureChildWithMargins_impl(View child, int parentWidthMeasureSpec, int widthUsed,
+        int parentHeightMeasureSpec, int heightUsed);
+
+    // ViewManager methods
+    // ViewParent methods
+}
diff --git a/android/media/update/ViewProvider.java b/android/media/update/ViewProvider.java
deleted file mode 100644
index 78c5b36..0000000
--- a/android/media/update/ViewProvider.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2017 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.update;
-
-import android.annotation.SystemApi;
-import android.graphics.Canvas;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-
-/**
- * Interface for connecting the public API to an updatable implementation.
- *
- * Each instance object is connected to one corresponding updatable object which implements the
- * runtime behavior of that class. There should a corresponding provider method for all public
- * methods.
- *
- * All methods behave as per their namesake in the public API.
- *
- * @see android.view.View
- *
- * @hide
- */
-// TODO @SystemApi
-public interface ViewProvider {
-    // TODO Add more (all?) methods from View
-    void onAttachedToWindow_impl();
-    void onDetachedFromWindow_impl();
-    CharSequence getAccessibilityClassName_impl();
-    boolean onTouchEvent_impl(MotionEvent ev);
-    boolean onTrackballEvent_impl(MotionEvent ev);
-    boolean onKeyDown_impl(int keyCode, KeyEvent event);
-    void onFinishInflate_impl();
-    boolean dispatchKeyEvent_impl(KeyEvent event);
-    void setEnabled_impl(boolean enabled);
-}
diff --git a/android/media/update/VolumeProvider2Provider.java b/android/media/update/VolumeProvider2Provider.java
new file mode 100644
index 0000000..5b5cfd3
--- /dev/null
+++ b/android/media/update/VolumeProvider2Provider.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2018 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.update;
+
+/**
+ * @hide
+ */
+public interface VolumeProvider2Provider {
+    int getControlType_impl();
+    int getMaxVolume_impl();
+    int getCurrentVolume_impl();
+    void setCurrentVolume_impl(int currentVolume);
+}