Chapter 8. Movies and Music

Support for media, both audio and visual, will hopefully be an essential part of your next immersive Android application. Both industries are on the forefront of mobile usage, and many modern mobile applications are leveraging this technology to build thoughtful and engaging applications. To this end, I’ll get you started with the basics for Android’s video and music libraries in this chapter. I’ll also point out a few things you’ll need to be aware of as you build a background media playback service: movies, music playback, background service, and what to watch out for.

Movies

Movie playback on an Android device boils down to the VideoView class. In this section, I’ll use a simple example application that will play through every video saved on a phone’s SD card. Here is the general process:

Image I’ll use the ContentProvider (something you’ll remember from our brief discussion in Chapter 6 when we uploaded the most recent photo) to request every video saved to the user’s external card.

Image After loading a Cursor (Android’s query result data object) with all the device’s videos, I’ll need a method to play the next one.

Image I’ll set up an activity as a listener so that when video playback is complete, I can call my playNextVideo method and move on to the next video in the cursor.

Image Last, I’ll clean up after my cursor when the user leaves the playback screen.

Before I can do any of these things, however, I need to place a VideoView on my main layout to work with.

Adding a VideoView

Placing a VideoView onscreen is as simple as adding it to your XML layout. Here’s what my main.xml file now looks like:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="vertical"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <VideoView
     android:id="@+id/my_video_view"
     android:layout_gravity="center"
     android:layout_width="match_parent"
     android:layout_height="match_parent" />

</LinearLayout>

Once the video view is in the screen’s layout, you can retrieve it, as you would any other view, with findViewById. I’m going to need access to the video view later when it’s time to switch videos. Instead of retrieving the view with findViewById each time, I’ll add a private data member to my Activity class. Next, I’ll need to configure the video player.

Setting Up for the VideoView

In the following code listing, I’m doing many normal onCreate sorts of things.

VideoView mVideoView;
@Override
public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);

   mVideoView = (VideoView) findViewById(R.id.my_video_view);
   mVideoView.setOnCompletionListener(this);

}

Here I’m setting the content view, retrieving and caching the video view with findViewById, and setting my activity as the video view’s onCompletionListener.

In order for the activity to pass itself into the VideoView as the onCompletionListener, I have to extend OnCompletionListener and implement my own onCompletion method. Here is what I’ve added to my activity:

public class MainActivity extends Activity
     implements OnCompletionListener{
   @Override
   public void onCompletion(MediaPlayer mp) {
   }
   //Rest of Activity code omitted
}

I now have a configured, yet very simplistic, video player. You’ll most likely want to have visual onscreen controls. Android’s VideoView allows you to implement and set up a MediaController for the VideoView class. If you’re looking to go further into video playback after this chapter, this would be an excellent place to start.

Getting Media to Play

Now that my video view is ready to roll, I can start hunting for things for it to actually play. For this simple example, I’m going to play every video on the device one after another until they’re all finished. To achieve this, I’ll use Android’s media ContentProvider (accessed with a call to getContentResolver). I’ll show you the code and then dig into some specifics. Here’s what onCreate looks like with the new code to fetch a cursor with all the media objects:

@Override
public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);

   mVideoView = (VideoView) findViewById(R.id.my_video_view);
   mVideoView.setOnCompletionListener(this);

   requestVideosFromPhone();
}

private void requestVideosFromPhone() {

   new AsyncTask<Void, Void, Cursor>() {
      @Override
      protected Cursor doInBackground(Void... params) {

         String projection[] = new String[] { Video.Media.DATA };
         final Cursor c = getContentResolver().query(
              Video.Media.EXTERNAL_CONTENT_URI,
              projection,
              null, null, null);
         return c;
      }

       @Override
       protected void onPostExecute(Cursor result) {
          if (result != null) {
              mMediaCursor = result;
              mMediaCursor.moveToFirst();
              playNextVideo();
          }
      }
   }.execute();
}

As you can see, I’m still fetching and caching the video view, but now I’m firing off an AsyncTask that will query Android’s ContentProvider for the Video.Media.DATA column of all media rows that are videos. Don’t let it scare you that I created an anonymous inner AsyncTask by calling new AsyncTask from inside the function. This technique is a great way to kick things off the main thread without having to declare an entire subclass class for it. That query inside the AsyncTask is fairly simple in that I want all videos on the external drive (SD card), and I only care about the data column for all those rows. This column for any particular row should always contain the path to the actual media content on disk. It’s this path that I’ll eventually hand off to the VideoView for playback.

