User login



Syndicate content
Add to Google


MusicDroid - Audio Player Part II

SDK Version: 
M3

Introduction

In part one of the MusicDroid tutorial we created a simple MP3 player that will list all of the songs on the SD card and allow the user to select a song to play. Now, we will move the MediaPlayer object into a remote service. This will allow the music to continue in the background while the user is doing other things on their phone.

Click here to download the complete source to reference for this tutorial.

What are services?

Services are components that run in the background and do not display a view for the user to interact with. These components must be listed in the Androidmanifest.xml file with a <service> element. When the Activity that started the service is closed Android will attempt to keep the service running if possible.

To create a new Service you extend the android.app.Service class. Then for your activity to connect to the service it would call either Context.startService() or Context.bindService(). Attempts to connect to services are asynchronous, that means that after you call one of the functions to start your service your code will not wait for it to connect to the service before continuing on. Instead, you must pass in a ServiceConnection object, and then ServiceConnection.onServiceConnected() will be called when the service is connected.

Interacting with services

We need a way to send and receive data from the service to our Activities which are using this service. This is done using an IInterface class. It is quite complicated to create this interface, so Google has made it easy on us. They have created the "Android Interface Definition Language" or more frequently referred to as AIDL. So, to create an Interface for a service you just need to create an AIDL file in your src folder (the same folder with all your other classes), and when you save the .aidl file it will automatically generate a class that extends IInterface based on your AIDL file.

For the MusicDroid project we are going to create a service called MDService (Music Droid Service). We will call our interface for this service MDSInterface. Here is MDSInterface.aidl:

  1. package com.helloandroid.android.musicdroid2;
  2.  
  3. interface MDSInterface {
  4.     void clearPlaylist();
  5.     void addSongPlaylist( in String song );
  6.     void playFile( in int position );
  7.  
  8.     void pause();
  9.     void stop();
  10.     void skipForward();
  11.     void skipBack();
  12. }

This is just a very basic outline of the functions that we will need to control a media player. Unfortunately, there are still some issues with AIDL and Interfaces, so you are kinda limited on the types of the arguments and return values. For example I would have liked to use a function "void setPlaylist( in List songs )", but due to a bug in the current SDK that will give an error. So, I'm making due by using a function "void addSongPlaylist( in String song )".

Notice the that the arguments in the functions say things like "in String song". This signifies the direction of the data, ie you are are reading the value from song, not intending to write the value of song. You can use "out String song" if that was the case.

So when you save this MDSInterface.aidl it will create a file called MDSInterface.java which defines the class MDSInterface. In the class MDSInterface is the is a public abstract class called Stub, and this is what you must create a subclass of in your MDService class.

So, to make this a little easier lets look at the MDService class, with all of the MediaPlayer specific code pulled out for the time being so you can see how the interface is created:

  1. public class MDService extends Service {
  2.  
  3.     private MediaPlayer mp = new MediaPlayer();
  4.     private List<String> songs = new ArrayList<String>();
  5.     private int currentPosition;
  6.  
  7.     private NotificationManager nm;
  8.     private static final int NOTIFY_ID = R.layout.songlist;
...
  1.     @Override
  2.     public IBinder getBinder() {
  3.         return mBinder;
  4.     }
...
  1.     private final MDSInterface.Stub mBinder = new MDSInterface.Stub() {
  2.  
  3.         public void playFile(int position) throws DeadObjectException {
  4.             try {
  5.                 currentPosition = position;
  6.                 playSong(MusicDroid.MEDIA_PATH + songs.get(position));
  7.  
  8.             } catch (IndexOutOfBoundsException e) {
  9.                 Log.e(getString(R.string.app_name), e.getMessage());
  10.             }
  11.         }
  12.  
  13.         public void addSongPlaylist(String song) throws DeadObjectException {
  14.             songs.add(song);
  15.         }
  16.  
  17.         public void clearPlaylist() throws DeadObjectException {
  18.             songs.clear();
  19.         }
  20.  
  21.         public void skipBack() throws DeadObjectException {
  22.             prevSong();
  23.  
  24.         }
  25.  
  26.         public void skipForward() throws DeadObjectException {
  27.             nextSong();
  28.         }
  29.  
  30.         public void pause() throws DeadObjectException {
  31.             Notification notification = new Notification(
  32.                     R.drawable.playbackpause, null, null, null, null);
  33.             nm.notify(NOTIFY_ID, notification);
  34.             mp.pause();
  35.         }
  36.  
  37.         public void stop() throws DeadObjectException {
  38.             nm.cancel(NOTIFY_ID);
  39.             mp.stop();
  40.         }
  41.  
  42.     };
  43. }

The code above is everything that you need to implement the interface. You'll see first on line 40 the getBinder() function. This will return the mBinder variable that is defined starting on line 86. This mBinder variable is the MDSInterface.Stub class that you must create to define all of those functions in the AIDL file.

Tip: In Eclipse to make overriding all these functions easier you can type in line 86 and hit enter. Then in the empty block starting on line 87 you can right click and goto "Source -> Override / Implement Methods"

So, we are implementing all of these interface functions that we defined earlier in the AIDL file. First in playFile(int) we simply set the currentPosition and call playSong(String) passing in the path to the song. This is very similar to the functionality that was built into the MusicDroid ListActivity in MusicDroid back in Part 1.

