[go: nahoru, domu]

blob: c445435e7b6958b23be7c89620b0a93fa3f085be [file] [log] [blame]
/*
* Copyright 2019 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 androidx.media2.common;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Pair;
import androidx.versionedparcelable.CustomVersionedParcelable;
import androidx.versionedparcelable.NonParcelField;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelize;
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.
*
* <ul>
* <li>Specify media items to {@link SessionPlayer} for playback.
* <li>Share media items across the processes.
* </ul>
*
* <p>Subclasses of the session player may only accept certain subclasses of the media items. Check
* the player documentation that you're interested in.
*
* <p>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.
*
* <p>When an object of the {@link MediaItem}'s subclass is sent across the process between {@link
* androidx.media2.session.MediaSession}/{@link androidx.media2.session.MediaController} or {@link
* androidx.media2.session.MediaLibraryService.MediaLibrarySession}/ {@link
* androidx.media2.session.MediaBrowser}, the object will sent as if it's {@link MediaItem}. The
* recipient cannot get the object with the subclasses' type. This will sanitize process specific
* information (e.g. {@link java.io.FileDescriptor}, {@link android.content.Context}, etc).
*
* <p>This object is thread safe.
*
* @deprecated androidx.media2 is deprecated. Please migrate to <a
* href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
*/
@Deprecated
@VersionedParcelize(isCustom = true)
public class MediaItem extends CustomVersionedParcelable {
private static final String TAG = "MediaItem";
// 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;
@NonParcelField
private final Object mLock = new Object();
@GuardedBy("mLock")
@ParcelField(1)
MediaMetadata mMetadata;
@ParcelField(2)
long mStartPositionMs = 0;
@ParcelField(3)
long mEndPositionMs = POSITION_UNKNOWN;
// WARNING: Adding a new ParcelField may break old library users (b/152830728)
@GuardedBy("mLock")
@NonParcelField
private final List<Pair<OnMetadataChangedListener, Executor>> mListeners = new ArrayList<>();
/**
* Used for VersionedParcelable
*/
MediaItem() {
}
/**
* Used by {@link MediaItem.Builder} and its subclasses
*/
// Note: Needs to be protected when we want to allow 3rd party player to define customized
// MediaItem.
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaItem(Builder builder) {
this(builder.mMetadata, builder.mStartPositionMs, builder.mEndPositionMs);
}
MediaItem(MediaItem item) {
this(item.mMetadata, item.mStartPositionMs, item.mEndPositionMs);
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaItem(@Nullable MediaMetadata metadata, long startPositionMs, long endPositionMs) {
if (startPositionMs > endPositionMs) {
throw new IllegalStateException("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 != SessionPlayer.UNKNOWN_TIME && endPositionMs != POSITION_UNKNOWN
&& endPositionMs > durationMs) {
throw new IllegalStateException("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("{Media Id=").append(getMediaId());
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 == metadata) {
return;
}
if (mMetadata != null && metadata != null
&& !TextUtils.equals(getMediaId(), metadata.getMediaId())) {
Log.w(TAG, "MediaItem'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(MediaItem.this, metadata);
}
});
}
}
/**
* Gets the metadata of the media.
*
* @return metadata from the session
*/
@Nullable
public 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;
}
/**
* 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
*/
@RestrictTo(LIBRARY_GROUP)
@Nullable
public String getMediaId() {
synchronized (mLock) {
return mMetadata != null
? mMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) : null;
}
}
/**
*/
@RestrictTo(LIBRARY_GROUP)
public 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));
}
}
/**
*/
@RestrictTo(LIBRARY_GROUP)
public 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 MediaItem}.
*
* @deprecated androidx.media2 is deprecated. Please migrate to <a
* href="https://developer.android.com/guide/topics/media/media3">androidx.media3</a>.
*/
@Deprecated
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;
/**
* Default constructor
*/
public Builder() {
}
/**
* Set the metadata of this instance. {@code null} for unset.
*
* @param metadata metadata
* @return this instance for chaining
*/
@NonNull
public 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 this instance for chaining
*/
@NonNull
public 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 this instance for chaining
*/
@NonNull
public Builder setEndPosition(long position) {
if (position < 0) {
position = POSITION_UNKNOWN;
}
mEndPositionMs = position;
return this;
}
/**
* Build {@link MediaItem}.
*
* @return a new {@link MediaItem}.
*/
@NonNull
public MediaItem build() {
return new MediaItem(this);
}
}
/**
*/
@RestrictTo(LIBRARY_GROUP)
public interface OnMetadataChangedListener {
/**
* Called when a media item's metadata is changed.
*/
void onMetadataChanged(@NonNull MediaItem item,
@Nullable MediaMetadata metadata);
}
/**
*/
@RestrictTo(LIBRARY)
@Override
public void onPreParceling(boolean isStream) {
if (getClass() != MediaItem.class) {
throw new RuntimeException("MediaItem's subclasses shouldn't be parcelized.");
}
super.onPreParceling(isStream);
}
}