Commit 7a4cf96f by tonihei Committed by Oliver Woodman

Improve housekeeping of ConcatenatingMediaSource callbacks.

When calling releaseSource(), all pending messages will be removed. That means
that all action-on-completion callbacks which are somewhere in flight are
just dropped without being called. This change adds code to keep track of the
current state of each callback to allow all of them being called when the
source is released.

Issue:#5464
PiperOrigin-RevId: 232312528
parent cd536a73
...@@ -58,6 +58,8 @@ ...@@ -58,6 +58,8 @@
* OkHttp extension: Upgrade OkHttp dependency to 3.12.1. * OkHttp extension: Upgrade OkHttp dependency to 3.12.1.
* MP3: Wider fix for issue where streams would play twice on some Samsung * MP3: Wider fix for issue where streams would play twice on some Samsung
devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)). devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)).
* Fix issue with dropped messages when releasing a `ConcatenatingMediaSource`
([#5464](https://github.com/google/ExoPlayer/issues/5464)).
### 2.9.4 ### ### 2.9.4 ###
......
...@@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; ...@@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.EventDispatcher;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -36,9 +35,11 @@ import java.util.Arrays; ...@@ -36,9 +35,11 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
...@@ -51,12 +52,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -51,12 +52,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
private static final int MSG_REMOVE = 1; private static final int MSG_REMOVE = 1;
private static final int MSG_MOVE = 2; private static final int MSG_MOVE = 2;
private static final int MSG_SET_SHUFFLE_ORDER = 3; private static final int MSG_SET_SHUFFLE_ORDER = 3;
private static final int MSG_NOTIFY_LISTENER = 4; private static final int MSG_UPDATE_TIMELINE = 4;
private static final int MSG_ON_COMPLETION = 5; private static final int MSG_ON_COMPLETION = 5;
// Accessed on any thread. // Accessed on any thread.
@GuardedBy("this")
private final List<MediaSourceHolder> mediaSourcesPublic; private final List<MediaSourceHolder> mediaSourcesPublic;
@Nullable private Handler playbackThreadHandler;
@GuardedBy("this")
private final Set<HandlerAndRunnable> pendingOnCompletionActions;
@GuardedBy("this")
@Nullable
private Handler playbackThreadHandler;
// Accessed on the playback thread only. // Accessed on the playback thread only.
private final List<MediaSourceHolder> mediaSourceHolders; private final List<MediaSourceHolder> mediaSourceHolders;
...@@ -67,8 +75,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -67,8 +75,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
private final Timeline.Window window; private final Timeline.Window window;
private final Timeline.Period period; private final Timeline.Period period;
private boolean listenerNotificationScheduled; private boolean timelineUpdateScheduled;
private EventDispatcher<Runnable> pendingOnCompletionActions; private Set<HandlerAndRunnable> nextTimelineUpdateOnCompletionActions;
private ShuffleOrder shuffleOrder; private ShuffleOrder shuffleOrder;
private int windowCount; private int windowCount;
private int periodCount; private int periodCount;
...@@ -127,7 +135,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -127,7 +135,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
this.mediaSourceByUid = new HashMap<>(); this.mediaSourceByUid = new HashMap<>();
this.mediaSourcesPublic = new ArrayList<>(); this.mediaSourcesPublic = new ArrayList<>();
this.mediaSourceHolders = new ArrayList<>(); this.mediaSourceHolders = new ArrayList<>();
this.pendingOnCompletionActions = new EventDispatcher<>(); this.nextTimelineUpdateOnCompletionActions = new HashSet<>();
this.pendingOnCompletionActions = new HashSet<>();
this.isAtomic = isAtomic; this.isAtomic = isAtomic;
this.useLazyPreparation = useLazyPreparation; this.useLazyPreparation = useLazyPreparation;
window = new Timeline.Window(); window = new Timeline.Window();
...@@ -148,13 +157,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -148,13 +157,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* Appends a {@link MediaSource} to the playlist and executes a custom action on completion. * Appends a {@link MediaSource} to the playlist and executes a custom action on completion.
* *
* @param mediaSource The {@link MediaSource} to be added to the list. * @param mediaSource The {@link MediaSource} to be added to the list.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* source has been added to the playlist. * source has been added to the playlist.
*/ */
public final synchronized void addMediaSource( public final synchronized void addMediaSource(
MediaSource mediaSource, Handler handler, Runnable actionOnCompletion) { MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, actionOnCompletion); addMediaSource(mediaSourcesPublic.size(), mediaSource, handler, onCompletionAction);
} }
/** /**
...@@ -169,7 +178,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -169,7 +178,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
index, index,
Collections.singletonList(mediaSource), Collections.singletonList(mediaSource),
/* handler= */ null, /* handler= */ null,
/* actionOnCompletion= */ null); /* onCompletionAction= */ null);
} }
/** /**
...@@ -178,14 +187,14 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -178,14 +187,14 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* @param index The index at which the new {@link MediaSource} will be inserted. This index must * @param index The index at which the new {@link MediaSource} will be inserted. This index must
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}. * be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSource The {@link MediaSource} to be added to the list. * @param mediaSource The {@link MediaSource} to be added to the list.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* source has been added to the playlist. * source has been added to the playlist.
*/ */
public final synchronized void addMediaSource( public final synchronized void addMediaSource(
int index, MediaSource mediaSource, Handler handler, Runnable actionOnCompletion) { int index, MediaSource mediaSource, Handler handler, Runnable onCompletionAction) {
addPublicMediaSources( addPublicMediaSources(
index, Collections.singletonList(mediaSource), handler, actionOnCompletion); index, Collections.singletonList(mediaSource), handler, onCompletionAction);
} }
/** /**
...@@ -199,7 +208,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -199,7 +208,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
mediaSourcesPublic.size(), mediaSourcesPublic.size(),
mediaSources, mediaSources,
/* handler= */ null, /* handler= */ null,
/* actionOnCompletion= */ null); /* onCompletionAction= */ null);
} }
/** /**
...@@ -208,13 +217,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -208,13 +217,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* *
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection. * sources are added in the order in which they appear in this collection.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* sources have been added to the playlist. * sources have been added to the playlist.
*/ */
public final synchronized void addMediaSources( public final synchronized void addMediaSources(
Collection<MediaSource> mediaSources, Handler handler, Runnable actionOnCompletion) { Collection<MediaSource> mediaSources, Handler handler, Runnable onCompletionAction) {
addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, actionOnCompletion); addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction);
} }
/** /**
...@@ -226,7 +235,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -226,7 +235,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* sources are added in the order in which they appear in this collection. * sources are added in the order in which they appear in this collection.
*/ */
public final synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) { public final synchronized void addMediaSources(int index, Collection<MediaSource> mediaSources) {
addPublicMediaSources(index, mediaSources, /* handler= */ null, /* actionOnCompletion= */ null); addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null);
} }
/** /**
...@@ -236,16 +245,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -236,16 +245,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* be in the range of 0 &lt;= index &lt;= {@link #getSize()}. * be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media
* sources are added in the order in which they appear in this collection. * sources are added in the order in which they appear in this collection.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* sources have been added to the playlist. * sources have been added to the playlist.
*/ */
public final synchronized void addMediaSources( public final synchronized void addMediaSources(
int index, int index,
Collection<MediaSource> mediaSources, Collection<MediaSource> mediaSources,
Handler handler, Handler handler,
Runnable actionOnCompletion) { Runnable onCompletionAction) {
addPublicMediaSources(index, mediaSources, handler, actionOnCompletion); addPublicMediaSources(index, mediaSources, handler, onCompletionAction);
} }
/** /**
...@@ -261,7 +270,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -261,7 +270,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* range of 0 &lt;= index &lt; {@link #getSize()}. * range of 0 &lt;= index &lt; {@link #getSize()}.
*/ */
public final synchronized void removeMediaSource(int index) { public final synchronized void removeMediaSource(int index) {
removePublicMediaSources(index, index + 1, /* handler= */ null, /* actionOnCompletion= */ null); removePublicMediaSources(index, index + 1, /* handler= */ null, /* onCompletionAction= */ null);
} }
/** /**
...@@ -275,13 +284,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -275,13 +284,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* *
* @param index The index at which the media source will be removed. This index must be in the * @param index The index at which the media source will be removed. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}. * range of 0 &lt;= index &lt; {@link #getSize()}.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* source has been removed from the playlist. * source has been removed from the playlist.
*/ */
public final synchronized void removeMediaSource( public final synchronized void removeMediaSource(
int index, Handler handler, Runnable actionOnCompletion) { int index, Handler handler, Runnable onCompletionAction) {
removePublicMediaSources(index, index + 1, handler, actionOnCompletion); removePublicMediaSources(index, index + 1, handler, onCompletionAction);
} }
/** /**
...@@ -300,7 +309,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -300,7 +309,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
*/ */
public final synchronized void removeMediaSourceRange(int fromIndex, int toIndex) { public final synchronized void removeMediaSourceRange(int fromIndex, int toIndex) {
removePublicMediaSources( removePublicMediaSources(
fromIndex, toIndex, /* handler= */ null, /* actionOnCompletion= */ null); fromIndex, toIndex, /* handler= */ null, /* onCompletionAction= */ null);
} }
/** /**
...@@ -314,15 +323,15 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -314,15 +323,15 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}. * removed. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param toIndex The final range index, pointing to the first media source that will be left * @param toIndex The final range index, pointing to the first media source that will be left
* untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}. * untouched. This index must be in the range of 0 &lt;= index &lt;= {@link #getSize()}.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* source range has been removed from the playlist. * source range has been removed from the playlist.
* @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0, * @throws IllegalArgumentException When the range is malformed, i.e. {@code fromIndex} &lt; 0,
* {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex} * {@code toIndex} &gt; {@link #getSize()}, {@code fromIndex} &gt; {@code toIndex}
*/ */
public final synchronized void removeMediaSourceRange( public final synchronized void removeMediaSourceRange(
int fromIndex, int toIndex, Handler handler, Runnable actionOnCompletion) { int fromIndex, int toIndex, Handler handler, Runnable onCompletionAction) {
removePublicMediaSources(fromIndex, toIndex, handler, actionOnCompletion); removePublicMediaSources(fromIndex, toIndex, handler, onCompletionAction);
} }
/** /**
...@@ -335,7 +344,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -335,7 +344,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
*/ */
public final synchronized void moveMediaSource(int currentIndex, int newIndex) { public final synchronized void moveMediaSource(int currentIndex, int newIndex) {
movePublicMediaSource( movePublicMediaSource(
currentIndex, newIndex, /* handler= */ null, /* actionOnCompletion= */ null); currentIndex, newIndex, /* handler= */ null, /* onCompletionAction= */ null);
} }
/** /**
...@@ -346,13 +355,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -346,13 +355,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* in the range of 0 &lt;= index &lt; {@link #getSize()}. * in the range of 0 &lt;= index &lt; {@link #getSize()}.
* @param newIndex The target index of the media source in the playlist. This index must be in the * @param newIndex The target index of the media source in the playlist. This index must be in the
* range of 0 &lt;= index &lt; {@link #getSize()}. * range of 0 &lt;= index &lt; {@link #getSize()}.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the media * @param onCompletionAction A {@link Runnable} which is executed immediately after the media
* source has been moved. * source has been moved.
*/ */
public final synchronized void moveMediaSource( public final synchronized void moveMediaSource(
int currentIndex, int newIndex, Handler handler, Runnable actionOnCompletion) { int currentIndex, int newIndex, Handler handler, Runnable onCompletionAction) {
movePublicMediaSource(currentIndex, newIndex, handler, actionOnCompletion); movePublicMediaSource(currentIndex, newIndex, handler, onCompletionAction);
} }
/** Clears the playlist. */ /** Clears the playlist. */
...@@ -363,12 +372,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -363,12 +372,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
/** /**
* Clears the playlist and executes a custom action on completion. * Clears the playlist and executes a custom action on completion.
* *
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the playlist * @param onCompletionAction A {@link Runnable} which is executed immediately after the playlist
* has been cleared. * has been cleared.
*/ */
public final synchronized void clear(Handler handler, Runnable actionOnCompletion) { public final synchronized void clear(Handler handler, Runnable onCompletionAction) {
removeMediaSourceRange(0, getSize(), handler, actionOnCompletion); removeMediaSourceRange(0, getSize(), handler, onCompletionAction);
} }
/** Returns the number of media sources in the playlist. */ /** Returns the number of media sources in the playlist. */
...@@ -392,20 +401,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -392,20 +401,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
* @param shuffleOrder A {@link ShuffleOrder}. * @param shuffleOrder A {@link ShuffleOrder}.
*/ */
public final synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) { public final synchronized void setShuffleOrder(ShuffleOrder shuffleOrder) {
setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* actionOnCompletion= */ null); setPublicShuffleOrder(shuffleOrder, /* handler= */ null, /* onCompletionAction= */ null);
} }
/** /**
* Sets a new shuffle order to use when shuffling the child media sources. * Sets a new shuffle order to use when shuffling the child media sources.
* *
* @param shuffleOrder A {@link ShuffleOrder}. * @param shuffleOrder A {@link ShuffleOrder}.
* @param handler The {@link Handler} to run {@code actionOnCompletion}. * @param handler The {@link Handler} to run {@code onCompletionAction}.
* @param actionOnCompletion A {@link Runnable} which is executed immediately after the shuffle * @param onCompletionAction A {@link Runnable} which is executed immediately after the shuffle
* order has been changed. * order has been changed.
*/ */
public final synchronized void setShuffleOrder( public final synchronized void setShuffleOrder(
ShuffleOrder shuffleOrder, Handler handler, Runnable actionOnCompletion) { ShuffleOrder shuffleOrder, Handler handler, Runnable onCompletionAction) {
setPublicShuffleOrder(shuffleOrder, handler, actionOnCompletion); setPublicShuffleOrder(shuffleOrder, handler, onCompletionAction);
} }
// CompositeMediaSource implementation. // CompositeMediaSource implementation.
...@@ -422,11 +431,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -422,11 +431,11 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
super.prepareSourceInternal(mediaTransferListener); super.prepareSourceInternal(mediaTransferListener);
playbackThreadHandler = new Handler(/* callback= */ this::handleMessage); playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
if (mediaSourcesPublic.isEmpty()) { if (mediaSourcesPublic.isEmpty()) {
notifyListener(); updateTimelineAndScheduleOnCompletionActions();
} else { } else {
shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size());
addMediaSourcesInternal(0, mediaSourcesPublic); addMediaSourcesInternal(0, mediaSourcesPublic);
scheduleListenerNotification(); scheduleTimelineUpdate();
} }
} }
...@@ -482,6 +491,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -482,6 +491,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
playbackThreadHandler.removeCallbacksAndMessages(null); playbackThreadHandler.removeCallbacksAndMessages(null);
playbackThreadHandler = null; playbackThreadHandler = null;
} }
timelineUpdateScheduled = false;
nextTimelineUpdateOnCompletionActions.clear();
dispatchOnCompletionActions(pendingOnCompletionActions);
} }
@Override @Override
...@@ -521,8 +533,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -521,8 +533,9 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
int index, int index,
Collection<MediaSource> mediaSources, Collection<MediaSource> mediaSources,
@Nullable Handler handler, @Nullable Handler handler,
@Nullable Runnable actionOnCompletion) { @Nullable Runnable onCompletionAction) {
Assertions.checkArgument((handler == null) == (actionOnCompletion == null)); Assertions.checkArgument((handler == null) == (onCompletionAction == null));
Handler playbackThreadHandler = this.playbackThreadHandler;
for (MediaSource mediaSource : mediaSources) { for (MediaSource mediaSource : mediaSources) {
Assertions.checkNotNull(mediaSource); Assertions.checkNotNull(mediaSource);
} }
...@@ -532,12 +545,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -532,12 +545,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
mediaSourcesPublic.addAll(index, mediaSourceHolders); mediaSourcesPublic.addAll(index, mediaSourceHolders);
if (playbackThreadHandler != null && !mediaSources.isEmpty()) { if (playbackThreadHandler != null && !mediaSources.isEmpty()) {
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
playbackThreadHandler playbackThreadHandler
.obtainMessage( .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction))
MSG_ADD, new MessageData<>(index, mediaSourceHolders, handler, actionOnCompletion))
.sendToTarget(); .sendToTarget();
} else if (actionOnCompletion != null && handler != null) { } else if (onCompletionAction != null && handler != null) {
handler.post(actionOnCompletion); handler.post(onCompletionAction);
} }
} }
...@@ -546,16 +559,17 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -546,16 +559,17 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
int fromIndex, int fromIndex,
int toIndex, int toIndex,
@Nullable Handler handler, @Nullable Handler handler,
@Nullable Runnable actionOnCompletion) { @Nullable Runnable onCompletionAction) {
Assertions.checkArgument((handler == null) == (actionOnCompletion == null)); Assertions.checkArgument((handler == null) == (onCompletionAction == null));
Handler playbackThreadHandler = this.playbackThreadHandler;
Util.removeRange(mediaSourcesPublic, fromIndex, toIndex); Util.removeRange(mediaSourcesPublic, fromIndex, toIndex);
if (playbackThreadHandler != null) { if (playbackThreadHandler != null) {
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
playbackThreadHandler playbackThreadHandler
.obtainMessage( .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction))
MSG_REMOVE, new MessageData<>(fromIndex, toIndex, handler, actionOnCompletion))
.sendToTarget(); .sendToTarget();
} else if (actionOnCompletion != null && handler != null) { } else if (onCompletionAction != null && handler != null) {
handler.post(actionOnCompletion); handler.post(onCompletionAction);
} }
} }
...@@ -564,23 +578,24 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -564,23 +578,24 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
int currentIndex, int currentIndex,
int newIndex, int newIndex,
@Nullable Handler handler, @Nullable Handler handler,
@Nullable Runnable actionOnCompletion) { @Nullable Runnable onCompletionAction) {
Assertions.checkArgument((handler == null) == (actionOnCompletion == null)); Assertions.checkArgument((handler == null) == (onCompletionAction == null));
Handler playbackThreadHandler = this.playbackThreadHandler;
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
if (playbackThreadHandler != null) { if (playbackThreadHandler != null) {
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
playbackThreadHandler playbackThreadHandler
.obtainMessage( .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction))
MSG_MOVE, new MessageData<>(currentIndex, newIndex, handler, actionOnCompletion))
.sendToTarget(); .sendToTarget();
} else if (actionOnCompletion != null && handler != null) { } else if (onCompletionAction != null && handler != null) {
handler.post(actionOnCompletion); handler.post(onCompletionAction);
} }
} }
@GuardedBy("this") @GuardedBy("this")
private void setPublicShuffleOrder( private void setPublicShuffleOrder(
ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable actionOnCompletion) { ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) {
Assertions.checkArgument((handler == null) == (actionOnCompletion == null)); Assertions.checkArgument((handler == null) == (onCompletionAction == null));
Handler playbackThreadHandler = this.playbackThreadHandler; Handler playbackThreadHandler = this.playbackThreadHandler;
if (playbackThreadHandler != null) { if (playbackThreadHandler != null) {
int size = getSize(); int size = getSize();
...@@ -590,20 +605,33 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -590,20 +605,33 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
.cloneAndClear() .cloneAndClear()
.cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size); .cloneAndInsert(/* insertionIndex= */ 0, /* insertionCount= */ size);
} }
HandlerAndRunnable callbackAction = createOnCompletionAction(handler, onCompletionAction);
playbackThreadHandler playbackThreadHandler
.obtainMessage( .obtainMessage(
MSG_SET_SHUFFLE_ORDER, MSG_SET_SHUFFLE_ORDER,
new MessageData<>(/* index= */ 0, shuffleOrder, handler, actionOnCompletion)) new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction))
.sendToTarget(); .sendToTarget();
} else { } else {
this.shuffleOrder = this.shuffleOrder =
shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder;
if (actionOnCompletion != null && handler != null) { if (onCompletionAction != null && handler != null) {
handler.post(actionOnCompletion); handler.post(onCompletionAction);
} }
} }
} }
@GuardedBy("this")
@Nullable
private HandlerAndRunnable createOnCompletionAction(
@Nullable Handler handler, @Nullable Runnable runnable) {
if (handler == null || runnable == null) {
return null;
}
HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable);
pendingOnCompletionActions.add(handlerAndRunnable);
return handlerAndRunnable;
}
// Internal methods. Called on the playback thread. // Internal methods. Called on the playback thread.
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
...@@ -614,7 +642,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -614,7 +642,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
(MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj); (MessageData<Collection<MediaSourceHolder>>) Util.castNonNull(msg.obj);
shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size()); shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size());
addMediaSourcesInternal(addMessage.index, addMessage.customData); addMediaSourcesInternal(addMessage.index, addMessage.customData);
scheduleListenerNotification(addMessage.handler, addMessage.actionOnCompletion); scheduleTimelineUpdate(addMessage.onCompletionAction);
break; break;
case MSG_REMOVE: case MSG_REMOVE:
MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj); MessageData<Integer> removeMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
...@@ -628,29 +656,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -628,29 +656,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
for (int index = toIndex - 1; index >= fromIndex; index--) { for (int index = toIndex - 1; index >= fromIndex; index--) {
removeMediaSourceInternal(index); removeMediaSourceInternal(index);
} }
scheduleListenerNotification(removeMessage.handler, removeMessage.actionOnCompletion); scheduleTimelineUpdate(removeMessage.onCompletionAction);
break; break;
case MSG_MOVE: case MSG_MOVE:
MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj); MessageData<Integer> moveMessage = (MessageData<Integer>) Util.castNonNull(msg.obj);
shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1); shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1);
shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1);
moveMediaSourceInternal(moveMessage.index, moveMessage.customData); moveMediaSourceInternal(moveMessage.index, moveMessage.customData);
scheduleListenerNotification(moveMessage.handler, moveMessage.actionOnCompletion); scheduleTimelineUpdate(moveMessage.onCompletionAction);
break; break;
case MSG_SET_SHUFFLE_ORDER: case MSG_SET_SHUFFLE_ORDER:
MessageData<ShuffleOrder> shuffleOrderMessage = MessageData<ShuffleOrder> shuffleOrderMessage =
(MessageData<ShuffleOrder>) Util.castNonNull(msg.obj); (MessageData<ShuffleOrder>) Util.castNonNull(msg.obj);
shuffleOrder = shuffleOrderMessage.customData; shuffleOrder = shuffleOrderMessage.customData;
scheduleListenerNotification( scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction);
shuffleOrderMessage.handler, shuffleOrderMessage.actionOnCompletion);
break; break;
case MSG_NOTIFY_LISTENER: case MSG_UPDATE_TIMELINE:
notifyListener(); updateTimelineAndScheduleOnCompletionActions();
break; break;
case MSG_ON_COMPLETION: case MSG_ON_COMPLETION:
EventDispatcher<Runnable> actionsOnCompletion = Set<HandlerAndRunnable> actions = (Set<HandlerAndRunnable>) Util.castNonNull(msg.obj);
(EventDispatcher<Runnable>) Util.castNonNull(msg.obj); dispatchOnCompletionActions(actions);
actionsOnCompletion.dispatch(Runnable::run);
break; break;
default: default:
throw new IllegalStateException(); throw new IllegalStateException();
...@@ -658,36 +684,48 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -658,36 +684,48 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
return true; return true;
} }
private void scheduleListenerNotification() { private void scheduleTimelineUpdate() {
scheduleListenerNotification(/* handler= */ null, /* actionOnCompletion= */ null); scheduleTimelineUpdate(/* onCompletionAction= */ null);
} }
private void scheduleListenerNotification( private void scheduleTimelineUpdate(@Nullable HandlerAndRunnable onCompletionAction) {
@Nullable Handler handler, @Nullable Runnable actionOnCompletion) { if (!timelineUpdateScheduled) {
if (!listenerNotificationScheduled) { getPlaybackThreadHandlerOnPlaybackThread().obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget();
Assertions.checkNotNull(playbackThreadHandler) timelineUpdateScheduled = true;
.obtainMessage(MSG_NOTIFY_LISTENER)
.sendToTarget();
listenerNotificationScheduled = true;
} }
if (actionOnCompletion != null && handler != null) { if (onCompletionAction != null) {
pendingOnCompletionActions.addListener(handler, actionOnCompletion); nextTimelineUpdateOnCompletionActions.add(onCompletionAction);
} }
} }
private void notifyListener() { private void updateTimelineAndScheduleOnCompletionActions() {
listenerNotificationScheduled = false; timelineUpdateScheduled = false;
EventDispatcher<Runnable> actionsOnCompletion = pendingOnCompletionActions; Set<HandlerAndRunnable> onCompletionActions = nextTimelineUpdateOnCompletionActions;
pendingOnCompletionActions = new EventDispatcher<>(); nextTimelineUpdateOnCompletionActions = new HashSet<>();
refreshSourceInfo( refreshSourceInfo(
new ConcatenatedTimeline( new ConcatenatedTimeline(
mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
/* manifest= */ null); /* manifest= */ null);
Assertions.checkNotNull(playbackThreadHandler) getPlaybackThreadHandlerOnPlaybackThread()
.obtainMessage(MSG_ON_COMPLETION, actionsOnCompletion) .obtainMessage(MSG_ON_COMPLETION, onCompletionActions)
.sendToTarget(); .sendToTarget();
} }
@SuppressWarnings("GuardedBy")
private Handler getPlaybackThreadHandlerOnPlaybackThread() {
// Write access to this value happens on the playback thread only, so playback thread reads
// don't need to be synchronized.
return Assertions.checkNotNull(playbackThreadHandler);
}
private synchronized void dispatchOnCompletionActions(
Set<HandlerAndRunnable> onCompletionActions) {
for (HandlerAndRunnable pendingAction : onCompletionActions) {
pendingAction.dispatch();
}
pendingOnCompletionActions.removeAll(onCompletionActions);
}
private void addMediaSourcesInternal( private void addMediaSourcesInternal(
int index, Collection<MediaSourceHolder> mediaSourceHolders) { int index, Collection<MediaSourceHolder> mediaSourceHolders) {
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
...@@ -784,7 +822,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -784,7 +822,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
} }
} }
mediaSourceHolder.isPrepared = true; mediaSourceHolder.isPrepared = true;
scheduleListenerNotification(); scheduleTimelineUpdate();
} }
private void removeMediaSourceInternal(int index) { private void removeMediaSourceInternal(int index) {
...@@ -897,15 +935,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -897,15 +935,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
public final int index; public final int index;
public final T customData; public final T customData;
@Nullable public final Handler handler; @Nullable public final HandlerAndRunnable onCompletionAction;
@Nullable public final Runnable actionOnCompletion;
public MessageData( public MessageData(int index, T customData, @Nullable HandlerAndRunnable onCompletionAction) {
int index, T customData, @Nullable Handler handler, @Nullable Runnable actionOnCompletion) {
this.index = index; this.index = index;
this.customData = customData; this.customData = customData;
this.handler = handler; this.onCompletionAction = onCompletionAction;
this.actionOnCompletion = actionOnCompletion;
} }
} }
...@@ -1155,5 +1190,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo ...@@ -1155,5 +1190,20 @@ public class ConcatenatingMediaSource extends CompositeMediaSource<MediaSourceHo
// Do nothing. // Do nothing.
} }
} }
private static final class HandlerAndRunnable {
private final Handler handler;
private final Runnable runnable;
public HandlerAndRunnable(Handler handler, Runnable runnable) {
this.handler = handler;
this.runnable = runnable;
}
public void dispatch() {
handler.post(runnable);
}
}
} }
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.os.ConditionVariable; import android.os.ConditionVariable;
...@@ -25,6 +26,7 @@ import com.google.android.exoplayer2.C; ...@@ -25,6 +26,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSource.SourceInfoRefreshListener;
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeMediaSource;
...@@ -42,7 +44,6 @@ import org.junit.After; ...@@ -42,7 +44,6 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
...@@ -416,7 +417,7 @@ public final class ConcatenatingMediaSourceTest { ...@@ -416,7 +417,7 @@ public final class ConcatenatingMediaSourceTest {
@Test @Test
public void testCustomCallbackBeforePreparationAddSingle() { public void testCustomCallbackBeforePreparationAddSingle() {
Runnable runnable = Mockito.mock(Runnable.class); Runnable runnable = mock(Runnable.class);
mediaSource.addMediaSource(createFakeMediaSource(), new Handler(), runnable); mediaSource.addMediaSource(createFakeMediaSource(), new Handler(), runnable);
verify(runnable).run(); verify(runnable).run();
...@@ -424,7 +425,7 @@ public final class ConcatenatingMediaSourceTest { ...@@ -424,7 +425,7 @@ public final class ConcatenatingMediaSourceTest {
@Test @Test
public void testCustomCallbackBeforePreparationAddMultiple() { public void testCustomCallbackBeforePreparationAddMultiple() {
Runnable runnable = Mockito.mock(Runnable.class); Runnable runnable = mock(Runnable.class);
mediaSource.addMediaSources( mediaSource.addMediaSources(
Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}),
...@@ -435,7 +436,7 @@ public final class ConcatenatingMediaSourceTest { ...@@ -435,7 +436,7 @@ public final class ConcatenatingMediaSourceTest {
@Test @Test
public void testCustomCallbackBeforePreparationAddSingleWithIndex() { public void testCustomCallbackBeforePreparationAddSingleWithIndex() {
Runnable runnable = Mockito.mock(Runnable.class); Runnable runnable = mock(Runnable.class);
mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), new Handler(), runnable); mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), new Handler(), runnable);
verify(runnable).run(); verify(runnable).run();
...@@ -443,7 +444,7 @@ public final class ConcatenatingMediaSourceTest { ...@@ -443,7 +444,7 @@ public final class ConcatenatingMediaSourceTest {
@Test @Test
public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { public void testCustomCallbackBeforePreparationAddMultipleWithIndex() {
Runnable runnable = Mockito.mock(Runnable.class); Runnable runnable = mock(Runnable.class);
mediaSource.addMediaSources( mediaSource.addMediaSources(
/* index */ 0, /* index */ 0,
...@@ -455,7 +456,7 @@ public final class ConcatenatingMediaSourceTest { ...@@ -455,7 +456,7 @@ public final class ConcatenatingMediaSourceTest {
@Test @Test
public void testCustomCallbackBeforePreparationRemove() { public void testCustomCallbackBeforePreparationRemove() {
Runnable runnable = Mockito.mock(Runnable.class); Runnable runnable = mock(Runnable.class);
mediaSource.addMediaSource(createFakeMediaSource()); mediaSource.addMediaSource(createFakeMediaSource());
mediaSource.removeMediaSource(/* index */ 0, new Handler(), runnable); mediaSource.removeMediaSource(/* index */ 0, new Handler(), runnable);
...@@ -464,7 +465,7 @@ public final class ConcatenatingMediaSourceTest { ...@@ -464,7 +465,7 @@ public final class ConcatenatingMediaSourceTest {
@Test @Test
public void testCustomCallbackBeforePreparationMove() { public void testCustomCallbackBeforePreparationMove() {
Runnable runnable = Mockito.mock(Runnable.class); Runnable runnable = mock(Runnable.class);
mediaSource.addMediaSources( mediaSource.addMediaSources(
Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}));
...@@ -589,6 +590,29 @@ public final class ConcatenatingMediaSourceTest { ...@@ -589,6 +590,29 @@ public final class ConcatenatingMediaSourceTest {
} }
@Test @Test
public void testCustomCallbackIsCalledAfterRelease() throws IOException {
DummyMainThread dummyMainThread = new DummyMainThread();
ConditionVariable callbackCalledCondition = new ConditionVariable();
try {
dummyMainThread.runOnMainThread(
() -> {
SourceInfoRefreshListener listener = mock(SourceInfoRefreshListener.class);
mediaSource.addMediaSources(Arrays.asList(createMediaSources(2)));
mediaSource.prepareSource(listener, /* mediaTransferListener= */ null);
mediaSource.moveMediaSource(
/* currentIndex= */ 0,
/* newIndex= */ 1,
new Handler(),
callbackCalledCondition::open);
mediaSource.releaseSource(listener);
});
assertThat(callbackCalledCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue();
} finally {
dummyMainThread.release();
}
}
@Test
public void testPeriodCreationWithAds() throws IOException, InterruptedException { public void testPeriodCreationWithAds() throws IOException, InterruptedException {
// Create concatenated media source with ad child source. // Create concatenated media source with ad child source.
Timeline timelineContentOnly = Timeline timelineContentOnly =
...@@ -973,7 +997,7 @@ public final class ConcatenatingMediaSourceTest { ...@@ -973,7 +997,7 @@ public final class ConcatenatingMediaSourceTest {
@Test @Test
public void testCustomCallbackBeforePreparationSetShuffleOrder() throws Exception { public void testCustomCallbackBeforePreparationSetShuffleOrder() throws Exception {
Runnable runnable = Mockito.mock(Runnable.class); Runnable runnable = mock(Runnable.class);
mediaSource.setShuffleOrder( mediaSource.setShuffleOrder(
new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 0), new Handler(), runnable); new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 0), new Handler(), runnable);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment