Sapan Diwakar

Software developer

Follow me on Twitter Check out my code on GitHub View some of my designs on Dribbble Take a look at my Linked In profile

[Tutorial] [How to] Manually create Android Media Player controls

This is an old post and lot of things have changed since then. An updated version is available here.

How to create good looking music player controls for android? Its simple. I'll show how in this post. Android provides very good functions for adding media player to your android apps. for this tutorial, we will be using the MediaPlayer class of android for playback. MediaPlayer class can be used to control playback of audio/video files and streams. An object of this class can fetch, decode, and play both audio and video with minimal setup. It supports several different media sources such as:

  • Local resources
  • Internal URIs, such as one you might obtain from a Content Resolver
  • External URLs (streaming)

Its very simple to use the media player to play audio files. All we need to do is to initialize a media player object, set the audio stream, prepare the audio for playback and then finally start the playback. The following snippet demonstrates how to play an audio file obtained from an external url. The media player takes care of streaming the audio automatically.

String url = "URL"; // your URL here  
MediaPlayer mediaPlayer = new MediaPlayer();  
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);  
mediaPlayer.setDataSource(url);  
mediaPlayer.prepare(); // might take long! (for buffering, etc)  
mediaPlayer.start();  

Preapre method prepares the audio for playback and can therefore take a long time. While the audio is preparing, the android's UI might seem to be non-functional and wouldn't respond to user requests. We will therefore use prepareAsync() method which prepares the media player in background on another thread and notifies the onPreparedListener when the prepare method is complete. Another thing that we want our media player to do is allow the user to work on other things while the media is playing in the background. This can be achieved using a background service rather than playing the music in the app's activity. Let's create a new Service by extending the Service class in android and associate build some functions to perform operations on media player.

public class MusicService extends Service implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnBufferingUpdateListener {

    private static final String ACTION_PLAY = "PLAY";
    private static String mUrl;
    private static MusicService mInstance = null;

    private MediaPlayer mMediaPlayer = null;    // The Media Player
    private int mBufferPosition;
    private static String mSongTitle;
    private static String mSongPicUrl;

    NotificationManager mNotificationManager;
    Notification mNotification = null;
    final int NOTIFICATION_ID = 1;


    // indicates the state our service:
    enum State {
        Retrieving, // the MediaRetriever is retrieving music
        Stopped, // media player is stopped and not prepared to play
        Preparing, // media player is preparing...
        Playing, // playback active (media player ready!). (but the media player may actually be
                    // paused in this state if we don't have audio focus. But we stay in this state
                    // so that we know we have to resume playback once we get focus back)
        Paused
        // playback paused (media player ready!)
    };

    State mState = State.Retrieving;

    @Override
    public void onCreate() {
        mInstance = this;
        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
    }

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent.getAction().equals(ACTION_PLAY)) {
            mMediaPlayer = new MediaPlayer(); // initialize it here
            mMediaPlayer.setOnPreparedListener(this);
            mMediaPlayer.setOnErrorListener(this);
            mMediaPlayer.setOnBufferingUpdateListener(this);
            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            initMediaPlayer();
        }
        return START_STICKY;
    }

    private void initMediaPlayer() {
        try {
            mMediaPlayer.setDataSource(mUrl);
        } catch (IllegalArgumentException e) {
            // ...
        } catch (IllegalStateException e) {
            // ...
        } catch (IOException e) {
            // ...
        }

        try {
            mMediaPlayer.prepareAsync(); // prepare async to not block main thread
        } catch (IllegalStateException e) {
            // ...
        }
        mState = State.Preparing;
    }

    public void restartMusic() {
        // Restart music
    }

    protected void setBufferPosition(int progress) {
        mBufferPosition = progress;
    }

    /** Called when MediaPlayer is ready */
    @Override
    public void onPrepared(MediaPlayer player) {
        // Begin playing music
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public void onDestroy() {
        if (mMediaPlayer != null) {
            mMediaPlayer.release();
        }
        mState = State.Retrieving;
    }

    public MediaPlayer getMediaPlayer() {
        return mMediaPlayer;
    }

    public void pauseMusic() {
        if (mState.equals(State.Playing)) {
            mMediaPlayer.pause();
            mState = State.Paused;
            updateNotification(mSongTitle + "(paused)");
        }
    }

    public void startMusic() {
        if (!mState.equals(State.Preparing) &&!mState.equals(State.Retrieving)) {
            mMediaPlayer.start();
            mState = State.Playing;
            updateNotification(mSongTitle + "(playing)");
        }
    }

    public boolean isPlaying() {
        if (mState.equals(State.Playing)) {
            return true;
        }
        return false;
    }

    public int getMusicDuration() {
        // Return current music duration
    }

    public int getCurrentPosition() {
        // Return current position
    }

    public int getBufferPercentage() {
        return mBufferPosition;
    }

    public void seekMusicTo(int pos) {
        // Seek music to pos
    }

    public static MusicService getInstance() {
        return mInstance;
    }

    public static void setSong(String url, String title, String songPicUrl) {
        mUrl = url;
        mSongTitle = title;
        mSongPicUrl = songPicUrl;
    }

    public String getSongTitle() {
        return mSongTitle;
    }

    public String getSongPicUrl() {
        return mSongPicUrl;
    }

    @Override
    public void onBufferingUpdate(MediaPlayer mp, int percent) {
        setBufferPosition(percent * getMusicDuration() / 100);
    }

    /** Updates the notification. */
    void updateNotification(String text) {
        // Notify NotificationManager of new intent
    }

    /**
     * Configures service as a foreground service. A foreground service is a service that's doing something the user is
     * actively aware of (such as playing music), and must appear to the user as a notification. That's why we create
     * the notification here.
     */
    void setUpAsForeground(String text) {
        PendingIntent pi =
                PendingIntent.getActivity(getApplicationContext(), 0, new Intent(getApplicationContext(), MusicActivity.class),
                        PendingIntent.FLAG_UPDATE_CURRENT);
        mNotification = new Notification();
        mNotification.tickerText = text;
        mNotification.icon = R.drawable.ic_mshuffle_icon;
        mNotification.flags |= Notification.FLAG_ONGOING_EVENT;
        mNotification.setLatestEventInfo(getApplicationContext(), getResources().getString(R.string.app_name), text, pi);
        startForeground(NOTIFICATION_ID, mNotification);
    }
}

Phew! That's a lot of code. Don't worry. I will break everything down for you. The onCreate method is called whenever a new service is created in android. We want our application to be able to communicate with the activity that builds the UI on android. Therefore, we store the instance of service as a static object in the service itself. Another alternative to this is to bind the service to the activity. But that's a bit complex to achieve and I would skip that for now. Since our service will run in background even when the user has not opened our application, we should therefore notify the user about what the service is doing. We will achieve that through notifications which are displayed in the top bar in android. We register the service as the onPreparedListener for music player so that we may be notified about the state of music player.

The most important point with music player is that it has a very complex state diagram and there are several restrictions on which functions can be called in which states. We therefore need to keep track of the state that music player is in. We will use our own enum for that purpose. The onBufferingUpdateListener is used to notify the user about the buffer progress of the song. That almost wraps up the backend that we need to play music. now lets get on to the front end, i.e. our activity that builds the UI.

Android does provide an easy way to have media player controls in android using the MediaController class. This creates floating controls and takes care of interacting with the media player. However, it doesn't offer any functions to modify the look and feel of the UI. We would therefore use our manually created music player controls for our music player. As with all other activities in android, we would first need to prepare out layout for the activity. Lets call the layout music.xml and add it to res/layout in our project.

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

         <ImageView 
            android:id="@+id/thumbnail"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:layout_gravity="center"
            android:adjustViewBounds="true"
            android:maxHeight="175dp"
            android:minHeight="175dp"
            android:scaleType="fitCenter"
            />
         <TextView android:gravity="center_vertical|center_horizontal" android:layout_height="wrap_content" android:text="TextView" android:layout_width="fill_parent" android:id="@+id/songName"></TextView>
          <LinearLayout
        android:id="@+id/linearLayout1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" >

        <ToggleButton
            android:id="@+id/playPauseButton"
            android:layout_width="30px"
            android:layout_height="30px"
            android:layout_marginLeft="5px"
            android:layout_marginTop="5px"
            android:background="@drawable/ic_play_pause"
            android:textOn=" android:textOff=" >
        </ToggleButton>

        <TextView
            android:id="@+id/musicCurrentLoc"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5px"
            android:layout_marginTop="5px" >
        </TextView>

        <SeekBar
            android:id="@+id/musicSeekBar"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5px"
            android:layout_weight="1" >
        </SeekBar>

        <TextView
            android:id="@+id/musicDuration"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5px"
            android:layout_marginTop="5px" >
        </TextView>

        <ImageButton
            android:id="@+id/shufflebutton"
            android:layout_width="35px"
            android:layout_height="35px"
            android:layout_marginLeft="5px"
            android:layout_marginTop="5px"
            android:src="@drawable/ic_music_shuffle" >
        </ImageButton>
    </LinearLayout>
     </LinearLayout>

This layout will give us a basic UI for our android app. If you are using eclipse for development, you can have a look at the graphical layout to see how it might look like:



The toggle button will act as the button for play/pause. The xml selector will take care of changing the image of the button as per its state. Let's define the selector in res/drawable/ic_play_pause.xml

<selector xmlns:android="http://schemas.android.com/apk/res/android">  
    <item android:state_checked="true"
        android:drawable="@drawable/ic_music_pause" /> <!-- pressed -->
    <item android:drawable="@drawable/ic_music_play" /> <!-- default/unchecked -->
</selector>  

We can now associate onClick action to the button to perform play/pause. Add the following code to your activity's onCreate method:

playPauseButton = (ToggleButton) findViewById(R.id.playPauseButton);

playPauseButton.setOnClickListener(new OnClickListener() {  
    @Override
    public void onClick(View v) {
        // Perform action on clicks
        if (playPauseButton.isChecked()) { // Checked - Pause icon visible
            start();
        } else { // Unchecked - Play icon visible
            pause();
        }
    }
});

Now we need to associate our seek bar with the music player. For this, we need to implement the OnSeekBarChangeListener to call respective functions in the media player. Have your activity implement the interface and then add the following code to the activity's onCreate method:

musicSeekBar = (SeekBar) findViewById(R.id.musicSeekBar);  
musicSeekBar.setOnSeekBarChangeListener(this);  

Implementing the interface would require you to have the following method defined in your activity:

@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {  
    if (fromUser) {
        seekTo(progress);
    }
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {  
    // TODO Auto-generated method stub

}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {  
    // TODO Auto-generated method stub

}

We would also need to check the status of music every second and update the seek bar manually. To do this, we start a new thread and poll the service for music progress, buffer progress and music duration every second.

new Thread(new Runnable() {  
    @Override
    public void run() {
        int currentPosition = 0;
        while (!musicThreadFinished) {
            try {
                Thread.sleep(1000);
                currentPosition = getCurrentPosition();
            } catch (InterruptedException e) {
                return;
            } catch (Exception e) {
                return;
            }
            final int total = getDuration();
            final String totalTime = getAsTime(total);
            final String curTime = getAsTime(currentPosition);

            musicSeekBar.setMax(total); //song duration
            musicSeekBar.setProgress(currentPosition);  //for current song progress
            musicSeekBar.setSecondaryProgress(getBufferPercentage());   // for buffer progress
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (isPlaying()) {
                        if (!playPauseButton.isChecked()) {
                            playPauseButton.setChecked(true);
                        }
                    } else {
                        if (playPauseButton.isChecked()) {
                            playPauseButton.setChecked(false);
                        }
                    }
                    musicDuration.setText(totalTime);
                    musicCurLoc.setText(curTime);
                }
            });
        }
    }
}).start();

Notice the runOnUiThread method. This method is necessary to update activity's UI components in a thread-safe way. We have almost everything ready to have our first music player ready. But to play the music, we would need to start the music service.

MusicService.setSong(songUrl, songTitle, songPicUrl);  
startService(new Intent("PLAY"));  

I have left a few things to fill in yourself like displaying the album art of the song on the image view and implementing the functionality of the shuffle next button. I hope this tutorial helps you in building an easy to use android media player. Here's a screenshot of what I was able to achieve with this: