국비교육(22-23)

89일차(1)/Android App(55) : mp3 파일 재생 예제 / Notification(3)

서리/Seori 2023. 2. 16. 00:41

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 기능을 사용 가능하다.