국비교육(22-23)

88일차(2)/Android App(54) : mp3 파일 재생 예제 / Notification(2)

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

88일차(2)/Android App(54) : mp3 파일 재생 예제 / Notification(2)

 

- 새 서비스 생성

 

 

MusicService 생성

package com.example.step23mp3player;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

public class MusicService extends Service {
    //서비스가 최초 활성화될 때 한번 호출되는 메소드
    @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!");
                break;
            case AppConstants.ACTION_PAUSE:
                Log.d("onStartCommand()", "pause!");
                break;
            case AppConstants.ACTION_STOP:
                Log.d("onStartCommand()", "stop!");
                break;
        }
        return START_NOT_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

 

 

- 객체를 매번 새로 생성하지 않고, 이미 생성된 것을 싱글톤으로 사용한다.

- onStartCommand 만 다시 호출되는 것이다.

 

- 활성화시킬 Intent가 각각의 버튼에서 사용되도록 한다.

 

- 활성화시킬 수 있는 객체를 PendingIntent 에 담기

 

- Service는 이미 만들어져 있는 상태에서 저 메소드만 반복적으로 실행되도록 한다.

- Intent 안에 구별할 수 있는 정보를 담아놓고, 해당 Intent로 각각 다른 동작을 해볼 것이다.

 

- 알림을 띄우면서 Action 버튼을 만들 수 있는데, 

각각의 Action 버튼은 고유한 PendingIntent 객체를 가지고 있게 할 수 있다.

PendingIntent 객체에 Intent 객체를 담으면서 Intent에 어떤 정보를 담아서 전달하면

그 정보를 서비스에서 읽어낼 수가 있다.

 

- 나중에 사용될 intent 객체를 가지고있는 객체PendingIntent 객체이다.

 

- 동일한 서비스를 활성화시킬 수 있는 intent인데 안에 있는 일부 정보만 다르게 담을 것이다.

 구별해서 사용할 수 있도록!

 


 

- 상수를 저장할 클래스 생성

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

 

MainActivity

package com.example.step23mp3player;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
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 androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

import java.util.concurrent.TimeUnit;

public class MainActivity extends AppCompatActivity implements MediaPlayer.OnPreparedListener {

    //채널의 아이디
    public static final String CHANNEL_ID="my_channel";
    //알림의 아이디
    public static final int NOTI_ID=999;

    MediaPlayer mp;
    //재생 준비가 되었는지 여부
    boolean isPrepared=false;
    ImageButton playBtn;
    ProgressBar progress;
    TextView time;
    SeekBar seek;
    //UI를 주기적으로 업데이트하기 위한 Handler
    Handler handler=new Handler(){
        /*
            이 Handler에 메세지를 한번만 보내면 아래의 handleMessage() 메소드가 1/10초마다 반복적으로 호출된다.
            handleMessage() 메소드는 UI 스레드 상에서 실행되기 때문에 마음대로 UI를 업데이트할 수 있다.
         */
        @Override
        public void handleMessage(@NonNull Message msg) {
            int currentTime=mp.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);
            //알림띄우기
            makeManualCancelNoti();
            //자신의 객체에 빈 메세지를 보내서 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.setEnabled(false);
        playBtn.setOnClickListener(v -> {
            //만일 준비되지 않았으면
            if(!isPrepared){
                return; //메소드를 여기서 종료
            }
            //음악 재생
            mp.start();

            //알림 띄우기
            makeManualCancelNoti();
            //핸들러에 메세지 보내기
            handler.sendEmptyMessageDelayed(0,100);
        });
        //일시중지 버튼
        ImageButton pauseBtn=findViewById(R.id.pauseBtn);
        pauseBtn.setOnClickListener(v -> {
            mp.pause();
        });

        //알림 채널 만들기
        createNotificationChannel();
    }

