Simple audio player using ExoPlayer library – Part 1

Simple audio player using ExoPlayer library – Part 1
  • Post Author:

1. Introduction

A simple application for playing music stored locally in an android mobile phone using the ExoPlayer library. ExoPlayer is an open-source media library that provides an alternative to Android’s MediaPlayer API for playing audio and video both locally and over the Internet.

I will call this application “Simple Player” and that is how I will refer to it throughout this post.

As the name suggests, Simple Player does not have the intricacy of other advance media players. Its a basic audio player to put into practise what I have learned so far about ExoPlayer library, more features will be added as I learn more about the ExoPlayer library.

2. Architecture

Simple app simple architecture. The app will consist of three main files

  1. MainActivity.kt, application entry point and fragment host
  2. MainFragment.kt, have logic to load music from media store and display it on a list using a loader. In addition, the player UI will be hosted on this fragment .
  3. MusicManager.kt, class to manage SimpleExoPlayer instance and carry out the various operations asked of it e.g play , stop, release…

When you start the application, it fetches music information from the device storage and displays on a list. To play music select an item from the list, this action triggers the player to start playing. Currently the application does not support navigating between songs, you have to manually pick a song to change.

3. The Code

Simple Player needs storage permission to retrieve media information so the required permission is added in manifest file.

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

Note if you are going to enable Exoplayer to automatically handle WakeLock an extra permission “android.permission.WAKE_LOCK” will be required.

Next dependencies are added

dependencies {
implementation 'com.google.android.exoplayer:exoplayer-core:2.11.3'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.11.3'
implementation 'com.squareup.picasso:picasso:2.71828'
}

MainActivity

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".MainActivity">

<fragment
android:id="@+id/fragment"
android:name="com.example.simpleplayer.MainFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>



</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


    }
}

MainFragment.kt

fragment_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
tools:context=".MainFragment">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/player_control_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_song" />

<com.google.android.exoplayer2.ui.PlayerControlView
android:id="@+id/player_control_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/list"
app:show_timeout="0"/>


MainFragment.kt

import android.Manifest
import android.content.pm.PackageManager
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.view.View
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.loader.app.LoaderManager
import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_main.*


class MainFragment : Fragment(R.layout.fragment_main), LoaderManager.LoaderCallbacks<Cursor>,
    OnSongClickListener {

    private val PERMISSIONS_REQUEST_READ_STORAGE: Int = 1200


    private var curFilter = ""
    private lateinit var mAdapter: AudioListAdapter
    private lateinit var musicManager: MusicManager


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Here, thisActivity is the current activity
        if (ContextCompat.checkSelfPermission(
                requireActivity(),
                Manifest.permission.READ_EXTERNAL_STORAGE
            )
            != PackageManager.PERMISSION_GRANTED
        ) {

            // Permission is not granted
            // Should we show an explanation?
            if (ActivityCompat.shouldShowRequestPermissionRationale(
                    requireActivity(),
                    Manifest.permission.READ_EXTERNAL_STORAGE
                )
            ) {

            } else {
                // No explanation needed, we can request the permission.
                ActivityCompat.requestPermissions(
                    requireActivity(),
                    arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
                    PERMISSIONS_REQUEST_READ_STORAGE
                )

            }
        } else {
            LoaderManager.getInstance(this).initLoader(0, null, this)
        }


    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>, grantResults: IntArray
    ) {
        when (requestCode) {
            PERMISSIONS_REQUEST_READ_STORAGE -> {
                if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
                    LoaderManager.getInstance(this).initLoader(0, null, this)
                }
                return
            }
            else -> {
                // Ignore all other requests.
            }
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        mAdapter = AudioListAdapter(view.context, this)

        with(list) {
            adapter = mAdapter
            list.layoutManager = LinearLayoutManager(view.context)
        }

        musicManager = MusicManager(requireContext(), player_control_view)
    }

    override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
        val baseUri: Uri = if (curFilter != null) {
            Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, Uri.encode(curFilter))
        } else {
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
        }

        return CursorLoader(
            requireContext(),
            baseUri,
            projection,
            selection,
            null,
            "${MediaStore.Video.Media.DISPLAY_NAME} ASC"  // Display in alphabetical order based on their display name.
        )

    }

    override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
        data?.let {
            mAdapter.setData(it)
        }

    }

    override fun onLoaderReset(loader: Loader<Cursor>) {
        mAdapter.setData(null)
    }

    var selection = MediaStore.Audio.Media.IS_MUSIC + " != 0"

    private val projection = arrayOf(
        MediaStore.Audio.Media._ID,
        MediaStore.Audio.Media.ARTIST,
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.DISPLAY_NAME,
        MediaStore.Audio.Media.ALBUM,
        MediaStore.Audio.Media.ALBUM_ID

    )
    override fun onClick(songUri: Uri) {
        musicManager.play(songUri)
    }

    override fun onDestroy() {
        super.onDestroy()
        musicManager.release()
    }
}

