Why listen to music from your device using media players created by other developers when you yourself are a developer. Yeah those media players are probably better than what you can build with your hands but you gotta start somewhere. Start with this simple project we’ve created for you. It’s a free project and is designed to teach you how to do the following:

  1. Load all songs from any android device and list them in recyclerview.
  2. Play a song when play button is clicked.
  3. Show song progress as the music plays.
  4. Pause a song when user clicks pause button.
  5. Jump/Seek to given period of song when user clicks the seekbar/progressbar.
  6. Automatically move to next song in the list when current song finishes.
  7. When we reach end of the list, move to the first song and start again.
  8. Navigate to next song when next button is clicked.
  9. Navigate to previous song when previous button is clicked.
  10. Design Music Player Controls widget.
  11. MVVM – Model View ViewModel.

This project is written in Java and utilises the inbuilt MediaPlayer APIs. In short we want to give you a template to use if you are a complete newbie when it comes to working with MediaPlayer.

Demo

Here is the demo:

Let’s start.

 

Creating Layouts

We will start by designing our layouts. Our layouts will be pretty simple. We have three layouts:

  1. model.xml – Our model or single-song layout. Will represent a single recyclerview item.
  2. content_home and activity_home.xml – The former will be included in the latter. Basically we include a recyclerview as well as player widget. We’ve included them in CollapsingToolbar Layout.

(a). model.xml

Model Layout will inflated into a single itemview in our recyclerview. It represents a row in our recyclerview. We need it to be very simple but more importantly to be able to contain our Play/Pause button. A user clicks the play button and we play the current song, the one clicked by the user. Meanwhile we turn that button from play to pause. If the user clicks the Pause button, we now pause the media player. If the user clicks play, we play it and so on.

Here is the design view:

Here is the code for this layout:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <androidx.cardview.widget.CardView
        android:id="@+id/songCard"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:elevation="100dp">

        <!--android:background="#00bcd4"-->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/cardview_light_background"
            android:orientation="horizontal"
            android:padding="10dp">

            <ImageButton
                android:id="@+id/playBtn"
                android:layout_width="45dp"
                android:layout_height="45dp"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="4dp"
                android:layout_marginRight="8dp"
                android:backgroundTint="@color/icons"
                android:foregroundGravity="center_vertical"
                android:src="@android:drawable/ic_media_play"
                android:tint="@color/colorAccent" />

            <TextView
                android:id="@+id/titleTV"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:foregroundGravity="center_horizontal"
                android:text="Title"
                android:textAppearance="?android:attr/textAppearanceLarge"
                android:textColor="@color/primary_text"
                android:textStyle="bold|italic" />

        </LinearLayout>

    </androidx.cardview.widget.CardView>
</layout>

(b). content_home.xml

This layout will contain our recyclerview as well as our player widget. The widget isn’t some third party library or something but we build it manually using:

  1. ImageButtons – Play,Pause,Next and Previous
  2. SeekBar – To show progress of song.
  3. TextViews – To show song’s current and total duration.

Here is our design for this layout:

Here is the layout for code:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:showIn="@layout/activity_home">

    <LinearLayout
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <LinearLayout
            android:layout_marginTop="20dp"
            android:layout_marginBottom="10dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimary"
            android:orientation="vertical">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                >
                <ImageButton
                    android:id="@+id/prevBtn"
                    android:layout_margin="5dp"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:src="@android:drawable/ic_media_previous"
                    android:background="@android:color/transparent"
                    />
                <ImageButton
                    android:id="@+id/playBtn"
                    android:layout_margin="5dp"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:src="@android:drawable/ic_media_play"
                    android:background="@android:color/transparent"
                    />
                <ImageButton
                    android:id="@+id/nextBtn"
                    android:layout_margin="5dp"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:src="@android:drawable/ic_media_next"
                    android:background="@android:color/transparent"
                    />
            </LinearLayout>

            <LinearLayout
                android:orientation="horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                >

                <TextView
                    android:id="@+id/currentPosTV"
                    android:layout_margin="5dp"
                    android:layout_width="wrap_content"
                    android:textColor="@android:color/white"
                    android:layout_height="wrap_content"
                    android:text="00:00:00"/>
                <SeekBar
                    android:id="@+id/progressSB"
                    android:layout_margin="5dp"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:max="100"/>
                <TextView
                    android:id="@+id/totalDurationTV"
                    android:textColor="@android:color/white"
                    android:layout_margin="5dp"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="00:00:00"/>
            </LinearLayout>

        </LinearLayout>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/songsRV"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>

    </LinearLayout>

</RelativeLayout>

(c). activity_home.xml

This is a container layout. It will contain our content_home layout. You can as well put them in one file. However separating them like this makes customizing either of them easier. Play with appBar height to find the appropriate figure for your device or emulator.

Take note that we are using CollapsingToolbar Layout. We will however remove nestedscrollview and just use a LinearLayout. This is because we will be adding a recyclerview to this layout and we don’t want to include a recyclerview inside a scrollview child. Doing so would prevent recycling of widgets in our recyclerview, thus defeating the whole purpose of using a recyclerview.

Here is our code:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:showIn="@layout/activity_home">

    <LinearLayout
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <LinearLayout
            android:layout_marginTop="20dp"
            android:layout_marginBottom="10dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/colorPrimary"
            android:orientation="vertical">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                >
                <ImageButton
                    android:id="@+id/prevBtn"
                    android:layout_margin="5dp"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:src="@android:drawable/ic_media_previous"
                    android:background="@android:color/transparent"
                    />
                <ImageButton
                    android:id="@+id/playBtn"
                    android:layout_margin="5dp"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:src="@android:drawable/ic_media_play"
                    android:background="@android:color/transparent"
                    />
                <ImageButton
                    android:id="@+id/nextBtn"
                    android:layout_margin="5dp"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:src="@android:drawable/ic_media_next"
                    android:background="@android:color/transparent"
                    />
            </LinearLayout>

            <LinearLayout
                android:orientation="horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                >

                <TextView
                    android:id="@+id/currentPosTV"
                    android:layout_margin="5dp"
                    android:layout_width="wrap_content"
                    android:textColor="@android:color/white"
                    android:layout_height="wrap_content"
                    android:text="00:00:00"/>
                <SeekBar
                    android:id="@+id/progressSB"
                    android:layout_margin="5dp"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:max="100"/>
                <TextView
                    android:id="@+id/totalDurationTV"
                    android:textColor="@android:color/white"
                    android:layout_margin="5dp"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="00:00:00"/>
            </LinearLayout>

        </LinearLayout>

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/songsRV"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>

    </LinearLayout>

</RelativeLayout>

Caching common variables

We want to define a class to statically cache some simple variables for easier and faster access throughout the application. Here are the items we cache:

  1. Song Position – We want to cache the position of the current song.
  2. Loaded Songs – We will read our disk only only once and cache the loaded songs statically.

The class is called CacheManager and it’s super simple:

public class CacheManager {
    public static int SONG_POSITION = 0;
    public static ArrayList<Song> SONGS_CACHE = new ArrayList<>();
}

Holding Common Constants

We are using MVVM design pattern. The whole point is to segregate our app into various classes or components that are easier to manage. The advantage is ease of testing as well as re-usability. For example our Constants class will contain our application constants:

public class Constants {
    public static final int MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;

    public static final int STOPPED = -1;
    public static final int LOADING = 0;
    public static final int PLAYING = 1;
}

Creating our Song Model Class

Our app is a music player. Our most fundamental entity is a Song object. It is what app is about. Hence we need to define programmatically, in Java. What does our app mean when it encounters, a song object? We define a Song using a class. The class will contain the Song’s properties as well as methods to expose those properties to our other class.

Some of the properties that our song will have include:

  1. Id – Identifier for the song
  2. Artist – Person who sang the song
  3. Title – of the song
  4. Data – Song data
  5. DisplayName – Public Name
  6. Duration – How long the song lasts.

Here is our code:

public class Song {

    private String id, artist, title, data, displayName, duration;
    private boolean isPlaying;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getArtist() {
        return artist;
    }

    public void setArtist(String artist) {
        this.artist = artist;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getData() {
        return data;
    }

    public void setData(String data) {
        this.data = data;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public String getDuration() {
        return duration;
    }

    public void setDuration(String duration) {
        this.duration = duration;
    }

    public boolean isPlaying() {
        return isPlaying;
    }

    public void setPlaying(boolean playing) {
        isPlaying = playing;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if(!(obj instanceof Song)){
            return false;
        }
        Song s= (Song) obj;
        return id == s.getId();
    }
}

Representing any Action

We will also create a simple class to represent a given action. So far we’ve represented a song. But what about if we could represent an action as well. For example the process of loading songs from the device, or the process of playing a song etc. Let’s create a simple class to represent these actions and their associated data:

public class RequestCall {
    private int status = Constants.STOPPED;
    private ArrayList<Song> songs=new ArrayList<>();

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public ArrayList<Song> getSongs() {
        return songs;
    }

    public void setSongs(ArrayList<Song> songs) {
        this.songs = songs;
    }
}

In the above you can see we have included two properties:

  1. Status – Status of the operation or action.
  2. Songs – Songs associated with that operation or action.

Songs Rrpository

We will have an important class which will involve most the logic we need to create our music player app. We don’t want to clutter these with UI code so that we can test these logic easily and also re-use them in our other projects. We will name this class our SongsRepository class.

Let’s start by creating the class:

public class SongsRepository {

}

Let’s look use the HowTo Approach to cover these logic:

1. How to Convert a Cursor to a Song object

Well we’ve already defined our Song class. And we will be passed a Cursor object. A Cursor is an interface that provides random read-write access to the result set returned by a database query.

    private Song convertToSong(Cursor cursor) {
        Song song = new Song();
        song.setId(cursor.getString(0));
        song.setArtist(cursor.getString(1));
        song.setTitle(cursor.getString(2));
        song.setData(cursor.getString(3));
        song.setDisplayName(cursor.getString(4));
        song.setDuration(cursor.getString(5));
        song.setPlaying(false);
        return song;
    }

We basically obtain the properties from our Cursor and pass them over to our Song object using the setter methods.

2. How to Fetch all Songs from the Device

We now need to fetch all our songs from the device, attach them to our requestCall object, which then we pass to our MutableLiveData as a generic parameter. Later on observers observing that LiveData will get those songs:

    /**
     * Fetch all songs from the device
     * @param c
     * @return
     */
    public MutableLiveData<RequestCall> fetchAllSongs(AppCompatActivity c) {
        RequestCall r = new RequestCall();
        r.setStatus(Constants.LOADING);
        MutableLiveData<RequestCall> mLiveData = new MutableLiveData<>();
        ArrayList<Song> songs = new ArrayList<>();
        r.setSongs(songs);
        mLiveData.setValue(r);

        String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";
        String[] projection = {
                MediaStore.Audio.Media._ID,
                MediaStore.Audio.Media.ARTIST,
                MediaStore.Audio.Media.TITLE,
                MediaStore.Audio.Media.DATA,
                MediaStore.Audio.Media.DISPLAY_NAME,
                MediaStore.Audio.Media.DURATION
        };

        Cursor cursor = c.managedQuery(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                projection,
                selection,
                null,
                null);

        while (cursor.moveToNext()) {
            songs.add(convertToSong(cursor));
        }
        r.setStatus(STOPPED);
        r.setSongs(songs);
        mLiveData.postValue(r);
        return mLiveData;
    }

3. How to Play a Song

Well here is how we play our song using the MediaPlayer class:

    /**
     * Play A Song
     * @param mMediaPlayer
     * @param c
     * @param song
     * @return
     */
    public MutableLiveData<RequestCall> play(MediaPlayer mMediaPlayer, Context c, Song song) {
        RequestCall r = new RequestCall();
        r.setStatus(STOPPED);
        r.setSongs(new ArrayList<>());
        MutableLiveData<RequestCall> mLiveData = new MutableLiveData<>();
        mLiveData.setValue(r);

        if (mMediaPlayer == null) {
            mMediaPlayer = MediaPlayer.create(c, Uri.parse(song.getData()));
        }
        mMediaPlayer.start();

        r.setStatus(PLAYING);
        mLiveData.postValue(r);
        return mLiveData;
    }

We’ve started by checking if the mediaplayer is null. If it is then we simply create using the static create method defined in the MediaPlayer class. Invoking start() is what plays the media player.

4. How to Pause a Song

Whatever starts must ultimately stop. Even the universe will supposedly one day stop. So must our song. Here is how we pause it:

    /**
     * Pause a MediaPlayer
     * @param mMediaPlayer
     * @param c
     * @param song
     * @return
     */
    public MutableLiveData<RequestCall> pause(MediaPlayer mMediaPlayer, Context c, Song song) {
        RequestCall r = new RequestCall();
        r.setSongs(new ArrayList<>());
        MutableLiveData<RequestCall> mLiveData = new MutableLiveData<>();
        mLiveData.setValue(r);

        if (mMediaPlayer == null) {
            mMediaPlayer = MediaPlayer.create(c, Uri.parse(song.getData()));
        } else {
            mMediaPlayer.pause();
            r.setStatus(STOPPED);
            mLiveData.postValue(r);
        }
        return mLiveData;
    }

The pause() method does the trick.

5. How to Obtain Time Based on Progress

We want to obtain time based on progress:

    public int getTimeFromProgress(int progress, int duration) {
        return (duration * progress) / 100;
    }

6. How to get Song Progress

When provided the current as well as total duration, we need to be able to calculate the song progress. Here is how e do it:

    /**
     * @param totalDuration
     * @param currentDuration
     * @return
     */
    public int getSongProgress(int totalDuration, int currentDuration) {
        return (currentDuration * 100) / totalDuration;
    }

7. How to create a simple timer

We want to create a simple timer for our player widget. When playing we want to be showing the progress,current duration as well as total duration. Here is how we do it:

    /**
     * Convert To Timer Mode
     * @param songDuration
     * @return
     */
    public String convertToTimerMode(String songDuration) {
        int duration = Integer.parseInt(songDuration);
        int hour = duration / (1000 * 60 * 60);
        int minute = (duration % (1000 * 60 * 60)) / (1000 * 60);
        int seconds = ((duration % (1000 * 60 * 60)) % (1000 * 60)) / (1000);
        String finalString = "";
        if (hour < 10)
            finalString += "0";
        finalString += hour + ":";
        if (minute < 10)
            finalString += "0";
        finalString += minute + ":";
        if (seconds < 10)
            finalString += "0";
        finalString += seconds;

        return finalString;
    }

Our ViewModel Class

Let’s now come and expose those functionalities we defined in the Repository class to our UI in a lifecycle conscious way.

public class SongsViewModel extends AndroidViewModel {
    private SongsRepository sr=new SongsRepository();
    public SongsViewModel(@NonNull Application application) {
        super(application);
    }
    public MutableLiveData<RequestCall> loadAllSongs(AppCompatActivity a){
        return sr.fetchAllSongs(a);
    }
    public MutableLiveData<RequestCall> play(MediaPlayer mMediaPlayer, Context c, Song s){
        return sr.play(mMediaPlayer,c,s);
    }
    public MutableLiveData<RequestCall> pause(MediaPlayer mMediaPlayer, Context c, Song s){
        return sr.pause(mMediaPlayer,c,s);
    }
    public int getTimeFromProgress(int progress, int duration){
        return sr.getTimeFromProgress(progress,duration);
    }
    public int getSongProgress(int totalDuration, int currentDuration){
        return sr.getSongProgress(totalDuration,currentDuration);
    }
    public String convertToTimerMode(String songDuration){
        return sr.convertToTimerMode(songDuration);
    }
}

Download

Just download the project below. We will be providing updates and support for this project.