[Must Have Joyce의 안드로이드 앱프로그래밍] 8장 뮤직 플레이어: MediaPlayer API, 서비스

알린·2024년 1월 24일
0

Raw 리소스 사용해 재생

  1. [res] 폴더에 [raw] 폴더를 생성해 사용할 mp3 파일을 넣어줌
  2. Activity에 다음 코드 작성하면 음악 재생 가능
val mPlayer: MediaPlayer? = MediaPlayer.create(this, R.raw.sample)
mPlayer?.start()

URI 사용해 재생

  1. 다음 코드 작성하면 음악 재생 가능
val mtUri: Uri = ... // URI 초기화
val mediaPlayer: MediaPlayer? = MediaPlayer().apply {
	setAudioStreamType(AudioManager.STREAM_MUSIC)
    setDataSource(applicationCOntext, myUri)
    prepare()
    start()
}

MediaPlayer 클래스의 함수

파일 준비하기

파일 로드 시키기 위한 함수

  • prepare(): 메인 스레드에서 실행(용량이 너무 크면 ANR 발생)
  • prepareAsync(): 백그라운드 스레드에서 실행
    👉 onPreparedListener 등록해 음악 준비가 완료되는 시점 알 수 있음
  • setDataResource()

파일 재생하기

  • start(): 재생
  • pause(): 일시정지

파일 멈추기

  • reset(): 재생 정지

음악 길이 찾기

  • getDuration(): 음악의 길이 얻을 수 있음(단위는 밀리초로 반환)

특정 구간으로 이동

  • seekTo(): 특정 위치로 이동

자원 해제하기

  • release(): 사용하던 메모리와 자원들을 해제

서비스와 생명주기

서비스

  • 백그라운드에서 꺼지지 않고 작업을 수행하는 안드로이드 4대 구성요소 중 하나
  • 독립된 구성요소이기 때문에 독립된 생명주기를 가짐

서비스의 유형

시작된 서비스

생명 주기
1. startService() 함수를 이용해 서비스 시작
2. 서비스가 시작되면 서비스 내의 콜백 메서드인 onCreate()onStartCommand()가 차례로 호출되어 시작된 상태가 됨
3. 시작된 서비스는 stopSelf() 함수로 중지하거나, 다른 구성요소가 stopService()를 호출해 서비스를 완전히 종료시키기 전까지는 계속 실행중인 상태임
4. 서비스가 종료되면 onDestroy()함수가 호출되어 서비스가 완전히 종료

바인드된 서비스

  • 다른 앱의 구성요소가 서비스에 접근할 수 있도록 만드는 것
  • 액티비티에서 바인드된 서비스 시작 코드
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import androidx.appcompat.app.AppCompatActivity
import android.os.IBinder

class MainActivity : AppCompatActivity() {
    val mServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            // 서비스 연결 성공
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            // 서비스 연결 실패
        }
    }
    private fun bindService() {
        val intent = Intent(this, AudioPlayerService::class.java)
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)
    }
}
  1. bindService() 함수를 호출할 때 인텐트 객체, 서비스 연결 관련 정보를 얻을 수 있는 Service Connection 구현 객체와 Context.BIND_AUTO_CREATE를 인수로 줌
  2. 구성요소가 서비스와 연결되면 해당 구현 객체의 onServiceConnected() 함수가 호출
  3. 호출된 함수에서 서비스와 통신을 가능하게 하는 IBinder 객체를 전달받음
  4. BIND_AUTO_CREATE는 bindService() 함수를 실행했을 때 해당 서비스가 없으면 서비스의 onCreate() 함수를 실행시켜 서비스를 생성

생명 주기

  1. bindService()함수를 구성요소(액티비티)에서 호출
  2. 만약 서비스가 생성되어 있지 않다면 onCreate() 함수 실행
  3. onBind()가 호출되었을 때 서비스는 IBinder 인터페이스 구현 객체를 bindService()를 호출한 구성요소에 전달
  4. IBinder를 받고 난 뒤부터 서비스와 상호작용 가능
  5. 모든 구성요소가 unbindService()호출해 서비스와 연결 끊으면 onUnbind() 실행
  6. onDestroy()가 호출되며 완전히 서비스 종료

시작되고 바인드된 서비스

  • 백그라운드에서 계속 남아있는 동시에 다른 구성요소와 연결되어 소통이 가능하게끔 하는 경우
  • startService() 함수와 bindService() 함수 둘 다를 실행해주면 됨

포그라운드 서비스와 백그라운드 서비스

포그라운드 서비스

  • 사용자가 서비스가 실행되고 있음을 능동적으로 인지할 수 있는 서비스
    👉 음악 재생할 때 상태 표시줄에 알림이 표시되는 것

백그라운드 서비스

  • 사용자가 보이지 않는 곳에서 조용히 작업을 수행하는 서비스

