
org.robolectric.shadows.ShadowMediaPlayer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of framework Show documentation
Show all versions of framework Show documentation
An alternative Android testing framework.
The newest version!
package org.robolectric.shadows;
import java.io.FileDescriptor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.HashMap;
import java.util.TreeMap;
import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.util.DataSource;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.shadows.ShadowMediaPlayer.State.*;
import static org.robolectric.shadows.util.DataSource.toDataSource;
/**
* Automated testing of media playback can be a difficult thing - especially
* testing that your code properly handles asynchronous errors and events. This
* near impossible task is made quite straightforward using this implementation
* of {@link MediaPlayer} with Robolectric.
*
* This shadow implementation provides much of the functionality needed to
* emulate {@link MediaPlayer} initialization and playback behavior without having
* to play actual media files. A summary of the features included are:
*
* * Construction-time callback hook {@link CreateListener} so that
* newly-created {@link MediaPlayer} instances can have their shadows configured
* before they are used.
* * Emulation of the {@link android.media.MediaPlayer.OnCompletionListener
* OnCompletionListener}, {@link android.media.MediaPlayer.OnErrorListener
* OnErrorListener}, {@link android.media.MediaPlayer.OnInfoListener
* OnInfoListener}, {@link android.media.MediaPlayer.OnPreparedListener
* OnPreparedListener} and
* {@link android.media.MediaPlayer.OnSeekCompleteListener
* OnSeekCompleteListener}.
* * Full support of the {@link MediaPlayer} internal states and their
* transition map.
* * Configure time parameters such as playback duration, preparation delay
* and (@link #setSeekDelay seek delay}.
* * Emulation of asynchronous callback events during playback through
* Robolectric's scheduling system using the {@link MediaInfo} inner class.
* * Emulation of error behavior when methods are called from invalid states,
* or to throw assertions when methods are invoked in invalid states (using
* {@link #setInvalidStateBehavior}).
* * Emulation of different playback behaviors based on the current data
* source, as passed in to {@link #setDataSource(String) setDataSource()}, using
* {@link #addMediaInfo}.
* * Emulation of exceptions when calling {@link #setDataSource} using
* {@link #addException}.
*
* Note: One gotcha with this shadow is that you need to either configure an
* exception or a {@link ShadowMediaPlayer.MediaInfo} instance for that data source
* (using {@link #addException(DataSource, IOException)} or
* {@link #addMediaInfo(DataSource, MediaInfo)} respectively) before
* calling {@link #setDataSource}, otherwise you'll get an
* {@link IllegalArgumentException}.
*
* The current features of {@code ShadowMediaPlayer} were focused on development
* for testing playback of audio tracks. Thus support for emulating timed text and
* video events is incomplete. None of these features would be particularly onerous
* to add/fix - contributions welcome, of course!
*
* @author Fr Jeremy Krieg, Holy Monastery of St Nectarios, Adelaide, Australia
*/
@Implements(MediaPlayer.class)
public class ShadowMediaPlayer extends ShadowPlayerBase {
public static void __staticInitializer__() {
// don't bind the JNI library
}
/**
* Listener that is called when a new MediaPlayer is constructed.
*
* @see #setCreateListener(CreateListener)
*/
protected static CreateListener createListener;
private static final Map exceptions = new HashMap<>();
private static final Map mediaInfo = new HashMap<>();
@RealObject
private MediaPlayer player;
/**
* Possible states for the media player to be in. These states are as defined
* in the documentation for {@link android.media.MediaPlayer}.
*/
public enum State {
IDLE, INITIALIZED, PREPARING, PREPARED, STARTED, STOPPED, PAUSED, PLAYBACK_COMPLETED, END, ERROR
}
/**
* Possible behavior modes for the media player when a method is invoked in an
* invalid state.
*
* @see #setInvalidStateBehavior
*/
public enum InvalidStateBehavior {
SILENT, EMULATE, ASSERT
}
/**
* Reference to the next playback event scheduled to run. We keep a reference
* to this handy in case we need to cancel it.
*/
private RunList nextPlaybackEvent;
/**
* Class for grouping events that are meant to fire at the same time. Also
* schedules the next event to run.
*/
@SuppressWarnings("serial")
private static class RunList extends ArrayList implements MediaEvent {
public RunList() {
// Set the default size to one as most of the time we will
// only have one event.
super(1);
}
@Override
public void run(MediaPlayer mp, ShadowMediaPlayer smp) {
for (MediaEvent e : this) {
e.run(mp, smp);
}
}
}
public interface MediaEvent {
public void run(MediaPlayer mp, ShadowMediaPlayer smp);
}
/**
* Class specifying information for an emulated media object. Used by
* ShadowMediaPlayer when setDataSource() is called to populate the shadow
* player with the specified values.
*/
public static class MediaInfo {
public int duration;
private int preparationDelay;
/** Map that maps time offsets to media events. */
public TreeMap events = new TreeMap<>();
/**
* Creates a new {@code MediaInfo} object with default duration (1000ms)
* and default preparation delay (0ms).
*/
public MediaInfo() {
this(1000, 0);
}
/**
* Creates a new {@code MediaInfo} object with the given duration and
* preparation delay. A completion callback event is scheduled at
* {@code duration} ms from the end.
*
* @param duration
* the duration (in ms) of this emulated media. A callback event
* will be scheduled at this offset to stop playback simulation and
* invoke the completion callback.
* @param preparationDelay
* the preparation delay (in ms) to emulate for this media. If set
* to -1, then {@link #prepare()} will complete instantly but
* {@link #prepareAsync()} will not complete automatically; you
* will need to call {@link #invokePreparedListener()} manually.
*/
public MediaInfo(int duration, int preparationDelay) {
this.duration = duration;
this.preparationDelay = preparationDelay;
scheduleEventAtOffset(duration, completionCallback);
}
/**
* Retrieves the current preparation delay for this media.
*
* @return The current preparation delay (in ms).
*/
public int getPreparationDelay() {
return preparationDelay;
}
/**
* Sets the current preparation delay for this media.
*
* @param preparationDelay
* the new preparation delay (in ms).
*/
public void setPreparationDelay(int preparationDelay) {
this.preparationDelay = preparationDelay;
}
/**
* Schedules a generic event to run at the specified playback offset. Events
* are run on the thread on which the {@link android.media.MediaPlayer
* MediaPlayer} was created.
*
* @param offset
* the offset from the start of playback at which this event will
* run.
* @param event
* the event to run.
*/
public void scheduleEventAtOffset(int offset, MediaEvent event) {
RunList runList = events.get(offset);
if (runList == null) {
// Given that most run lists will only contain one event,
// we use 1 as the default capacity.
runList = new RunList();
events.put(offset, runList);
}
runList.add(event);
}
/**
* Schedules an error event to run at the specified playback offset. A
* reference to the actual MediaEvent that is scheduled is returned, which can
* be used in a subsequent call to {@link #removeEventAtOffset}.
*
* @param offset
* the offset from the start of playback at which this error will
* trigger.
* @param what
* the value for the {@code what} parameter to use in the call to
* {@link android.media.MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
* onError()}.
* @param extra
* the value for the {@code extra} parameter to use in the call to
* {@link android.media.MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
* onError()}.
* @return A reference to the MediaEvent object that was created and scheduled.
*/
public MediaEvent scheduleErrorAtOffset(int offset, int what, int extra) {
ErrorCallback callback = new ErrorCallback(what, extra);
scheduleEventAtOffset(offset, callback);
return callback;
}
/**
* Schedules an info event to run at the specified playback offset. A
* reference to the actual MediaEvent that is scheduled is returned, which can
* be used in a subsequent call to {@link #removeEventAtOffset}.
*
* @param offset
* the offset from the start of playback at which this event will
* trigger.
* @param what
* the value for the {@code what} parameter to use in the call to
* {@link android.media.MediaPlayer.OnInfoListener#onInfo(MediaPlayer, int, int)
* onInfo()}.
* @param extra
* the value for the {@code extra} parameter to use in the call to
* {@link android.media.MediaPlayer.OnInfoListener#onInfo(MediaPlayer, int, int)
* onInfo()}.
* @return A reference to the MediaEvent object that was created and scheduled.
*/
public MediaEvent scheduleInfoAtOffset(int offset, final int what,
final int extra) {
MediaEvent callback = new MediaEvent() {
@Override
public void run(MediaPlayer mp, ShadowMediaPlayer smp) {
smp.invokeInfoListener(what, extra);
}
};
scheduleEventAtOffset(offset, callback);
return callback;
}
/**
* Schedules a simulated buffer underrun event to run at the specified
* playback offset. A reference to the actual MediaEvent that is scheduled is
* returned, which can be used in a subsequent call to
* {@link #removeEventAtOffset}.
*
* This event will issue an {@link MediaPlayer.OnInfoListener#onInfo
* onInfo()} callback with {@link MediaPlayer#MEDIA_INFO_BUFFERING_START} to
* signal the start of buffering and then call {@link #doStop()} to
* internally pause playback. Finally it will schedule an event to fire
* after {@code length} ms which fires a
* {@link MediaPlayer#MEDIA_INFO_BUFFERING_END} info event and invokes
* {@link #doStart()} to resume playback.
*
* @param offset
* the offset from the start of playback at which this underrun
* will trigger.
* @param length
* the length of time (in ms) for which playback will be paused.
* @return A reference to the MediaEvent object that was created and scheduled.
*/
public MediaEvent scheduleBufferUnderrunAtOffset(int offset, final int length) {
final MediaEvent restart = new MediaEvent() {
@Override
public void run(MediaPlayer mp, ShadowMediaPlayer smp) {
smp.invokeInfoListener(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0);
smp.doStart();
}
};
MediaEvent callback = new MediaEvent() {
@Override
public void run(MediaPlayer mp, ShadowMediaPlayer smp) {
smp.doStop();
smp.invokeInfoListener(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0);
smp.postEventDelayed(restart, length);
}
};
scheduleEventAtOffset(offset, callback);
return callback;
}
/**
* Removes the specified event from the playback schedule at the given
* playback offset.
*
* @param offset
* the offset at which the event was scheduled.
* @param event
* the event to remove.
* @see ShadowMediaPlayer.MediaInfo#removeEvent(ShadowMediaPlayer.MediaEvent)
*/
public void removeEventAtOffset(int offset, MediaEvent event) {
RunList runList = events.get(offset);
if (runList != null) {
runList.remove(event);
if (runList.isEmpty()) {
events.remove(offset);
}
}
}
/**
* Removes the specified event from the playback schedule at all playback
* offsets where it has been scheduled.
*
* @param event
* the event to remove.
* @see ShadowMediaPlayer.MediaInfo#removeEventAtOffset(int,ShadowMediaPlayer.MediaEvent)
*/
public void removeEvent(MediaEvent event) {
for (Iterator> iter = events.entrySet()
.iterator(); iter.hasNext();) {
Entry entry = iter.next();
RunList runList = entry.getValue();
runList.remove(event);
if (runList.isEmpty()) {
iter.remove();
}
}
}
}
public void postEvent(MediaEvent e) {
Message msg = handler.obtainMessage(MEDIA_EVENT, e);
handler.sendMessage(msg);
}
public void postEventDelayed(MediaEvent e, long delay) {
Message msg = handler.obtainMessage(MEDIA_EVENT, e);
handler.sendMessageDelayed(msg, delay);
}
/**
* Callback interface for clients that wish to be informed when a new
* {@link MediaPlayer} instance is constructed.
*
* @see #setCreateListener
*/
public static interface CreateListener {
/**
* Method that is invoked when a new {@link MediaPlayer} is created. This
* method is invoked at the end of the constructor, after all of the default
* setup has been completed.
*
* @param player
* reference to the newly-created media player object.
* @param shadow
* reference to the corresponding shadow object for the
* newly-created media player (provided for convenience).
*/
public void onCreate(MediaPlayer player, ShadowMediaPlayer shadow);
}
/** Current state of the media player. */
private State state = IDLE;
/** Delay for calls to {@link #seekTo} (in ms). */
private int seekDelay = 0;
private int auxEffect;
private int audioSessionId;
private int audioStreamType;
private boolean looping;
private int pendingSeek = -1;
/** Various source variables from setDataSource() */
private Uri sourceUri;
private int sourceResId;
private DataSource dataSource;
/** The time (in ms) at which playback was last started/resumed. */
private long startTime = -1;
/**
* The offset (in ms) from the start of the current clip at which the last
* call to seek/pause was. If the MediaPlayer is not in the STARTED state,
* then this is equal to currentPosition; if it is in the STARTED state and no
* seek is pending then you need to add the number of ms since start() was
* called to get the current position (see {@link #startTime}).
*/
private int startOffset = 0;
private int videoHeight;
private int videoWidth;
private float leftVolume;
private float rightVolume;
private MediaPlayer.OnCompletionListener completionListener;
private MediaPlayer.OnSeekCompleteListener seekCompleteListener;
private MediaPlayer.OnPreparedListener preparedListener;
private MediaPlayer.OnInfoListener infoListener;
private MediaPlayer.OnErrorListener errorListener;
/**
* Flag indicating how the shadow media player should behave when a method is
* invoked in an invalid state.
*/
private InvalidStateBehavior invalidStateBehavior = InvalidStateBehavior.SILENT;
private Handler handler;
private static final MediaEvent completionCallback = new MediaEvent() {
@Override
public void run(MediaPlayer mp, ShadowMediaPlayer smp) {
if (mp.isLooping()) {
smp.startOffset = 0;
smp.doStart();
} else {
smp.doStop();
smp.invokeCompletionListener();
}
}
};
private static final MediaEvent preparedCallback = new MediaEvent() {
@Override
public void run(MediaPlayer mp, ShadowMediaPlayer smp) {
smp.invokePreparedListener();
}
};
private static final MediaEvent seekCompleteCallback = new MediaEvent() {
@Override
public void run(MediaPlayer mp, ShadowMediaPlayer smp) {
smp.invokeSeekCompleteListener();
}
};
/**
* Callback to use when a method is invoked from an invalid state. Has
* {@code what = -38} and {@code extra = 0}, which are values that
* were determined by inspection.
*/
private static final ErrorCallback invalidStateErrorCallback = new ErrorCallback(
-38, 0);
public static final int MEDIA_EVENT = 1;
/** Callback to use for scheduled errors. */
private static class ErrorCallback implements MediaEvent {
private int what;
private int extra;
public ErrorCallback(int what, int extra) {
this.what = what;
this.extra = extra;
}
@Override
public void run(MediaPlayer mp, ShadowMediaPlayer smp) {
smp.invokeErrorListener(what, extra);
}
}
@Implementation
public static MediaPlayer create(Context context, int resId) {
MediaPlayer mp = new MediaPlayer();
ShadowMediaPlayer shadow = shadowOf(mp);
shadow.sourceResId = resId;
try {
shadow.setState(INITIALIZED);
mp.prepare();
} catch (Exception e) {
return null;
}
return mp;
}
@Implementation
public static MediaPlayer create(Context context, Uri uri) {
MediaPlayer mp = new MediaPlayer();
try {
mp.setDataSource(context, uri);
mp.prepare();
} catch (Exception e) {
return null;
}
return mp;
}
public void __constructor__() {
// Contract of audioSessionId is that if it is 0 (which represents
// the master mix) then that's an error. By default it generates
// an ID that is unique system-wide. We could simulate guaranteed
// uniqueness (get AudioManager's help?) but it's probably not
// worth the effort - a random non-zero number will probably do.
Random random = new Random();
audioSessionId = random.nextInt(Integer.MAX_VALUE) + 1;
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MEDIA_EVENT:
MediaEvent e = (MediaEvent)msg.obj;
e.run(player, ShadowMediaPlayer.this);
scheduleNextPlaybackEvent();
break;
}
}
};
// This gives test suites a chance to customize the MP instance
// and its shadow when it is created, without having to modify
// the code under test in order to do so.
if (createListener != null) {
createListener.onCreate(player, this);
}
// Ensure that the real object is set up properly.
Shadow.invokeConstructor(MediaPlayer.class, player);
}
/**
* Common code path for all {@code setDataSource()} implementations.
*
* * Checks for any specified exceptions for the specified data source and throws them.
* * Checks the current state and throws an exception if it is in an invalid state.
* * If no exception is thrown in either of the previous two steps, then {@link #doSetDataSource(DataSource)} is called to set the data source.
* * Sets the player state to {@code INITIALIZED}.
*
* Usually this method would not be called directly, but indirectly through one of the
* other {@link #setDataSource(String)} implementations, which use {@link DataSource#toDataSource(String)}
* methods to convert their discrete parameters into a single {@link DataSource} instance.
*
* @param dataSource the data source that is being set.
* @throws IOException if the specified data source has been configured to throw an IO exception.
* @see #addException(DataSource, IOException)
* @see #addException(DataSource, RuntimeException)
* @see #doSetDataSource(DataSource)
*/
public void setDataSource(DataSource dataSource) throws IOException {
Exception e = exceptions.get(dataSource);
if (e != null) {
e.fillInStackTrace();
if (e instanceof IOException) {
throw (IOException)e;
} else if (e instanceof RuntimeException) {
throw (RuntimeException)e;
}
throw new AssertionError("Invalid exception type for setDataSource: <" + e + '>');
}
checkStateException("setDataSource()", idleState);
doSetDataSource(dataSource);
state = INITIALIZED;
}
/**
* Sets the data source without doing any other emulation. Sets the
* internal data source only.
* Calling directly can be useful for setting up a {@link ShadowMediaPlayer}
* instance during specific testing so that you don't have to clutter your
* tests catching exceptions you know won't be thrown.
*
* @param dataSource the data source that is being set.
* @see #setDataSource(DataSource)
*/
public void doSetDataSource(DataSource dataSource) {
if (mediaInfo.get(dataSource) == null) {
throw new IllegalArgumentException("Don't know what to do with dataSource " + dataSource +
" - either add an exception with addException() or media info with addMediaInfo()");
}
this.dataSource = dataSource;
}
@Implementation
public void setDataSource(String path) throws IOException {
setDataSource(toDataSource(path));
}
@Implementation
public void setDataSource(Context context, Uri uri, Map headers) throws IOException {
setDataSource(toDataSource(context, uri, headers));
sourceUri = uri;
}
@Implementation
public void setDataSource(String uri, Map headers) throws IOException {
setDataSource(toDataSource(uri, headers));
}
@Implementation
public void setDataSource(FileDescriptor fd, long offset, long length) throws IOException {
setDataSource(toDataSource(fd, offset, length));
}
public static MediaInfo getMediaInfo(DataSource dataSource) {
return mediaInfo.get(dataSource);
}
public static void addMediaInfo(DataSource dataSource, MediaInfo info) {
mediaInfo.put(dataSource, info);
}
public static void addException(DataSource dataSource, RuntimeException e) {
exceptions.put(dataSource, e);
}
public static void addException(DataSource dataSource, IOException e) {
exceptions.put(dataSource, e);
}
/**
* Checks states for methods that only log when there is an error. Such
* methods throw an {@link IllegalArgumentException} when invoked in the END
* state, but log an error in other disallowed states. This method will either
* emulate this behavior or else will generate an assertion if invoked from a
* disallowed state if {@link #setAssertOnError assertOnError} is set.
*
* @param method
* the name of the method being tested.
* @param allowedStates
* the states that this method is allowed to be called from.
* @see #setAssertOnError
* @see #checkStateError(String, EnumSet)
* @see #checkStateException(String, EnumSet)
*/
private void checkStateLog(String method, EnumSet allowedStates) {
switch (invalidStateBehavior) {
case SILENT:
break;
case EMULATE:
if (state == END) {
String msg = "Can't call " + method + " from state " + state;
throw new IllegalStateException(msg);
}
break;
case ASSERT:
if (!allowedStates.contains(state) || state == END) {
String msg = "Can't call " + method + " from state " + state;
throw new AssertionError(msg);
}
}
}
/**
* Checks states for methods that asynchronously invoke
* {@link android.media.MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)
* onError()} when invoked in an illegal state. Such methods always throw
* {@link IllegalStateException} rather than invoke {@code onError()} if
* they are invoked from the END state.
*
* This method will either emulate this behavior by posting an
* {@code onError()} callback to the current thread's message queue (or
* throw an {@link IllegalStateException} if invoked from the END state), or
* else it will generate an assertion if {@link #setAssertOnError
* assertOnError} is set.
*
* @param method
* the name of the method being tested.
* @param allowedStates
* the states that this method is allowed to be called from.
* @see #getHandler
* @see #setAssertOnError
* @see #checkStateLog(String, EnumSet)
* @see #checkStateException(String, EnumSet)
*/
private boolean checkStateError(String method, EnumSet allowedStates) {
if (!allowedStates.contains(state)) {
switch (invalidStateBehavior) {
case SILENT:
break;
case EMULATE:
if (state == END) {
String msg = "Can't call " + method + " from state " + state;
throw new IllegalStateException(msg);
}
state = ERROR;
postEvent(invalidStateErrorCallback);
return false;
case ASSERT:
String msg = "Can't call " + method + " from state " + state;
throw new AssertionError(msg);
}
}
return true;
}
/**
* Checks states for methods that synchronously throw an exception when
* invoked in an illegal state. This method will likewise throw an
* {@link IllegalArgumentException} if it determines that the method has been
* invoked from a disallowed state, or else it will generate an assertion if
* {@link #setAssertOnError assertOnError} is set.
*
* @param method
* the name of the method being tested.
* @param allowedStates
* the states that this method is allowed to be called from.
* @see #setAssertOnError
* @see #checkStateLog(String, EnumSet)
* @see #checkStateError(String, EnumSet)
*/
private void checkStateException(String method, EnumSet allowedStates) {
if (!allowedStates.contains(state)) {
String msg = "Can't call " + method + " from state " + state;
switch (invalidStateBehavior) {
case SILENT:
break;
case EMULATE:
throw new IllegalStateException(msg);
case ASSERT:
throw new AssertionError(msg);
}
}
}
@Implementation
public void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) {
completionListener = listener;
}
@Implementation
public void setOnSeekCompleteListener(
MediaPlayer.OnSeekCompleteListener listener) {
seekCompleteListener = listener;
}
@Implementation
public void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) {
preparedListener = listener;
}
@Implementation
public void setOnInfoListener(MediaPlayer.OnInfoListener listener) {
infoListener = listener;
}
@Implementation
public void setOnErrorListener(MediaPlayer.OnErrorListener listener) {
errorListener = listener;
}
@Implementation
public boolean isLooping() {
checkStateException("isLooping()", nonEndStates);
return looping;
}
static private final EnumSet nonEndStates = EnumSet
.complementOf(EnumSet.of(END));
static private final EnumSet nonErrorStates = EnumSet
.complementOf(EnumSet.of(ERROR, END));
@Implementation
public void setLooping(boolean looping) {
checkStateError("setLooping()", nonErrorStates);
this.looping = looping;
}
@Implementation
public void setVolume(float left, float right) {
checkStateError("setVolume()", nonErrorStates);
leftVolume = left;
rightVolume = right;
}
@Implementation
public boolean isPlaying() {
checkStateError("isPlaying()", nonErrorStates);
return state == STARTED;
}
private static EnumSet preparableStates = EnumSet.of(INITIALIZED,
STOPPED);
/**
* Simulates {@link MediaPlayer#prepareAsync()}. Sleeps for
* {@link MediaInfo#getPreparationDelay() preparationDelay} ms by calling
* {@link SystemClock#sleep(long)} before calling
* {@link #invokePreparedListener()}.
*
* If {@code preparationDelay} is not positive and non-zero, there is no
* sleep.
*
* @see MediaInfo#setPreparationDelay(int)
* @see #invokePreparedListener()
*/
@Implementation
public void prepare() {
checkStateException("prepare()", preparableStates);
MediaInfo info = getMediaInfo();
if (info.preparationDelay > 0) {
SystemClock.sleep(info.preparationDelay);
}
invokePreparedListener();
}
/**
* Simulates {@link MediaPlayer#prepareAsync()}. Sets state to PREPARING and
* posts a callback to {@link #invokePreparedListener()} if the current
* preparation delay for the current media (see {@link #getMediaInfo()}) is >=
* 0, otherwise the test suite is responsible for calling
* {@link #invokePreparedListener()} directly if required.
*
* @see MediaInfo#setPreparationDelay(int)
* @see #invokePreparedListener()
*/
@Implementation
public void prepareAsync() {
checkStateException("prepareAsync()", preparableStates);
state = PREPARING;
MediaInfo info = getMediaInfo();
if (info.preparationDelay >= 0) {
postEventDelayed(preparedCallback, info.preparationDelay);
}
}
private static EnumSet startableStates = EnumSet.of(PREPARED, STARTED,
PAUSED, PLAYBACK_COMPLETED);
/**
* Simulates private native method {@link MediaPlayer#_start()}. Sets state to STARTED and calls
* {@link #doStart()} to start scheduling playback callback events.
*
* If the current state is PLAYBACK_COMPLETED, the current position is reset
* to zero before starting playback.
*
* @see #doStart()
*/
@Implementation
public void start() {
if (checkStateError("start()", startableStates)) {
if (state == PLAYBACK_COMPLETED) {
startOffset = 0;
}
state = STARTED;
doStart();
}
}
private void scheduleNextPlaybackEvent() {
if (!isReallyPlaying()) {
return;
}
final int currentPosition = getCurrentPositionRaw();
MediaInfo info = getMediaInfo();
Entry event = info.events.higherEntry(currentPosition);
if (event == null) {
// This means we've "seeked" past the end. Get the last
// event (which should be the completion event) and
// invoke that, setting the position to the duration.
postEvent(completionCallback);
} else {
final int runListOffset = event.getKey();
nextPlaybackEvent = event.getValue();
postEventDelayed(nextPlaybackEvent, runListOffset - currentPosition);
}
}
/**
* Tests to see if the player is really playing.
*
* The player is defined as "really playing" if simulated playback events
* (including playback completion) are being scheduled and invoked and
* {@link #getCurrentPosition currentPosition} is being updated as time
* passes. Note that while the player will normally be really playing if in
* the STARTED state, this is not always the case - for example, if a pending
* seek is in progress, or perhaps a buffer underrun is being simulated.
*
* @return {@code true} if the player is really playing or
* {@code false} if the player is internally paused.
* @see #doStart
* @see #doStop
*/
public boolean isReallyPlaying() {
return startTime >= 0;
}
/**
* Starts simulated playback. Until this method is called, the player is not
* "really playing" (see {@link #isReallyPlaying} for a definition of
* "really playing").
*
* This method is used internally by the various shadow method implementations
* of the MediaPlayer public API, but may also be called directly by the test
* suite if you wish to simulate an internal pause. For example, to simulate
* a buffer underrun (player is in PLAYING state but isn't actually advancing
* the current position through the media), you could call {@link #doStop()} to
* mark the start of the buffer underrun and {@link #doStart()} to mark its
* end and restart normal playback (which is what
* {@link ShadowMediaPlayer.MediaInfo#scheduleBufferUnderrunAtOffset(int, int) scheduleBufferUnderrunAtOffset()}
* does).
*
* @see #isReallyPlaying()
* @see #doStop()
*/
public void doStart() {
startTime = SystemClock.uptimeMillis();
scheduleNextPlaybackEvent();
}
/**
* Pauses simulated playback. After this method is called, the player is no
* longer "really playing" (see {@link #isReallyPlaying} for a definition of
* "really playing").
*
* This method is used internally by the various shadow method implementations
* of the MediaPlayer public API, but may also be called directly by the test
* suite if you wish to simulate an internal pause.
*
* @see #isReallyPlaying()
* @see #doStart()
*/
public void doStop() {
startOffset = getCurrentPositionRaw();
if (nextPlaybackEvent != null) {
handler.removeMessages(MEDIA_EVENT);
nextPlaybackEvent = null;
}
startTime = -1;
}
private static final EnumSet pausableStates = EnumSet.of(STARTED,
PAUSED, PLAYBACK_COMPLETED);
/**
* Simulates {@link MediaPlayer#_pause()}. Invokes {@link #doStop()} to suspend
* playback event callbacks and sets the state to PAUSED.
*
* @see #doStop()
*/
@Implementation
public void _pause() {
if (checkStateError("pause()", pausableStates)) {
doStop();
state = PAUSED;
}
}
static final EnumSet allStates = EnumSet.allOf(State.class);
/**
* Simulates call to {@link MediaPlayer#_release()}. Calls {@link #doStop()} to
* suspend playback event callbacks and sets the state to END.
*/
@Implementation
public void _release() {
checkStateException("release()", allStates);
doStop();
state = END;
handler.removeMessages(MEDIA_EVENT);
}
/**
* Simulates call to {@link MediaPlayer#_reset()}. Calls {@link #doStop()} to
* suspend playback event callbacks and sets the state to IDLE.
*/
@Implementation
public void _reset() {
checkStateException("reset()", nonEndStates);
doStop();
state = IDLE;
handler.removeMessages(MEDIA_EVENT);
startOffset = 0;
}
static private final EnumSet stoppableStates = EnumSet.of(PREPARED,
STARTED, PAUSED, STOPPED, PLAYBACK_COMPLETED);
/**
* Simulates call to {@link MediaPlayer#release()}. Calls {@link #doStop()} to
* suspend playback event callbacks and sets the state to STOPPED.
*/
@Implementation
public void _stop() {
if (checkStateError("stop()", stoppableStates)) {
doStop();
state = STOPPED;
}
}
private static final EnumSet attachableStates = EnumSet.of(
INITIALIZED, PREPARING, PREPARED, STARTED, PAUSED, STOPPED,
PLAYBACK_COMPLETED);
@Implementation
public void attachAuxEffect(int effectId) {
checkStateError("attachAuxEffect()", attachableStates);
auxEffect = effectId;
}
@Implementation
public int getAudioSessionId() {
checkStateException("getAudioSessionId()", allStates);
return audioSessionId;
}
/**
* Simulates call to {@link MediaPlayer#getCurrentPosition()}. Simply does the
* state validity checks and then invokes {@link #getCurrentPositionRaw()} to
* calculate the simulated playback position.
*
* @return The current offset (in ms) of the simulated playback.
* @see #getCurrentPositionRaw()
*/
@Implementation
public int getCurrentPosition() {
checkStateError("getCurrentPosition()", attachableStates);
return getCurrentPositionRaw();
}
/**
* Simulates call to {@link MediaPlayer#getDuration()}. Retrieves the duration
* as defined by the current {@link MediaInfo} instance.
*
* @return The duration (in ms) of the current simulated playback.
* @see addMediaInfo
*/
@Implementation
public int getDuration() {
checkStateError("getDuration()", stoppableStates);
return getMediaInfo().duration;
}
@Implementation
public int getVideoHeight() {
checkStateLog("getVideoHeight()", attachableStates);
return videoHeight;
}
@Implementation
public int getVideoWidth() {
checkStateLog("getVideoWidth()", attachableStates);
return videoWidth;
}
private static final EnumSet seekableStates = EnumSet.of(PREPARED,
STARTED, PAUSED, PLAYBACK_COMPLETED);
/**
* Simulates seeking to specified position. The seek will complete after
* {@link #seekDelay} ms (defaults to 0), or else if seekDelay is negative
* then the controlling test is expected to simulate seek completion by
* manually invoking {@link #invokeSeekCompleteListener}.
*
* @param seekTo
* the offset (in ms) from the start of the track to seek to.
*/
@Implementation
public void seekTo(int seekTo) {
boolean success = checkStateError("seekTo()", seekableStates);
// Cancel any pending seek operations.
handler.removeMessages(MEDIA_EVENT, seekCompleteCallback);
if (success) {
// Need to call doStop() before setting pendingSeek,
// because if pendingSeek is called it changes
// the behavior of getCurrentPosition(), which doStop()
// depends on.
doStop();
pendingSeek = seekTo;
if (seekDelay >= 0) {
postEventDelayed(seekCompleteCallback, seekDelay);
}
}
}
static private final EnumSet idleState = EnumSet.of(IDLE);
@Implementation
public void setAudioSessionId(int sessionId) {
checkStateError("setAudioSessionId()", idleState);
audioSessionId = sessionId;
}
static private final EnumSet nonPlayingStates = EnumSet.of(IDLE,
INITIALIZED, STOPPED);
@Implementation
public void setAudioStreamType(int audioStreamType) {
checkStateError("setAudioStreamType()", nonPlayingStates);
this.audioStreamType = audioStreamType;
}
/**
* Sets a listener that is invoked whenever a new shadowed {@link MediaPlayer}
* object is constructed.
*
* Registering a listener gives you a chance to
* customize the shadowed object appropriately without needing to modify the
* application-under-test to provide access to the instance at the appropriate
* point in its life cycle. This is useful because normally a new
* {@link MediaPlayer} is created and {@link #setDataSource setDataSource()}
* is invoked soon after, without a break in the code. Using this callback
* means you don't have to change this common pattern just so that you can
* customize the shadow for testing.
*
* @param createListener
* the listener to be invoked
*/
public static void setCreateListener(CreateListener createListener) {
ShadowMediaPlayer.createListener = createListener;
}
/**
* Retrieves the {@link Handler} object used by this
* {@code ShadowMediaPlayer}. Can be used for posting custom asynchronous
* events to the thread (eg, asynchronous errors). Use this for scheduling
* events to take place at a particular "real" time (ie, time as measured by
* the scheduler). For scheduling events to occur at a particular playback
* offset (no matter how long playback may be paused for, or where you seek
* to, etc), see {@link MediaInfo#scheduleEventAtOffset(int, ShadowMediaPlayer.MediaEvent)} and
* its various helpers.
*
* @return Handler object that can be used to schedule asynchronous events on
* this media player.
*/
public Handler getHandler() {
return handler;
}
/**
* Retrieves current flag specifying the behavior of the media player when a
* method is invoked in an invalid state. See
* {@link #setInvalidStateBehavior(InvalidStateBehavior)} for a discussion of
* the available modes and their associated behaviors.
*
* @return The current invalid state behavior mode.
* @see #setInvalidStateBehavior
*/
public InvalidStateBehavior getInvalidStateBehavior() {
return invalidStateBehavior;
}
/**
* Specifies how the media player should behave when a method is invoked in an
* invalid state. Three modes are supported (as defined by the
* {@link InvalidStateBehavior} enum):
*
* ### {@link InvalidStateBehavior#SILENT}
* No invalid state checking is done at all. All methods can be
* invoked from any state without throwing any exceptions or invoking the
* error listener.
*
* This mode is provided primarily for backwards compatibility, and for this
* reason it is the default. For proper testing one of the other two modes is
* probably preferable.
*
* ### {@link InvalidStateBehavior#EMULATE}
* The shadow will attempt to emulate the behavior of the actual
* {@link MediaPlayer} implementation. This is based on a reading of the
* documentation and on actual experiments done on a Jelly Bean device. The
* official documentation is not all that clear, but basically methods fall
* into three categories:
* * Those that log an error when invoked in an invalid state but don't
* throw an exception or invoke {@code onError()}. An example is
* {@link #getVideoHeight()}.
* * Synchronous error handling: methods always throw an exception (usually
* {@link IllegalStateException} but don't invoke {@code onError()}.
* Examples are {@link #prepare()} and {@link #setDataSource(String)}.
* * Asynchronous error handling: methods don't throw an exception but
* invoke {@code onError()}.
*
* Additionally, all three methods behave synchronously (throwing
* {@link IllegalStateException} when invoked from the END state.
*
* To complicate matters slightly, the official documentation sometimes
* contradicts observed behavior. For example, the documentation says it is
* illegal to call {@link #setDataSource} from the ERROR state - however, in
* practice it works fine. Conversely, the documentation says that it is legal
* to invoke {@link #getCurrentPosition()} from the INITIALIZED state, however
* testing showed that this caused an error. Wherever there is a discrepancy
* between documented and observed behavior, this implementation has gone with
* the most conservative implementation (ie, it is illegal to invoke
* {@link #setDataSource} from the ERROR state and likewise illegal to invoke
* {@link #getCurrentPosition()} from the INITIALIZED state.
*
* ### {@link InvalidStateBehavior#ASSERT}
* The shadow will raise an assertion any time that a method is
* invoked in an invalid state. The philosophy behind this mode is that to
* invoke a method in an invalid state is a programming error - a bug, pure
* and simple. As such it should be discovered and eliminated at development and
* testing time, rather than anticipated and handled at runtime. Asserting is
* a way of testing for these bugs during testing.
*
* @param invalidStateBehavior
* the behavior mode for this shadow to use during testing.
* @see #getInvalidStateBehavior()
*/
public void setInvalidStateBehavior(InvalidStateBehavior invalidStateBehavior) {
this.invalidStateBehavior = invalidStateBehavior;
}
/**
* Retrieves the currently selected {@link MediaInfo}. This instance is used
* to define current duration, preparation delay, exceptions for
* {@code setDataSource()}, playback events, etc.
*
* @return The currently selected {@link MediaInfo}.
* @see #addMediaInfo
* @see #doSetDataSource(DataSource)
*/
public MediaInfo getMediaInfo() {
return mediaInfo.get(dataSource);
}
/**
* Sets the current position, bypassing the normal state checking. Use with
* care.
*
* @param position
* the new playback position.
*/
public void setCurrentPosition(int position) {
startOffset = position;
}
/**
* Retrieves the current position without doing the state checking that the
* emulated version of {@link #getCurrentPosition()} does.
*
* @return The current playback position within the current clip.
*/
public int getCurrentPositionRaw() {
int currentPos = startOffset;
if (isReallyPlaying()) {
currentPos += (int) (SystemClock.uptimeMillis() - startTime);
}
return currentPos;
}
/**
* Retrieves the current duration without doing the state checking that the
* emulated version does.
*
* @return The duration of the current clip loaded by the player.
*/
public int getDurationRaw() {
return getMediaInfo().duration;
}
/**
* Retrieves the current state of the {@link MediaPlayer}. Uses the states as
* defined in the {@link MediaPlayer} documentation.
*
* @return The current state of the {@link MediaPlayer}, as defined in the
* MediaPlayer documentation.
* @see #setState
* @see MediaPlayer
*/
public State getState() {
return state;
}
/**
* Forces the @link MediaPlayer} into the specified state. Uses the states as
* defined in the {@link MediaPlayer} documentation.
*
* Note that by invoking this method directly you can get the player into an
* inconsistent state that a real player could not be put in (eg, in the END
* state but with playback events still happening). Use with care.
*
* @param state
* the new state of the {@link MediaPlayer}, as defined in the
* MediaPlayer documentation.
* @see #getState
* @see MediaPlayer
*/
public void setState(State state) {
this.state = state;
}
/**
* Note: This has a funny name at the
* moment to avoid having to produce an API-specific shadow -
* if it were called {@code getAudioStreamType()} then
* the {@code RobolectricWiringTest} will inform us that
* it should be annotated with {@link Implementation}, because
* there is a private method in the later API versions with
* the same name, however this would fail on earlier versions.
*
* @return audioStreamType
*/
public int getTheAudioStreamType() {
return audioStreamType;
}
/**
* @return seekDelay
*/
public int getSeekDelay() {
return seekDelay;
}
/**
* Sets the length of time (ms) that seekTo() will delay before completing.
* Default is 0. If set to -1, then seekTo() will not call the
* OnSeekCompleteListener automatically; you will need to call
* invokeSeekCompleteListener() manually.
*
* @param seekDelay
* length of time to delay (ms)
*/
public void setSeekDelay(int seekDelay) {
this.seekDelay = seekDelay;
}
/**
* Useful for assertions.
*
* @return The current {@code auxEffect} setting.
*/
public int getAuxEffect() {
return auxEffect;
}
/**
* Retrieves the pending seek setting.
*
* @return The position to which the shadow player is seeking for the seek in
* progress (ie, after the call to {@link #seekTo} but before a call
* to {@link #invokeSeekCompleteListener()}). Returns {@code -1}
* if no seek is in progress.
*/
public int getPendingSeek() {
return pendingSeek;
}
/**
* Retrieves the data source (if any) that was passed in to
* {@link #setDataSource(DataSource)}.
*
* Useful for assertions.
*
* @return The source passed in to {@code setDataSource}.
*/
public DataSource getDataSource() {
return dataSource;
}
/**
* Retrieves the source path (if any) that was passed in to
* {@link MediaPlayer#setDataSource(Context, Uri, Map)} or
* {@link MediaPlayer#setDataSource(Context, Uri)}.
*
* @return The source Uri passed in to {@code setDataSource}.
*/
public Uri getSourceUri() {
return sourceUri;
}
/**
* Retrieves the resource ID used in the call to {@link #create(Context, int)}
* (if any).
*
* @return The resource ID passed in to {@code create()}, or
* {@code -1} if a different method of setting the source was
* used.
*/
public int getSourceResId() {
return sourceResId;
}
/**
* Retrieves the current setting for the left channel volume.
*
* @return The left channel volume.
*/
public float getLeftVolume() {
return leftVolume;
}
/**
* @return The right channel volume.
*/
public float getRightVolume() {
return rightVolume;
}
private static EnumSet preparedStates = EnumSet.of(PREPARED, STARTED,
PAUSED, PLAYBACK_COMPLETED);
/**
* Tests to see if the player is in the PREPARED state.
* This is mainly used for backward compatibility.
* {@link #getState} may be more useful for new testing applications.
*
* @return {@code true} if the MediaPlayer is in the PREPARED state,
* false otherwise.
*/
public boolean isPrepared() {
return preparedStates.contains(state);
}
/**
* @return the OnCompletionListener
*/
public MediaPlayer.OnCompletionListener getOnCompletionListener() {
return completionListener;
}
/**
* @return the OnPreparedListener
*/
public MediaPlayer.OnPreparedListener getOnPreparedListener() {
return preparedListener;
}
/**
* Allows test cases to simulate 'prepared' state by invoking callback. Sets
* the player's state to PREPARED and invokes the
* {@link MediaPlayer.OnPreparedListener#onPrepared preparedListener()}
*/
public void invokePreparedListener() {
state = PREPARED;
if (preparedListener == null)
return;
preparedListener.onPrepared(player);
}
/**
* Simulates end-of-playback. Changes the player into PLAYBACK_COMPLETED state
* and calls
* {@link MediaPlayer.OnCompletionListener#onCompletion(MediaPlayer)
* onCompletion()} if a listener has been set.
*/
public void invokeCompletionListener() {
state = PLAYBACK_COMPLETED;
if (completionListener == null)
return;
completionListener.onCompletion(player);
}
/**
* Allows test cases to simulate seek completion by invoking callback.
*/
public void invokeSeekCompleteListener() {
int duration = getMediaInfo().duration;
setCurrentPosition(pendingSeek > duration ? duration
: pendingSeek < 0 ? 0 : pendingSeek);
pendingSeek = -1;
if (state == STARTED) {
doStart();
}
if (seekCompleteListener == null) {
return;
}
seekCompleteListener.onSeekComplete(player);
}
/**
* Allows test cases to directly simulate invocation of the OnInfo event.
*
* @param what
* parameter to pass in to {@code what} in
* {@link MediaPlayer.OnInfoListener#onInfo(MediaPlayer, int, int)}.
* @param extra
* parameter to pass in to {@code extra} in
* {@link MediaPlayer.OnInfoListener#onInfo(MediaPlayer, int, int)}.
*/
public void invokeInfoListener(int what, int extra) {
if (infoListener != null) {
infoListener.onInfo(player, what, extra);
}
}
/**
* Allows test cases to directly simulate invocation of the OnError event.
*
* @param what
* parameter to pass in to {@code what} in
* {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)}.
* @param extra
* parameter to pass in to {@code extra} in
* {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)}.
*/
public void invokeErrorListener(int what, int extra) {
// Calling doStop() un-schedules the next event and
// stops normal event flow from continuing.
doStop();
state = ERROR;
boolean handled = errorListener != null
&& errorListener.onError(player, what, extra);
if (!handled) {
// The documentation isn't very clear if onCompletion is
// supposed to be called from non-playing states
// (ie, states other than STARTED or PAUSED). Testing
// revealed that onCompletion is invoked even if playback
// hasn't started or is not in progress.
invokeCompletionListener();
// Need to set this again because
// invokeCompletionListener() will set the state
// to PLAYBACK_COMPLETED
state = ERROR;
}
}
@Resetter
public static void resetStaticState() {
createListener = null;
exceptions.clear();
mediaInfo.clear();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy