Thursday, December 17, 2009

MusicDroid - Audio Player Part II

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:


      package com.helloandroid.android.musicdroid2;

       

      interface MDSInterface {

              void clearPlaylist();

              void addSongPlaylist( in String song );

              void playFile( in int position );

       

              void pause();

              void stop();

              void skipForward();

              void skipBack();

      }

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:


      public class MDService extends Service {

       

              private MediaPlayer mp = new MediaPlayer();

              private List<String> songs = new ArrayList<String>();

              private int currentPosition;

       

              private NotificationManager nm;

              private static final int NOTIFY_ID = R.layout.songlist;

...


              @Override

              public IBinder getBinder() {

                      return mBinder;

              }

...


              private final MDSInterface.Stub mBinder = new MDSInterface.Stub() {

       

                      public void playFile(int position) throws DeadObjectException {

                              try {

                                      currentPosition = position;

                                      playSong(MusicDroid.MEDIA_PATH + songs.get(position));

       

                              } catch (IndexOutOfBoundsException e) {

                                      Log.e(getString(R.string.app_name), e.getMessage());

                              }

                      }

       

                      public void addSongPlaylist(String song) throws DeadObjectException {

                              songs.add(song);

                      }

       

                      public void clearPlaylist() throws DeadObjectException {

                              songs.clear();

                      }

       

                      public void skipBack() throws DeadObjectException {

                              prevSong();

       

                      }

       

                      public void skipForward() throws DeadObjectException {

                              nextSong();

                      }

       

                      public void pause() throws DeadObjectException {

                              Notification notification = new Notification(

                                              R.drawable.playbackpause, null, null, null, null);

                              nm.notify(NOTIFY_ID, notification);

                              mp.pause();

                      }

       

                      public void stop() throws DeadObjectException {

                              nm.cancel(NOTIFY_ID);

                              mp.stop();

                      }

       

              };

      }

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:


      private void playSong(String file) {

              try {

       

                      Notification notification = new Notification(

                                      R.drawable.playbackstart, file, null, file, null);

                      nm.notify(NOTIFY_ID, notification);

       

                      mp.reset();

                      mp.setDataSource(file);

                      mp.prepare();

                      mp.start();

       

                      mp.setOnCompletionListener(new OnCompletionListener() {

       

                              public void onCompletion(MediaPlayer arg0) {

                                      nextSong();

                              }

                      });

       

              } catch (IOException e) {

                      Log.e(getString(R.string.app_name), e.getMessage());

              }

      }

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:


      @Override

      protected void onCreate() {

              super.onCreate();

              nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

             

      }

       

      @Override

      protected void onDestroy() {

              mp.stop();

              mp.release();

              nm.cancel(NOTIFY_ID);

      }

       

      @Override

      public IBinder getBinder() {

              return mBinder;

      }

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:


      private void nextSong() {

              // Check if last song or not

              if (++currentPosition >= songs.size()) {

                      currentPosition = 0;

                      nm.cancel(NOTIFY_ID);

              } else {

                      playSong(MusicDroid.MEDIA_PATH + songs.get(currentPosition));

              }

      }

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():


      private void prevSong() {

              if (mp.getCurrentPosition() < 3000 && currentPosition >= 1) {

                      playSong(MusicDroid.MEDIA_PATH + songs.get(--currentPosition));

              } else {

                      playSong(MusicDroid.MEDIA_PATH + songs.get(currentPosition));

              }

      }

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...

<!--pagebreak-->

Using Services

In order to use a remote service first we must add a line to our AndroidManifest.xml file inside our application tag to define our service, here is that line:

<service class=".MDService" android:process=":remote" />

Now we can use this service in our MusicDroid ListActivity class that we created in the previous tutorial. We will now try to bind to our newly created service in the onCreate(Bundle) function of our ListActivity:


      public class MusicDroid extends ListActivity {

       

              public static final String MEDIA_PATH = new String("/sdcard/");

              private List<String> songs = new ArrayList<String>();

              private MDSInterface mpInterface;

             

              @Override

              public void onCreate(Bundle icicle) {

                      super.onCreate(icicle);

                      setContentView(R.layout.songlist);

                      this.bindService(new Intent(MusicDroid.this,MDService.class),

                                      null, mConnection, Context.BIND_AUTO_CREATE);

              }

      }

We are binding to our service using a new Intent object, and we are creating the Intent object using the direct reference to a class. You could use a Action and Category to bind to a service also, but for this we can just refer to it directly. We pass in the mConnection variable, this is a ServiceConnection object, and when the service is connected it will call the mConnection.onServiceConnected method, and when disconnected it will call mConnection.onServiceDisconnected. Here is how we define mConnection:


      private ServiceConnection mConnection = new ServiceConnection()

      {

              public void onServiceConnected(ComponentName className, IBinder service) {

                      mpInterface = MDSInterface.Stub.asInterface((IBinder)service);

                      updateSongList();

              }

       

              public void onServiceDisconnected(ComponentName className) {

                      mpInterface = null;

              }

      };

So, when the service is connected we can initialize our mpInterface class on line 74. Now that we have this mpInterface class we need to populate the playlist by adding each song using mpInterface.addSongPlaylist(String), we do this in the updateSongList() function:


      public void updateSongList() {

              try {

                      File home = new File(MEDIA_PATH);

                      File fileList[] = home.listFiles(new Mp3Filter());

                      mpInterface.clearPlaylist();

                      if (fileList != null) {

                              for (File file : fileList ) {

                                      songs.add(file.getName());

                                      mpInterface.addSongPlaylist(file.getName());

                              }

             

                              ArrayAdapter<String> songList = new ArrayAdapter<String>(this,

                                              R.layout.song_item, songs);

                              setListAdapter(songList);

                      }

              } catch(DeadObjectException e) {

                      Log.e(getString(R.string.app_name), e.getMessage());

              }

      }

This function is pretty much identical to it's first incarnation in the previous tutorial. The only thing we needed to add is the mpInterface.addSongPlaylist(String) call on line 49 so that our Service is aware of the entire playlist, so that it can play the next song without having help from another Activity.

And lastly, we must play a song when a user clicks on a song, so here is that method:


      @Override

      protected void onListItemClick(ListView l, View v, int position, long id) {

              try {

                      mpInterface.playFile(position);

              } catch(DeadObjectException e) {

                      Log.e(getString(R.string.app_name), e.getMessage());

              }

      }

We simply use the mpInterface.playFile(int) method that we designed earlier in this tutorial.

So now we have a playlist ListActivity and a Service to handle playing the music, but we still have no way to control the music. In the next section we will create a graphical user interface for the controls, introducing ImageViews and Animation...

No comments:

Post a Comment