From 728e13b7b9f19ef8a4b578f3078bd40f235ac914 Mon Sep 17 00:00:00 2001 From: Sungsoo Lim Date: Tue, 18 Dec 2018 11:58:08 +0900 Subject: [PATCH] Add MediaItem2 Bug: 121216661 Test: build Change-Id: I7dfd36bbc1feeeb0a80641940579b3d6187bd2f2 --- media/java/android/media/MediaItem2.java | 324 +++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 media/java/android/media/MediaItem2.java 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. + * + *

+ * 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); + } +}