본문 바로가기
안드로이드 자바

[Android][Java] Service 사용 이해를 위한 예제(2)

by teamnova 2023. 3. 25.
728x90

오늘은 지난시간에 이어 서비스 사용 예제를 작성해 보겠습니다.

이전 포스팅을 보지 못하신 분은 다음 포스팅을 먼저 읽고 오시길 추천드립니다. 

https://stickode.tistory.com/736

 

이전 포스팅에서 제가 생각한 문제점은 크게 두가지 였습니다. 

 

 

1. 다시 앱을 실행시켰을때 서비스가 중복되게 됨

2. 유저에게 서비스가 실행되고 있음을 알리는 Notification 이 노출되어있고 

 

이 두가지 문제를 중점으로 지난주 작성한 코드를 수정해 보도록 하겠습니다. 

 

1. 앱 재실행 시 서비스 중복 확인

 

먼저 다시 앱을 실행시켰을때 서비스가 중복되지 않게 하는 방법 입니다. 

서비스를 실행시키는 코드가 포함된 컴포넌트에 다음과같은 메서드를 작성합니다. 

 

public boolean foregroundServiceRunning(){
    ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);// 액티비티 매니져를 통해 작동중인 서비스 가져오기

    for(ActivityManager.RunningServiceInfo service: activityManager.getRunningServices(Integer.MAX_VALUE)) {// 작동중인 서비스수 만큼 반복
        if(MyForegroundService.class.getName().equals(service.service.getClassName())) {// 비교한 서비스의 이름이 MyForgroundService 와 같다면
            return true;// true 반환
        }
    }

    return false;// 기본은 false 로 설정
}

 

해당 메서드는 boolean 값을 리턴받는 메서드로

 

ActivityManager 클래스를 이용해 현재 실행중인 서비스를 확인하고

만약 작동중인 서비스중에 MyForegroundService 와 동일한 서비스 이름이 있다면 True 를 반환해 주고 

그 외에는 false 를 반환해 주는 메서드입니다.

 

이를 이용해 새로운 서비스를 실행해 주어야 할지 그러지 말아야 할지 판별할 수 있도록  원하는 위치에  코딩을 해줍니다.

예제의 경우 onCreate 에서 다음과 같이 작성해 주었습니다.

 

if (!foregroundServiceRunning()){// 이미 작동중인 동일한 서비스가 없다면 실행
    Intent serviceIntent = new Intent(this, MyForegroundService.class);// MyBackgroundService 를 실행하는 인텐트 생성
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {// 빌드 버전코드 "O" 보다 높은 버전일 경우
        startForegroundService(serviceIntent);// 서비스 인텐트를 전달한 서비스 시작 메서드 실행
    }
}

앞서 작성한 메서드를 이용해 동일한 서비스가 작동하고 있지 않은경우에만 해당 서비스를 실행시키는 코드를 작성해 줍니다.

 

2. 노티피케이션 지우기 

카톡을 비롯한 다른 메신저 어플은 앱이 꺼져있더라도 노티피케이션 없이 대기하고 있다가 다른 유저가 보낸 메시지를 수신할 수 있는등의 기능을 수행할 수 있습니다. 

 

서비스적인 요소가 노티피케이션 없이 실행을 하고 있다는뜻인데 어떻게 가능한걸까요? 

같은 방법인지는 알 수 없지만 기존에 포스팅된 자료 중 표면적으로나마 해결이 가능한 방식으로 예제를 작성해 보았습니다. 

 

더 좋은 방법이 있다면 공유 부탁드립니다 ㅠ 

 

혹 제 설명이 부족하다면 다음 포스팅을 참고 부탁드립니다.

https://forest71.tistory.com/185

 

지난 시간 포스팅에 의하면 우리가 알고있는 서비스의 특징은 다음과 같습니다.

 

앱이 API 레벨 26 이상을 대상으로 전용 앱이 포그라운드에 있지 않을 때 시스템에서 백그라운드 서비스 실행에 대한 서비스 실행에 대한 제한을 적용한다.

 

앱이 포그라운드에 있지 않을때는 시스템에서 백그라운드 서비스를 실행할 수 없으므로

위 블로그에서 제시한 해결방안은

앱이 켜진동안 작동하던 서비스를 앱이 꺼짐과 동시에 종료 시켜주고

그와 동시에  짧은 시간동안 포그라운드 서비스를 작동시키고 실행된 포그라운드 서비스가  바로 백그라운드 서비스를 실행해 서비스를 유지하는 형식을 취하고 있습니다.

 

제가 이해한 바 간단하게 흐름을 설명하자면 다음과 같습니다.

 

1. 앱종료 => onDestroy 에서 동작중이던 서비스 종료 (이하 A서비스)

2. A 서비스가 종료됨과 동시에 1초뒤 실행하는 알람을 설정