Here is the the playSong(String) method:

  1. private void playSong(String file) {
  2.     try {
  3.  
  4.         Notification notification = new Notification(
  5.                 R.drawable.playbackstart, file, null, file, null);
  6.         nm.notify(NOTIFY_ID, notification);
  7.  
  8.         mp.reset();
  9.         mp.setDataSource(file);
  10.         mp.prepare();
  11.         mp.start();
  12.  
  13.         mp.setOnCompletionListener(new OnCompletionListener() {
  14.  
  15.             public void onCompletion(MediaPlayer arg0) {
  16.                 nextSong();
  17.             }
  18.         });
  19.  
  20.     } catch (IOException e) {
  21.         Log.e(getString(R.string.app_name), e.getMessage());
  22.     }
  23. }

Note that this is almost identical to the playSong function from Part 1. However, those really paying attention will notice a difference on lines 47-49. As you see we are now going to create a notification each time a song plays.

To do this we will need to add 2 files to the res/drawable folder, playbackstart.png and playbackpause.png. Once they are added to the "res/drawable" folder they will be referenced by their int value for their id. The first argument for the Notification constructor in the top status bar icon to use. Since this is the playSong() function we want to use the playbackstart.png, which can be referred to as "R.drawable.playbackstart". For the second and fourth parameter we are passing in the filename, this is the text that will be displayed on the status bar animation.

After we create out Notification object we will use our NotificationManager to initiate the notification. this is done on line 49, with nm.notify(int,Notification). We pass in an int, NOTIFY_ID that we will use refer to this notification icon when we need to modify or remove it, along with the Notification that we created.

The NotificationManager is initialized in our onCreate() function, and we make sure to remove the icon with nm.cancel(int) when the service is destroyed in the onDestroy() function:

  1. @Override
  2. protected void onCreate() {
  3.     super.onCreate();
  4.     nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
  5.    
  6. }
  7.  
  8. @Override
  9. protected void onDestroy() {
  10.     mp.stop();
  11.     mp.release();
  12.     nm.cancel(NOTIFY_ID);
  13. }
  14.  
  15. @Override
  16. public IBinder getBinder() {
  17.     return mBinder;
  18. }

We must also cancel the notification after playing the last song on the playlist in our very familiar nextSong() function that is moved into our new Service:

  1. private void nextSong() {
  2.     // Check if last song or not
  3.     if (++currentPosition >= songs.size()) {
  4.         currentPosition = 0;
  5.         nm.cancel(NOTIFY_ID);
  6.     } else {
  7.         playSong(MusicDroid.MEDIA_PATH + songs.get(currentPosition));
  8.     }
  9. }

One last function that we have created is prevSong(). We saw it called above on line 107 to handle the work in skipBack() from the interface. Here is prevSong():

  1. private void prevSong() {
  2.     if (mp.getCurrentPosition() < 3000 && currentPosition >= 1) {
  3.         playSong(MusicDroid.MEDIA_PATH + songs.get(--currentPosition));
  4.     } else {
  5.         playSong(MusicDroid.MEDIA_PATH + songs.get(currentPosition));
  6.     }
  7. }

This function is designed to for the skip back functionality that will be shown in Part 3. The idea here is that if you hit the skip back button in the controls then your song will restart, and if you hit it again then it will go the previous song. So here we go to the previous song if we are less than 3 seconds into the song, and we are not listening to the first song on the list (position 0).

So there you have it, a service to handle everything. On the next page we will look at how to bind to this service, and the changes that we need to make to the MusicDroid class to use the new service...

Comments

Submitted by Anonymous (not verified) on Sat, 01/19/2008 - 15:48.

What could be great about this example is closing the application leaving the service alive. I've tried this and if I press the "back key" the emulator kills the activity *and the service*. This is not good, I'd like to listen to music after closing the activity! Any idea about how to do it? Thanks

Submitted by hobbs on Sat, 01/19/2008 - 16:50.

Yeah, I should have called Context.startService() to keep the service running after the activities are done binding to the service. See the third paragraph in my post on the forum:
http://www.helloandroid.com/forum?&c=showthread&ThreadID=24

I'll try to get this added into this tutorial also eventually

Submitted by Anonymous (not verified) on Tue, 02/26/2008 - 03:32.

I am developping some client server application. I am confused whether i will design the connection class as service or simple java class which takes care of http connection, sending and receiving data.
I dont need the connection to be persistant throughout the application(ie. polling type).
What is advantage of Service over simple java class? whic one will be the good option for my requirement ?

Submitted by hobbs on Tue, 02/26/2008 - 12:19.

The only reason to create it as a service would be if the connection needs to be used when your application is not running. So if your only using your connection when your app is running just use a normal class and do it in a seperate thread...good luck!

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
Because you are not logged in, we must determine if you are a human or an Android designed to spam the internet. (Hint: All characters are lowercase)
  _                            
| | __ __ _ _ __ __ _
| |/ / / _` | | '_ \ / _` |
| < | (_| | | |_) | | (_| |
|_|\_\ \__,_| | .__/ \__, |
|_| |___/
Enter the code depicted in ASCII art style.