1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.O; 4 import static org.robolectric.shadows.ShadowMediaPlayer.State.END; 5 import static org.robolectric.shadows.ShadowMediaPlayer.State.ERROR; 6 import static org.robolectric.shadows.ShadowMediaPlayer.State.IDLE; 7 import static org.robolectric.shadows.ShadowMediaPlayer.State.INITIALIZED; 8 import static org.robolectric.shadows.ShadowMediaPlayer.State.PAUSED; 9 import static org.robolectric.shadows.ShadowMediaPlayer.State.PLAYBACK_COMPLETED; 10 import static org.robolectric.shadows.ShadowMediaPlayer.State.PREPARED; 11 import static org.robolectric.shadows.ShadowMediaPlayer.State.PREPARING; 12 import static org.robolectric.shadows.ShadowMediaPlayer.State.STARTED; 13 import static org.robolectric.shadows.ShadowMediaPlayer.State.STOPPED; 14 import static org.robolectric.shadows.util.DataSource.toDataSource; 15 16 import android.content.Context; 17 import android.media.MediaPlayer; 18 import android.net.Uri; 19 import android.os.Handler; 20 import android.os.Looper; 21 import android.os.Message; 22 import android.os.SystemClock; 23 import java.io.FileDescriptor; 24 import java.io.IOException; 25 import java.util.ArrayList; 26 import java.util.EnumSet; 27 import java.util.HashMap; 28 import java.util.Iterator; 29 import java.util.Map; 30 import java.util.Map.Entry; 31 import java.util.Random; 32 import java.util.TreeMap; 33 import org.robolectric.annotation.Implementation; 34 import org.robolectric.annotation.Implements; 35 import org.robolectric.annotation.RealObject; 36 import org.robolectric.annotation.Resetter; 37 import org.robolectric.shadow.api.Shadow; 38 import org.robolectric.shadows.util.DataSource; 39 40 /** 41 * Automated testing of media playback can be a difficult thing - especially 42 * testing that your code properly handles asynchronous errors and events. This 43 * near impossible task is made quite straightforward using this implementation 44 * of {@link MediaPlayer} with Robolectric. 45 * 46 * This shadow implementation provides much of the functionality needed to 47 * emulate {@link MediaPlayer} initialization and playback behavior without having 48 * to play actual media files. A summary of the features included are: 49 * 50 * * Construction-time callback hook {@link CreateListener} so that 51 * newly-created {@link MediaPlayer} instances can have their shadows configured 52 * before they are used. 53 * * Emulation of the {@link android.media.MediaPlayer.OnCompletionListener 54 * OnCompletionListener}, {@link android.media.MediaPlayer.OnErrorListener 55 * OnErrorListener}, {@link android.media.MediaPlayer.OnInfoListener 56 * OnInfoListener}, {@link android.media.MediaPlayer.OnPreparedListener 57 * OnPreparedListener} and 58 * {@link android.media.MediaPlayer.OnSeekCompleteListener 59 * OnSeekCompleteListener}. 60 * * Full support of the {@link MediaPlayer} internal states and their 61 * transition map. 62 * * Configure time parameters such as playback duration, preparation delay 63 * and (@link #setSeekDelay seek delay}. 64 * * Emulation of asynchronous callback events during playback through 65 * Robolectric's scheduling system using the {@link MediaInfo} inner class. 66 * * Emulation of error behavior when methods are called from invalid states, 67 * or to throw assertions when methods are invoked in invalid states (using 68 * {@link #setInvalidStateBehavior}). 69 * * Emulation of different playback behaviors based on the current data 70 * source, as passed in to {@link #setDataSource(String) setDataSource()}, using 71 * {@link #addMediaInfo}. 72 * * Emulation of exceptions when calling {@link #setDataSource} using 73 * {@link #addException}. 74 * 75 * <b>Note</b>: One gotcha with this shadow is that you need to either configure an 76 * exception or a {@link ShadowMediaPlayer.MediaInfo} instance for that data source 77 * (using {@link #addException(DataSource, IOException)} or 78 * {@link #addMediaInfo(DataSource, MediaInfo)} respectively) <i>before</i> 79 * calling {@link #setDataSource}, otherwise you'll get an 80 * {@link IllegalArgumentException}. 81 * 82 * The current features of {@code ShadowMediaPlayer} were focused on development 83 * for testing playback of audio tracks. Thus support for emulating timed text and 84 * video events is incomplete. None of these features would be particularly onerous 85 * to add/fix - contributions welcome, of course! 86 * 87 * @author Fr Jeremy Krieg, Holy Monastery of St Nectarios, Adelaide, Australia 88 */ 89 @Implements(MediaPlayer.class) 90 public class ShadowMediaPlayer extends ShadowPlayerBase { 91 @Implementation 92 protected static void __staticInitializer__() { 93 // don't bind the JNI library 94 } 95 96 /** 97 * Listener that is called when a new MediaPlayer is constructed. 98 * 99 * @see #setCreateListener(CreateListener) 100 */ 101 protected static CreateListener createListener; 102 103 private static final Map<DataSource, Exception> exceptions = new HashMap<>(); 104 private static final Map<DataSource, MediaInfo> mediaInfo = new HashMap<>(); 105 106 @RealObject 107 private MediaPlayer player; 108 109 /** 110 * Possible states for the media player to be in. These states are as defined 111 * in the documentation for {@link android.media.MediaPlayer}. 112 */ 113 public enum State { 114 IDLE, INITIALIZED, PREPARING, PREPARED, STARTED, STOPPED, PAUSED, PLAYBACK_COMPLETED, END, ERROR 115 } 116 117 /** 118 * Possible behavior modes for the media player when a method is invoked in an 119 * invalid state. 120 * 121 * @see #setInvalidStateBehavior 122 */ 123 public enum InvalidStateBehavior { 124 SILENT, EMULATE, ASSERT 125 } 126 127 /** 128 * Reference to the next playback event scheduled to run. We keep a reference 129 * to this handy in case we need to cancel it. 130 */ 131 private RunList nextPlaybackEvent; 132 133 /** 134 * Class for grouping events that are meant to fire at the same time. Also 135 * schedules the next event to run. 136 */ 137 @SuppressWarnings("serial") 138 private static class RunList extends ArrayList<MediaEvent> implements MediaEvent { 139 140 public RunList() { 141 // Set the default size to one as most of the time we will 142 // only have one event. 143 super(1); 144 } 145 146 @Override 147 public void run(MediaPlayer mp, ShadowMediaPlayer smp) { 148 for (MediaEvent e : this) { 149 e.run(mp, smp); 150 } 151 } 152 } 153 154 public interface MediaEvent { 155 public void run(MediaPlayer mp, ShadowMediaPlayer smp); 156 } 157 158 /** 159 * Class specifying information for an emulated media object. Used by 160 * ShadowMediaPlayer when setDataSource() is called to populate the shadow 161 * player with the specified values. 162 */ 163 public static class MediaInfo { 164 public int duration; 165 private int preparationDelay; 166 167 /** Map that maps time offsets to media events. */ 168 public TreeMap<Integer, RunList> events = new TreeMap<>(); 169 170 /** 171 * Creates a new {@code MediaInfo} object with default duration (1000ms) 172 * and default preparation delay (0ms). 173 */ 174 public MediaInfo() { 175 this(1000, 0); 176 } 177 178 /** 179 * Creates a new {@code MediaInfo} object with the given duration and 180 * preparation delay. A completion callback event is scheduled at 181 * {@code duration} ms from the end. 182 * 183 * @param duration 184 * the duration (in ms) of this emulated media. A callback event 185 * will be scheduled at this offset to stop playback simulation and 186 * invoke the completion callback. 187 * @param preparationDelay 188 * the preparation delay (in ms) to emulate for this media. If set 189 * to -1, then {@link #prepare()} will complete instantly but 190 * {@link #prepareAsync()} will not complete automatically; you 191 * will need to call {@link #invokePreparedListener()} manually. 192 */ 193 public MediaInfo(int duration, int preparationDelay) { 194 this.duration = duration; 195 this.preparationDelay = preparationDelay; 196 197 scheduleEventAtOffset(duration, completionCallback); 198 } 199 200 /** 201 * Retrieves the current preparation delay for this media. 202 * 203 * @return The current preparation delay (in ms). 204 */ 205 public int getPreparationDelay() { 206 return preparationDelay; 207 } 208 209 /** 210 * Sets the current preparation delay for this media. 211 * 212 * @param preparationDelay 213 * the new preparation delay (in ms). 214 */ 215 public void setPreparationDelay(int preparationDelay) { 216 this.preparationDelay = preparationDelay; 217 } 218 219 /** 220 * Schedules a generic event to run at the specified playback offset. Events 221 * are run on the thread on which the {@link android.media.MediaPlayer 222 * MediaPlayer} was created. 223 * 224 * @param offset 225 * the offset from the start of playback at which this event will 226 * run. 227 * @param event 228 * the event to run. 229 */ 230 public void scheduleEventAtOffset(int offset, MediaEvent event) { 231 RunList runList = events.get(offset); 232 if (runList == null) { 233 // Given that most run lists will only contain one event, 234 // we use 1 as the default capacity. 235 runList = new RunList(); 236 events.put(offset, runList); 237 } 238 runList.add(event); 239 } 240 241 /** 242 * Schedules an error event to run at the specified playback offset. A 243 * reference to the actual MediaEvent that is scheduled is returned, which can 244 * be used in a subsequent call to {@link #removeEventAtOffset}. 245 * 246 * @param offset 247 * the offset from the start of playback at which this error will 248 * trigger. 249 * @param what 250 * the value for the {@code what} parameter to use in the call to 251 * {@link android.media.MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int) 252 * onError()}. 253 * @param extra 254 * the value for the {@code extra} parameter to use in the call to 255 * {@link android.media.MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int) 256 * onError()}. 257 * @return A reference to the MediaEvent object that was created and scheduled. 258 */ 259 public MediaEvent scheduleErrorAtOffset(int offset, int what, int extra) { 260 ErrorCallback callback = new ErrorCallback(what, extra); 261 scheduleEventAtOffset(offset, callback); 262 return callback; 263 } 264 265 /** 266 * Schedules an info event to run at the specified playback offset. A 267 * reference to the actual MediaEvent that is scheduled is returned, which can 268 * be used in a subsequent call to {@link #removeEventAtOffset}. 269 * 270 * @param offset 271 * the offset from the start of playback at which this event will 272 * trigger. 273 * @param what 274 * the value for the {@code what} parameter to use in the call to 275 * {@link android.media.MediaPlayer.OnInfoListener#onInfo(MediaPlayer, int, int) 276 * onInfo()}. 277 * @param extra 278 * the value for the {@code extra} parameter to use in the call to 279 * {@link android.media.MediaPlayer.OnInfoListener#onInfo(MediaPlayer, int, int) 280 * onInfo()}. 281 * @return A reference to the MediaEvent object that was created and scheduled. 282 */ 283 public MediaEvent scheduleInfoAtOffset(int offset, final int what, 284 final int extra) { 285 MediaEvent callback = new MediaEvent() { 286 @Override 287 public void run(MediaPlayer mp, ShadowMediaPlayer smp) { 288 smp.invokeInfoListener(what, extra); 289 } 290 }; 291 scheduleEventAtOffset(offset, callback); 292 return callback; 293 } 294 295 /** 296 * Schedules a simulated buffer underrun event to run at the specified 297 * playback offset. A reference to the actual MediaEvent that is scheduled is 298 * returned, which can be used in a subsequent call to 299 * {@link #removeEventAtOffset}. 300 * 301 * This event will issue an {@link MediaPlayer.OnInfoListener#onInfo 302 * onInfo()} callback with {@link MediaPlayer#MEDIA_INFO_BUFFERING_START} to 303 * signal the start of buffering and then call {@link #doStop()} to 304 * internally pause playback. Finally it will schedule an event to fire 305 * after {@code length} ms which fires a 306 * {@link MediaPlayer#MEDIA_INFO_BUFFERING_END} info event and invokes 307 * {@link #doStart()} to resume playback. 308 * 309 * @param offset 310 * the offset from the start of playback at which this underrun 311 * will trigger. 312 * @param length 313 * the length of time (in ms) for which playback will be paused. 314 * @return A reference to the MediaEvent object that was created and scheduled. 315 */ 316 public MediaEvent scheduleBufferUnderrunAtOffset(int offset, final int length) { 317 final MediaEvent restart = new MediaEvent() { 318 @Override 319 public void run(MediaPlayer mp, ShadowMediaPlayer smp) { 320 smp.invokeInfoListener(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0); 321 smp.doStart(); 322 } 323 }; 324 MediaEvent callback = new MediaEvent() { 325 @Override 326 public void run(MediaPlayer mp, ShadowMediaPlayer smp) { 327 smp.doStop(); 328 smp.invokeInfoListener(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0); 329 smp.postEventDelayed(restart, length); 330 } 331 }; 332 scheduleEventAtOffset(offset, callback); 333 return callback; 334 } 335 336 /** 337 * Removes the specified event from the playback schedule at the given 338 * playback offset. 339 * 340 * @param offset 341 * the offset at which the event was scheduled. 342 * @param event 343 * the event to remove. 344 * @see ShadowMediaPlayer.MediaInfo#removeEvent(ShadowMediaPlayer.MediaEvent) 345 */ 346 public void removeEventAtOffset(int offset, MediaEvent event) { 347 RunList runList = events.get(offset); 348 if (runList != null) { 349 runList.remove(event); 350 if (runList.isEmpty()) { 351 events.remove(offset); 352 } 353 } 354 } 355 356 /** 357 * Removes the specified event from the playback schedule at all playback 358 * offsets where it has been scheduled. 359 * 360 * @param event 361 * the event to remove. 362 * @see ShadowMediaPlayer.MediaInfo#removeEventAtOffset(int,ShadowMediaPlayer.MediaEvent) 363 */ 364 public void removeEvent(MediaEvent event) { 365 for (Iterator<Entry<Integer, RunList>> iter = events.entrySet() 366 .iterator(); iter.hasNext();) { 367 Entry<Integer, RunList> entry = iter.next(); 368 RunList runList = entry.getValue(); 369 runList.remove(event); 370 if (runList.isEmpty()) { 371 iter.remove(); 372 } 373 } 374 } 375 } 376 377 public void postEvent(MediaEvent e) { 378 Message msg = handler.obtainMessage(MEDIA_EVENT, e); 379 handler.sendMessage(msg); 380 } 381 382 public void postEventDelayed(MediaEvent e, long delay) { 383 Message msg = handler.obtainMessage(MEDIA_EVENT, e); 384 handler.sendMessageDelayed(msg, delay); 385 } 386 387 /** 388 * Callback interface for clients that wish to be informed when a new 389 * {@link MediaPlayer} instance is constructed. 390 * 391 * @see #setCreateListener 392 */ 393 public static interface CreateListener { 394 /** 395 * Method that is invoked when a new {@link MediaPlayer} is created. This 396 * method is invoked at the end of the constructor, after all of the default 397 * setup has been completed. 398 * 399 * @param player 400 * reference to the newly-created media player object. 401 * @param shadow 402 * reference to the corresponding shadow object for the 403 * newly-created media player (provided for convenience). 404 */ 405 public void onCreate(MediaPlayer player, ShadowMediaPlayer shadow); 406 } 407 408 /** Current state of the media player. */ 409 private State state = IDLE; 410 411 /** Delay for calls to {@link #seekTo} (in ms). */ 412 private int seekDelay = 0; 413 414 private int auxEffect; 415 private int audioSessionId; 416 private int audioStreamType; 417 private boolean looping; 418 private int pendingSeek = -1; 419 /** Various source variables from setDataSource() */ 420 private Uri sourceUri; 421 private int sourceResId; 422 private DataSource dataSource; 423 424 /** The time (in ms) at which playback was last started/resumed. */ 425 private long startTime = -1; 426 427 /** 428 * The offset (in ms) from the start of the current clip at which the last 429 * call to seek/pause was. If the MediaPlayer is not in the STARTED state, 430 * then this is equal to currentPosition; if it is in the STARTED state and no 431 * seek is pending then you need to add the number of ms since start() was 432 * called to get the current position (see {@link #startTime}). 433 */ 434 private int startOffset = 0; 435 436 private int videoHeight; 437 private int videoWidth; 438 private float leftVolume; 439 private float rightVolume; 440 private MediaPlayer.OnCompletionListener completionListener; 441 private MediaPlayer.OnSeekCompleteListener seekCompleteListener; 442 private MediaPlayer.OnPreparedListener preparedListener; 443 private MediaPlayer.OnInfoListener infoListener; 444 private MediaPlayer.OnErrorListener errorListener; 445 446 /** 447 * Flag indicating how the shadow media player should behave when a method is 448 * invoked in an invalid state. 449 */ 450 private InvalidStateBehavior invalidStateBehavior = InvalidStateBehavior.SILENT; 451 private Handler handler; 452 453 private static final MediaEvent completionCallback = new MediaEvent() { 454 @Override 455 public void run(MediaPlayer mp, ShadowMediaPlayer smp) { 456 if (mp.isLooping()) { 457 smp.startOffset = 0; 458 smp.doStart(); 459 } else { 460 smp.doStop(); 461 smp.invokeCompletionListener(); 462 } 463 } 464 }; 465 466 private static final MediaEvent preparedCallback = new MediaEvent() { 467 @Override 468 public void run(MediaPlayer mp, ShadowMediaPlayer smp) { 469 smp.invokePreparedListener(); 470 } 471 }; 472 473 private static final MediaEvent seekCompleteCallback = new MediaEvent() { 474 @Override 475 public void run(MediaPlayer mp, ShadowMediaPlayer smp) { 476 smp.invokeSeekCompleteListener(); 477 } 478 }; 479 480 /** 481 * Callback to use when a method is invoked from an invalid state. Has 482 * {@code what = -38} and {@code extra = 0}, which are values that 483 * were determined by inspection. 484 */ 485 private static final ErrorCallback invalidStateErrorCallback = new ErrorCallback( 486 -38, 0); 487 488 public static final int MEDIA_EVENT = 1; 489 490 /** Callback to use for scheduled errors. */ 491 private static class ErrorCallback implements MediaEvent { 492 private int what; 493 private int extra; 494 495 public ErrorCallback(int what, int extra) { 496 this.what = what; 497 this.extra = extra; 498 } 499 500 @Override 501 public void run(MediaPlayer mp, ShadowMediaPlayer smp) { 502 smp.invokeErrorListener(what, extra); 503 } 504 } 505 506 @Implementation 507 protected static MediaPlayer create(Context context, int resId) { 508 MediaPlayer mp = new MediaPlayer(); 509 ShadowMediaPlayer shadow = Shadow.extract(mp); 510 shadow.sourceResId = resId; 511 try { 512 shadow.setState(INITIALIZED); 513 mp.setDataSource("android.resource://" + context.getPackageName() + "/" + resId); 514 mp.prepare(); 515 } catch (Exception e) { 516 return null; 517 } 518 519 return mp; 520 } 521 522 @Implementation 523 protected static MediaPlayer create(Context context, Uri uri) { 524 MediaPlayer mp = new MediaPlayer(); 525 try { 526 mp.setDataSource(context, uri); 527 mp.prepare(); 528 } catch (Exception e) { 529 return null; 530 } 531 532 return mp; 533 } 534 535 @Implementation 536 protected void __constructor__() { 537 // Contract of audioSessionId is that if it is 0 (which represents 538 // the master mix) then that's an error. By default it generates 539 // an ID that is unique system-wide. We could simulate guaranteed 540 // uniqueness (get AudioManager's help?) but it's probably not 541 // worth the effort - a random non-zero number will probably do. 542 Random random = new Random(); 543 audioSessionId = random.nextInt(Integer.MAX_VALUE) + 1; 544 Looper myLooper = Looper.myLooper(); 545 if (myLooper != null) { 546 handler = getHandler(myLooper); 547 } else { 548 handler = getHandler(Looper.getMainLooper()); 549 } 550 // This gives test suites a chance to customize the MP instance 551 // and its shadow when it is created, without having to modify 552 // the code under test in order to do so. 553 if (createListener != null) { 554 createListener.onCreate(player, this); 555 } 556 // Ensure that the real object is set up properly. 557 Shadow.invokeConstructor(MediaPlayer.class, player); 558 } 559 560 private Handler getHandler(Looper looper) { 561 return new Handler(looper) { 562 @Override 563 public void handleMessage(Message msg) { 564 switch (msg.what) { 565 case MEDIA_EVENT: 566 MediaEvent e = (MediaEvent) msg.obj; 567 e.run(player, ShadowMediaPlayer.this); 568 scheduleNextPlaybackEvent(); 569 break; 570 } 571 } 572 }; 573 } 574 575 /** 576 * Common code path for all {@code setDataSource()} implementations. 577 * 578 * * Checks for any specified exceptions for the specified data source and throws them.</li> 579 * * Checks the current state and throws an exception if it is in an invalid state.</li> 580 * * If no exception is thrown in either of the previous two steps, then {@link #doSetDataSource(DataSource)} is called to set the data source.</li> 581 * * Sets the player state to {@code INITIALIZED}.</li> 582 * 583 * Usually this method would not be called directly, but indirectly through one of the 584 * other {@link #setDataSource(String)} implementations, which use {@link DataSource#toDataSource(String)} 585 * methods to convert their discrete parameters into a single {@link DataSource} instance. 586 * 587 * @param dataSource the data source that is being set. 588 * @throws IOException if the specified data source has been configured to throw an IO exception. 589 * @see #addException(DataSource, IOException) 590 * @see #addException(DataSource, RuntimeException) 591 * @see #doSetDataSource(DataSource) 592 */ 593 public void setDataSource(DataSource dataSource) throws IOException { 594 Exception e = exceptions.get(dataSource); 595 if (e != null) { 596 e.fillInStackTrace(); 597 if (e instanceof IOException) { 598 throw (IOException)e; 599 } else if (e instanceof RuntimeException) { 600 throw (RuntimeException)e; 601 } 602 throw new AssertionError("Invalid exception type for setDataSource: <" + e + '>'); 603 } 604 checkStateException("setDataSource()", idleState); 605 doSetDataSource(dataSource); 606 state = INITIALIZED; 607 } 608 609 /** 610 * Sets the data source without doing any other emulation. Sets the 611 * internal data source only. 612 * Calling directly can be useful for setting up a {@link ShadowMediaPlayer} 613 * instance during specific testing so that you don't have to clutter your 614 * tests catching exceptions you know won't be thrown. 615 * 616 * @param dataSource the data source that is being set. 617 * @see #setDataSource(DataSource) 618 */ 619 public void doSetDataSource(DataSource dataSource) { 620 if (mediaInfo.get(dataSource) == null) { 621 throw new IllegalArgumentException("Don't know what to do with dataSource " + dataSource + 622 " - either add an exception with addException() or media info with addMediaInfo()"); 623 } 624 this.dataSource = dataSource; 625 } 626 627 @Implementation 628 protected void setDataSource(String path) throws IOException { 629 setDataSource(toDataSource(path)); 630 } 631 632 @Implementation 633 protected void setDataSource(Context context, Uri uri, Map<String, String> headers) 634 throws IOException { 635 setDataSource(toDataSource(context, uri, headers)); 636 sourceUri = uri; 637 } 638 639 @Implementation 640 protected void setDataSource(String uri, Map<String, String> headers) throws IOException { 641 setDataSource(toDataSource(uri, headers)); 642 } 643 644 @Implementation 645 protected void setDataSource(FileDescriptor fd, long offset, long length) throws IOException { 646 setDataSource(toDataSource(fd, offset, length)); 647 } 648 649 public static MediaInfo getMediaInfo(DataSource dataSource) { 650 return mediaInfo.get(dataSource); 651 } 652 653 public static void addMediaInfo(DataSource dataSource, MediaInfo info) { 654 mediaInfo.put(dataSource, info); 655 } 656 657 public static void addException(DataSource dataSource, RuntimeException e) { 658 exceptions.put(dataSource, e); 659 } 660 661 public static void addException(DataSource dataSource, IOException e) { 662 exceptions.put(dataSource, e); 663 } 664 665 /** 666 * Checks states for methods that only log when there is an error. Such 667 * methods throw an {@link IllegalArgumentException} when invoked in the END 668 * state, but log an error in other disallowed states. This method will either 669 * emulate this behavior or else will generate an assertion if invoked from a 670 * disallowed state if {@link #setAssertOnError assertOnError} is set. 671 * 672 * @param method 673 * the name of the method being tested. 674 * @param allowedStates 675 * the states that this method is allowed to be called from. 676 * @see #setAssertOnError 677 * @see #checkStateError(String, EnumSet) 678 * @see #checkStateException(String, EnumSet) 679 */ 680 private void checkStateLog(String method, EnumSet<State> allowedStates) { 681 switch (invalidStateBehavior) { 682 case SILENT: 683 break; 684 case EMULATE: 685 if (state == END) { 686 String msg = "Can't call " + method + " from state " + state; 687 throw new IllegalStateException(msg); 688 } 689 break; 690 case ASSERT: 691 if (!allowedStates.contains(state) || state == END) { 692 String msg = "Can't call " + method + " from state " + state; 693 throw new AssertionError(msg); 694 } 695 } 696 } 697 698 /** 699 * Checks states for methods that asynchronously invoke 700 * {@link android.media.MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int) 701 * onError()} when invoked in an illegal state. Such methods always throw 702 * {@link IllegalStateException} rather than invoke {@code onError()} if 703 * they are invoked from the END state. 704 * 705 * This method will either emulate this behavior by posting an 706 * {@code onError()} callback to the current thread's message queue (or 707 * throw an {@link IllegalStateException} if invoked from the END state), or 708 * else it will generate an assertion if {@link #setAssertOnError 709 * assertOnError} is set. 710 * 711 * @param method 712 * the name of the method being tested. 713 * @param allowedStates 714 * the states that this method is allowed to be called from. 715 * @see #getHandler 716 * @see #setAssertOnError 717 * @see #checkStateLog(String, EnumSet) 718 * @see #checkStateException(String, EnumSet) 719 */ 720 private boolean checkStateError(String method, EnumSet<State> allowedStates) { 721 if (!allowedStates.contains(state)) { 722 switch (invalidStateBehavior) { 723 case SILENT: 724 break; 725 case EMULATE: 726 if (state == END) { 727 String msg = "Can't call " + method + " from state " + state; 728 throw new IllegalStateException(msg); 729 } 730 state = ERROR; 731 postEvent(invalidStateErrorCallback); 732 return false; 733 case ASSERT: 734 String msg = "Can't call " + method + " from state " + state; 735 throw new AssertionError(msg); 736 } 737 } 738 return true; 739 } 740 741 /** 742 * Checks states for methods that synchronously throw an exception when 743 * invoked in an illegal state. This method will likewise throw an 744 * {@link IllegalArgumentException} if it determines that the method has been 745 * invoked from a disallowed state, or else it will generate an assertion if 746 * {@link #setAssertOnError assertOnError} is set. 747 * 748 * @param method 749 * the name of the method being tested. 750 * @param allowedStates 751 * the states that this method is allowed to be called from. 752 * @see #setAssertOnError 753 * @see #checkStateLog(String, EnumSet) 754 * @see #checkStateError(String, EnumSet) 755 */ 756 private void checkStateException(String method, EnumSet<State> allowedStates) { 757 if (!allowedStates.contains(state)) { 758 String msg = "Can't call " + method + " from state " + state; 759 switch (invalidStateBehavior) { 760 case SILENT: 761 break; 762 case EMULATE: 763 throw new IllegalStateException(msg); 764 case ASSERT: 765 throw new AssertionError(msg); 766 } 767 } 768 } 769 770 @Implementation 771 protected void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) { 772 completionListener = listener; 773 } 774 775 @Implementation 776 protected void setOnSeekCompleteListener(MediaPlayer.OnSeekCompleteListener listener) { 777 seekCompleteListener = listener; 778 } 779 780 @Implementation 781 protected void setOnPreparedListener(MediaPlayer.OnPreparedListener listener) { 782 preparedListener = listener; 783 } 784 785 @Implementation 786 protected void setOnInfoListener(MediaPlayer.OnInfoListener listener) { 787 infoListener = listener; 788 } 789 790 @Implementation 791 protected void setOnErrorListener(MediaPlayer.OnErrorListener listener) { 792 errorListener = listener; 793 } 794 795 @Implementation 796 protected boolean isLooping() { 797 checkStateException("isLooping()", nonEndStates); 798 return looping; 799 } 800 801 static private final EnumSet<State> nonEndStates = EnumSet 802 .complementOf(EnumSet.of(END)); 803 static private final EnumSet<State> nonErrorStates = EnumSet 804 .complementOf(EnumSet.of(ERROR, END)); 805 806 @Implementation 807 protected void setLooping(boolean looping) { 808 checkStateError("setLooping()", nonErrorStates); 809 this.looping = looping; 810 } 811 812 @Implementation 813 protected void setVolume(float left, float right) { 814 checkStateError("setVolume()", nonErrorStates); 815 leftVolume = left; 816 rightVolume = right; 817 } 818 819 @Implementation 820 protected boolean isPlaying() { 821 checkStateError("isPlaying()", nonErrorStates); 822 return state == STARTED; 823 } 824 825 private static EnumSet<State> preparableStates = EnumSet.of(INITIALIZED, 826 STOPPED); 827 828 /** 829 * Simulates {@link MediaPlayer#prepareAsync()}. Sleeps for {@link MediaInfo#getPreparationDelay() 830 * preparationDelay} ms by calling {@link SystemClock#sleep(long)} before calling {@link 831 * #invokePreparedListener()}. 832 * 833 * <p>If {@code preparationDelay} is not positive and non-zero, there is no sleep. 834 * 835 * @see MediaInfo#setPreparationDelay(int) 836 * @see #invokePreparedListener() 837 */ 838 @Implementation 839 protected void prepare() { 840 checkStateException("prepare()", preparableStates); 841 MediaInfo info = getMediaInfo(); 842 if (info.preparationDelay > 0) { 843 SystemClock.sleep(info.preparationDelay); 844 } 845 invokePreparedListener(); 846 } 847 848 /** 849 * Simulates {@link MediaPlayer#prepareAsync()}. Sets state to PREPARING and posts a callback to 850 * {@link #invokePreparedListener()} if the current preparation delay for the current media (see 851 * {@link #getMediaInfo()}) is >= 0, otherwise the test suite is responsible for calling {@link 852 * #invokePreparedListener()} directly if required. 853 * 854 * @see MediaInfo#setPreparationDelay(int) 855 * @see #invokePreparedListener() 856 */ 857 @Implementation 858 protected void prepareAsync() { 859 checkStateException("prepareAsync()", preparableStates); 860 state = PREPARING; 861 MediaInfo info = getMediaInfo(); 862 if (info.preparationDelay >= 0) { 863 postEventDelayed(preparedCallback, info.preparationDelay); 864 } 865 } 866 867 private static EnumSet<State> startableStates = EnumSet.of(PREPARED, STARTED, 868 PAUSED, PLAYBACK_COMPLETED); 869 870 /** 871 * Simulates private native method {@link MediaPlayer#_start()}. Sets state to STARTED and calls 872 * {@link #doStart()} to start scheduling playback callback events. 873 * 874 * <p>If the current state is PLAYBACK_COMPLETED, the current position is reset to zero before 875 * starting playback. 876 * 877 * @see #doStart() 878 */ 879 @Implementation 880 protected void start() { 881 if (checkStateError("start()", startableStates)) { 882 if (state == PLAYBACK_COMPLETED) { 883 startOffset = 0; 884 } 885 state = STARTED; 886 doStart(); 887 } 888 } 889 890 private void scheduleNextPlaybackEvent() { 891 if (!isReallyPlaying()) { 892 return; 893 } 894 final int currentPosition = getCurrentPositionRaw(); 895 MediaInfo info = getMediaInfo(); 896 Entry<Integer, RunList> event = info.events.higherEntry(currentPosition); 897 if (event == null) { 898 // This means we've "seeked" past the end. Get the last 899 // event (which should be the completion event) and 900 // invoke that, setting the position to the duration. 901 postEvent(completionCallback); 902 } else { 903 final int runListOffset = event.getKey(); 904 nextPlaybackEvent = event.getValue(); 905 postEventDelayed(nextPlaybackEvent, runListOffset - currentPosition); 906 } 907 } 908 909 /** 910 * Tests to see if the player is really playing. 911 * 912 * The player is defined as "really playing" if simulated playback events 913 * (including playback completion) are being scheduled and invoked and 914 * {@link #getCurrentPosition currentPosition} is being updated as time 915 * passes. Note that while the player will normally be really playing if in 916 * the STARTED state, this is not always the case - for example, if a pending 917 * seek is in progress, or perhaps a buffer underrun is being simulated. 918 * 919 * @return {@code true} if the player is really playing or 920 * {@code false} if the player is internally paused. 921 * @see #doStart 922 * @see #doStop 923 */ 924 public boolean isReallyPlaying() { 925 return startTime >= 0; 926 } 927 928 /** 929 * Starts simulated playback. Until this method is called, the player is not 930 * "really playing" (see {@link #isReallyPlaying} for a definition of 931 * "really playing"). 932 * 933 * This method is used internally by the various shadow method implementations 934 * of the MediaPlayer public API, but may also be called directly by the test 935 * suite if you wish to simulate an internal pause. For example, to simulate 936 * a buffer underrun (player is in PLAYING state but isn't actually advancing 937 * the current position through the media), you could call {@link #doStop()} to 938 * mark the start of the buffer underrun and {@link #doStart()} to mark its 939 * end and restart normal playback (which is what 940 * {@link ShadowMediaPlayer.MediaInfo#scheduleBufferUnderrunAtOffset(int, int) scheduleBufferUnderrunAtOffset()} 941 * does). 942 * 943 * @see #isReallyPlaying() 944 * @see #doStop() 945 */ 946 public void doStart() { 947 startTime = SystemClock.uptimeMillis(); 948 scheduleNextPlaybackEvent(); 949 } 950 951 /** 952 * Pauses simulated playback. After this method is called, the player is no 953 * longer "really playing" (see {@link #isReallyPlaying} for a definition of 954 * "really playing"). 955 * 956 * This method is used internally by the various shadow method implementations 957 * of the MediaPlayer public API, but may also be called directly by the test 958 * suite if you wish to simulate an internal pause. 959 * 960 * @see #isReallyPlaying() 961 * @see #doStart() 962 */ 963 public void doStop() { 964 startOffset = getCurrentPositionRaw(); 965 if (nextPlaybackEvent != null) { 966 handler.removeMessages(MEDIA_EVENT); 967 nextPlaybackEvent = null; 968 } 969 startTime = -1; 970 } 971 972 private static final EnumSet<State> pausableStates = EnumSet.of(STARTED, 973 PAUSED, PLAYBACK_COMPLETED); 974 975 /** 976 * Simulates {@link MediaPlayer#_pause()}. Invokes {@link #doStop()} to suspend playback event 977 * callbacks and sets the state to PAUSED. 978 * 979 * @see #doStop() 980 */ 981 @Implementation 982 protected void _pause() { 983 if (checkStateError("pause()", pausableStates)) { 984 doStop(); 985 state = PAUSED; 986 } 987 } 988 989 static final EnumSet<State> allStates = EnumSet.allOf(State.class); 990 991 /** 992 * Simulates call to {@link MediaPlayer#_release()}. Calls {@link #doStop()} to suspend playback 993 * event callbacks and sets the state to END. 994 */ 995 @Implementation 996 protected void _release() { 997 checkStateException("release()", allStates); 998 doStop(); 999 state = END; 1000 handler.removeMessages(MEDIA_EVENT); 1001 } 1002 1003 /** 1004 * Simulates call to {@link MediaPlayer#_reset()}. Calls {@link #doStop()} to suspend playback 1005 * event callbacks and sets the state to IDLE. 1006 */ 1007 @Implementation 1008 protected void _reset() { 1009 checkStateException("reset()", nonEndStates); 1010 doStop(); 1011 state = IDLE; 1012 handler.removeMessages(MEDIA_EVENT); 1013 startOffset = 0; 1014 } 1015 1016 static private final EnumSet<State> stoppableStates = EnumSet.of(PREPARED, 1017 STARTED, PAUSED, STOPPED, PLAYBACK_COMPLETED); 1018 1019 /** 1020 * Simulates call to {@link MediaPlayer#release()}. Calls {@link #doStop()} to suspend playback 1021 * event callbacks and sets the state to STOPPED. 1022 */ 1023 @Implementation 1024 protected void _stop() { 1025 if (checkStateError("stop()", stoppableStates)) { 1026 doStop(); 1027 state = STOPPED; 1028 } 1029 } 1030 1031 private static final EnumSet<State> attachableStates = EnumSet.of( 1032 INITIALIZED, PREPARING, PREPARED, STARTED, PAUSED, STOPPED, 1033 PLAYBACK_COMPLETED); 1034 1035 @Implementation 1036 protected void attachAuxEffect(int effectId) { 1037 checkStateError("attachAuxEffect()", attachableStates); 1038 auxEffect = effectId; 1039 } 1040 1041 @Implementation 1042 protected int getAudioSessionId() { 1043 checkStateException("getAudioSessionId()", allStates); 1044 return audioSessionId; 1045 } 1046 1047 /** 1048 * Simulates call to {@link MediaPlayer#getCurrentPosition()}. Simply does the state validity 1049 * checks and then invokes {@link #getCurrentPositionRaw()} to calculate the simulated playback 1050 * position. 1051 * 1052 * @return The current offset (in ms) of the simulated playback. 1053 * @see #getCurrentPositionRaw() 1054 */ 1055 @Implementation 1056 protected int getCurrentPosition() { 1057 checkStateError("getCurrentPosition()", attachableStates); 1058 return getCurrentPositionRaw(); 1059 } 1060 1061 /** 1062 * Simulates call to {@link MediaPlayer#getDuration()}. Retrieves the duration as defined by the 1063 * current {@link MediaInfo} instance. 1064 * 1065 * @return The duration (in ms) of the current simulated playback. 1066 * @see #addMediaInfo(DataSource, MediaInfo) 1067 */ 1068 @Implementation 1069 protected int getDuration() { 1070 checkStateError("getDuration()", stoppableStates); 1071 return getMediaInfo().duration; 1072 } 1073 1074 @Implementation 1075 protected int getVideoHeight() { 1076 checkStateLog("getVideoHeight()", attachableStates); 1077 return videoHeight; 1078 } 1079 1080 @Implementation 1081 protected int getVideoWidth() { 1082 checkStateLog("getVideoWidth()", attachableStates); 1083 return videoWidth; 1084 } 1085 1086 private static final EnumSet<State> seekableStates = EnumSet.of(PREPARED, 1087 STARTED, PAUSED, PLAYBACK_COMPLETED); 1088 1089 /** 1090 * Simulates seeking to specified position. The seek will complete after {@link #seekDelay} ms 1091 * (defaults to 0), or else if seekDelay is negative then the controlling test is expected to 1092 * simulate seek completion by manually invoking {@link #invokeSeekCompleteListener}. 1093 * 1094 * @param seekTo the offset (in ms) from the start of the track to seek to. 1095 */ 1096 @Implementation 1097 protected void seekTo(int seekTo) { 1098 seekTo(seekTo, MediaPlayer.SEEK_PREVIOUS_SYNC); 1099 } 1100 1101 @Implementation(minSdk = O) 1102 protected void seekTo(long seekTo, int mode) { 1103 boolean success = checkStateError("seekTo()", seekableStates); 1104 // Cancel any pending seek operations. 1105 handler.removeMessages(MEDIA_EVENT, seekCompleteCallback); 1106 1107 if (success) { 1108 // Need to call doStop() before setting pendingSeek, 1109 // because if pendingSeek is called it changes 1110 // the behavior of getCurrentPosition(), which doStop() 1111 // depends on. 1112 doStop(); 1113 pendingSeek = (int) seekTo; 1114 if (seekDelay >= 0) { 1115 postEventDelayed(seekCompleteCallback, seekDelay); 1116 } 1117 } 1118 } 1119 1120 static private final EnumSet<State> idleState = EnumSet.of(IDLE); 1121 1122 @Implementation 1123 protected void setAudioSessionId(int sessionId) { 1124 checkStateError("setAudioSessionId()", idleState); 1125 audioSessionId = sessionId; 1126 } 1127 1128 static private final EnumSet<State> nonPlayingStates = EnumSet.of(IDLE, 1129 INITIALIZED, STOPPED); 1130 1131 @Implementation 1132 protected void setAudioStreamType(int audioStreamType) { 1133 checkStateError("setAudioStreamType()", nonPlayingStates); 1134 this.audioStreamType = audioStreamType; 1135 } 1136 1137 /** 1138 * Sets a listener that is invoked whenever a new shadowed {@link MediaPlayer} 1139 * object is constructed. 1140 * 1141 * Registering a listener gives you a chance to 1142 * customize the shadowed object appropriately without needing to modify the 1143 * application-under-test to provide access to the instance at the appropriate 1144 * point in its life cycle. This is useful because normally a new 1145 * {@link MediaPlayer} is created and {@link #setDataSource setDataSource()} 1146 * is invoked soon after, without a break in the code. Using this callback 1147 * means you don't have to change this common pattern just so that you can 1148 * customize the shadow for testing. 1149 * 1150 * @param createListener 1151 * the listener to be invoked 1152 */ 1153 public static void setCreateListener(CreateListener createListener) { 1154 ShadowMediaPlayer.createListener = createListener; 1155 } 1156 1157 /** 1158 * Retrieves the {@link Handler} object used by this 1159 * {@code ShadowMediaPlayer}. Can be used for posting custom asynchronous 1160 * events to the thread (eg, asynchronous errors). Use this for scheduling 1161 * events to take place at a particular "real" time (ie, time as measured by 1162 * the scheduler). For scheduling events to occur at a particular playback 1163 * offset (no matter how long playback may be paused for, or where you seek 1164 * to, etc), see {@link MediaInfo#scheduleEventAtOffset(int, ShadowMediaPlayer.MediaEvent)} and 1165 * its various helpers. 1166 * 1167 * @return Handler object that can be used to schedule asynchronous events on 1168 * this media player. 1169 */ 1170 public Handler getHandler() { 1171 return handler; 1172 } 1173 1174 /** 1175 * Retrieves current flag specifying the behavior of the media player when a 1176 * method is invoked in an invalid state. See 1177 * {@link #setInvalidStateBehavior(InvalidStateBehavior)} for a discussion of 1178 * the available modes and their associated behaviors. 1179 * 1180 * @return The current invalid state behavior mode. 1181 * @see #setInvalidStateBehavior 1182 */ 1183 public InvalidStateBehavior getInvalidStateBehavior() { 1184 return invalidStateBehavior; 1185 } 1186 1187 /** 1188 * Specifies how the media player should behave when a method is invoked in an 1189 * invalid state. Three modes are supported (as defined by the 1190 * {@link InvalidStateBehavior} enum): 1191 * 1192 * ### {@link InvalidStateBehavior#SILENT} 1193 * No invalid state checking is done at all. All methods can be 1194 * invoked from any state without throwing any exceptions or invoking the 1195 * error listener. 1196 * 1197 * This mode is provided primarily for backwards compatibility, and for this 1198 * reason it is the default. For proper testing one of the other two modes is 1199 * probably preferable. 1200 * 1201 * ### {@link InvalidStateBehavior#EMULATE} 1202 * The shadow will attempt to emulate the behavior of the actual 1203 * {@link MediaPlayer} implementation. This is based on a reading of the 1204 * documentation and on actual experiments done on a Jelly Bean device. The 1205 * official documentation is not all that clear, but basically methods fall 1206 * into three categories: 1207 * * Those that log an error when invoked in an invalid state but don't 1208 * throw an exception or invoke {@code onError()}. An example is 1209 * {@link #getVideoHeight()}. 1210 * * Synchronous error handling: methods always throw an exception (usually 1211 * {@link IllegalStateException} but don't invoke {@code onError()}. 1212 * Examples are {@link #prepare()} and {@link #setDataSource(String)}. 1213 * * Asynchronous error handling: methods don't throw an exception but 1214 * invoke {@code onError()}. 1215 * 1216 * Additionally, all three methods behave synchronously (throwing 1217 * {@link IllegalStateException} when invoked from the END state. 1218 * 1219 * To complicate matters slightly, the official documentation sometimes 1220 * contradicts observed behavior. For example, the documentation says it is 1221 * illegal to call {@link #setDataSource} from the ERROR state - however, in 1222 * practice it works fine. Conversely, the documentation says that it is legal 1223 * to invoke {@link #getCurrentPosition()} from the INITIALIZED state, however 1224 * testing showed that this caused an error. Wherever there is a discrepancy 1225 * between documented and observed behavior, this implementation has gone with 1226 * the most conservative implementation (ie, it is illegal to invoke 1227 * {@link #setDataSource} from the ERROR state and likewise illegal to invoke 1228 * {@link #getCurrentPosition()} from the INITIALIZED state. 1229 * 1230 * ### {@link InvalidStateBehavior#ASSERT} 1231 * The shadow will raise an assertion any time that a method is 1232 * invoked in an invalid state. The philosophy behind this mode is that to 1233 * invoke a method in an invalid state is a programming error - a bug, pure 1234 * and simple. As such it should be discovered and eliminated at development and 1235 * testing time, rather than anticipated and handled at runtime. Asserting is 1236 * a way of testing for these bugs during testing. 1237 * 1238 * @param invalidStateBehavior 1239 * the behavior mode for this shadow to use during testing. 1240 * @see #getInvalidStateBehavior() 1241 */ 1242 public void setInvalidStateBehavior(InvalidStateBehavior invalidStateBehavior) { 1243 this.invalidStateBehavior = invalidStateBehavior; 1244 } 1245 1246 /** 1247 * Retrieves the currently selected {@link MediaInfo}. This instance is used 1248 * to define current duration, preparation delay, exceptions for 1249 * {@code setDataSource()}, playback events, etc. 1250 * 1251 * @return The currently selected {@link MediaInfo}. 1252 * @see #addMediaInfo 1253 * @see #doSetDataSource(DataSource) 1254 */ 1255 public MediaInfo getMediaInfo() { 1256 return mediaInfo.get(dataSource); 1257 } 1258 1259 /** 1260 * Sets the current position, bypassing the normal state checking. Use with 1261 * care. 1262 * 1263 * @param position 1264 * the new playback position. 1265 */ 1266 public void setCurrentPosition(int position) { 1267 startOffset = position; 1268 } 1269 1270 /** 1271 * Retrieves the current position without doing the state checking that the 1272 * emulated version of {@link #getCurrentPosition()} does. 1273 * 1274 * @return The current playback position within the current clip. 1275 */ 1276 public int getCurrentPositionRaw() { 1277 int currentPos = startOffset; 1278 if (isReallyPlaying()) { 1279 currentPos += (int) (SystemClock.uptimeMillis() - startTime); 1280 } 1281 return currentPos; 1282 } 1283 1284 /** 1285 * Retrieves the current duration without doing the state checking that the 1286 * emulated version does. 1287 * 1288 * @return The duration of the current clip loaded by the player. 1289 */ 1290 public int getDurationRaw() { 1291 return getMediaInfo().duration; 1292 } 1293 1294 /** 1295 * Retrieves the current state of the {@link MediaPlayer}. Uses the states as 1296 * defined in the {@link MediaPlayer} documentation. 1297 * 1298 * @return The current state of the {@link MediaPlayer}, as defined in the 1299 * MediaPlayer documentation. 1300 * @see #setState 1301 * @see MediaPlayer 1302 */ 1303 public State getState() { 1304 return state; 1305 } 1306 1307 /** 1308 * Forces the @link MediaPlayer} into the specified state. Uses the states as 1309 * defined in the {@link MediaPlayer} documentation. 1310 * 1311 * Note that by invoking this method directly you can get the player into an 1312 * inconsistent state that a real player could not be put in (eg, in the END 1313 * state but with playback events still happening). Use with care. 1314 * 1315 * @param state 1316 * the new state of the {@link MediaPlayer}, as defined in the 1317 * MediaPlayer documentation. 1318 * @see #getState 1319 * @see MediaPlayer 1320 */ 1321 public void setState(State state) { 1322 this.state = state; 1323 } 1324 1325 /** 1326 * Note: This has a funny name at the 1327 * moment to avoid having to produce an API-specific shadow - 1328 * if it were called {@code getAudioStreamType()} then 1329 * the {@code RobolectricWiringTest} will inform us that 1330 * it should be annotated with {@link Implementation}, because 1331 * there is a private method in the later API versions with 1332 * the same name, however this would fail on earlier versions. 1333 * 1334 * @return audioStreamType 1335 */ 1336 public int getTheAudioStreamType() { 1337 return audioStreamType; 1338 } 1339 1340 /** 1341 * @return seekDelay 1342 */ 1343 public int getSeekDelay() { 1344 return seekDelay; 1345 } 1346 1347 /** 1348 * Sets the length of time (ms) that seekTo() will delay before completing. 1349 * Default is 0. If set to -1, then seekTo() will not call the 1350 * OnSeekCompleteListener automatically; you will need to call 1351 * invokeSeekCompleteListener() manually. 1352 * 1353 * @param seekDelay 1354 * length of time to delay (ms) 1355 */ 1356 public void setSeekDelay(int seekDelay) { 1357 this.seekDelay = seekDelay; 1358 } 1359 1360 /** 1361 * Useful for assertions. 1362 * 1363 * @return The current {@code auxEffect} setting. 1364 */ 1365 public int getAuxEffect() { 1366 return auxEffect; 1367 } 1368 1369 /** 1370 * Retrieves the pending seek setting. 1371 * 1372 * @return The position to which the shadow player is seeking for the seek in 1373 * progress (ie, after the call to {@link #seekTo} but before a call 1374 * to {@link #invokeSeekCompleteListener()}). Returns {@code -1} 1375 * if no seek is in progress. 1376 */ 1377 public int getPendingSeek() { 1378 return pendingSeek; 1379 } 1380 1381 /** 1382 * Retrieves the data source (if any) that was passed in to 1383 * {@link #setDataSource(DataSource)}. 1384 * 1385 * Useful for assertions. 1386 * 1387 * @return The source passed in to {@code setDataSource}. 1388 */ 1389 public DataSource getDataSource() { 1390 return dataSource; 1391 } 1392 1393 /** 1394 * Retrieves the source path (if any) that was passed in to 1395 * {@link MediaPlayer#setDataSource(Context, Uri, Map)} or 1396 * {@link MediaPlayer#setDataSource(Context, Uri)}. 1397 * 1398 * @return The source Uri passed in to {@code setDataSource}. 1399 */ 1400 public Uri getSourceUri() { 1401 return sourceUri; 1402 } 1403 1404 /** 1405 * Retrieves the resource ID used in the call to {@link #create(Context, int)} 1406 * (if any). 1407 * 1408 * @return The resource ID passed in to {@code create()}, or 1409 * {@code -1} if a different method of setting the source was 1410 * used. 1411 */ 1412 public int getSourceResId() { 1413 return sourceResId; 1414 } 1415 1416 /** 1417 * Retrieves the current setting for the left channel volume. 1418 * 1419 * @return The left channel volume. 1420 */ 1421 public float getLeftVolume() { 1422 return leftVolume; 1423 } 1424 1425 /** 1426 * @return The right channel volume. 1427 */ 1428 public float getRightVolume() { 1429 return rightVolume; 1430 } 1431 1432 private static EnumSet<State> preparedStates = EnumSet.of(PREPARED, STARTED, 1433 PAUSED, PLAYBACK_COMPLETED); 1434 1435 /** 1436 * Tests to see if the player is in the PREPARED state. 1437 * This is mainly used for backward compatibility. 1438 * {@link #getState} may be more useful for new testing applications. 1439 * 1440 * @return {@code true} if the MediaPlayer is in the PREPARED state, 1441 * false otherwise. 1442 */ 1443 public boolean isPrepared() { 1444 return preparedStates.contains(state); 1445 } 1446 1447 /** 1448 * @return the OnCompletionListener 1449 */ 1450 public MediaPlayer.OnCompletionListener getOnCompletionListener() { 1451 return completionListener; 1452 } 1453 1454 /** 1455 * @return the OnPreparedListener 1456 */ 1457 public MediaPlayer.OnPreparedListener getOnPreparedListener() { 1458 return preparedListener; 1459 } 1460 1461 /** 1462 * Allows test cases to simulate 'prepared' state by invoking callback. Sets 1463 * the player's state to PREPARED and invokes the 1464 * {@link MediaPlayer.OnPreparedListener#onPrepared preparedListener()} 1465 */ 1466 public void invokePreparedListener() { 1467 state = PREPARED; 1468 if (preparedListener == null) 1469 return; 1470 preparedListener.onPrepared(player); 1471 } 1472 1473 /** 1474 * Simulates end-of-playback. Changes the player into PLAYBACK_COMPLETED state 1475 * and calls 1476 * {@link MediaPlayer.OnCompletionListener#onCompletion(MediaPlayer) 1477 * onCompletion()} if a listener has been set. 1478 */ 1479 public void invokeCompletionListener() { 1480 state = PLAYBACK_COMPLETED; 1481 if (completionListener == null) 1482 return; 1483 completionListener.onCompletion(player); 1484 } 1485 1486 /** 1487 * Allows test cases to simulate seek completion by invoking callback. 1488 */ 1489 public void invokeSeekCompleteListener() { 1490 int duration = getMediaInfo().duration; 1491 setCurrentPosition(pendingSeek > duration ? duration 1492 : pendingSeek < 0 ? 0 : pendingSeek); 1493 pendingSeek = -1; 1494 if (state == STARTED) { 1495 doStart(); 1496 } 1497 if (seekCompleteListener == null) { 1498 return; 1499 } 1500 seekCompleteListener.onSeekComplete(player); 1501 } 1502 1503 /** 1504 * Allows test cases to directly simulate invocation of the OnInfo event. 1505 * 1506 * @param what 1507 * parameter to pass in to {@code what} in 1508 * {@link MediaPlayer.OnInfoListener#onInfo(MediaPlayer, int, int)}. 1509 * @param extra 1510 * parameter to pass in to {@code extra} in 1511 * {@link MediaPlayer.OnInfoListener#onInfo(MediaPlayer, int, int)}. 1512 */ 1513 public void invokeInfoListener(int what, int extra) { 1514 if (infoListener != null) { 1515 infoListener.onInfo(player, what, extra); 1516 } 1517 } 1518 1519 /** 1520 * Allows test cases to directly simulate invocation of the OnError event. 1521 * 1522 * @param what 1523 * parameter to pass in to {@code what} in 1524 * {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)}. 1525 * @param extra 1526 * parameter to pass in to {@code extra} in 1527 * {@link MediaPlayer.OnErrorListener#onError(MediaPlayer, int, int)}. 1528 */ 1529 public void invokeErrorListener(int what, int extra) { 1530 // Calling doStop() un-schedules the next event and 1531 // stops normal event flow from continuing. 1532 doStop(); 1533 state = ERROR; 1534 boolean handled = errorListener != null 1535 && errorListener.onError(player, what, extra); 1536 if (!handled) { 1537 // The documentation isn't very clear if onCompletion is 1538 // supposed to be called from non-playing states 1539 // (ie, states other than STARTED or PAUSED). Testing 1540 // revealed that onCompletion is invoked even if playback 1541 // hasn't started or is not in progress. 1542 invokeCompletionListener(); 1543 // Need to set this again because 1544 // invokeCompletionListener() will set the state 1545 // to PLAYBACK_COMPLETED 1546 state = ERROR; 1547 } 1548 } 1549 1550 @Resetter 1551 public static void resetStaticState() { 1552 createListener = null; 1553 exceptions.clear(); 1554 mediaInfo.clear(); 1555 } 1556 } 1557