AI2 Component  (Version nb184)
Player.java
Go to the documentation of this file.
1 // -*- mode: java; c-basic-offset: 2; -*-
2 // Copyright 2009-2011 Google, All Rights reserved
3 // Copyright 2011-2020 MIT, All rights reserved
4 // Released under the Apache License, Version 2.0
5 // http://www.apache.org/licenses/LICENSE-2.0
6 
7 package com.google.appinventor.components.runtime;
8 
9 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
10 
11 import android.app.Activity;
12 import android.content.Context;
13 import android.media.AudioManager;
14 import android.media.MediaPlayer;
15 import android.media.MediaPlayer.OnCompletionListener;
16 import android.os.Vibrator;
33 import java.io.IOException;
34 
35 // TODO: This implementation does nothing about releasing the Media
36 // Player resources when the application stops. This needs to be handled
37 // at the application level, not just at the component level.
38 // We do release a previously used MediaPlayer before creating a new one.
39 //
40 // TODO: This implementation fails when there are multiple media
41 // players in an application. This appears to be a bug in the
42 // Android SDK, or possibly in ODE, but we need to investigate more
43 // fully.
44 //
45 // TODO: Do more extensive testing of how state is handled here to see
46 // if the state restrictions are adequate given the API, and prove that
47 // there can't be deadlock or starvation.
66 @DesignerComponent(version = YaVersion.PLAYER_COMPONENT_VERSION,
67  description = "Multimedia component that plays audio and " +
68  "controls phone vibration. The name of a multimedia field is " +
69  "specified in the <code>Source</code> property, which can be set in " +
70  "the Designer or in the Blocks Editor. The length of time for a " +
71  "vibration is specified in the Blocks Editor in milliseconds " +
72  "(thousandths of a second).\n" +
73  "<p>For supported audio formats, see " +
74  "<a href=\"http://developer.android.com/guide/appendix/media-formats.html\"" +
75  " target=\"_blank\">Android Supported Media Formats</a>.</p>\n" +
76  "<p>This component is best for long sound files, such as songs, " +
77  "while the <code>Sound</code> component is more efficient for short " +
78  "files, such as sound effects.</p>",
79  category = ComponentCategory.MEDIA,
80  nonVisible = true,
81  iconName = "images/player.png")
82 @SimpleObject
83 @UsesPermissions(permissionNames = "android.permission.VIBRATE, android.permission.INTERNET")
84 public final class Player extends AndroidNonvisibleComponent
86 
87  private MediaPlayer player;
88  private final Vibrator vibe;
89 
91  public enum State { INITIAL, PREPARED, PLAYING, PAUSED_BY_USER, PAUSED_BY_EVENT; }
92  private String sourcePath;
93 
94  // determines if playing should loop
95  private boolean loop;
96 
97  // choices on player policy: Foreground, Always
98  private boolean playOnlyInForeground;
99  // status of audio focus
100  private boolean focusOn;
101  private AudioManager am;
102  private final Activity activity;
103  // Flag if SDK level >= 8
104  private static final boolean audioFocusSupported;
105  private Object afChangeListener;
106 
107  static{
109  audioFocusSupported = true;
110  } else {
111  audioFocusSupported = false;
112  }
113  }
114 
115  /*
116  * playerState encodes a simplified version of the full MediaPlayer state space, that should be
117  * adequate, given this API:
118  * 0 (INITIAL) : player initial state
119  * 1 (PREPARED) : player prepared but not started
120  * 2 (PLAYING) : player is playing
121  * 3 (PAUSED_BY_USER) : player was playing and is now paused by a user action
122  * 4 (PAUSED_BY_EVENT) : player was playing and is now paused by lifecycle events or audio focus interrupts
123  * The allowable transitions are:
124  * Start: must be called in state 1, 2, 3 or 4, results in state 2
125  * Pause (User method): must be called in state 2, results in state 3
126  * pause (Lifecycle method): must be called in state 2, results in state 4; will go back to
127  * state 2 automatically
128  * Stop: must be called in state 1, 2, 3 or 4, results in state 1
129  * We can simplify this to remove state 0 and use a simple boolean after we're
130  * more confident that there are no start-up problems.
131  */
132 
138  public Player(ComponentContainer container) {
139  super(container.$form());
140  activity = container.$context();
141  sourcePath = "";
142  vibe = (Vibrator) form.getSystemService(Context.VIBRATOR_SERVICE);
143  form.registerForOnDestroy(this);
144  form.registerForOnResume(this);
145  form.registerForOnPause(this);
146  form.registerForOnStop(this);
147  // Make volume buttons control media, not ringer.
148  form.setVolumeControlStream(AudioManager.STREAM_MUSIC);
149  loop = false;
150  playOnlyInForeground = false;
151  focusOn = false;
152  am = (audioFocusSupported) ? FroyoUtil.setAudioManager(activity) : null;
153  afChangeListener = (audioFocusSupported) ? FroyoUtil.setAudioFocusChangeListener(this) : null;
154  }
155 
159  @SimpleProperty(
160  category = PropertyCategory.BEHAVIOR)
161  public String Source() {
162  return sourcePath;
163  }
164 
174  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_ASSET,
175  defaultValue = "")
176  @SimpleProperty
177  @UsesPermissions(READ_EXTERNAL_STORAGE)
178  public void Source(String path) {
179  final String tempPath = (path == null) ? "" : path;
180  if (MediaUtil.isExternalFile(form, tempPath)
181  && form.isDeniedPermission(READ_EXTERNAL_STORAGE)) {
182  form.askPermission(READ_EXTERNAL_STORAGE, new PermissionResultHandler() {
183  @Override
184  public void HandlePermissionResponse(String permission, boolean granted) {
185  if (granted) {
186  Player.this.Source(tempPath);
187  } else {
188  form.dispatchPermissionDeniedEvent(Player.this, "Source", permission);
189  }
190  }
191  });
192  return;
193  }
194 
195  sourcePath = tempPath;
196 
197  // Clear the previous MediaPlayer.
198  if (playerState == State.PREPARED || playerState == State.PLAYING || playerState == State.PAUSED_BY_USER) {
199  player.stop();
200  playerState = State.INITIAL;
201  }
202  if (player != null) {
203  player.release();
204  player = null;
205  }
206 
207  if (sourcePath.length() > 0) {
208  player = new MediaPlayer();
209  player.setOnCompletionListener(this);
210 
211  try {
212  MediaUtil.loadMediaPlayer(player, form, sourcePath);
213 
214  } catch (PermissionException e) {
215  player.release();
216  player = null;
217  form.dispatchPermissionDeniedEvent(this, "Source", e);
218  return;
219  } catch (IOException e) {
220  player.release();
221  player = null;
222  form.dispatchErrorOccurredEvent(this, "Source",
223  ErrorMessages.ERROR_UNABLE_TO_LOAD_MEDIA, sourcePath);
224  return;
225  }
226 
227  player.setAudioStreamType(AudioManager.STREAM_MUSIC);
228  if (audioFocusSupported) {
229  requestPermanentFocus();
230  }
231  // The Simple API is set up so that the user never has to call prepare.
232  prepare();
233  // Player should now be in state 1. (If prepare failed, we are in state 0.)
234  }
235  }
236 
240  private void requestPermanentFocus() {
241  // Request permanent focus on music stream
242  focusOn = (FroyoUtil.focusRequestGranted(am, afChangeListener)) ? true : false;
243  if (!focusOn)
244  form.dispatchErrorOccurredEvent(this, "Source",
245  ErrorMessages.ERROR_UNABLE_TO_FOCUS_MEDIA, sourcePath);
246  }
247 
251  @SimpleProperty(
252  description = "Reports whether the media is playing",
253  category = PropertyCategory.BEHAVIOR)
254  public boolean IsPlaying() {
255  if (playerState == State.PREPARED || playerState == State.PLAYING) {
256  return player.isPlaying();
257  }
258  return false;
259  }
260 
264  @SimpleProperty(
265  description = "If true, the player will loop when it plays. Setting Loop while the player " +
266  "is playing will affect the current playing.",
267  category = PropertyCategory.BEHAVIOR)
268  public boolean Loop() {
269  return loop;
270  }
271 
278  @DesignerProperty(
279  editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN,
280  defaultValue = "False")
281  @SimpleProperty
282  public void Loop(boolean shouldLoop) {
283  // set the desired looping right now if the player is prepared.
284  if (playerState == State.PREPARED || playerState == State.PLAYING || playerState == State.PAUSED_BY_USER) {
285  player.setLooping(shouldLoop);
286  }
287  // even if the player is not prepared, it will be set according to
288  // Loop the next time it is started
289  loop = shouldLoop;
290  }
291 
297  @DesignerProperty(
298  editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_FLOAT,
299  defaultValue = "50")
300  @SimpleProperty(
301  description = "Sets the volume to a number between 0 and 100")
302  public void Volume(int vol) {
303  if (playerState == State.PREPARED || playerState == State.PLAYING || playerState == State.PAUSED_BY_USER) {
304  if (vol > 100 || vol < 0) {
305  form.dispatchErrorOccurredEvent(this, "Volume", ErrorMessages.ERROR_PLAYER_INVALID_VOLUME, vol);
306  } else {
307  player.setVolume(((float) vol) / 100, ((float) vol) / 100);
308  }
309  }
310  }
311 
317  @SimpleProperty(
318  description = "If true, the player will pause playing when leaving the current screen; " +
319  "if false (default option), the player continues playing"+
320  " whenever the current screen is displaying or not.",
321  category = PropertyCategory.BEHAVIOR)
322  public boolean PlayOnlyInForeground() {
323  return playOnlyInForeground;
324  }
325 
333  @DesignerProperty(
334  editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN,
335  defaultValue = "False")
336  @SimpleProperty
337  public void PlayOnlyInForeground(boolean shouldForeground) {
338  playOnlyInForeground = shouldForeground;
339  }
340 
345  @SimpleFunction
346  public void Start() {
347  if (audioFocusSupported && !focusOn) {
348  requestPermanentFocus();
349  }
350  if (playerState == State.PREPARED || playerState == State.PLAYING || playerState == State.PAUSED_BY_USER || playerState == State.PAUSED_BY_EVENT ) {
351  player.setLooping(loop);
352  player.start();
353  playerState = State.PLAYING;
354  // Player should now be in state 2(PLAYING)
355  }
356  }
357 
361  @SimpleFunction
362  public void Pause() {
363  if (player == null) return; //Do nothing if the player is not
364  boolean wasPlaying = player.isPlaying();
365  if (playerState == State.PLAYING) {
366  player.pause();
367  if (wasPlaying) {
368  playerState = State.PAUSED_BY_USER;
369  // Player should now be in state 3(PAUSED_BY_USER).
370  }
371  }
372  }
373 
378  public void pause() {
379  if (player == null) return; //Do nothing if the player is not playing
380  if (playerState == State.PLAYING) {
381  player.pause();
382  playerState = State.PAUSED_BY_EVENT;
383  // Player should now be in state 4(PAUSED_BY_EVENT).
384  }
385  }
386 
390  @SimpleFunction
391  public void Stop() {
392  if (audioFocusSupported && focusOn) {
393  abandonFocus();
394  }
395  if (playerState == State.PLAYING || playerState == State.PAUSED_BY_USER || playerState == State.PAUSED_BY_EVENT) {
396  player.stop();
397  prepare();
398  if (player != null) { // If prepare fails, the player is released and set to null
399  player.seekTo(0); // So we cannot seek
400  }
401  // Player should now be in state 1(PREPARED). (If prepare failed, we are in state 0 (INITIAL).)
402  }
403  }
404 
408  private void abandonFocus() {
409  // Abandon focus
410  FroyoUtil.abandonFocus(am, afChangeListener);
411  focusOn = false;
412  }
413 
414  // TODO: Reconsider whether vibrate should be here or in a separate component.
418  @SimpleFunction
419  public void Vibrate(long milliseconds) {
420  vibe.vibrate(milliseconds);
421  }
422 
423  @SimpleEvent(description = "The PlayerError event is no longer used. " +
424  "Please use the Screen.ErrorOccurred event instead.",
425  userVisible = false)
426  public void PlayerError(String message) {
427  }
428 
429  private void prepare() {
430  // This should be called only after player.stop() or directly after
431  // initialization
432  try {
433  player.prepare();
434  playerState = State.PREPARED;
435  } catch (IOException ioe) {
436  player.release();
437  player = null;
438  playerState = State.INITIAL;
439  form.dispatchErrorOccurredEvent(this, "Source",
440  ErrorMessages.ERROR_UNABLE_TO_PREPARE_MEDIA, sourcePath);
441  }
442  }
443 
444  // OnCompletionListener implementation
445  @Override
446  public void onCompletion(MediaPlayer m) {
447  Completed();
448  }
449 
453  @SimpleEvent
454  public void Completed() {
455  //Once you've finished playback be sure to call abandonAudioFocus() according to Android developer reference.
456  if (audioFocusSupported && focusOn) {
457  abandonFocus();
458  }
459 
460  EventDispatcher.dispatchEvent(this, "Completed");
461  }
462 
467  @SimpleEvent(description = "This event is signaled when another player has started" +
468  " (and the current player is playing or paused, but not stopped).")
469  public void OtherPlayerStarted() {
470  EventDispatcher.dispatchEvent(this, "OtherPlayerStarted");
471  }
472 
473  // OnResumeListener implementation
474  @Override
475  public void onResume() {
476  if (playOnlyInForeground && playerState == State.PAUSED_BY_EVENT) {
477  Start();
478  }
479  }
480 
481  // OnPauseListener implementation
482 
483  @Override
484  public void onPause() {
485  if (player == null) return; //Do nothing if the player is not ready
486  if (playOnlyInForeground && player.isPlaying()) {
487  pause();
488  }
489  }
490 
491  @Override
492  public void onStop() {
493  if (player == null) return; //Do nothing if the player is not
494  if (playOnlyInForeground && player.isPlaying()) {
495  pause();
496  }
497  }
498 
499  // OnDestroyListener implementation
500  @Override
501  public void onDestroy() {
502  prepareToDie();
503  }
504 
505  // Deleteable implementation
506  @Override
507  public void onDelete() {
508  prepareToDie();
509  }
510 
511  private void prepareToDie() {
512  // TODO(lizlooney) - add descriptively named constants for these magic numbers.
513  if (audioFocusSupported && focusOn) {
514  abandonFocus();
515  }
516  if ((player != null) && (playerState != State.INITIAL)) {
517  player.stop();
518  }
519  playerState = State.INITIAL;
520  if (player != null) {
521  player.release();
522  player = null;
523  }
524  vibe.cancel();
525  }
526 }
com.google.appinventor.components.annotations.SimpleFunction
Definition: SimpleFunction.java:23
com.google.appinventor.components.runtime.util.ErrorMessages
Definition: ErrorMessages.java:17
com.google.appinventor.components.runtime.Player.State.PAUSED_BY_EVENT
PAUSED_BY_EVENT
Definition: Player.java:91
com.google.appinventor.components.runtime.util
-*- mode: java; c-basic-offset: 2; -*-
Definition: AccountChooser.java:7
com.google.appinventor.components.runtime.Player.State.PREPARED
PREPARED
Definition: Player.java:91
com.google.appinventor.components.common.YaVersion
Definition: YaVersion.java:14
com.google.appinventor.components.annotations.DesignerProperty
Definition: DesignerProperty.java:25
com.google.appinventor.components
com.google.appinventor.components.runtime.Player.State.PAUSED_BY_USER
PAUSED_BY_USER
Definition: Player.java:91
com.google.appinventor.components.runtime.Player.State.INITIAL
INITIAL
Definition: Player.java:91
com.google.appinventor.components.runtime.util.MediaUtil
Definition: MediaUtil.java:53
com.google.appinventor.components.annotations.DesignerComponent
Definition: DesignerComponent.java:22
com.google.appinventor.components.annotations.SimpleEvent
Definition: SimpleEvent.java:20
com.google.appinventor.components.runtime.util.FroyoUtil
Definition: FroyoUtil.java:28
com.google.appinventor.components.annotations.UsesPermissions
Definition: UsesPermissions.java:21
com.google.appinventor.components.runtime.OnResumeListener
Definition: OnResumeListener.java:14
com.google.appinventor.components.runtime.Player.State.PLAYING
PLAYING
Definition: Player.java:91
com.google.appinventor.components.runtime.AndroidNonvisibleComponent
Definition: AndroidNonvisibleComponent.java:17
com.google.appinventor.components.runtime.util.SdkLevel
Definition: SdkLevel.java:19
com.google.appinventor.components.runtime.OnPauseListener
Definition: OnPauseListener.java:14
com.google.appinventor.components.annotations.SimpleProperty
Definition: SimpleProperty.java:23
com.google.appinventor.components.runtime.Player.playerState
State playerState
Definition: Player.java:90
com.google.appinventor.components.annotations.PropertyCategory
Definition: PropertyCategory.java:13
com.google.appinventor.components.runtime.errors.PermissionException
Definition: PermissionException.java:16
com.google.appinventor.components.runtime.util.SdkLevel.getLevel
static int getLevel()
Definition: SdkLevel.java:45
com.google.appinventor.components.runtime
Copyright 2009-2011 Google, All Rights reserved.
Definition: AccelerometerSensor.java:8
com.google.appinventor.components.runtime.Component
Definition: Component.java:17
com.google.appinventor.components.runtime.Player
Definition: Player.java:84
com.google.appinventor.components.runtime.Deleteable
Definition: Deleteable.java:15
com.google.appinventor.components.common
Definition: ComponentCategory.java:7
com.google.appinventor.components.common.ComponentCategory
Definition: ComponentCategory.java:48
com.google.appinventor.components.runtime.util.SdkLevel.LEVEL_FROYO
static final int LEVEL_FROYO
Definition: SdkLevel.java:25
com.google.appinventor.components.runtime.OnStopListener
Definition: OnStopListener.java:15
com.google.appinventor.components.runtime.Player.State
Definition: Player.java:91
com.google.appinventor.components.annotations.SimpleObject
Definition: SimpleObject.java:23
com.google
com
com.google.appinventor.components.runtime.errors
Definition: ArrayIndexOutOfBoundsError.java:7
com.google.appinventor.components.common.PropertyTypeConstants
Definition: PropertyTypeConstants.java:14
com.google.appinventor.components.runtime.OnDestroyListener
Definition: OnDestroyListener.java:15
com.google.appinventor.components.annotations
com.google.appinventor