Note that it is possible to pass URIs to the video view. The video playback mechanism will find the path to the object for you. I would, however, like to show you the harder way so that you’ll be more informed.

The Cursor object (a class Android uses to wrap database query responses) can be null if the external media card is removed (or mounted into USB storage mode), so I’ll need to check for a null cursor or one with no results before moving on. Typically in this case, I’d display a message to the user about their SD card being unavailable, but I’ll leave that task up to your imagination.

Last, I’ll get and cache the column index for the data row. This will make it easier for my playNextVideo method to interact with the cursor’s results.

Adding Permissions

Starting in Android 4.4, the permission READ_EXTERNAL_STORAGE, which has been in since API 16, is being enforced. To future proof your application and let your users know what permissions you are using, add this permission into your manifest:

<manifest>
   ...
   <uses-permission
      android:name="android.permission.READ_EXTERNAL_STORAGE"/>
   ...
</manifest>

Loading and Playing Media

At this point, you have a video view, a cursor full of media to play, and a listener configured to tell you when media playback is finished. Let’s put the final piece into the game, the code in playNextVideo:

private void playNextVideo() {
  if (!mMediaCursor.isAfterLast()) {
     final String path = mMediaCursor.getString(
        mMediaCursor.getColumnIndexOrThrow(
          Video.Media.DATA));

      Toast.makeText(getBaseContext(),
        "Playing: " + path, Toast.LENGTH_SHORT).show();
      mVideoView.setVideoPath(path);
      mVideoView.start();

      // Advance the cursor
      mMediaCursor.moveToNext();
  } else {
      Toast.makeText(getBaseContext(),
         "End of Line.", Toast.LENGTH_SHORT).show();
  }
}

My first task is to check if the cursor is after the last piece of media and make sure we haven’t run out of stuff to play. When I know I’ve got a valid row from the cursor, I can tell the video view what it should render next. Video views can accept both a path defined as a string as well as the URI for a piece of media declared in the content provider. As I mentioned earlier, the data column of the cursor contains the file path to the media itself. I’ll pull this out of the Cursor, hand it off to the video view, and then start playback. After playback has started, I will advance the cursor to the next position so that it is ready for checking when the current video finishes.


Image Tip

You’re not limited to just file paths—you can hand the video view a URL, and it will query and play the media found there.


Recall that earlier I registered my activity as the OnCompletionListener for the video view so that when a video is finished it will notify me via the OnCompletion call. In that method, I just need to call back into my playNextVideo code and we’re playing!

@Override
public void onCompletion(MediaPlayer mp) {
   playNextVideo();
}

At this point, the pieces are in place, videos play, and you’re almost done!

Cleanup

You’ve seen me do this at least once before, but it’s always important to close any cursors you request from the content provider. In past cases, I’ve requested data with a query, pulled out the relevant information, and immediately closed the cursor. In this case, however, I need to keep the cursor around for when the video change has to occur. This does not get me off the hook; I still need to close it down, so I’ll need to add that code to my activity’s onDestroy method:

@Override
public void onDestroy() {
   if (mMediaCursor != null) {
       mMediaCursor.close();
   }
}

The Rest, as They Say, Is Up to You

I’ve shown you the very basics of loading and playing video content. Now it’s time for you to explore it on your own. Think about loading a video from a remote location (hint: encoding a URL as a URI) or building a progress bar (hint: getCurrentProgress calls on the VideoView).

Because errors are to media playback as swearing is to sailors, registering for an onErrorListener is never a bad idea. Android will, if you pass it a class that implements the OnErrorListener interface, tell you if it has hiccups playing your media files. As always, check the documentation for more information on playback.

Music

Music playback, in general, revolves around the MediaPlayer class. This is in a sense very similar to what you’ve just done with the video view (except you don’t need a View object to render into).

Media players, if used to play music, should end up in their own services, with one notable exception: games and application sound effects. Building a sound effect example will make for a very simple way to get into the basics of audio playback.

MediaPlayer and State

