국비교육(22-23)

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

서리/Seori 2023. 2. 14. 18:02

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

 

 

MainActivity

package com.example.step23mp3player;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
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);

        //띄울 알림을 구성하기
        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",null))
                .addAction(new NotificationCompat.Action(android.R.drawable.ic_media_play,"Pause",null))
                .addAction(new NotificationCompat.Action(android.R.drawable.ic_media_play,"Stop",null))
                .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;
        }
    }

}

 

- mp3 재생 앱에 여러가지 옵션을 추가해볼 예정

 

 

* 위의 try문 안에서 수행하는 작업 

- MediaPlayer 객체 생성

- 음악을 재생할 stream type 설정

- 원격지 서버 경로로부터 특정 mp3 파일 로딩

- 약간 시간이 소요되는데, 재생할 준비가 끝나면 알려줄 수 있는 리스너 설정

 (여기서 리스너의 this는 MainActivity를 가리킨다)

 

 

- 마지막으로 preparedAsync() 로 비동기 로딩한다.

- onStart() 는 빠르게 수행되고 끝나야 하기 때문에, 음악을 로딩하는 작업을 비동기 처리로 빼는 것!

 

 

- preparedAsync() 메소드 안에서는 준비상태를 true로 바꾸고,

 플레이 버튼을 사용 가능하게 한 다음, 전체 재생시간을 ProgressBar의 max값으로 세팅해준다.

 


 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical">

    <ImageButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/ic_media_play"
        android:tooltipText="재생버튼"
        android:id="@+id/playBtn"/>
    <ImageButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/ic_media_pause"
        android:tooltipText="일시정지"
        android:id="@+id/pauseBtn"/>
    <ProgressBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
        android:id="@+id/progress"/>
    <SeekBar
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
        android:id="@+id/seek"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:id="@+id/time"/>
</LinearLayout>

 

- ProgressBar와의 비교를 위해 Seekbar 를 추가해보았다!

 

- MainActivity에서는 seek라는 필드를 만들고 참조값을 얻어와 사용하면 된다.

 

 

- TimeUnit 일부 수정. 분-초를 분리해 계산하므로 분에 해당되는 초를 빼주는 과정이 필요!

 

- 이렇게 ProgressBar, SeekBar 2개의 진행 바가 같이 재생된다.

(사용방법은 거의 비슷하다)

 

- 이런 UI가 지속적으로 진행되는 것으로 보이려면 실시간으로(주기적으로) 계속 업데이트를 해주어야 한다.

 

- 그래서 핸들러를 만들어놓고+재생할 준비가 끝나면 핸들러에 빈 메세지를 보내는 메소드를 만들어서 사용하는 것!

- Handler 객체 만들기 → onPrepared() 에서 Handler에 빈 메세지 보내기 → 메소드 안에서 자기 자신에게 다시 메시지를 보내기

- handleMessage() 라는 메소드가 1초에 10번씩 호출되는 구조이다.

 

- Handler에 메세지를 한번만 보내면 아래의 handleMessage() 메소드가 1/10초마다 반복적으로 호출된다.
- handleMessage() 메소드는 UI 스레드 상에서 실행되기 때문에 마음대로 UI를 업데이트할 수 있다.

 

- 이 메소드에서 여기를 업데이트 해주고 있는 것이다.

 

- 나중에는 이 재생 작업을 서비스에서 하도록 수정할 것이다!

- 액티비티를 닫아도 음악이 재생되는 구조로 바꿀 예정이다.

- 액티비티가 없이도 제어를 하도록 만들려면? 알림창에 재생, 일시정지 등 제어 가능한 버튼을 달아서 띄우기!

 


 

[ 알림에서 음악을 제어하게 하기 ]
- 알림을 지속적으로 update 하기 위해서는 같은 아이디로 알림을 주기적으로 계속 보내야 한다.
- 알림에 있는 특정 UI를 클릭하면 해당 정보를 서비스에서 받아서 처리하도록 한다.

 

- 음악 진행상황이 이런 알림창에서 쭉 진행되는 것이 보일 수 있도록!

 

 

** 알림 채널 만들기

 

- 새 채널 아이디 만들어주기

 

- 아래에 CreateNotificationChannel() 메소드를 추가

 

- onCreate() 안에 알림 채널 만들기

 

new NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_LOW)

- 또한 매번 업데이트할 때마다 진동이 울리지 않도록 중요도를 IMPORTANCE_LOW 로 설정!(중요)

 


 

- 새 메소드 추가 makeManualCancelNoti()

 

 

- 띄울 알림의 이름 추가, 우선순위 설정(setPriority), Action 추가(addAction), 액션의 이름 등 설정

 

 

- setProgress 알림창에 어떤 진행 바를 띄우는 것!

setProgress (Max값, 현재 값, 상호작용여부) 로 인자를 넣어준다.

- setAutoCancel(false)로 자동 취소 되는 알림이 아닌것으로 설정

-> 그리고 이 알림을 1/10초마다 계속 호출한다(UI의 진행 바 업데이트를 위해)

 

 

AndroidManifest.xml

- 해당 앱에서 알림을 보내는 것이 가능하도록 permission 추가! POST_NOTIFICATIONS

 

- 알림의 아이디도 추가해준다.

 

- 만약 Manifest import 과정에서 오류가 발생하면 android.Manifest로 import 해주기!

 

- permissionCheckResult 메소드

- 위에서 설정한 알림을 어떤 권한을 부여받으면 띄운다.

 

- 반복적으로 수행되는 핸들러 안에 알림을 띄우는 메소드를 넣어준다.

 

- 알림은 1/10초마다 계속 뜨고,

 음악이 진행되고 있다면 화면에서도, 알림 창에서도 업데이트가 된다.

 

 

- 알림을 허용하고 재생해주면 이렇게 알림의 진행 바에서도 진행상황을 볼 수 있다.

- 알림에 재생버튼, 일시정지, 멈춤 기능 등을 지정할 수 있다. (현재 기능은 없는 상태)

 


 

- addAction() : 알림 안에서 할 동작을 지정하기

- 액션의 아이콘을 표시하기는 지금은 지원이 안되고, 알림의 타이틀만 이곳에 나타난다.

 

 

- 여기에 PendingIntent 객체를 전달할 수 있다. 이 객체를 전달하면서 intent를 넣어준다.

- 그러면 저 play, pause, stop 버튼을 눌렀을때 이 intent를 받아줄 수 있는 무언가가 실행된다.

 

- Intent 객체를 받는 것은 보통 셋중 하나이다. 1) Activity, 2) BroadCastReceiver, 3) Service

- 우리는 Service가 받도록 하게 해서 시킬것이다!

 

 

- 글자와 함께 현재 재생시간을 텍스트로 출력하기!

 

- handler 안에 있던 현재 재생시간을 알아내는 코드를 알림을 띄우는 곳으로 복사해주기!

 

- 이것을 알림 안에 .setContentText() 로 가져와서 출력해준다.

 

- 그러면 알림창에서도 이렇게 실시간으로 텍스트에 시간이 표시되는 것을 볼 수 있다.

 

- 주기적으로 같은 아이디로 알림을 계속 보낸다.

(하지만 진동은 반복적으로 울리지 않도록 알림의 중요도를 낮춰준다)