    @Override
    protected void onStart() {
        super.onStart();
        //음악을 재생할 준비를 한다.
        try {
            mp=new MediaPlayer();
            mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mp.setDataSource("http://192.168.0.34:9000/boot07/resources/upload/mp3piano.mp3");
            mp.setOnPreparedListener(this);
            //로딩하기
            mp.prepareAsync();
        }catch (Exception e){
            Log.e("MainActivity", e.getMessage());
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        mp.stop();
        mp.release();
    }

    //재생할 준비가 끝나면 호출되는 메소드
    @Override
    public void onPrepared(MediaPlayer mp) {
        Toast.makeText(this, "로딩 완료!", Toast.LENGTH_SHORT).show();
        isPrepared=true;
        playBtn.setEnabled(true);
        //전체 재생 시간을 ProgressBar의 최대값으로 설정한다.
        progress.setMax(mp.getDuration());
        seek.setMax(mp.getDuration());
        Log.e("전체 시간", "duration:"+mp.getDuration());
        //handler 객체에 빈 메세지를 보내서 handleMessage() 가 일정 시간 이후에 호출되도록 한다.
        handler.sendEmptyMessageDelayed(0, 100); // 1/10초 이후에
    }

    //앱의 사용자가 알림을 직접 관리 할수 있도록 알림 체널을 만들어야한다.

    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(CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW);
            //채널의 설명을 적고
            channel.setDescription(text);
            //알림 메니저 객체를 얻어내서
            NotificationManager notiManager=(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
            //알림 채널을 만든다.
            notiManager.createNotificationChannel(channel);

        }

    }
    //수동으로 취소하는 알림을 띄우는 메소드
    public void makeManualCancelNoti(){
        if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
            //권한이 필요한 목록을 배열에 담는다.
            String[] permissions={android.Manifest.permission.POST_NOTIFICATIONS};
            //배열을 전달해서 해당 권한을 부여하도록 요청한다.
            ActivityCompat.requestPermissions(this,
                    permissions,
                    0); //요청의 아이디
            return;
        }
        //현재 재생 시간을 문자열로 얻어낸다.
        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)));


        //인텐트 전달자 객체
        //PendingIntent pendingIntent = PendingIntent.getActivity(this, NOTI_ID, intent, PendingIntent.FLAG_MUTABLE);

        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 pStop=PendingIntent.getService(this, 1, iPlay, PendingIntent.FLAG_MUTABLE);

        //띄울 알림을 구성하기
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, 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",pStop))
                .setProgress(mp.getDuration(), mp.getCurrentPosition(), false)
                //.setContentIntent(pendingIntent)  //인텐트 전달자 객체
                .setAutoCancel(false); //자동 취소 되는 알림인지 여부

        //알림 만들기
        Notification noti=builder.build();

        //알림 메니저를 이용해서 알림을 띄운다.
        NotificationManagerCompat.from(this).notify(NOTI_ID , noti);
    }
    @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){
                    //자동 취소 알림 띄우기
                    makeManualCancelNoti();
                }else{//권한을 부여 하지 않았다면
                    Toast.makeText(this, "알림을 띄울 권한이 필요합니다.",
                            Toast.LENGTH_SHORT).show();
                }
                break;
        }
    }

}

 

- 액티비티를 활성화시킬거면 getActivity(), 서비스를 활성화시키려면 getService() 를 사용하면 된다.

- 활성화시킬 Intent를 가지고있는 객체에 따라... getXX 메소드로 사용

 

 

- 이것을 여기 intent 인자 자리에 넣어서 서비스를 활성화시키기!

 

 

- 이런 액션을 가지고있는 intent를 담고 있는 pendingIntent를

  notification의 addAction() 의 인자로 넣어주는 것이다.

 

- play라는 액션을 얻어내서 작업을 하겠다는 것!

 

 

- 각 Intent가 액션만 다르게 가지고 있는 상태이다. 같은 Service를 사용한다.

- 액션명은 임의로 정한 것! 상수화 해놓았다.

- 만들어놓은 저 Intent를 PendingIntent에 담아서 서비스를 활성화시키기

 

 

- MusicService 안에서 같은 메소드를 활용하지만 들어오는 intent 값이 다르다.

→ 들어오는 intent 값으로 분기할 수 있다!

 

- setAction() 했던 것은 getAction() 메소드로 읽어오면 된다.

- Switch문으로 분기해서, 각각의 case가 로그로 찍히도록 해본다.

 

 

- 알림의 버튼을 클릭했을 때 위와 같이 로그가 찍힌다.