/*
* Copyright 2021 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.media3.session;
import android.support.v4.media.session.MediaSessionCompat.QueueItem;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
/**
* An immutable class to represent the current {@link Timeline} backed by {@link QueueItem}.
*
* <p>This supports the fake item that represents the removed but currently playing media item. In
* that case, a fake item would be inserted at the end of the {@link MediaItem media item list}
* converted from {@link QueueItem queue item list}. Without the fake item support, the timeline
* should be always recreated to handle the case when the fake item is no longer necessary and
* timeline change isn't precisely detected. Queue item doesn't support equals(), so it's better not
* to use equals() on the converted MediaItem.
*/
/* package */ final class QueueTimeline extends Timeline {
public static final QueueTimeline DEFAULT =
new QueueTimeline(ImmutableList.of(), ImmutableMap.of(), /* fakeMediaItem= */ null);
private static final Object FAKE_WINDOW_UID = new Object();
private final ImmutableList<MediaItem> mediaItems;
private final Map<MediaItem, Long> unmodifiableMediaItemToQueueIdMap;
@Nullable private final MediaItem fakeMediaItem;
private QueueTimeline(
ImmutableList<MediaItem> mediaItems,
Map<MediaItem, Long> unmodifiableMediaItemToQueueIdMap,
@Nullable MediaItem fakeMediaItem) {
this.mediaItems = mediaItems;
this.unmodifiableMediaItemToQueueIdMap = unmodifiableMediaItemToQueueIdMap;
this.fakeMediaItem = fakeMediaItem;
}
public QueueTimeline(QueueTimeline queueTimeline) {
this.mediaItems = queueTimeline.mediaItems;
this.unmodifiableMediaItemToQueueIdMap = queueTimeline.unmodifiableMediaItemToQueueIdMap;
this.fakeMediaItem = queueTimeline.fakeMediaItem;
}
public QueueTimeline copyWithFakeMediaItem(@Nullable MediaItem fakeMediaItem) {
return new QueueTimeline(mediaItems, unmodifiableMediaItemToQueueIdMap, fakeMediaItem);
}
public QueueTimeline copyWithNewMediaItem(int replaceIndex, MediaItem newMediaItem) {
ImmutableList.Builder<MediaItem> newMediaItemsBuilder = new ImmutableList.Builder<>();
newMediaItemsBuilder.addAll(mediaItems.subList(0, replaceIndex));
newMediaItemsBuilder.add(newMediaItem);
newMediaItemsBuilder.addAll(mediaItems.subList(replaceIndex + 1, mediaItems.size()));
return new QueueTimeline(
newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem);
}
public QueueTimeline copyWithNewMediaItems(int index, List<MediaItem> newMediaItems) {
ImmutableList.Builder<MediaItem> newMediaItemsBuilder = new ImmutableList.Builder<>();
newMediaItemsBuilder.addAll(mediaItems.subList(0, index));
newMediaItemsBuilder.addAll(newMediaItems);
newMediaItemsBuilder.addAll(mediaItems.subList(index, mediaItems.size()));
return new QueueTimeline(
newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem);
}
public QueueTimeline copyWithRemovedMediaItems(int fromIndex, int toIndex) {
ImmutableList.Builder<MediaItem> newMediaItemsBuilder = new ImmutableList.Builder<>();
newMediaItemsBuilder.addAll(mediaItems.subList(0, fromIndex));
newMediaItemsBuilder.addAll(mediaItems.subList(toIndex, mediaItems.size()));
return new QueueTimeline(
newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem);
}
public QueueTimeline copyWithMovedMediaItems(int fromIndex, int toIndex, int newIndex) {
List<MediaItem> list = new ArrayList<>(mediaItems);
Util.moveItems(list, fromIndex, toIndex, newIndex);
return new QueueTimeline(
new ImmutableList.Builder<MediaItem>().addAll(list).build(),
unmodifiableMediaItemToQueueIdMap,
fakeMediaItem);
}
public static QueueTimeline create(List<QueueItem> queue) {
ImmutableList.Builder<MediaItem> mediaItemsBuilder = new ImmutableList.Builder<>();
IdentityHashMap<MediaItem, Long> mediaItemToQueueIdMap = new IdentityHashMap<>();
for (int i = 0; i < queue.size(); i++) {
QueueItem queueItem = queue.get(i);
MediaItem mediaItem = MediaUtils.convertToMediaItem(queueItem);
mediaItemsBuilder.add(mediaItem);
mediaItemToQueueIdMap.put(mediaItem, queueItem.getQueueId());
}
return new QueueTimeline(
mediaItemsBuilder.build(),
Collections.unmodifiableMap(mediaItemToQueueIdMap),
/* fakeMediaItem= */ null);
}
public long getQueueId(int mediaItemIndex) {
@Nullable MediaItem mediaItem = mediaItems.get(mediaItemIndex);
if (mediaItem == null) {
return QueueItem.UNKNOWN_ID;
}
Long queueId = unmodifiableMediaItemToQueueIdMap.get(mediaItem);
return queueId == null ? QueueItem.UNKNOWN_ID : queueId;
}
@Nullable
public MediaItem getMediaItemAt(int mediaItemIndex) {
if (mediaItemIndex >= 0 && mediaItemIndex < mediaItems.size()) {
return mediaItems.get(mediaItemIndex);
}
return (mediaItemIndex == mediaItems.size()) ? fakeMediaItem : null;
}
public int findIndexOf(MediaItem mediaItem) {
if (mediaItem == fakeMediaItem) {
return mediaItems.size();
}
int mediaItemIndex = mediaItems.indexOf(mediaItem);
return mediaItemIndex == -1 ? C.INDEX_UNSET : mediaItemIndex;
}
@Override
public int getWindowCount() {
return mediaItems.size() + ((fakeMediaItem == null) ? 0 : 1);
}
@Override
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
// TODO(b/149713425): Set duration if it's available from MediaMetadataCompat.
MediaItem mediaItem;
if (windowIndex == mediaItems.size() && fakeMediaItem != null) {
mediaItem = fakeMediaItem;
} else {
mediaItem = mediaItems.get(windowIndex);
}
return getWindow(window, mediaItem, windowIndex);
}
@Override
public int getPeriodCount() {
return getWindowCount();
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
// TODO(b/149713425): Set duration if it's available from MediaMetadataCompat.
period.set(
/* id= */ null,
/* uid= */ null,
/* windowIndex= */ periodIndex,
/* durationUs= */ C.TIME_UNSET,
/* positionInWindowUs= */ 0);
return period;
}
@Override
public int getIndexOfPeriod(Object uid) {
throw new UnsupportedOperationException();
}
@Override
public Object getUidOfPeriod(int periodIndex) {
throw new UnsupportedOperationException();
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof QueueTimeline)) {
return false;
}
QueueTimeline other = (QueueTimeline) obj;
return mediaItems == other.mediaItems
&& unmodifiableMediaItemToQueueIdMap == other.unmodifiableMediaItemToQueueIdMap
&& fakeMediaItem == other.fakeMediaItem;
}
@Override
public int hashCode() {
return Objects.hashCode(mediaItems, unmodifiableMediaItemToQueueIdMap, fakeMediaItem);
}
private static Window getWindow(Window window, MediaItem mediaItem, int windowIndex) {
window.set(
FAKE_WINDOW_UID,
mediaItem,
/* manifest= */ null,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* liveConfiguration= */ null,
/* defaultPositionUs= */ 0,
/* durationUs= */ C.TIME_UNSET,
/* firstPeriodIndex= */ windowIndex,
/* lastPeriodIndex= */ windowIndex,
/* positionInFirstPeriodUs= */ 0);
return window;
}
}