package com.bsj0420.ex61backgroundtaskbythread;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
MyThread myThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_start).setOnClickListener(v->{
//백그라운드에서 반복작업을 수행하는 별도의 스레드 객체를 생성 및 실행
if (myThread != null) return; //이미 동작하는 상황
myThread = new MyThread();
myThread.start(); //myThread.run() 하면 메인스레드가 해버림
// start() : 자동 run 메소드 발동
});
findViewById(R.id.btn_stop).setOnClickListener(v->{
//스타트 버튼 안누르고 눌렀을 때 대비
if(myThread != null){
//myThread.stop(); //리소스 관리가 안돼서 안드로이드는 금지
//스레드는 run()메소드가 종료되면 멈춤, 1회성 객체라서 다시 실행불가
//while문 때문에 run메소드가 종료되지 않고 있음 그러니 멈추려면
//while문의 조건값 isRun을 false로 변경
myThread.isRun = false;
//스레드는 멈추나 스레드(1회성) 객체는 살아있어서 나중에 또 눌러도 동작 안함
//때문에 참조변수를 null을 넣어서 연결을 끊어준다
myThread = null;
}else {
Toast.makeText(this, "Thread 객체를 참조하고 있지 않습니다", Toast.LENGTH_SHORT).show();
}
});
}
//디바이스의 뒤로가기버튼을 클릭했을 떄 반응하는 콜백 메소드
@Override
public void onBackPressed() {
//super.onBackPressed(); // 얘가 안꺼지게 만드는 애
//기본 메인 액티비티는 백버튼을 눌러도 종료되지않아 강제 종료
finish();
}
//백그라운드 동작을 하는 별도 스레드 클래스 설계
class MyThread extends Thread{
boolean isRun = true;
@Override
public void run() {
while (isRun) {
//별도 스레드는 토스트 못띄움 UI작업 불가!!! -> runOnUiThread
runOnUiThread( () ->{ //new Runnable 매개변수 없으니까 () 써야함
Toast.makeText(MainActivity.this, "백그라운드 작업", Toast.LENGTH_SHORT).show();
Log.i("Ex61","백그라운드 작업");
});
//5초 정도 대기
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}//myThread
}
☝ 스레드 멈추기
스레드는 run()메소드가 종료되면 멈춤, 1회성 객체라서 다시 실행불가 같은 스레드를 다시 동작하게 만들려면 참조변수를 null로 만들고 새롭게 객체를 만들어 줘야함~!
☝ 서비스를 실행하는 방법은 2종류가 있다
1. 'startService()로 서비스가 실행되면' 자동 실행하는 콜백 메소드 존재 : onStartCommand()
2. bindService()로 서비스가 시작되면 자동으로 실행되는 메소드 : onBind()
1. Service를 상속받은 java class 만들기
package com.bsj0420.ex62service;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import androidx.annotation.Nullable;
public class MyService extends Service {
//서비스는 안드로이드의 4대 주요 컴포넌트이므로 매니패스트에 등록해야함
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
2. 매니패스트에 만든 서비스 등록
3. 메인 액티비티에서 인텐트를 이용해 서비스 실행
다만 26 버전 이후 인텐트를 서비스로 보내려면 startForegroundService(intnet)로 보내야함 근데 얘는 퍼미션이 필요하다...
근데 하필 동적 퍼미션이라 사용자에게 앱 사용전에 허용을 받아야함,,,
-> 여기서 동적 퍼미션 허용/거부 선택하는 다이아로그 띄워준다
package com.bsj0420.ex62service;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_start).setOnClickListener(v->{
//백그라운드 작업을 Service 컴포넌트 이용
//서비스 제어할 인텐트
Intent intent= new Intent(this, MyService.class);
//2.
//오레오부터 Foregreaoun Service(26버전) 도입 - 퍼미션 필요
if(Build.VERSION.SDK_INT>=26){
startForegroundService(intent);
}else{
startService(intent);
}
});
findViewById(R.id.btn_stop).setOnClickListener(v->{
Intent intent= new Intent(this, MyService.class);
stopService(intent);
});
//3.알림에 대한 동적 퍼미션 체크 및 요청
//3-1. 스스로 체크
int checkResult = checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS);
//checkSelfPermission(매니패스트에 퍼미션 등록한 이름)
if(checkResult == PackageManager.PERMISSION_DENIED) { //퍼미션이 거부되면...
//4-2 대행사로
//퍼미션 요청 결과를 받아주는 대행사 객체를 활용
permissionResultLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
}
}
//4. 퍼미션 요청 결과를 받아주는 대행사 객체 생성 및 계약 등록 : 반드시 멤버로
ActivityResultLauncher<String> permissionResultLauncher= registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback<Boolean>() {
@Override
public void onActivityResult(Boolean result) {
if(result) Toast.makeText(MainActivity.this, "알림 허용", Toast.LENGTH_SHORT).show();
else Toast.makeText(MainActivity.this, "알림 불가. 서비스도 불가", Toast.LENGTH_SHORT).show();
}
});
@Override
public void onBackPressed() {
finish();
}
}
4. 서비스에서 동작
① 매니 패스트한테 Notification 퍼미션 등록
② 26버전 이상은 startForegroundService(intent)로 인텐트 보내야함
startForegroundService로 실행된 서비스 객체는 반드시! startForeground(Notification이 필요)라는 메소드를 호출해야만 한다!!!!!!
③ 매개변수로 들어갈 NotificationManagerCompat 먼저 생성
④ 알람객체 생성
⑤ startForeground(id(암거나), notification);
package com.bsj0420.ex62service;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
public class MyService extends Service {
MediaPlayer mp; //스레드 따로 만들 필요없은 자체가 스레드상속받고 있음
//서비스는 안드로이드의 4대 주요 컴포넌트이므로 매니패스트에 등록해야함
//'startService()로 서비스가 실행되면' 자동 실행하는 콜백 메소드 존재 : onStartCommand
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Toast.makeText(this, "onStartCommand", Toast.LENGTH_SHORT).show();
//오레오 버전부터는 "Forground(전경) Service" 라는 개념이 도입됨
//사용자에게 서비스가 구동되고 있다는 느낌을 주기 위한 것 반드시 알림(Notification)을 보이도록 강제하는 서비스 개념
// 알림 객체 만들고 ForgroundService로 실행하라고 요청
// startForeground()라는 메소드를 호출해야만 함.
//startForegroundService로 실행된 서비스 객체는 반드시
//startForeground(Notification이 필요)라는 메소드를 호출해야만 한다!!!!!!
// 알림객체를 만들고 포어그라운드 서비스로 실행하라고 요청!!
//3.
NotificationManagerCompat manager= NotificationManagerCompat.from(this);
NotificationCompat.Builder builder= null;
if(Build.VERSION.SDK_INT>=26){
NotificationChannelCompat channel= new NotificationChannelCompat.Builder("ch1", NotificationManager.IMPORTANCE_HIGH).setName("Ex62 알림채널").build();
manager.createNotificationChannel(channel);
builder= new NotificationCompat.Builder(this, "ch1");
}else{
builder= new NotificationCompat.Builder(this, "");
}
builder.setSmallIcon(R.drawable.ic_noti);
builder.setContentTitle("Ex62 뮤직 서비스");
builder.setContentText("뮤직 서비스 실행중");
//음악 재생/정지 버튼을 가진 메인액티비티를 알림창이 클릭됐을 때 실행 되도록
Intent i = new Intent(this, MainActivity.class);
PendingIntent pendingIntent= PendingIntent.getActivity(this, 11, i, PendingIntent.FLAG_IMMUTABLE);//잠시 보류
builder.setContentIntent(pendingIntent);
//4.알림객체 생성
Notification notification = builder.build();
//5.
//포어그라운드로 실행하도록...
startForeground(1, notification); //startForegroundService로 시작한건 이걸로 만들어야함
if(mp == null) {
//서비스도 Context를 상속받아 만들었기때문에 context 자리에 this 사용 가능
mp = MediaPlayer.create(this,R.raw.kalimba);
//아래 두가지 속성은 한다
mp.setVolume(0.7f,0.7f); //소프트웨어의 볼륨 0.0 ~ 1.0
mp.setLooping(true);
}
mp.start(); //일시정지 했을 경우도 있으니 if문 밖에서 스타트
//메모리 문제로 프로세스가 강제로 서비스를 kill 시켜버리는 경우가 있음
//데스크탑은 마지막 넘이 천천히 실행되나
// 모바일은 액티비티보다 서비스를 먼저 죽임
//다시 메모리문제가 해결되면 운영체제가 자동으로 다시 서비스를 실행시키도록 설정 :START_STICKY
// 죽임 그냥 순응하고 갈래... : START_NOT_STICKY
return START_STICKY;
}
//stopService()를 실행하여 서비스가 종료도면 자동으로 실행되는 메소드
@Override
public void onDestroy() {
if(mp != null) {
mp.stop(); //스레드를 상속받아서 stop() 메소드 있음
mp.release(); //미디어는 용량이 커서 gpu의 램이 감당하는데 cpu램에선 stop하면 지우는데
//미디어 프로그램은 release() 해줘야 GPU에서 삭제됨
mp = null;
}
super.onDestroy();
}
//bindService()로 서비스가 시작되면 자동으로 실행되는 메소드
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
일시정지 기능 만들어보기
💡 스타트서비스와 바인드서비스의 라이프사이클
- 스타트서비스는 onCreate를 한번만 한다
이번 예제는 서비스를 실행하고 바인드를 만들것이다
서비스 클래스를 만들면 반드시 매니패스트에 등록
☝ 서비스 스타트를 실행 뒤 바인드를 하면 동작되는 라이프 사이클 순서
💡 bindService() 의 flag : 설정
1) BIND_AUTO_CREATE : 서비스 객체가 없으면 자동으로 만들겠다 라는 뜻
스타트 서비스 안하고 바로 바인드 할 때 이거 쓰면 됨
2) 0 = 만들지않겠다
- 백그라운드 작업이 여러개 있을 수 있다 바인드가 여러개 있을 수 있다
- 만약 메인액티비티가 없어지면 서비스스타트 안하고 BIND_AUTO_CREATE로 바인드로만 만들면 메인액티비티가 사라질 때 바인드도 같이 사라짐
- 스타트 서비스로 시작하면 독립단위로 움직이기 떄문에 바인드는 영향을 받지 않는다
서비스와 바인드 각각 호출
📢 바인드는 노티피케이션 안만들어도 된다!!!!
-> 근데 동작하고 있다는 것을 알기위해 개발자가 그냥 만듦
package com.bsj0420.ex63servicebind;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
//1. 서비스 참조
MyMusicService musicService = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_play).setOnClickListener(view -> clickPlay());
findViewById(R.id.btn_pause).setOnClickListener(view -> clickPause());
findViewById(R.id.btn_stop).setOnClickListener(view -> clickStop());
}
private void clickStop() {
if(musicService != null) {
musicService.stopMusic();
unbindService(connection); //바인드도 끊어버리기 - 터널 끊어버림
musicService = null;
}
//바인드로만 실행 해으면 위 if문으로 끝내면 사라짐
//하지만 현재 내 코드에선 스타트 서비스로 시작했기때문에
//onUnbind 하면 다시 액티비티로 돌아옴 때문에
//완전하게 서비스를 종료하기 위해서 인텐트로 종료를 보내야함
//바인드를 하면 포어그라운드 안해도 됨
Intent intent = new Intent(this, MyMusicService.class);
stopService(intent);
//액티비티도 종료
finish();
}
private void clickPause() {
if(musicService != null) musicService.pauseMusic();
}
private void clickPlay() {
if(musicService != null) musicService.playMusic();
//보통 개발자들은 이때 노티피케이션 생성한다 -> 바인드는 안만들어도 돼서
// 동작하고 있다는 것을 알기위해 개발자가 그냥 만듦
}
//2. clickPlay() 클릭하기 전에 서비스 만들것
//액티비티가 화면에 보여질 때 자동으로 발동하는 콜백 메소드
@Override
protected void onResume() {
super.onResume();
if(musicService == null){
//뮤직 서비스를 실행하고 연결하기
Intent intent = new Intent(this,MyMusicService.class);
startService(intent); //서비스 객체가 없으면 ceate하고 onStartCommond()를 호출함
// 하지만 이미 서비스가 있는데 또 스타트 하라고 하면 onStartCommond()만 호출함
//Service 객체와 바인드 연결(bind)
bindService(intent,connection,0); //flag : 설정
// BIND_AUTO_CREATE : 서비스 객체가 없으면 자동으로 만들겠다 라는 뜻
// 스타트 서비스 안하고 바로 바인드 할 때 이거 쓰면 됨
// 0 = 만들지않겠다
//백그라운드 작업이 여러개 있을 수 있다 바인드가 여러개 있을 수 있다
//만약 메인액티비티가 없어지면
//스타트 안하고 BIND_AUTO_CREATE로 바인드로만 만들면 메인액티비티가 사라질 때 바인드도 같이 사라짐
//스타트 서비스로 시작하면 독립단위로 움직이기 떄문에 바인드는 영향을 받지 않는다
}
}
//마이 뮤직서비스와 연결하는 터널 객체 : ServiceConnection
ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) { //개통됐을 때
//여기가 발동해야 연결된것 - 그것을 확인하기 위한 토스트
Toast.makeText(MainActivity.this, "바인드 연결", Toast.LENGTH_SHORT).show();
// 매개변수 IBinder : 뮤직서비스 onBind()메소드에서 터널을 통해 넘어올 객체
// onServiceConnected 매개변수 iBinder를 내 바인드로 형변환
MyMusicService.MyBinder binder = (MyMusicService.MyBinder) iBinder; //내가 만든 바인드로 형변환
//이너 클래스라서 아웃터 네임 자동 붙음
musicService = ((MyMusicService.MyBinder) iBinder).getServiceObject();
// .getServiceObject() : 서비스클래스 내에 만들어 둔 주소값 가져오는 메소드
}
@Override
public void onServiceDisconnected(ComponentName componentName) {//막혔을 떄
}
};
}
package com.bsj0420.ex63servicebind;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Binder;
import android.os.IBinder;
import android.widget.Toast;
import androidx.annotation.Nullable;
public class MyMusicService extends Service {
//서비스 객체가 생성되면 자동으로 발동하는 콜백 메소드
@Override
public void onCreate() {
super.onCreate();
//라이브사이클 확인을 위한 토스트
Toast.makeText(this, "onCreate", Toast.LENGTH_SHORT).show();
}
//startService()로 실행하면 자동으로 발동하는 콜백메소드
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//라이브사이클 확인을 위한 토스트
Toast.makeText(this, "onStartCommand", Toast.LENGTH_SHORT).show();
return START_STICKY;
}
//bindService()를 실행하면 자동으로 발동하는 콜백메소드
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new MyBinder(); //객체 생성하여 터널로 넘어가기
//서비스 객체의 주소를 리턴해주는 기능을 가진 객체
}
//터널을 통해서 메인액티비티로 넘어갈 IBinder 객체 클래스
class MyBinder extends Binder {
//마이뮤직 서비스클래스 객체의 주소값을 리턴 해주는 기능 메소드 설계
public MyMusicService getServiceObject(){
return MyMusicService.this; //.this : 본인 안에서 본인 주소값 부르는 키워드
}
}
//음악 재생 객체 및 기능 메소드
MediaPlayer mp; //액티비티에서 참조할 참조변수
public void playMusic(){
if(mp == null){
mp = MediaPlayer.create(this,R.raw.dragon_flight);
mp.setVolume(0.7f,0.7f); //소프트웨어의 볼륨 0.0 ~ 1.0
mp.setLooping(true);
}
if(!mp.isPlaying()) mp.start(); //플레이 중이 아니면 스타트
}
public void pauseMusic(){
if(mp != null && mp.isPlaying()){
mp.pause();
}
}
public void stopMusic(){
if(mp != null){
mp.stop();
mp.release();
mp = null;
}
}
}