startForegroundService()

  • startForegroundService()함수를 호출하고 나서 서비스 생성 이후 5초 이내에 startForeground(식별자, 사용자에게 보여질 알림) 함수를 통해 알림을 보여주어야함

알림 채널

  • 안드로이드O 부터는 반드시 알림 채널을 사용하여 사용자에게 알림을 보여줘야 함
  • 알림 채널: 알림을 용도나 중요도에 따라 구분하여 사용자가 앱의 알림을 관리할 수 있게 함

다음은 MusicPlayerService.kt 내 startForegroundService() 함수

    fun startForegroundService() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            val mChannel = NotificationChannel( // 알림 채널을 생성
                "CHANNEL_ID",
                "CHANNEL_NAME",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationManager.createNotificationChannel(mChannel)
        }
        // 알림 생성
        val notification: Notification = Notification.Builder(this, "CHANNEL_ID")
            .setSmallIcon(R.drawable.ic_play) // 알림 아이콘
            .setContentTitle("뮤직 플레이어 앱")   // 알림의 제목을 설정
            .setContentText("앱이 실행 중입니다.")  // 알림의 내용을 설정
            .build()
        startForeground(1, notification)  // startForeground(알림 id, 알림 내용)
    }

stopForeground(boolean)

  • true 👉 포그라운드 서비스 멈춤

onStartCommand()

  • startService()를 호출하면 실행되는 콜백함수
  • 반드시 정수값 반환해야함

반환되는 정수값

  • START_STICKY: 시스템이 서비스를 중단하면 서비스를 다시 실행하고 onStartCommand() 함수 호출
  • START_NOT_STICKY: 시스템이 서비스를 중단시키면 서비스를 재생성하지 않음
  • START_REDELIVER_INTENT: 시스템이 서비스를 중단하면 서비스를 다시 실행하고 onStartCommand() 함수 호출.
    서비스가 종료되기 전 마지막으로 전달된 인텐트 재전달(반드시 명령을 실행해야 하는 경우에 쓰임)

다음은 MusicPlayerService.kt 내 onStartCommand() 함수

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return START_STICKY
    }

bindService()

  • bindService(인수1, 인수2, 인수3)

    인수1: 어떤 서비스와 바인드 할건지
    인수2: 구현한대로 연결되면 onServiceConnected()를 실행하고 연결이 끊기면 onServiceDisconnected()를 실행
    인수3: 바인드할 시점에 서비스가 실행되지 않은 상태라면 서비스를 생성


Vector 이미지 추가

  1. [drawable] ➡️ [New] ➡️ [Vector Asset]
  2. Clip art에서 원하는 이미지 선택 ➡️ [Next] ➡️ [finish]

매니패스트 파일 권한 요청

  • Android 9(API 레벨 28) 이상을 대상으로 하는 앱에 대해 포그라운드 서비스 권하는 요청 필수
  • manifest 내부에 아래 코드 삽입
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
  • application 내부에 아래 코드 삽입
<service android:name="com.example.mediaplayerapp.MusicPlayerService"/>

전체 코드

MainActivity.kt

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.IBinder
import android.view.View
import android.widget.Button

class MainActivity : AppCompatActivity(), View.OnClickListener {
    lateinit var btn_start : Button
    lateinit var btn_pause : Button
    lateinit var btn_stop : Button
    var mService: MusicPlayerService? = null  // 서비스 변수

    // 서비스 구성요소 연결 상태 모니터링
    val mServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            // MusicPlayerBinder로 형변환 해줌
            mService = (service as MusicPlayerService.MusicPlayerBinder).getService()
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            // 서비스가 끊기면 mService를 null로 만듦
            mService = null
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        btn_start = findViewById(R.id.btn_start)
        btn_pause = findViewById(R.id.btn_pause)
        btn_stop = findViewById(R.id.btn_stop)