3. 알람은 A서비스를 백그라운드로 실행하는 포그라운드 서비스 를 실행(이하 B서비스)

4. B서비스는 백그라운드로 A서비스 를 실행해주고 B서비스 스스로 종료 

5. 2~4 과정의 반복

 

이제 구현을 해보겠습니다. 

 

일단 현재 필요한 클래스들을 먼저 작성해 주겠습니다.

 

AlarmReciver

package com.example.service_ex;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;

public class AlarmReciver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            Intent in = new Intent(context, RestartService.class);
            context.startForegroundService(in);
        } else {
            Intent in = new Intent(context, MyForegroundService.class);
            context.startService(in);
        }
    }
}

알람에 의해 작동하는 브로드캐스트 리시버 입니다. 

RestartService 를 실행해 줍니다. 

 

MyBroadcastReciver

지난시간에 이미 작성해둔 클래스 입니다. 

다음과 같이 변경해 주시면 됩니다. 

public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {

    if(intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)){
        Intent serviceIntent = new Intent(context, RestartService.class);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            context.startForegroundService(serviceIntent);
        }else{
            context.startService(serviceIntent);
        }
    }

    }
}

 

Intent 의 클래스를 RestartService 로 변경해 준 코드입니다. 

 

RestartService

public class RestartService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Log.i("정보태그", "RestartService");

        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "default");
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setContentTitle(null);
        builder.setContentText(null);
        Intent notificationIntent = new Intent(this, MainActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
        builder.setContentIntent(pendingIntent);

        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            manager.createNotificationChannel(new NotificationChannel("default", "기본 채널", NotificationManager.IMPORTANCE_NONE));
        }

        Notification notification = builder.build();
        startForeground(9, notification);

        /////////////////////////////////////////////////////////////////////
        Intent in = new Intent(this, MyForegroundService.class);
        startService(in);

        stopForeground(true);
        stopSelf();

        return START_NOT_STICKY;
    }
}

 

MyForgroundService 를 백그라운드에서 실행시켜줄 서비스 입니다. 

MyForgroundService  를 실행시키자 마자 스스로를 종료시켜서 notification 이 발생하긴 하지만 매우 짧게 만들어 줍니다.

 

 

 MainActivity 의 onDestroy 에서 실행중이던 서비스를 종료 시키는 코드를 작성해 주겠습니다.

 

if (serviceIntent!=null) {
    stopService(serviceIntent);// 서비스 정지시켜줌
    serviceIntent = null;
}

 

다음 MyForegroundService 에서 서비스가 종료될 경우 알람을 세팅하는 메서드를 작성해보겠습니다.

 

protected void setAlarmTimer() {
    final Calendar c = Calendar.getInstance();
    c.setTimeInMillis(System.currentTimeMillis());
    c.add(Calendar.SECOND, 1);
    Intent intent = new Intent(this, AlarmRecever.class);
    PendingIntent sender = PendingIntent.getBroadcast(this, 0,intent,0);

    AlarmManager mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
    mAlarmManager.set(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), sender);
}

 

작성된 메서드는 MyForegroundService 의 OnDestroy 에서 다음과같이 선언해 줍니다.

 

serviceIntent = null;
setAlarmTimer();

if(mThread != null && mThread.isAlive() ){
    mThread.interrupt();// 스레드 종료
    mThread = null;// 스레드 제거
    mCount = 0;// 카운트 초기화
}

 

새로 선언한 서비스와 브로드캐스트 리시버는 꼭 잊지말고 매니페스트에 선언해주셔야 합니다.

<service android:name=".RestartService"></service>
<receiver android:name=".AlarmRecever"></receiver>

<receiver android:name=".MyBroadcastReceiver"></receiver>

 

 

이전 예제에서 startForegroundService 로 선언해 두었던 코드들은  startService 로 변경해줍시다.(전체코드 첨부하겠습니다. 확인후 예제 실행해 주세요)

 

여기까지 구현을 하고 나면

Oreo 이상 버전에선 2~5분 이후 서비스가 종료되는걸 확인할 수 있는데 이 문제는 절전 모드 문제로

REQUEST_IGNORE_BATTERY_OPTIMIZATION를 이용하여 

절전모드를 사용하지 않는 예외 앱으로 처리하면 됩니다. 

 

먼저, AndroidManifest.xml에 다음 권한을 추가합니다.

<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

그리고, MainActivity.java 파일에 절전 모드를 해제하는 권한을 얻는 코드를 추가해 줍니다.

 

PowerManager pm = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
boolean isWhiteListing = false;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
    isWhiteListing = pm.isIgnoringBatteryOptimizations(getApplicationContext().getPackageName());
}
if (!isWhiteListing) {
    Intent intent = new Intent();
    intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
    intent.setData(Uri.parse("package:" + getApplicationContext().getPackageName()));
    startActivity(intent);
}