You do not simply walk into Mordor. Similarly, you do not simply run about playing things willy-nilly. It requires care, attention to detail, and an understanding of the media player’s states. Here they are, in the order you’re most likely to encounter them:

Image Idle. In this state, the MediaPlayer doesn’t know anything and, consequently, cannot actually do anything. To move on to the initialized state, you’ll need to tell it which file it’s going to play. This is done through the setDataSource method.

Image Initialized. At this point, the media player knows what you’d like it to play, but it hasn’t acquired the data to do so. This is particularly important to understand when dealing with playing remote audio from a URL. Calling prepare or prepareAsync will move it into the prepared state. It will also load enough data from either the file system or the Internet to be ready for playback.

Image Prepared. After calling prepare or prepareAsync (and getting a callback), your media player is ready to rock! At this point, you can call seek (to move the playhead) or start (to begin playback).

Image Playing. Audio is pumping, people are dancing (OK, maybe not), and life is good. In this state, you can call pause to halt the audio or seek to move the play position. You end the party by calling stop, which will move the media player back to the initialized state.

Just because you’ve run out of media to play doesn’t mean your player drops into the idle state. It will keep the current file loaded if you want to call start (which will restart the audio from the beginning) or seek (to move the playhead to a particular place). Only when you call stop or reset does the MediaPlayer clear its buffers and return to the initialized state, ready for you to call prepare again.

Playing a Sound

At its most straightforward, media playback is actually quite easy. Android gives you helper methods to shepherd your media player from the idle state to the prepared state if you can specify a file or resource id right away. In this example case, you can record your own WAV file or use the beeeep file that I included in the example project. When you have the sound file, add the file to a new resource folder called raw/, which you should create at /res/raw/. Any asset that you want to be placed in your application, such as text files, sound files, or anything else that doesn’t make sense for the normal resource hierarchy, should exist here so the application can reference it and load it directly.

Further, I’ve added a button (which you should be a pro at by now) that, when pressed, will play the recorded audio. Once the button is defined (R.id.beep_button) in the main.xml layout file and the audio beeeep.wav file is placed in the raw/ folder, the following code should work like a charm:

MediaPlayer mBeeper;
@Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      Button beep = (Button) findViewById(R.id.beep_button);
      beep.setOnClickListener(this);
      mBeeper = MediaPlayer.create(this, R.raw.beeeep);            }

As you can see, I’m retrieving the beep_button from the activity_main.xml layout (which I told the activity would be my screen’s layout) and setting my activity as the click listener for the button. Last, I use the media player’s create helper method to initialize and prepare the media player with the beeeep.wav file from the raw/ directory.

Playing a Sound Effect

Remember that loading media, even from the res/ folder, can take some time. With this in mind, I’ve added the media player as a private data member to my Activity class. This means I can load it once in my onCreate method and then use it every time the user presses the button. Speaking of button pressing, here’s the code to play the sound effect when the button is pressed:

@Override
public void onClick(View v) {
   mBeeper.start();
}

Cleanup

In order to be a good citizen, there’s one more step you need to take: releasing your resources! That’s right, when your activity closes down, you need to tell the media player that you’re finished with it, like so:

@Override
public void onDestroy(){
   if(mBeeper != null){
      mBeeper.stop();
      mBeeper.release();
      mBeeper = null;
   }
}

Checking for null before performing the cleanup is a good precaution. If, for whatever reason, there isn’t enough memory to load the resource or it fails for another reason, you won’t have any null pointer exceptions on your hands.

It Really Is That Simple

There’s nothing complex about simple sound effect playback. Once you have a media player in the prepared state, you can call start on it as many times as you like to produce the desired effect. Just remember to clean it up once you’re finished. Your users will thank you later. Now let’s move on to something a little more tricky.

Longer-Running Music Playback

You didn’t think I’d let you off that easy, did you? Remember two chapters ago when I showed you how to build a service in a separate process by using an AIDL file? I told you you’d need it for longer-running music playback. Here’s a quick recap of that process:

1. Create a service, define a few methods to control music playback, and declare the service in your manifest.

2. Create an Android Interface Definition Language (AIDL) file to define how the service will talk to any of the activities.

3. Bind an activity to the service, and, when the callback is hit, save the binder in order to call the service’s methods.

If most, or any, of those steps don’t make sense, take a gander back at Chapter 6.