AudioListAdapter.kt

import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.MediaStore
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Picasso


class AudioListAdapter(private val context: Context, private val listener: OnSongClickListener) :
    RecyclerView.Adapter<AudioListAdapter.ViewHolder>() {

    var dataCursor: Cursor? = null

    class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
        var title = v.findViewById<View>(R.id.song_title) as TextView
        var image = v.findViewById<View>(R.id.song_note_image) as ImageView
        var artist = v.findViewById<View>(R.id.song_artist) as TextView
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val cardview: View = LayoutInflater.from(context)
            .inflate(R.layout.item_song, parent, false)
        return ViewHolder(cardview)
    }

    fun setData(cursor: Cursor?) {
        dataCursor = cursor
        notifyDataSetChanged()
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {

        if (dataCursor!!.moveToPosition(position)) {

            val title =
                dataCursor!!.getString(dataCursor!!.getColumnIndex(MediaStore.Audio.Media.TITLE))
            holder.title.text = title
            val artist =
                dataCursor!!.getString(dataCursor!!.getColumnIndex(MediaStore.Audio.Media.ARTIST))
            holder.artist.text = artist
            val albumId =
                dataCursor!!.getLong(dataCursor!!.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID))
            val sArtworkUri = Uri.parse("content://media/external/audio/albumart")
            val albumArtUri = ContentUris.withAppendedId(sArtworkUri, albumId!!)

            Picasso.get()
                .load(albumArtUri)
                .placeholder(R.drawable.ic_music_note_vector)
                .error(R.drawable.ic_music_note_vector)
                .into(holder.image)

            val songId =
                dataCursor!!.getLong(dataCursor!!.getColumnIndexOrThrow(MediaStore.Audio.Media._ID))

            val contentUri: Uri =
                ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, songId!!)

            holder.itemView.setOnClickListener {
                listener.onClick(contentUri)
            }
        }
    }

    override fun getItemCount(): Int {
        return dataCursor?.count ?: 0
    }

}

OnSongClickListener.kt

interface OnSongClickListener {
    fun onClick(songUri: Uri)
}

MusicManager.kt

import android.content.Context
import android.net.Uri
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util

class MusicManager(private val context: Context, private val controlView: PlayerControlView) {


    private var oldSongUri: Uri? = null
    private var player: SimpleExoPlayer? = null


    private fun initializePlayer() {
        val audioAttributes =
            AudioAttributes.Builder()
                .setUsage(C.USAGE_MEDIA)
                .setContentType(C.CONTENT_TYPE_MUSIC)
                .build()

        player = SimpleExoPlayer.Builder(context).build().apply {
            //setHandleWakeLock(true)
            setHandleAudioBecomingNoisy(true)
            setAudioAttributes(audioAttributes, true)
            addListener(playerEventListener)
        }

        controlView.player = player

    }

    private val playerEventListener = object : Player.EventListener {
        override fun onPlayerStateChanged(
            playWhenReady: Boolean,
            playbackState: Int
        ) {

        }
    }

    fun release() {
        player?.stop()
        player?.release()
    }

    fun play(uri: Uri) {
        if (player == null)
            initializePlayer()

        if (uri != oldSongUri) {
            if (player!!.isPlaying)
                pause()
            val mediaSource = buildMediaSource(uri)
            player!!.prepare(mediaSource)
            play()
        }
        oldSongUri = uri
    }

    private fun play() {
        player?.playWhenReady = true
    }

    private fun pause() {
        player?.playWhenReady = false
    }

    private fun buildMediaSource(uri: Uri): MediaSource {

        return ProgressiveMediaSource.Factory(
            DefaultDataSourceFactory(
                context,
                Util.getUserAgent(context, context.getString(R.string.app_name))
            )
        ).createMediaSource(uri)

    }
}

In the next article I will be adding a repository that will have the responsibility of providing songs data.

Github link