89일차(1)/Android App(55) : mp3 파일 재생 예제 / Notification(3)
- Service 를 상속하고 서비스 onbind() 메소드를 오버라이드했다.
- onCreate는 생성자처럼 최초 1번만 활성화되는 것!
- onStartCommand는 이것을 활성화시킬 intent가 도착하면 여러번 호출되는 것.
- 이전예제에서는 액티비티에서 MediaPlayer 를 사용해서 재생하게 되어있는데,
Service 안에서 재생하는 것으로 바꾸어 볼 예정!
(액티비티의 활성화/비활성화와 상관없이 백그라운드에서 계속 재생될 수 있도록 하기 위해서!)
- Onstart에서 서비스를 시작시키고 → 서비스 바인딩 설정 → 알림바의 기능버튼을 사용할 수 있도록 수정할 것
- 액티비티의 내용을 service로 옮기고 서비스에서 음악 재생할 수 있도록 바꿔보기!
- 2개의 필드 추가
- boolean 타입은 필드만 선언하면 기본값은 false가 들어간다.
- url을 넣어주면 해당 url의 음악을 로딩하는 메소드 initMusic() 을 만들어준다.
- 이 메소드는 Activity에서 여러번 호출할 예정이다.
- 음원을 최초 로딩하고 재생할 준비를 하는 메소드이다.
- 초기화 후, mp가 null이면 MediaPlayer 객체 생성해주기!
- 서비스를 리스너로 만들기
- 여기서 exception이 발생하므로 try~catch 문으로 묶어준다.
- catch 문에서는 예외 로그를 찍어주기
- 이 메소드에 음원의 위치(url)만 다르게 바꾸어 넣어주면 여러번 반복해 사용할 수 있다.
mp.reset()
- 메소드가 여러번 호출될 것을 가정해서 초기화 작업을 넣어준 것이다.
- 상태값을 관리하기! 어떤 상태에 따라서 할 작업이 달라지는 경우 필드로 상태값을 관리해준다.
- 상태값이 여러개인 경우 상태값에 따른 숫자나 문자열을 부여해서 관리하면 되고,
두 가지이면 boolean 타입으로 관리하면 된다.
- 서비스에 3개의 메소드 만들어두기
- intent가 액션을 가지고 있으면 이 중 하나가 수행된다. (파란 화살표)
- 액티비티에서 액션을 가지지 않고 코드를 수행할 수도 있다. (빨간 화살표)
- 하지만 알림에서 클릭시에는 액션을 가지고 있다!
이 각각의 case안에 개별 액션에 대한 메소드를 넣어주기
- 저 case 구문 안에 직접 mp.start() 해도 상관없다.
MusicService
package com.example.step23mp3player;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import java.util.concurrent.TimeUnit;
/*
MusicService를 이용해서 음악을 재생하는 방법
- initMusic() 메소드를 호출하면서 음원의 위치를 넣어주고
- 음원 로딩이 완료되면 자동으로 play 된다.
*/
public class MusicService extends Service implements MediaPlayer.OnPreparedListener {
//필요한 필드 정의하기
MediaPlayer mp;
boolean isPrepared; //음원 재생 준비가 완료되었는지 여부
//음원을 로딩하는 메소드 url을 넣어주면 해당 url의 음악을 로딩하는 메소드
public void initMusic(String url) {
isPrepared = false;
if (mp == null) {
mp = new MediaPlayer();
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
mp.setOnPreparedListener(this); //음원 로딩이 완료되었는지 감시할 리스너 등록
}
//만일 현재 재생중이면
if (mp.isPlaying()) {
mp.stop(); //재생을 중지하고
}
mp.reset(); //초기화
try {
mp.setDataSource(url);
} catch (Exception e) {
Log.e("initMusic()", e.getMessage());
}
//비동기로 로딩을 시킨다.
mp.prepareAsync();
}
//재생하는 메소드
public void playMusic() {
mp.start();
}
//일시정지하는 메소드
public void pauseMusic() {
mp.pause();
}
//정지하는 메소드
public void stopMusic() {
mp.stop();
}
//재생이 준비되었는지 여부를 리턴하는 메소드
public boolean isPrepared() {
return isPrepared;
}
//MediaPlayer 객체의 참조값을 리턴하는 메소드
public MediaPlayer getMp() {
return mp;
}
//서비스가 최초 활성화될 때 한번 호출되는 메소드
@Override
public void onCreate() {
super.onCreate();
}
//최초 활성화 혹은 이미 활성화된 이후 이 서비스를 활성화 하는 Intent가 도착하면 호출되는 메소드
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//알림에 띄워진 액션 버튼을 눌렀을때 분기해서 필요한 동작을 한다.
switch (intent.getAction()) {
case AppConstants.ACTION_PLAY:
Log.d("onStartCommand()", "play!");
playMusic();
break;
case AppConstants.ACTION_PAUSE:
Log.d("onStartCommand()", "pause!");
pauseMusic();
break;
case AppConstants.ACTION_STOP:
Log.d("onStartCommand()", "stop!");
stopMusic();
break;
}
return START_NOT_STICKY;
}
//Binder 클래스를 상속받아서 LocalBinder 클래스를 정의한다.
public class LocalBinder extends Binder {
//서비스의 참조값을 리턴해주는 메소드
public MusicService getService() {
Log.e("####", "리턴함");
return MusicService.this;
}
}
//필드에 바인더 객체의 참조값 넣어두기
final IBinder binder = new LocalBinder();
//어디에선가(액티비티) 바인딩(연결)이 되면 호출되는 메소드
@Override
public IBinder onBind(Intent intent) {
return binder;
}
//새로운 음원 로딩이 완료되면 호출되는 메소드
@Override
public void onPrepared(MediaPlayer mp) {
//재생할 준비가 되었다고 상태값을 바꿔준다.
isPrepared = true;
//준비가 되면 자동으로 재생을 시작한다.
playMusic();
handler.removeMessages(0);
handler.sendEmptyMessageDelayed(0,100);
}
@Override
public void onDestroy() {
if(mp != null){
//MediaPlayer 해제하기
mp.stop();
mp.release();
mp = null;
}
handler.removeMessages(0);
super.onDestroy();
}
Handler handler=new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
//음악을 control 할 수 있는 알림을 띄운다.
makeManualCancelNoti();
handler.sendEmptyMessageDelayed(0,100);
}
};
//수동으로 취소하는 알림을 띄우는 메소드
public void makeManualCancelNoti() {
//현재 재생 시간을 문자열로 얻어낸다.
int currentTime = mp.getCurrentPosition();
String info = String.format("%d min, %d sec",
TimeUnit.MILLISECONDS.toMinutes(currentTime),
TimeUnit.MILLISECONDS.toSeconds(currentTime)
- TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(currentTime)));
Intent iPlay = new Intent(this, MusicService.class);
iPlay.setAction(AppConstants.ACTION_PLAY);
PendingIntent pIntentPlay = PendingIntent.getService(this, 1, iPlay, PendingIntent.FLAG_MUTABLE);
Intent iPause = new Intent(this, MusicService.class);
iPlay.setAction(AppConstants.ACTION_PAUSE);
PendingIntent pIntentPause = PendingIntent.getService(this, 1, iPlay, PendingIntent.FLAG_MUTABLE);
Intent iStop = new Intent(this, MusicService.class);
iPlay.setAction(AppConstants.ACTION_STOP);
PendingIntent pIntentStop = PendingIntent.getService(this, 1, iPlay, PendingIntent.FLAG_MUTABLE);
//띄울 알림을 구성하기
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, AppConstants.CHANNEL_ID)
.setSmallIcon(android.R.drawable.star_on) //알림의 아이콘
.setContentTitle("쇼팽 녹턴") //알림의 제목
.setContentText(info)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) //알림의 우선순위
.addAction(new NotificationCompat.Action(android.R.drawable.ic_media_play, "Play", pIntentPlay))
.addAction(new NotificationCompat.Action(android.R.drawable.ic_media_play, "Pause", pIntentPause))
.addAction(new NotificationCompat.Action(android.R.drawable.ic_media_play, "Stop", pIntentStop))
.setProgress(mp.getDuration(), mp.getCurrentPosition(), false)
//.setContentIntent(pendingIntent) //인텐트 전달자 객체
.setAutoCancel(false); //자동 취소 되는 알림인지 여부
//알림 만들기
Notification noti = builder.build();
//만일 알림 권한이 없다면
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
//메소드를 여기서 종료
return;
}
//알림 매니저를 이용해서 알림을 띄운다.
NotificationManagerCompat.from(this).notify(AppConstants.NOTI_ID, noti);
}
}
- 실행된 액티비티를 서비스에 연결해야 한다. url도 받아와야 한다.
→ 바인딩 작업이 필요!
- binder 객체를 리턴해주는 메소드 onBind 생성
- 이 binder 객체를 액티비티에서 받아가서 xx.getService() 하면 MusicService 의 참조값을 얻어낼 수 있다.
- isPrepared() 메소드 추가!
- isPrepared 라는 필드만 있어도 이렇게 현재 필드의 값을 리턴해주는 메소드가 자동으로 만들어진다.
- mediaPlayer도 get만 써도 자동으로 리턴하는 함수를 만들어준다.
- MediaPlayer를 제어하는 방법은 2가지가 있다.
1) 어딘가에서 이 메소드를 호출해서 재생/일시중지/정지 시키는 것
2) intent를 사용해서 호출하는 방법
- 알림을 통해서 MediaPlayer를 사용하는 것은 intent를 통해서 제어한다.
- Activity를 통해서 하는것은 굳이 intent 를 사용하지 않아도 된다. 그냥 서비스에 바인딩해서 서비스가 가지고 있는 메소드를 통해서 제어할 수 있다.
- 자원 해제가 필요할 경우를 위해 onDestroy() 메소드도 오버라이드
MainActivity
package com.example.step23mp3player;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.widget.ImageButton;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import java.util.concurrent.TimeUnit;
public class MainActivity extends AppCompatActivity {
MediaPlayer mp;
//재생 준비가 되었는지 여부
boolean isPrepared=false;
ImageButton playBtn;
ProgressBar progress;
TextView time;
SeekBar seek;
//서비스의 참조값을 저장할 필드
MusicService service;
//서비스에 연결되었는지 여부
Boolean isConnected;
//서비스 연결 객체
ServiceConnection sConn=new ServiceConnection() {
//서비스에 연결이 되었을 때 호출되는 메소드
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
//MusicService 객체의 참조값을 얻어와서 필드에 저장
//IBinder 객체를 원래 type으로 캐스팅
MusicService.LocalBinder IBinder=(MusicService.LocalBinder)binder;
service=IBinder.getService();
//연결되었다고 표시
isConnected=true;
//핸들러에 메세지 보내기
handler.removeMessages(0); //만일 핸들러가 동작중에 있으면 메세지를 제거하고
handler.sendEmptyMessageDelayed(0,100); //다시 보내기
}
//서비스에 연결이 해제되었을 때 호출되는 메소드
@Override
public void onServiceDisconnected(ComponentName name) {
//연결이 해제되었다고 표시
isConnected=false;
}
};
//UI를 주기적으로 업데이트하기 위한 Handler
Handler handler=new Handler(){
/*
이 Handler에 메세지를 한번만 보내면 아래의 handleMessage() 메소드가 1/10초마다 반복적으로 호출된다.
handleMessage() 메소드는 UI 스레드 상에서 실행되기 때문에 마음대로 UI를 업데이트할 수 있다.
*/
@Override
public void handleMessage(@NonNull Message msg) {
if(service.isPrepared()){
//전체 재생시간
int maxTime=service.getMp().getDuration();
progress.setMax(maxTime);
seek.setMax(maxTime);
//현재 재생 위치
int currentTime=service.getMp().getCurrentPosition();
//음악 재생이 시작된 이후에 주기적으로 계속 실행이 되어야 한다.
progress.setProgress(currentTime);
seek.setProgress(currentTime);
//현재 재생 시간을 TextView에 출력하기.
String info=String.format("%d min, %d sec",
TimeUnit.MILLISECONDS.toMinutes(currentTime),
TimeUnit.MILLISECONDS.toSeconds(currentTime)
-TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(currentTime)));
time.setText(info);
}
//자신의 객체에 빈 메세지를 보내서 handleMessage() 가 일정 시간 이후에 호출되도록 한다.
handler.sendEmptyMessageDelayed(0, 100); // 1/10초 이후에
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//TextView의 참조값 얻어와서 필드에 저장
time=findViewById(R.id.time);
// %d는 숫자, %s 는 문자.
String info=String.format("%d min, %d sec", 0, 0);
time.setText(info);
//ProgressBar의 참조값 얻어오기
progress=findViewById(R.id.progress);
seek=findViewById(R.id.seek);
//재생 버튼
playBtn=findViewById(R.id.playBtn);
//재생 버튼을 눌렀을 때
playBtn.setOnClickListener(v -> {
//서비스의 initMusic() 메소드를 호출해서 음악이 재생되도록 한다.
service.initMusic("http://192.168.0.34:9000/boot07/resources/upload/mp3piano.mp3");
});
//일시중지 버튼
ImageButton pauseBtn=findViewById(R.id.pauseBtn);
pauseBtn.setOnClickListener(v -> {
service.pauseMusic();
});
//알림 채널 만들기
createNotificationChannel();
}
@Override
protected void onStart() {
super.onStart();
//MusicService 에 연결할 인텐트 객체
Intent intent=new Intent(this, MusicService.class);
//서비스 시작시키기
//startService(intent);
//Activity의 bindService() 메소드를 이용해서 연결한다.
//만일 서비스가 시작이 되지 않았으면 서비스 객체를 생성해서
//시작할 준비만 된 서비스에 바인딩이 된다.
bindService(intent, sConn, Context.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
if(isConnected){
//서비스 바인딩 해제
unbindService(sConn);
isConnected=false;
}
}
//앱의 사용자가 알림을 직접 관리 할수 있도록 알림 체널을 만들어야 한다.
public void createNotificationChannel(){
//알림 채널을 지원하는 기기인지 확인해서
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//알림 채널을 만들기
//셈플 데이터
String name="Music Player";
String text="Control";
//알림 채널 객체를 얻어내서
//알림을 1/10 초마다 새로 보낼 예정이기 때문에 진동은 울리지 않도록 IMPORTANT_LOW 로 설정한다.
NotificationChannel channel=
new NotificationChannel(AppConstants.CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
//채널의 설명을 적고
channel.setDescription(text);
//알림 메니저 객체를 얻어내서
NotificationManager notiManager=(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
//알림 채널을 만든다.
notiManager.createNotificationChannel(channel);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 0:
//권한을 부여 했다면
if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
}else{//권한을 부여 하지 않았다면
Toast.makeText(this, "알림을 띄울 권한이 필요합니다.",
Toast.LENGTH_SHORT).show();
}
break;
}
}
}
- 플레이 버튼을 눌렀을 때 실행되는 이 리스너 안의 작업은 이제 필요없다! 지워준다.
- 새 필드 추가해주기
- MusicService와 바인딩하면 서비스의 참조값을 넣어서 저장해두고 필요할 때 사용할 것!
- ServiceConnection 객체는 인터페이스이므로 넣어주면 자동으로 추상메소드를 override해준다.
- onStart 에서 이제 음악을 재생할 준비를 하는 대신,
서비스를 시작시키면서 바인딩까지 해준다.
- 익명의 이너클래스를 이용해서 참조값을 전달하고, 서비스에 바인딩!
- onStart() 안에서의 작업 : 바인딩 연결
- onStop() 안에서의 작업 : 바인딩 해제
- 바인딩을 연결하건 해제하건 메소드의 인자로 ServiceConnection 객체가 들어간다.
- 여기서 리턴한 binder 값이 ServiceConnection의 인자로 들어간다.
- 참조값이 들어가는 변수명을 binder로 바꿔주기
- binder 객체를 받아서 연결여부를 바꿔주기
- Service에서 생성한 mp의 참조값이 액티비티에서 필요하다면
서비스의 getMp() 메소드를 통해서 받아올 수 있다.
- 하지만 MediaPlayer의 참조값이 null일 수도 있다. 무조건 service.getMp() 로 얻어낼 수는 없다.
- initMusic() 메소드를 호출하면서 음원의 위치를 넣어주고,
음원 로딩이 완료되면 자동으로 play 하도록 한다!
- 자동으로 play되도록 하려면 onPrepared 메소드안에 playMusic() 메소드를 넣어준다.
- 버튼을 눌렀을때 initMusic() 을 호출하면서 url이 전달되도록 한다.
- startService() 로 서비스 시작시키기
- init은 초기화. 재생 버튼을 누르면 항상 처음부터 다시 시작된다!
- 현재 진행 바는 작동하지 않고 있는데, 이 작업은 handler 안에서 해주면 된다.
- currentPosition 값과 Max값이 필요하다.
- MusicService의 여기서 리턴되는 mp 값을 사용해서
액티비티에서 MusicService의 작업에 필요한 값을 얻어올 수 있다.
- 액티비티 Handler 안에서 getMp() 로 참조값을 얻어낸다.
- 단 Service가 준비되었을 때에만!
- 이렇게 핸들러의 if 문 안에 코딩해주기
- 메소드 밖에서 핸들러에 메시지를 한번 보내야 동작한다.
- 서비스가 연결될 때 핸들러에 빈 메세지 보내기!
- 알림띄우기
AppConstants
package com.example.step23mp3player;
public class AppConstants {
public static final String ACTION_PLAY="action_start";
public static final String ACTION_PAUSE="action_pause";
public static final String ACTION_STOP="action_stop";
//채널의 아이디
public static final String CHANNEL_ID="my_channel";
//알림의 아이디
public static final int NOTI_ID=999;
}
- MainActivity에서 사용하는 상수값을 상수 클래스(AppConstants)로 옮겨놓기
- 채널을 만드는 것은 Activity에서 하고,
알림을 띄우는 것은 Service에서 띄우도록 하기
- MainActivity의 onPrepared는 필요없으므로 삭제하고,
MakeManualCancelNoti() 메소드를 서비스로 옮기기
- 권한을 확인하는 과정도 필요 없으므로 삭제
- 오류가 발생하는데, add permission check 를 눌러주면 알아서 체크해주고,
권한 체크가 되어있지 않으면 메소드를 종료시키는 메소드를 자동완성으로 만들어준다.
- 알림을 지속적으로 계속 띄워야 한다(progressBar의 진행을 위해)
→ MusicService 안에 핸들러 메소드를 생성해서 이 메소드 안에서 처리하기!
- 핸들러 메소드 생성, 반복 메시지 보내기
- 위에서 처음에 한번만 메시지를 보내주면 핸들러 안에서 메시지를 계속 보내서 반복 업데이트된다.
- play, pause, stop을 누르면 각각 코드가 이런 경로로 진행된다.
- onStartCommand() 메소드가 호출되면서 들어온 intent 값에 따라 안에 있는 메소드가 호출된다.
- Service로 변경되어도 동일하게 진행 바가 진행되고,
알림창에서 play, pause, stop 기능을 사용 가능하다.
'국비교육(22-23)' 카테고리의 다른 글
90일차(2)/Spring Boot(14) : mp3 파일 업로드, metadata 추출 (0) | 2023.02.17 |
---|---|
90일차(1)/Android App(56) : mp3 파일 재생 예제 / 재생목록 출력(ListView) (0) | 2023.02.16 |
88일차(2)/Android App(54) : mp3 파일 재생 예제 / Notification(2) (0) | 2023.02.15 |
88일차(1)/Android App(53) : mp3 파일 재생 예제 / Notification(1) (0) | 2023.02.14 |
86일차(2)/Android App(52) : mp3 파일 재생 예제 / ProgressBar 사용 (0) | 2023.02.11 |