In this section, I’ll show you how to turn the empty service into one that actually plays music in the background. The example music service will have methods to pause, play, set a data source, and ask it what the title of the current song is. To show you this service in practice, I’ll have my activity play the most recently added song on my new background music service.

Binding to the Music Service

There is a little overlap here with Chapter 6, but it’s worth covering how this works again before I dive into the music service itself. I’ve added the following code to the onCreate method of our handy MusicExampleActivity.

public void onCreate(Bundle savedInstanceState) {
   //Button code omitted
   Intent serviceIntent = new Intent(
      getApplicationContext(), MusicService.class);
   startService(serviceIntent);
   bindService(serviceIntent, this, Service.START_STICKY);
}

You’ll notice that I’m actually starting the service before I attempt to bind to it. Although you can ask the bind service call to start the service for you, this is not a good idea when building a music service. That’s because when you unbind from the service, which you must do whenever your activity is destroyed, it will shut the service down. This, as you might imagine, would be bad if you’d like music to continue playing in the background after your activity has closed.

Finding the Most Recent Track

In the activity’s onResume() method I’ve added a call to a function named requestMostRecentAudio, which will query Android’s content provider for the most recent track. Since this is in onResume, this will be called every time the app returns from the background.

I’ve also added a button to my screen that, when the query returns with some media, will become enabled via a BroadcastReceiver catching an intent from the MusicService, allowing you to click and play it (we will cover that in a few pages). Assuming it has both a track to play and a valid service, I can start playing music. Here’s the code that runs when the application hits onResume:

@Override
protected void onResume() {
   super.onResume();

   // Register IntentFilters to listen for Broadcasts from the
   // MusicService
   IntentFilter filter = new IntentFilter();
   filter.addAction(MusicService.PLAYING);
   filter.addAction(MusicService.PLAYBACK_PREPARED);
   registerReceiver(mPlayPauseReceiver, filter);

   // Disable the button until media is prepared
   mPlayPauseButton.setEnabled(false);
   mPlayPauseButton.setText("Preparing...");

   requestMostRecentAudio();
}
/**
* Request the most recently added audio file from the system
*/
private void requestMostRecentAudio() {

   // The columns which to return
   String[] projection = new String[] {
      MediaStore.Audio.Media._ID,
      MediaStore.Audio.Media.DATE_ADDED };

   // The order in which to return the results
   String sortOrder =
      MediaStore.Audio.Media.DATE_ADDED + " Desc Limit 1";

   CursorLoader cursorLoader = new CursorLoader(this,
         MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
         projection,
         null,
         null,
         sortOrder);

   cursorLoader.registerListener(R.id.id_music_loader, this);
   cursorLoader.startLoading();
}

In this code, I’m using a cursor loader to fetch my rather bizarre query. I’m asking the content provider for all possible audio tracks, but I’m sorting the results in descending order of their addition date (that is, when they were added to the phone) and limiting it to one result. This will, when the loader finishes, return a cursor with one record (the most recent song added to the library).

Listening for Intents

To make sure our UI properly reflects when media is ready to be played (Figure 8.1), we are going to listen for a broadcast intent from the service. There are a few ways to accomplish this task, but I find that the broadcast intent communication paradigm is an effective way to get messages from one part of your app to another.

Image

Figure 8.1 Keep the button disabled until a successful broadcast from the music service is received.

/**
 * Catch Broadcasts from the MediaService indicating what state the button
should reflect
 */
private BroadcastReceiver mPlayPauseReceiver = new BroadcastReceiver() {
   @Override
   public void onReceive(Context context, Intent intent) {
      final String action = intent.getAction();
      if (MusicService.PLAYING.equals(action)) {

          try {
             mPlayPauseButton.setText(
                "Pause " + mService.getSongTitle());
          } catch (RemoteException e) {
             e.printStackTrace();
          }

       } else if (MusicService.PLAYBACK_PREPARED.equals(action)) {
          mPlayPauseButton.setText("Play");
          mPlayPauseButton.setEnabled(true);
       }
   }
};

The intent communication is pretty straightforward, and in this instance we are using the action of the broadcast intent to determine what behavior we should be applying to the button. If the action is MusicService.PLAYING, indicating that the music is playing, we want to show the pause button with the name of the song that is playing. Otherwise, we want to show the user the play button and make sure it’s enabled so they can click it.

