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() 로 가져와서 출력해준다.
- 그러면 알림창에서도 이렇게 실시간으로 텍스트에 시간이 표시되는 것을 볼 수 있다.
- 주기적으로 같은 아이디로 알림을 계속 보낸다.
(하지만 진동은 반복적으로 울리지 않도록 알림의 중요도를 낮춰준다)
'국비교육(22-23)' 카테고리의 다른 글
89일차(1)/Android App(55) : mp3 파일 재생 예제 / Notification(3) (0) | 2023.02.16 |
---|---|
88일차(2)/Android App(54) : mp3 파일 재생 예제 / Notification(2) (0) | 2023.02.15 |
86일차(2)/Android App(52) : mp3 파일 재생 예제 / ProgressBar 사용 (0) | 2023.02.11 |
86일차(1)/Android App(51) : mp3 파일 재생 예제 / MediaPlayer(서버) (0) | 2023.02.10 |
85일차(2)/Android App(50) : Service / Binder 객체로 mp3파일 재생하기 (0) | 2023.02.10 |