blob: ff0d43e41350e204f30dca446adc0f12dae0785a [file] [log] [blame]
/*
* 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.
* <p>
* This API is not generally intended for third party application developers.
* Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
* <a href="{@docRoot}reference/androidx/media2/package-summary.html">Media2 Library</a>
* for consistent behavior across all devices.
* <p>
*/
public final 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 @android.annotation.NonNull Parcelable.Creator<MediaItem2> CREATOR =
new Parcelable.Creator<MediaItem2>() {
@Override
public MediaItem2 createFromParcel(Parcel in) {
return new MediaItem2(in);
}
@Override
public MediaItem2[] newArray(int size) {
return new MediaItem2[size];
}
};
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<Pair<OnMetadataChangedListener, Executor>> 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<Pair<OnMetadataChangedListener, Executor>> 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<OnMetadataChangedListener, Executor> 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<OnMetadataChangedListener, Executor> 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 final 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);
}
}