The music starts playing as soon as the app is launched.
Press the volume up and volume down buttons of the emulator or device.
When the
Activity
object is destroyed,
playback stops.
When the
Activity
object is recreated,
playback starts again at the beginning of the audio file.
The
onPause
and
onStop
methods set the
mediaPlayer
variable to
null
to allow the
MediaPlayer
object to be garbage collected.
I’m doing this in
onStop
because it says to do this in
Releasing
the Media Player.
I’m also doing this in
onPause
because
onStop
is not always called.
This app is a stripped-down, barely legal example of a
MediaPlayer
object.
We should have plugged at least two listeners into it, a
MediaPlayer.OnCompletionListener
and a
MediaPlayer.OnErrorListener
.
See
Handling
asynchronous errors.
We also committed a major breach of etiquette by not running the
MediaPlayer
under the control of an
AudioManager
.
The
AudioManager
asks this app to be quiet while other apps are playing,
and asks the other apps for silence while this one is playing.
It allows only one app to play at any given time.
See
Handling
audio focus
and
Managing
Audio Focus.
MainActivity.java
.
We set the
mediaPlayer
to
null
to make it liable to Java garbage collection.
activity_main.xml
strings.xml
musette.mid
.
Bach Musette in D major,
BWV
Anh. 126.
The
MIDI audio format
is on Android’s list of
Supported
Media Formats.
AndroidManifest.xml
.
See
Manifest
Declarations.
build.gradle
(Module: app)
In the Android Studio
project
view,
select the
app/res
folder.
File → New… → Android resource directory
New Resource Directory
Resource type: raw
OK
For the raw folder, see Grouping Resource Types and Using Media Player.
MIDI is on Android’s list of
Supported
Media Formats.
I put the file
musette.mid
on my Desktop.
Make sure you can play it before you add it to the project.
file musette.mid musette.mid: Standard MIDI data (format 1) using 3 tracks at 1/384
Control-click on
musette.mid
and select Copy “musette.mid”.
The control-click on the raw folder and select Paste.
Copy file /Users/myname/Desktop/musette.mid
New name: musette.mid
To directory: /Users/myname/AndroidStudioProjects/MediaPlayer/app/src/main/res/raw
OK
In
MainActivity.java
,
click on the word
MainActivity
.
Code → Override Methods…
Select
onPause
and
onStop
.
You can just type each name and press OK.
We have to call
release
in these two methods.
See
Releasing
the Media Player.
Media → MediaPlayer → Play Audio from Local File or from Resources
platform_development/samples/ApiDemos/src/com/example/android/apis/media/MediaPlayerDemo_Audio.java
Activity
.
platform_development/samples/ApiDemos/res/raw/test_cbr.mp3
platform_development/samples/ApiDemos/AndroidManifest.xml
Activity
.
To get the above “Play Audio from Local File” to work
on the Genymotion Galaxy S5,
I copied
(“pushed”)
an audio file
(musette.mid
)
into the S5’s Music folder.
adb devices adb -s 192.168.57.101:5555 shell find / -type d -name Music /mnt/shell/emulated/0/Music /data/media/0/Music adb -s 192.168.57.101:5555 push musette.mid /mnt/shell/emulated/0/Music 822 KB/s (1765 bytes in 0.002s) adb -s 192.168.57.101:5555 shell find / -type f -name musette.mid /mnt/shell/emulated/0/Music/musette.mid /data/media/0/Music/musette.mid adb -s 192.168.57.101:5555 shell ls -l /storage/emulated lrwxrwxrwx root root 2015-08-25 16:51 legacy -> /mnt/shell/emulated/0Launch the Fie Manager app to make sure
musette.mid
was copied to the Music folder.
As I compiled ApiDemos using
these instructions,
I changed
path="";to
path="file:///storage/emulated/0/Music/musette.mid";in the
playAudio
method of class
MediaPlayerDemo_Audio
in
MediaPlayerDemo_Audio.java
.
MediaPlayer
.
mediaPlayer.setLooping(true); //repeat forever mediaPlayer.setVolume(1.0f, 1.0f); //left, right; 1.0f is maximum
MediaPlayer
’s audio file.
Give the
TextView
an id:
android:id="@+id/textView"
.
Print three digits to the right of the decimal point
because the duration is in milliseconds.
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { int milliseconds = mp.getDuration(); int hours = milliseconds / (1000 * 60 * 60); milliseconds -= 1000 * 60 * 60 * hours; int minutes = milliseconds / (1000 * 60); milliseconds -= 1000 * 60 * minutes; float seconds = milliseconds / 1000f; String s = String.format("Duration: %02d:%02d:%06.3f", hours, minutes, seconds); TextView textView = (TextView)findViewById(R.id.textView); textView.append("\n" + s); } });
Duration: 00:00:19.995
RelativeLayout
.
Create two
string
resources
in
strings.xml
.
<string name="play">Play</string> <string name="pause">Pause</string>Create a
Button
in
activity_main.xml
.
<Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/play"/>Create a listener in the
onCreate
method of the
Activity
.
Button button = (Button)findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Button button = (Button)view; int id; if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); id = R.string.play; } else { mediaPlayer.start(); //or resume from where we left off id = R.string.pause; } button.setText(id); } }); //mediaPlayer.start(); //Remove this statement.
button
will have to be
final
.
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mediaPlayer.pause(); mediaPlayer.seekTo(0); button.setText(R.string.play); } });
activity_main.xml
,
add a second
TextView
to display our progress in milliseconds.
Give the
TextView
a fixed width and the attributes
android:gravity="right"
and
android:typeface="monospace"
.
Define this class inside of class
MainActivity
.
The
isCancelled
(together with the
cancel
below)
ensures that the
PositionerTask
thread will not outlive the
Activity
that created it.
This will allow a second
Activity
to create a second
PositionerTask
thread.
(There can be only at most one
PositionerTask
thread at any given time.)
private class PositionerTask extends AsyncTask<Void, Integer, Void> { //This method is executed by the second thread. //It gets its arguments from the execute method. @Override protected Void doInBackground(Void... v) { while (!isCancelled()) { int milliseconds = mediaPlayer.getCurrentPosition(); publishProgress(milliseconds); //Sleep for 1/10 of a second. try { Thread.sleep(100L); //milliseconds } catch (InterruptedException interruptedException) { } } } //This method is executed by the UI thread. //It gets its arguments from the publishProgress method. @Override protected void onProgressUpdate(Integer... position) { int milliseconds = position[0].intValue(); Display milliseconds in the second TextView: setText(String.valueOf(milliseconds)); } }Give class
MainActivity
this field:
private PositionerTask positionerTask;In the
onCreate
method of the
MainActivity
,
after creating the
MediaPlayer
,
positionerTask = new PositionerTask(); positionerTask.execute();Add the following code to the start of
onPause
and
onStop
.
The
cancel
causes the
isCancelled
in
doInBackground
to return
true
.
if (positionerTask != null) { positionerTask.cancel(true); positionerTask = null; }
SeekBar
to show our progress.
If the
SeekBar
is ever destroyed and re-created, the
android:saveEnabled="false"
will re-create it with its original
progress
value of zero.
<SeekBar android:id="@+id/seekBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:progress="0" android:saveEnabled="false"/>In
onProgressUpdate
,
//assume that int milliseconds is where we are now. int duration = mediaPlayer.getDuration(); float fraction = (float)milliseconds / duration; SeekBar seekBar = (SeekBar)MainActivity.this.findViewById(R.id.seekBar); seekBar.setProgress((int)(fraction * seekBar.getMax()));
SeekBar
touch-sensitive
so you can drag it to move to a different place in the sound file.
In the
onCreate
method of class
MainActivity
,
SeekBar seekBar = (SeekBar)MainActivity.this.findViewById(R.id.seekBar); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { float fraction = (float)progress / seekBar.getMax(); int duration = mediaPlayer.getDuration(); mediaPlayer.seekTo((int)(fraction * duration)); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } });
MediaPlayer.create
calls
prepare
,
which does not return until the
MediaPlayer
is prepared to play.
This can take several seconds—too long for the UI thread.
Instead of creating the
MediaPlayer
with
create
,
we should create it with
new
,
setDataSource
,
and
prepareAsync
.
prepareAsync
.
always returns immediately
even if the
MediaPlayer
is not yet prepared to play.
It does its work in a separate thread, and then calls the
OnPreparedListener
.
mediaPlayer = new MediaPlayer(); Uri uri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.musette); try { mediaPlayer.setDataSource(this, uri); } catch (IOExceptionToast toast = Toast.makeText(MediaPlayerService.this, iOException.toString(), Toast.LENGTH_LONG); toast.show(); } mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mp.start(); } }); mediaPlayer.prepareAsync(); //will eventually call the onPrepared method of the OnPreparedListener
Activity
object
by changing the orientation of the device (portrait to landscape)
while the app is running.
You’ll have to put an
if (mediaPlayer != null) {around the body of the
while
loop in
doInBackground
.
Note that the music cuts off.
An easy way to keep the
Activity
alive and the music playing
is by adding the following attribute to the
<activity>
element in
AndroidManifest.xml
.
android:configChanges="orientation|screenSize"
To demonstrate that the
Activity
still notices the change in orientation,
even though
Activity
is not killed by it,
add the following method to the
Activity
.
@Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); String s; if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { s = "portrait"; } else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { s = "landscape"; } else { s = "Orientation unknown."; } Toast toast = Toast.makeText(this, s, Toast.LENGTH_LONG); toast.show(); }See Handling the Configuation Change Yourself.
Activity
object immortal.
Let’s attempt to keep the music playing after the death of the
Activity
.
Restore the mortality of the
Activity
by removing the
android:configChanges
attribute from the
<activity>
element.
A change of orientation will now destroy the
Activity
object and the
mediaPlayer
variable inside it, leaving the
MediaPlayer
object vulnerable to garbage collection
because there are no more references to it.
Even if we remove the following code from
onPause
and
onStop
,
if (mediaPlayer != null) { mediaPlayer.release(); mediaPlayer = null; }there will still be no more references to the
MediaPlayer
object.
If the
MediaPlayer
object gets garbage collected, the music will stop.
If the
MediaPlayer
object doesn’t get garbage collected, the music will keep playing.
There is no way to predict whether the garbage collector
will actually collect the
MediaPlayer
.
Even if we try to prod the garbage collector into activity
(which is the last thing in the world we want to do
if we want to keep the music playing)
by saying
System.gc();
in the
onDestroy
method of the
Activity
,
there is still no way to predict whether the
MediaPlayer
will be garbage collected.
So we don’t know whether the music will keep playing.
And there’s another problem.
The next incarnation of the
Activity
object will start the sound file playing again,
so we might have two copies playing simultaneously
(unpleasantly out of sync with each other).
So how can we “uncouple” the lifespan of the
MediaPlayer
from the lifespan of the
Activity
?
See the next example,
MediaPlayerService.