diff --git a/media/java/android/media/MediaItem2.java b/media/java/android/media/MediaItem2.java
new file mode 100644
index 0000000000000..aa2a937956c5a
--- /dev/null
+++ b/media/java/android/media/MediaItem2.java
@@ -0,0 +1,324 @@
+/*
+ * 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 android.media.MediaMetadata.METADATA_KEY_MEDIA_ID;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * A class with information on a single media item with the metadata information. Here are use
+ * cases.
+ *
+ * - Specify media items to {@link SessionPlayer2} for playback.
+ *
- Share media items across the processes.
+ *
+ *
+ * Subclasses of the session player may only accept certain subclasses of the media items. Check
+ * the player documentation that you're interested in.
+ *
+ * When it's shared across the processes, we cannot guarantee that they contain the right values
+ * because media items are application dependent especially for the metadata.
+ *
+ * This object is thread safe.
+ *
+ * This API is not generally intended for third party application developers.
+ * Use the Support Library
+ * {@link androidx.media2.MediaItem} for consistent behavior across all devices.
+ *
+ * @hide
+ */
+public class MediaItem2 implements Parcelable {
+ private static final String TAG = "MediaItem2";
+
+ // intentionally less than long.MAX_VALUE.
+ // Declare this first to avoid 'illegal forward reference'.
+ static final long LONG_MAX = 0x7ffffffffffffffL;
+
+ /**
+ * Used when a position is unknown.
+ *
+ * @see #getEndPosition()
+ */
+ public static final long POSITION_UNKNOWN = LONG_MAX;
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ @Override
+ public MediaItem2 createFromParcel(Parcel in) {
+ return new MediaItem2(in);
+ }
+
+ @Override
+ public MediaItem2[] newArray(int size) {
+ return new MediaItem2[size];
+ }
+ };
+
+ // TODO: Use SessionPlayer2.UNKNOWN_TIME instead
+ private static final long UNKNOWN_TIME = -1;
+
+ private final long mStartPositionMs;
+ private final long mEndPositionMs;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private MediaMetadata mMetadata;
+ @GuardedBy("mLock")
+ private final List> mListeners = new ArrayList<>();
+
+ /**
+ * Used by {@link MediaItem2.Builder}.
+ */
+ // Note: Needs to be protected when we want to allow 3rd party player to define customized
+ // MediaItem2.
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaItem2(Builder builder) {
+ this(builder.mMetadata, builder.mStartPositionMs, builder.mEndPositionMs);
+ }
+
+ /**
+ * Used by Parcelable.Creator.
+ */
+ // Note: Needs to be protected when we want to allow 3rd party player to define customized
+ // MediaItem2.
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaItem2(Parcel in) {
+ this(in.readParcelable(MediaItem2.class.getClassLoader()), in.readLong(), in.readLong());
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaItem2(MediaItem2 item) {
+ this(item.mMetadata, item.mStartPositionMs, item.mEndPositionMs);
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaItem2(@Nullable MediaMetadata metadata, long startPositionMs, long endPositionMs) {
+ if (startPositionMs > endPositionMs) {
+ throw new IllegalArgumentException("Illegal start/end position: "
+ + startPositionMs + " : " + endPositionMs);
+ }
+ if (metadata != null && metadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
+ long durationMs = metadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
+ if (durationMs != UNKNOWN_TIME && endPositionMs != POSITION_UNKNOWN
+ && endPositionMs > durationMs) {
+ throw new IllegalArgumentException("endPositionMs shouldn't be greater than"
+ + " duration in the metdata, endPositionMs=" + endPositionMs
+ + ", durationMs=" + durationMs);
+ }
+ }
+ mMetadata = metadata;
+ mStartPositionMs = startPositionMs;
+ mEndPositionMs = endPositionMs;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder(getClass().getSimpleName());
+ synchronized (mLock) {
+ sb.append("{mMetadata=").append(mMetadata);
+ sb.append(", mStartPositionMs=").append(mStartPositionMs);
+ sb.append(", mEndPositionMs=").append(mEndPositionMs);
+ sb.append('}');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Sets metadata. If the metadata is not {@code null}, its id should be matched with this
+ * instance's media id.
+ *
+ * @param metadata metadata to update
+ * @see MediaMetadata#METADATA_KEY_MEDIA_ID
+ */
+ public void setMetadata(@Nullable MediaMetadata metadata) {
+ List> listeners = new ArrayList<>();
+ synchronized (mLock) {
+ if (mMetadata != null && metadata != null
+ && !TextUtils.equals(getMediaId(), metadata.getString(METADATA_KEY_MEDIA_ID))) {
+ Log.d(TAG, "MediaItem2's media ID shouldn't be changed");
+ return;
+ }
+ mMetadata = metadata;
+ listeners.addAll(mListeners);
+ }
+
+ for (Pair pair : listeners) {
+ final OnMetadataChangedListener listener = pair.first;
+ pair.second.execute(new Runnable() {
+ @Override
+ public void run() {
+ listener.onMetadataChanged(MediaItem2.this);
+ }
+ });
+ }
+ }
+
+ /**
+ * Gets the metadata of the media.
+ *
+ * @return metadata from the session
+ */
+ public @Nullable MediaMetadata getMetadata() {
+ synchronized (mLock) {
+ return mMetadata;
+ }
+ }
+
+ /**
+ * Return the position in milliseconds at which the playback will start.
+ * @return the position in milliseconds at which the playback will start
+ */
+ public long getStartPosition() {
+ return mStartPositionMs;
+ }
+
+ /**
+ * Return the position in milliseconds at which the playback will end.
+ * {@link #POSITION_UNKNOWN} means ending at the end of source content.
+ * @return the position in milliseconds at which the playback will end
+ */
+ public long getEndPosition() {
+ return mEndPositionMs;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mMetadata, 0);
+ dest.writeLong(mStartPositionMs);
+ dest.writeLong(mEndPositionMs);
+ }
+
+ /**
+ * Gets the media id for this item. If it's not {@code null}, it's a persistent unique key
+ * for the underlying media content.
+ *
+ * @return media Id from the session
+ */
+ @Nullable String getMediaId() {
+ synchronized (mLock) {
+ return mMetadata != null
+ ? mMetadata.getString(METADATA_KEY_MEDIA_ID) : null;
+ }
+ }
+
+ void addOnMetadataChangedListener(Executor executor, OnMetadataChangedListener listener) {
+ synchronized (mLock) {
+ for (Pair pair : mListeners) {
+ if (pair.first == listener) {
+ return;
+ }
+ }
+ mListeners.add(new Pair<>(listener, executor));
+ }
+ }
+
+ void removeOnMetadataChangedListener(OnMetadataChangedListener listener) {
+ synchronized (mLock) {
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ if (mListeners.get(i).first == listener) {
+ mListeners.remove(i);
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Builder for {@link MediaItem2}.
+ */
+ public static class Builder {
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ MediaMetadata mMetadata;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ long mStartPositionMs = 0;
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ long mEndPositionMs = POSITION_UNKNOWN;
+
+ /**
+ * Set the metadata of this instance. {@code null} for unset.
+ *
+ * @param metadata metadata
+ * @return this instance for chaining
+ */
+ public @NonNull Builder setMetadata(@Nullable MediaMetadata metadata) {
+ mMetadata = metadata;
+ return this;
+ }
+
+ /**
+ * Sets the start position in milliseconds at which the playback will start.
+ * Any negative number is treated as 0.
+ *
+ * @param position the start position in milliseconds at which the playback will start
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setStartPosition(long position) {
+ if (position < 0) {
+ position = 0;
+ }
+ mStartPositionMs = position;
+ return this;
+ }
+
+ /**
+ * Sets the end position in milliseconds at which the playback will end.
+ * Any negative number is treated as maximum length of the media item.
+ *
+ * @param position the end position in milliseconds at which the playback will end
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setEndPosition(long position) {
+ if (position < 0) {
+ position = POSITION_UNKNOWN;
+ }
+ mEndPositionMs = position;
+ return this;
+ }
+
+ /**
+ * Build {@link MediaItem2}.
+ *
+ * @return a new {@link MediaItem2}.
+ */
+ public @NonNull MediaItem2 build() {
+ return new MediaItem2(this);
+ }
+ }
+
+ interface OnMetadataChangedListener {
+ void onMetadataChanged(MediaItem2 item);
+ }
+}