        btn_start.setOnClickListener(this)
        btn_pause.setOnClickListener(this)
        btn_stop.setOnClickListener(this)
    }

    override fun onClick(v: View?) {
        when(v?.id) {
            R.id.btn_start -> {
                play()
            }
            R.id.btn_pause -> {
                pause()
            }
            R.id.btn_stop -> {
                stop()
            }
        }
    }

    override fun onResume() {
        // 액티비티가 사용자에게 보일 때마다 실행되는 콜백 함수
        super.onResume()
        // 서비스 실행 처리
        if (mService == null) {  // 아직 서비스가 액티비티와 연결되지 않았을 때
            // 안드로이드O 이상이면 startForegroundService 사용
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                startForegroundService(Intent(this, MusicPlayerService::class.java))
            } else {
                startService(Intent(applicationContext, MusicPlayerService::class.java))
            }
            // 액티비티를 서비스와 바인드시킴
            val intent = Intent(this, MusicPlayerService::class.java)
            bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)
        }
    }

    override fun onPause() {
        super.onPause()
        // 사용자가 액티비티를 떠났을 때 처리
        if (mService != null) {            if (!mService!!.isPlaying()) {  // mService가 재생되고 있지 않다면
                mService!!.stopSelf()  // 서비스 중단
            }
            unbindService(mServiceConnection) // 서비스로부터 연결 끊기
            mService = null
        }
    }
    private fun play() {
        mService?.play()
    }
    private fun pause() {
        mService?.pause()
    }
    private fun stop() {
        mService?.stop()
    }
}

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

    <Button
        android:id="@+id/btn_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:backgroundTint="@color/pink"
        android:text="@string/start"
        app:layout_constraintBottom_toTopOf="@+id/btn_pause"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_pause"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:backgroundTint="@color/purple"
        android:text="@string/pause"
        app:layout_constraintBottom_toTopOf="@+id/btn_stop"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_start" />

    <Button
        android:id="@+id/btn_stop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:backgroundTint="@color/skyBlue"
        android:text="@string/stop"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_pause" />

</androidx.constraintlayout.widget.ConstraintLayout>

MusicPlayerService.kt

import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.media.MediaPlayer
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.widget.Toast
import androidx.annotation.RequiresApi

class MusicPlayerService : Service() {
    var mMediaPlayer: MediaPlayer? = null  // 미디어 플레이어 객체를 null로 초기화
    var mBinder: MusicPlayerBinder = MusicPlayerBinder()

    inner class MusicPlayerBinder : Binder() {
        fun getService(): MusicPlayerService {
            return this@MusicPlayerService
        }
    }
    // 서비스를 startService()로 생성하든, bindService()로 생성하든 onCreate()는 처음에 한 번만 실행
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate() {
        super.onCreate()

        // 앱이 실행되고 있다는 알림 생성
        startForegroundService()  // 포그라운드 서비스 시작
    }

    // 바인드
    override fun onBind(intent: Intent?): IBinder {
        return mBinder
    }

    // 시작된 상태 (백그라운드)
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return START_STICKY
    }

    // 서비스 종료
    override fun onDestroy() {
        super.onDestroy()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            stopForeground(true)
        }
    }

    @SuppressLint("ForegroundServiceType")
    @RequiresApi(Build.VERSION_CODES.O)
    fun startForegroundService() {
        // 안드로이드O 부터는 반드시 알림 채널을 사용하여 사용자에게 알림을 보여줘야 함
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            // 알림 채널: 알림을 용도나 중요도에 따라 구분하여 사용자가 앱의 알림을 관리할 수 있게 함
            val mChannel = NotificationChannel( // 알림 채널을 생성
                "CHANNEL_ID",
                "CHANNEL_NAME",
                NotificationManager.IMPORTANCE_DEFAULT
            )
            notificationManager.createNotificationChannel(mChannel)
        }

        // 알림 생성
        val notification: Notification = Notification.Builder(this, "CHANNEL_ID")
            .setSmallIcon(R.drawable.ic_play) // 알림 아이콘
            .setContentTitle("뮤직 플레이어 앱")   // 알림의 제목을 설정
            .setContentText("앱이 실행 중입니다.")  // 알림의 내용을 설정
            .build()

        startForeground(1, notification)  // startForeground(알림 id, 알림 내용)
    }

    fun isPlaying() : Boolean{
        return (mMediaPlayer != null && mMediaPlayer?.isPlaying ?: false)
    }

    fun play() {
        if (mMediaPlayer == null) {  // 음악이 재생 중이지 않을 때
            // 음악 파일의 리소스를 가져와 미디어 플레이어 객체를 할당
            mMediaPlayer = MediaPlayer.create(this, R.raw.sample)

            mMediaPlayer?.setVolume(1.0f, 1.0f) // 볼륨 지정
            mMediaPlayer?.isLooping = true  // 반복재생 여부
            mMediaPlayer?.start()  // 음악 재생
        } else {  // 음악이 재생 중일 때
            if (mMediaPlayer!!.isPlaying) {
                Toast.makeText(this, "이미 음악이 실행 중입니다.", Toast.LENGTH_SHORT).show()
            } else {
                mMediaPlayer?.start()  // 음악 재생
            }
        }
    }

    fun pause() {
        mMediaPlayer?.let {
            if (it.isPlaying) {
                it.pause()
            }
        }
    }

    fun stop() {
        mMediaPlayer?.let {
            if(it.isPlaying) {
                it.stop()
                it.release()
                mMediaPlayer = null
            }
        }
    }

}

실행 화면

profile
Android 짱이 되고싶은 개발 기록 (+ ios도 조금씩,,👩🏻‍💻)

0개의 댓글