When the cursor with my data is ready, my activity’s onLoadComplete will be called, at which point I can tell my music service what to play:

@Override
public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {

   if (loader.getId() == R.id.id_music_loader) {
      if (cursor == null || !cursor.moveToFirst()) {
         Toast.makeText(getBaseContext(),
           "No Music to Play",
            Toast.LENGTH_LONG).show();
          return;
       } else if (mService == null) {
          Toast.makeText(getBaseContext(),
            "No Service to play Music!", Toast.LENGTH_LONG).show();
          return;
       }
       try {
          long id = cursor.getLong(
             cursor.getColumnIndexOrThrow(
                MediaStore.Audio.Media._ID));
          mService.setDataSource(id);
       } catch (RemoteException e) {
          Log.e(TAG, "Failed to set data source", e);
       } finally {
          cursor.close();
       }
   }
}

When the loader hits my callback, I’ll first need to check if it actually found any data. By checking if(!cursor.moveToFirst()), I’m moving to the first and only record in the cursor, but I’m also making sure there actually is a record for me to look at. (If the cursor is empty, moveToFirst will return false.)

Next, I’ll need to make sure that my service bind in the onCreate method was successful. Once I know that the service is valid, I’ll finally get the media ID by calling getLong on the cursor to acquire the media’s unique ID. It is with this ID that I’ll tell the music service via setDataSource what it should play.

Playing the Audio in the Service

Now that you can see how the ID is acquired, I’ll switch over to the music service and show you how the handoff occurs over there. Here’s what setDataSource looks like from the service’s perspective (which we defined the skeleton for earlier):

private MediaPlayer mPlayer;
private String mCurrentTitle;
private String mDataSource;

private void setDataSource(long id) {

   // We only want these columns
   String[] projection = {
      MediaStore.Audio.Media._ID,
      MediaStore.Audio.Media.DATA,
      MediaStore.Audio.Media.TITLE };

   // The "WHERE" clause, excluding there WHERE
   String selection = MediaStore.Audio.Media._ID + "=?";

   // The arguments for the selection
   String[] selectionArgs = new String[] { String.valueOf(id) };

   final Cursor c = getContentResolver().query(
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
        projection,
        selection,
        selectionArgs,
        null);

   if (c != null && c.moveToFirst()) {
       try {
          mDataSource = c.getString(
            c.getColumnIndexOrThrow(
               MediaStore.Audio.Media.DATA));
            mCurrentTitle = c.getString(
              c.getColumnIndexOrThrow(
                 MediaStore.Audio.Media.TITLE));
            prepareMedia();

         } finally {
           c.close();
         }
     }
}
/**
 * Prepares Media for playback
 */
private void prepareMedia() {
   mPlayer.reset();
   try {

      mPlayer.setDataSource(mDataSource);
   } catch (IllegalStateException e) {
       // In a bad state!
       e.printStackTrace();
   } catch (IOException e) {
       // Couldn't find the file!
       e.printStackTrace();
   }
   // Reset the player back to the beginning
   mPlayer.prepareAsync();
   mPlayer.setOnPreparedListener(
     new MediaPlayer.OnPreparedListener() {
     @Override
     public void onPrepared(MediaPlayer mp) {
         // Send broadcast so the activity can update its
         // button text
         sendBroadcast(new Intent(PLAYBACK_PREPARED));
     }
   });
}

While this code is a little bit long, most of it should look similar to tasks you’ve already done.

1. I’m querying the content provider for the id passed into the method.

2. I’m making sure that the music is actually there first by checking if the cursor came back null (which can happen if the SD card has been removed). I’m also checking that there’s a valid row in the cursor.

3. When I’m sure the cursor is valid and contains the data for a song to play, I call prepareMedia, which is a method I created to prepare the media player for playback with the new data source. This method will reset the player (in case it was already playing something else), set the data source for it, and tell the media player to prepare. Once these methods are finished, the media player is ready to start playback.

With that, your service is ready to go when the activity calls play.

Play time

Now that the service has a data source and is prepared, the activity can call play, which will trigger the following code to run and post a notification to the status bar (Figure 8.2):

/**
 * Begin or resume playback
 */