실행하면 다음과 같은 결과를 얻을 수 있습니다. 

 

 

실행중이던 서비스를 죽이더라도 노티피케이션 없이 백그라운드에서 서비스가 작동하는것을 확인할 수 있습니다.

 

지난주에 이어 예상치 못한 수정된 요소가 있을 수 있습니다. 

전체코드를 확인해 보시길 추천드립니다.

 

코드 첨부 하겠습니다. 

 

MainActivity

public class MainActivity extends AppCompatActivity {

    private Intent serviceIntent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        serviceIntent = new Intent(this, MyForegroundService.class);// MyBackgroundService 를 실행하는 인텐트 생성

        PowerManager pm = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
        boolean isWhiteListing = false;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            isWhiteListing = pm.isIgnoringBatteryOptimizations(getApplicationContext().getPackageName());
        }
        if (!isWhiteListing) {
            Intent intent = new Intent();
            intent.setAction(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
            intent.setData(Uri.parse("package:" + getApplicationContext().getPackageName()));
            startActivity(intent);
        }

        if (!foregroundServiceRunning()){// 이미 작동중인 동일한 서비스가 없다면 실행
            serviceIntent = new Intent(this, MyForegroundService.class);// MyBackgroundService 를 실행하는 인텐트 생성
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {// 빌드 버전코드 "O" 보다 높은 버전일 경우
                startService(serviceIntent);// 서비스 인텐트를 전달한 서비스 시작 메서드 실행
            }
        }else{
            serviceIntent = MyForegroundService.serviceIntent;
        }


    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (serviceIntent!=null) {
            stopService(serviceIntent);// 서비스 정지시켜줌
            serviceIntent = null;
        }

    }

    public boolean foregroundServiceRunning(){
        ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);// 액티비티 매니져를 통해 작동중인 서비스 가져오기

        for(ActivityManager.RunningServiceInfo service: activityManager.getRunningServices(Integer.MAX_VALUE)) {// 작동중인 서비스수 만큼 반복
            if(MyForegroundService.class.getName().equals(service.service.getClassName())) {// 비교한 서비스의 이름이 MyForgroundService 와 같다면
                return true;// true 반환
            }
        }

        return false;// 기본은 false 로 설정
    }
}

 

Manifests

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.service_ex">

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Service_Ex"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".MyBackgroundService"></service>
        <service android:name=".MyForegroundService"></service>
        <service android:name=".RestartService"></service>
        <receiver android:name=".AlarmRecever"></receiver>

        <receiver android:name=".MyBroadcastReceiver"></receiver>
    </application>

</manifest>

 

 

MyForegroundService

노티피케이션 과 StartForeground 를 주석처리 해 두었습니다.

public class MyForegroundService extends Service {

    int count = 0;
    public static Intent serviceIntent = null;
    private Thread mThread;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        if(mThread == null){// mThred 가 없는경우
            mThread = new Thread("MyThread"){// 스레드 생성
                @Override
                public void run() {
                    while (!Thread.currentThread().isInterrupted()) {// 인터럽트 된 상태가 아닐경우 계속 실행

                        Log.e("Service", "서비스가 실행 중입니다..."+count);
                        try {
                            count++;// 서비스 변수 변화
                            Thread.sleep(2000);
                        } catch (Exception e) {
                            e.printStackTrace();
                            this.interrupt();
                        }


//                        final String CHANNELID = "Foreground Service ID";
//                        NotificationChannel channel = new NotificationChannel(
//                                CHANNELID,
//                                CHANNELID,
//                                NotificationManager.IMPORTANCE_LOW);
//
//                        getSystemService(NotificationManager.class).createNotificationChannel(channel);
//                        Notification.Builder notification = new Notification.Builder(context, CHANNELID)
//                                .setContentText("서비스가 실행중입니다.")
//                                .setContentTitle("Service_EX4")
//                                .setSmallIcon(R.drawable.ic_launcher_background);
//
//                        Notification  not = notification.build();
//
//                        startForeground(888, not);

                    }


                }
            };
            mThread.start();
        }

        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        Log.e(TAG,"MyService onDestroy");
        super.onDestroy();

        serviceIntent = null;
        setAlarmTimer();

        if(mThread != null && mThread.isAlive() ){
            mThread.interrupt();// 스레드 종료
            mThread = null;// 스레드 제거
            count = 0;// 카운트 초기화
        }


    }

    protected void setAlarmTimer() {
        final Calendar c = Calendar.getInstance();
        c.setTimeInMillis(System.currentTimeMillis());
        c.add(Calendar.SECOND, 1);
        Intent intent = new Intent(this, AlarmRecever.class);
        PendingIntent sender = PendingIntent.getBroadcast(this, 0,intent,0);

        AlarmManager mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        mAlarmManager.set(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), sender);
    }


}