private void play() {

   if (mPlayer != null) {
      mPlayer.start();
   }
   // Place a notification in the bar so we can run without being
   // killed by the system
   Notification notification =
   buildSimpleNotification("Now Playing ", getSongTitle());
   startForeground(1, notification);

   // Send broadcast so the activity can update its button text
   sendBroadcast(new Intent(PLAYING));
}

Image

Figure 8.2 Recent audio will show in the notification bar while it’s playing.

You’ll need to start media playback and make sure the service switches to running in the foreground. buildSimpleNotification is a method I defined back in Chapter 6 that builds an icon for the status bar to keep your service alive. If you need a refresher on how to put services into foreground mode, review Chapter 6 or look at the sample code for this chapter.

Lastly, I send a broadcast intent letting anyone who is listening know that we have started playing. This is how we will adjust the state of our play button back in our main activity.

All good things must end end... hopefully

At some point, the music has to stop—either because it’s run out of songs to play or because the user has killed it off. Because I want the service to last beyond the run of my activity, I’ll need to have the service close itself down after it has finished playing its media. You can find the appropriate time to shut down the service by registering it as an onCompletionListener with the media player. The line of code looks like this:

mPlayer.setOnCompletionListener(this);

You can call it at any point after the player is created. Of course, your service will need to implement OnCompletionListener and have the correct onCompletion method.

@Override
public void onCompletion(MediaPlayer mp) {
   performStop();
}
/**
 * Stop and reset the playback
 */
private void performStop() {

   if (mPlayer.isPlaying()) {
      mPlayer.stop();
   }

   // Prepare media for the next playback
   prepareMedia();

   // Remove our notification since we aren't playing
   stopForeground(true);
   stopSelf();
}

This means that once the media is finished, the service will call stop on itself, which, because of the lifecycle of the service, will trigger Android to call the service’s onDestroy method—the perfect place to clean up. Once the cleanup is finished, the service will be deallocated and cease running.

Cleanup

Cleanup is essential when dealing with media players. If you don’t handle this section correctly, a lot of the device’s memory can get lost in the shuffle. Here’s the onDestroy method where I clean up the media player:

@Override
public void onDestroy(){
   super.onDestroy();
   if(mPlayer != null) {
      mPlayer.stop();
      mPlayer.release();
   }
}

I must be careful, because an incorrect data source ID or bad media file could leave either of these references null, which would crash the service quite handily when I try to shut them down.

Interruptions

When you’re writing music software for Android devices, it’s vitally important that you remember that the hardware on which your software is running is, in fact, a phone. This means you’ll need to watch out for several things.

Image Audio focus. You’ll need to use the AudioManager class (introduced in Android 2.2) to register an audio focus listener, because other applications may want to play alerts, navigational directions, or their own horrible music. This is vital to making an Android music playback application play nice with the rest of the system.

Image Controls built into headphones. You’ll want your service to register to receive headset button intents through your manifest (or at runtime when your service is started). At the very least, set up your service to pause when the headset control is clicked.

Image Phone calls. By watching the phone’s call state either through the Telephony Manager or with the audio focus tools, you absolutely must watch for incoming phone calls. You must stop all audio when the phone rings. Nothing will enrage your users (and hurt your ratings) more than not accommodating phone calls.

Image Missing SD card. You’ll want to make sure your app handles a missing or removed SD card correctly. Users can mount their external cards as removable drives with the USB cable at any point. Android will alert you if you listen for the ACTION_MEDIA_REMOVED intent.

This might seem like a lot of things to look out for (and it is), but never fear, the developers at Google have released an open-source media player (which they ship with the Android source code) that can be a great guide for dealing with this stuff. As always, the documentation will have a lot on the subject as well.

Wrapping Up

In this chapter, I showed you how to

Image Play a simple video

Image Play a sound effect when a button is pressed

Image Take a previously created service interface and create a functional media player from it

You should now be comfortable with the essentials of media playback. If you’re looking to go further with videos (which I hope you are), you’ll want to look into using a controller to modify the state of the video view.

Your next step to expand the media playback service is to think about how you’d pass arrays of IDs (playlists) and how you’d deal with updating those playlists on the fly (as users change them).

Android can be a very powerful media platform if you’re careful and treat it with care. Go forth and make a crop of better music players—if for no other reason than so I can use them myself.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.8.206