[Android] 포그라운드, 백그라운드, 서비스의 개념과 Notification을 이용한 포그라운드 서비스 (Foreground Service) 만들기

 

포그라운드(Foreground)와 백그라운드(Background), 그리고 서비스

이전에 다른 앱 위에 그리기 기능을 만들 때 안드로이드의 Service를 사용한 적이 있었다. 서비스 기능은 안드로이드에서 매우 중요한 기능 중 하나이며, 백그라운드에서 다운로드를 진행하거나, 음악을 플레이하는 등 다양한 형태의 앱을 만들 수 있게 해준다. 


서비스는 기본적으로 백그라운드 서비스 (Background Service)포그라운드 서비스 (Foreground Service)로 나누게 된다. 여러 블로그에서 포그라운드와 백그라운드의 차이를 다양한 언어로 나누는데, 필자는 이론적인 이야기를 좋아하지 않아서... 개인적으로 둘의 차이를 설명하라고 한다면 화면에 보여지냐, 혹은 보이지 않느냐의 차이라고 설명할 것 같다. 

가령 모바일 게임을 하다가 잠시 홈 화면으로 나갔다가 몇 초 후에 다시 게임으로 돌아오면 게임이 종료되지 않고 계속 진행되는 것을 가끔씩 볼 수 있을 것이다. 물론 게임 개발사가 게임 화면을 오래 벗어날 때 강제로 종료하게 만들거나 안드로이드 시스템이 자원을 많이 잡아먹는 백그라운드 작업이라 판단하여 종료할 수 있지만, 잠깐 나갔다 들어오는 것은 유지되는 것을 볼 수 있다. 즉, 화면에 보이지는 않지만 앱이 계속 실행 중인 상태를 백그라운드 프로세스라고 볼 수 있으며, 이는 안드로이드 시스템이 자원이 부족하다고 판단할 경우 강제로 종료할 수 있다. 이와 반대로 화면에서 앱이 어떤 작업을 하고 있는지 눈으로 직접 확인할 수 있는 것을 포그라운드 프로세스라고 볼 수 있으며, 쉽게 말해 유튜브를 보거나 웹서핑을 하는 등을 포그라운드 프로세스라고 보면 되겠다.


그렇다면 서비스 (Service)는 무엇인가? 서비스는 앱이 UI 없이 백그라운드에서 특정 시간동안 실행되는 것을 의미한다고 볼 수 있다. 그런데 포그라운드는 눈에 보이는 작업인데 그렇다면 포그라운드 서비스는 눈에 보이는 작업을 백그라운드에서 실행 중이라는 것인가? 그리고 백그라운드에서 작업하는 것이라면 뭐하러 백그라운드 서비스라는 이름을 지었는가?

일단 서비스의 개념에 대해서는 링크에서 확인할 수 있는데, https://developer.android.com/guide/components/services?hl=ko 서비스에 있어서 포그라운드와 백그라운드의 요점은 "앱과 상호작용을 하고 있는지, 하지 않는지"를 나누는 것이라고 보면 어떨까 싶다. 즉, 앱이 무언가를 하고 있다는 것을 눈으로 보고, 손으로 터치하는 등 앱과 상호 작용을 할 수 있다면 포그라운드 서비스, 앱이 어떤 작업을 하고는 있지만 사용자가 직관적으로 확인할 수 없다면 백그라운드 서비스라고 보면 되겠다. 예를 들어, 알림창에서 앱이 무언가를 다운로드하거나 "앱이 실행 중..." 이런 창을 본 적이 있다면, 이것을 포그라운드 서비스라고 보면 될 것이고, 게임을 업데이트하는 중에 해당 앱을 종료해도 업데이트가 계속 되는 것은 백그라운드 서비스라고 보면 되겠다.


위의 링크를 보면, 서비스를 3가지로 나눠놨는데, 위에서 얘기한 포그라운드 서비스, 백그라운드 서비스 2개와 바인드 서비스라는 것이 하나 더 나온다. 이건 또 무엇이냐? 

포그라운드와 백그라운드 서비스는 앱에서 서비스를 "시작"하는 것이다. 실제로도 startService()를 호출하여 사용하며 그 의미는 "내(앱)가 이제 종료되든 백그라운드 프로세스 상태가 되든 (startService()를 이용해) 너에게 맡긴 작업은 계속 하렴."이다. 즉 서비스를 한 번 시작하면 작업이 완료되거나 불가피한 상황으로 종료되는 것이 아니면 앱이 종료되었다고 해도 작업은 계속하게 된다.

바인드 서비스는 조금 다른데, 앱에서 어떤 서비스를 바인드하면, "내(앱)가 종료되기 전까지는 너(서비스)는 나와 계속 통신을 해야한단다."라는 의미가 된다. 마치 서버와 클라이언트가 통신하는 것 마냥 작동하게 되는데, 앱이 해당 서비스를 바인드한 액티비티들(여러 액티비티가 한 서비스에 바인드할 수 있다.)이 전부 종료되기 전까지는 서비스가 유지되며, 액티비티들이 모두 종료되면 서비스도 종료되어진다.

쉽게 요약하자면, 포그라운드 서비스, 백그라운드 서비스는 서비스를 시작한 시점부터 서비스에게 맡긴 작업을 끝내기 전까지는 앱이 종료되든 말든 계속 실행되는 것이며, 바인드 서비스는 앱과 계속 통신을 하다가 서비스를 부른 액티비티들이 전부 종료되면 작업이 끝나지 않아도 종료되는 것이다.



* 2022.01.06 내용 추가

내용을 조금 바꾸는 것이 좋을 것 같습니다. API 26, 오레오 버전 이상부터는 백그라운드 서비스 실행 제한이 걸립니다. 이 때는 Work Manager나 Alarm Manager를 이용해 예약된 작업을 해야 합니다.

https://developer.android.com/guide/components/services?hl=ko



알림(Notification)을 이용한 포그라운드 서비스(Foreground Service) 만들기

내용이 조금 길어졌는데, 이제 위에서 얘기한 서비스 중 포그라운드 서비스를 만들어보자. 이전 포스트에서 현재 실행 중인 앱의 이름을 가져오는 기능을 구현한 적이 있었는데, 이걸 포그라운드 서비스에서 실행하여 알림창에 실행 중인 앱의 이름을 띄워보도록 하겠다. 이전 포스트는 아래의 링크에서 볼 수 있다.

https://keykat7.blogspot.com/2021/01/android-UsagestatsManager.html

서비스를 사용하기 앞서 AndroidManifest.xml에 권한 설정과 서비스 등록을 하자.


<?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.test">

    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
        tools:ignore="ProtectedPermissions"/>
        <!-- 서비스 권한 설정 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Test">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- 서비스 등록 -->
        <service android:name=".testService"></service>
    </application>

</manifest>


일단 서비스를 상속받는 클래스를 만들어 알림창을 가지는 포그라운드 서비스를 만들어주자. 코드는 기능을 구현하기 위해 조금씩 수정할 것이지만, 기본적인 알림창 구현의 형태는 아래와 같다.


package com.example.test;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

public class testService extends Service {

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

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

        // PendingIntent를 이용하면 포그라운드 서비스 상태에서 알림을 누르면 앱의 MainActivity를 다시 열게 된다.
        Intent testIntent = new Intent(getApplicationContext(), MainActivity.class);
        PendingIntent pendingIntent 
                = PendingIntent.getActivity(this, 0, testIntent, PendingIntent.FLAG_CANCEL_CURRENT);
        
        // 오래오 윗버젼일 때는 아래와 같이 채널을 만들어 Notification과 연결해야 한다.
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("channel", "play!!",
                    NotificationManager.IMPORTANCE_DEFAULT);
            
            // Notification과 채널 연걸
            NotificationManager mNotificationManager = ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE));
            mNotificationManager.createNotificationChannel(channel);

            // Notification 세팅
            NotificationCompat.Builder notification
                    = new NotificationCompat.Builder(getApplicationContext(), "channel")
                    .setSmallIcon(R.drawable.round_account_box_24)
                    .setContentTitle("현재 실행 중인 앱 이름")
                    .setContentIntent(pendingIntent)
                    .setContentText("");

            // id 값은 0보다 큰 양수가 들어가야 한다.
            mNotificationManager.notify(1, notification.build());
            // foreground에서 시작
            startForeground(1, notification.build());
        }

        return START_STICKY;
    }
}


갑자기 많은 내용이 나왔는데, 조금 생소할 만한 내용을 차근차근 한 번 보도록 하겠다.

PendingIntent : 쉽게 말해, 다른 컴포넌트에게 파라미터로 받은 인텐트를 특정한 시점에 실행하게 하는 인텐트이다. 위에서 PendingIntent.getActivity라는 것을 사용했는데, 특정 시점에서 testIntent를 이용해 액티비티를 실행하라는 의미이며, 여기서의 특정 시점은 알림창을 터치했을 때를 의미하게 된다. 이 PendingIntent는 밑의 notification에서 setContentIntent로 연결함으로써 알림창을 터치했을 때 해당 인텐트가 실행된다. PendingIntent의 파라미터 마지막에 Flag가 달려있는데, 해당 인텐트를 업데이트할지, 제거하고 재설정할지 등을 결정하는 것이다. 플래그 종류는 링크에서 확인할 수 있으며, 주로 FLAG_CANCEL_CURRENT와 FLAG_UPDATE_CURRENT를 많이 사용하며, 용도에 맞게 사용하면 되겠다. 

https://developer.android.com/reference/android/app/PendingIntent


NotificationChannel : 앱의 알림창을 관리하는 채널이라고 보면 되겠다. 파라미터가 3개 있는데, 첫번째는 채널의 ID, 두번째는 채널의 내용, 세번째는 해당 채널을 사용하는 포그라운드 알림의 중요도를 나타낸다. 사진으로 보면 이해하기가 쉬운데, 아래의 사진을 보면 위에서 등록한 채널의 내용이 나오는데, 앱이 여러 알림창을 가지고 있다면 여기서 관리를 하게 된다. (해당 화면은 앱 정보의 Notification 혹은 알림에서 볼 수 있다.)





NotificationManager, NotificationCompat.Builder : 굳이 설명하지 않아도 무엇을 하는지 알 수 있을 것이라고 생각하지만, 알림창에 채널을 연결하고 알림창의 디자인을 하는 부분이다. 아래의 링크에서 좀 더 자세한 내용을 확인할 수 있다. 참고로 NotificationManager.notify()에 첫번째 파라미터에 들어오는 id는 0보다 큰 양수가 들어와야 하는데, id를 제대로 넣지 않으면 did not then call service.startforeground() 와 같은 에러가 발생할 수 있다. 포그라운드 서비스와 알림의 연결은 5초 이내로 이루어져야 하는데, id를 제대로 넣지 않아 5초 이내로 연결을 하지 못해서 발생하는 것이다. 주의하도록 하자.

https://developer.android.com/training/notify-user/build-notification?hl=ko


START_STICKY : 해당 포그라운드 서비스가 불가피하게 시스템에 의해 종료되었을 때, 서비스를 재생성할지 혹은 그냥 종료된 채로 끝날지를 결정하는 것이다. 보통은 세가지 경우를 많이 사용하게 된다.

START_NOT_STICKY : 강제 종료되면 재생성되지 않음.

START_STICKY : 재생성 후 onStartCommand() 다시 호출. 강제 종료된 후의 마지막 Intent는 다시 호출하지 않음.

START_REDELIVER_INTENT : 재생성 후 onStartCommand() 다시 호출. 강제 종료시에 전달된 마지막 Intent 다시 호출.

그 외의 내용은 아래의 링크에서 확인할 수 있다. 가능하면 링크를 남기지 않고 모두 설명하고 싶지만, 내용과 시간 상 생략하겠다. (원래 안드로이드 개발은 안드로이드 문서와 가까워야하는 법이므로 찾아보는 버릇을 들이면 좋다고 생각한다. 절대 귀찮은 것이 아니다.)

https://developer.android.com/reference/android/app/Service



사실 포그라운드 서비스 구현은 위의 내용이 끝이다. 아래의 내용은 포그라운드 서비스와 이전에 포스팅한 현재 실행 중인 앱 이름 가져오기 기능을 이용해 알림창에 현재 실행 중인 앱의 이름을 띄우는 기능을 구현한 것이다. 관심이 있다면 코드를 분석해보아도 좋다. 



포그라운드 서비스를 이용해 현재 실행 중인 앱 이름을 알림창에 띄우기


MainActivity.java


package com.example.test;

import android.app.AppOpsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    public static MainActivity mainActivity;

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

        //runOnUiThread를 Service에서 쓰기 위해 임시로 static으로 해당 액티비티 클래스를 얻게끔 한다.
        mainActivity = this;

        // 서비스 인텐트 생성 후 서비스 실행. 이 때 오레오 이전 버전과 이후 버전에서 서비스를 시작하는 방식이 조금 다르다.
        Intent serviceIntent = new Intent(MainActivity.this, testService.class);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            startForegroundService(serviceIntent);
        else startService(serviceIntent);

        // 이전 포스트 내용과 차이가 없음. 패키지 이름을 가져오지 못하면 버튼을 누르도록 하자.
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if(!checkPermission())
                    startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
            }
        });
    }

    // 이전 포스트 내용과 같은 내용. 권한 가져오기.
    private boolean checkPermission(){

        boolean granted = false;

        AppOpsManager appOps = (AppOpsManager) getApplicationContext()
                .getSystemService(Context.APP_OPS_SERVICE);

        int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
                android.os.Process.myUid(), getApplicationContext().getPackageName());

        if (mode == AppOpsManager.MODE_DEFAULT) {
            granted = (getApplicationContext().checkCallingOrSelfPermission(
                    android.Manifest.permission.PACKAGE_USAGE_STATS) == PackageManager.PERMISSION_GRANTED);
        }
        else {
            granted = (mode == AppOpsManager.MODE_ALLOWED);
        }

        return granted;
    }
}

testService.java


package com.example.test;

import android.app.AppOpsManager;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.util.LongSparseArray;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

public class testService extends Service {

    CheckPackageNameThread checkPackageNameThread;
    NotificationCompat.Builder notification;
    NotificationManager mNotificationManager;

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

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

        // PendingIntent를 이용하면 포그라운드 서비스 상태에서 알림을 누르면 앱의 MainActivity를 다시 열게 된다.
        Intent testIntent = new Intent(getApplicationContext(), MainActivity.class);
        PendingIntent pendingIntent
                = PendingIntent.getActivity(this, 0, testIntent, PendingIntent.FLAG_CANCEL_CURRENT);

        // 오래오 윗버젼일 때는 아래와 같이 채널을 만들어 Notification과 연결해야 한다.
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel("channel", "play!!",
                    NotificationManager.IMPORTANCE_DEFAULT);

            // Notification과 채널 연걸
            mNotificationManager = ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE));
            mNotificationManager.createNotificationChannel(channel);

            // Notification 세팅
            notification
                    = new NotificationCompat.Builder(getApplicationContext(), "channel")
                    .setSmallIcon(R.drawable.round_account_box_24)
                    .setContentTitle("현재 실행 중인 앱 이름")
                    .setContentIntent(pendingIntent)
                    .setContentText("");

            // id 값은 0보다 큰 양수가 들어가야 한다.
            mNotificationManager.notify(1, notification.build());
            // foreground에서 시작
            startForeground(1, notification.build());
        }

        // 이전 포스트에서 패키지 이름을 2초마다 가져오는 스레드를 서비스에서 실행해준다. 서비스가 실행되면 이 스레드도 같이 실행된다.
        checkPackageNameThread = new CheckPackageNameThread();
        checkPackageNameThread.start();

        return START_STICKY;
    }



    private class CheckPackageNameThread extends Thread{

        public void run(){
            while(true){
                if(!checkPermission()) continue;

                // runOnUiThread를 이용해 UI 스레드에 해당 작업을 큐에 넣어 알림의 내용 UI를 변경할 수 있게 해준다.
                MainActivity.mainActivity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        notification.setContentText(getPackageName(getApplicationContext()));
                        mNotificationManager.notify(1, notification.build());
                    }
                });

                try {
                    sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 아래부터는 이전 포스트와 같은 내용....................................................................
    private boolean checkPermission(){

        boolean granted = false;

        AppOpsManager appOps = (AppOpsManager) getApplicationContext()
                .getSystemService(Context.APP_OPS_SERVICE);

        int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
                android.os.Process.myUid(), getApplicationContext().getPackageName());

        if (mode == AppOpsManager.MODE_DEFAULT) {
            granted = (getApplicationContext().checkCallingOrSelfPermission(
                    android.Manifest.permission.PACKAGE_USAGE_STATS) == PackageManager.PERMISSION_GRANTED);
        }
        else {
            granted = (mode == AppOpsManager.MODE_ALLOWED);
        }

        return granted;
    }


    public static String getPackageName(@NonNull Context context) {

        // UsageStatsManager 선언
        UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);

        long lastRunAppTimeStamp = 0L;

        // 얼마만큼의 시간동안 수집한 앱의 이름을 가져오는지 정하기 (begin ~ end 까지의 앱 이름을 수집한다)
        final long INTERVAL = 10000;
        final long end = System.currentTimeMillis();
        // 1 minute ago
        final long begin = end - INTERVAL;

        //
        LongSparseArray packageNameMap = new LongSparseArray<>();

        // 수집한 이벤트들을 담기 위한 UsageEvents
        final UsageEvents usageEvents = usageStatsManager.queryEvents(begin, end);

        // 이벤트가 여러개 있을 경우 (최소 존재는 해야 hasNextEvent가 null이 아니니까)
        while (usageEvents.hasNextEvent()) {

            // 현재 이벤트를 가져오기
            UsageEvents.Event event = new UsageEvents.Event();
            usageEvents.getNextEvent(event);

            // 현재 이벤트가 포그라운드 상태라면 = 현재 화면에 보이는 앱이라면
            if(isForeGroundEvent(event)) {
                // 해당 앱 이름을 packageNameMap에 넣는다.
                packageNameMap.put(event.getTimeStamp(), event.getPackageName());
                // 가장 최근에 실행 된 이벤트에 대한 타임스탬프를 업데이트 해준다.
                if(event.getTimeStamp() > lastRunAppTimeStamp) {
                    lastRunAppTimeStamp = event.getTimeStamp();
                }
            }
        }
        // 가장 마지막까지 있는 앱의 이름을 리턴해준다.
        return packageNameMap.get(lastRunAppTimeStamp, "").toString();
    }


    private static boolean isForeGroundEvent(UsageEvents.Event event) {

        if(event == null) return false;

        if(BuildConfig.VERSION_CODE >= 29)
            return event.getEventType() == UsageEvents.Event.ACTIVITY_RESUMED;

        return event.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND;
    }
    //..............................................................................................
}

무리 없이 따라왔다면 아래와 같은 결과를 얻을 수 있을 것이다.

크롬을 실행했을 때. 뒤의 화면은 크롬을 맨 처음 시작할 때의 화면이다.

실행 중인 앱 모음 등 스마트폰 시스템과 관련된 창을 열었을 때.

구글 플레이를 열었을 때.




마치며..

안드로이드의 서비스는 매우 중요한 기능 중 하나이며 위의 내용은 서비스의 방대한 내용 중 극히 일부분만 설명했을 뿐이다. 서비스로 만들 수 있는 앱의 콘텐츠는 무궁무진할 것이며 항상 사용할 준비가 되어 있어야하는 것이 좋다. 정리를 제대로 못해서 혹시 공부하는데 오히려 방해가 되었는지 걱정이 된다.. 이 포스트에서 설명하지 않았던 나머지 서비스들과 심도있는 내용까지 철저히 공부하여 서비스를 능숙하게 사용할 수 있길 바란다. 


댓글

  1. 내용 잘 보았습니다. 많은 도움이 되었습니다.

    내용을 보다 궁금한 점이 있습니다.

    1.
    상기 코드에서 오레오 버전 이상이 아니면 노티가 안되는 소스 같아 보입니다만
    이하 버전에서는 어떻게 되나요?

    2.
    PendingIntent.getActivity(this, 0, testIntent, PendingIntent.FLAG_CANCEL_CURRENT);
    에서 PendingIntent.FLAG_CANCEL_CURRENT 는 기능으로 봤을때 구체적으로 어떠한 역할을 하는지 그림이 잘 그려지지 않습니다.

    3.
    그리고 .notify, .startForeground 실행시
    notification.build()를 각각 해주는게 맞는건지 궁금합니다.


    그럼 수고세요.


    ==============



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

    // PendingIntent를 이용하면 포그라운드 서비스 상태에서 알림을 누르면 앱의 MainActivity를 다시 열게 된다.
    Intent testIntent = new Intent(getApplicationContext(), MainActivity.class);
    PendingIntent pendingIntent
    = PendingIntent.getActivity(this, 0, testIntent, PendingIntent.FLAG_CANCEL_CURRENT);

    // 오래오 윗버젼일 때는 아래와 같이 채널을 만들어 Notification과 연결해야 한다.
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    NotificationChannel channel = new NotificationChannel("channel", "play!!",
    NotificationManager.IMPORTANCE_DEFAULT);

    // Notification과 채널 연걸
    NotificationManager mNotificationManager = ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE));
    mNotificationManager.createNotificationChannel(channel);

    // Notification 세팅
    NotificationCompat.Builder notification
    = new NotificationCompat.Builder(getApplicationContext(), "channel")
    .setSmallIcon(R.drawable.round_account_box_24)
    .setContentTitle("현재 실행 중인 앱 이름")
    .setContentIntent(pendingIntent)
    .setContentText("");

    // id 값은 0보다 큰 양수가 들어가야 한다.
    mNotificationManager.notify(1, notification.build());
    // foreground에서 시작
    startForeground(1, notification.build());
    }

    return START_STICKY;
    }

    답글삭제
    답글
    1. 안녕하세요! 읽어주셔서 감사합니다. 꽤나 예전에 쓴 글이기도 하고 학습하는 목적으로 쓴 글이라 지금보면 잘못된 부분이 많네요.. 지적해주셔서 감사합니다. 답변 드리겠습니다!

      1. 오레오 버전 이후부턴 채널을 반드시 생성해야 합니다. 따라서 버전 체크를 한 후에 NotificationChannel을 생성하고 이것을 Notification.Builder에 채널을 연결해주어야 합니다. 오레오 이전 버전은 채널을 생성하지 않아도 되구요. 그런데 위의 코드에선 오레오 버전 체크를 하고 이후에서만 채널 생성과 빌더를 만들었네요 ㅋㅋ.. 조금 어색한 코드라고 보시면 될 것 같습니다.
      쉽게 말해서 오레오 이후 버전이라면 채널과 빌더를 생성한 후 빌더에 채널을 넣고, 그렇지 않으면 빌더를 생성만 해주시면 됩니다.

      빌더 생성
      if (오레오 이상) {
      채널 생성
      builder = new Builder(context, "channel")
      } else {
      builder = new Builder(context)
      }

      이런 느낌으로 가시면 될 것 같아요.

      2. 이건 안드로이드 공식 문서에서 확인하실 수 있습니다! 링크 달아놓겠습니다.
      https://developer.android.com/reference/android/app/PendingIntent

      3. 이것도 잘못된 것 같네요. 대략적으로 val build = Builder.build() 한 후에 build를 notify와 startForeground에 넣어주면 될 것 같습니다. 애당초 이렇게 쓰는 것이 아닌데, 아마 이 당시에 notify와 startForeground에 저런 식으로 넣으면 된다고 기록하려고 적은 것 같네요.

      삭제
    2. 감사합니다.
      좋은 하루되세요.

      삭제
  2. 안녕하세요 만약 안드로이드폰에 악성앱이 깔려서 권한이 넘어가 장악되었을경우
    카메라의경우 해커가 염탐가능하잖아요
    만약 사용자가 카메라를 사용하는와중에 뜨는 폰 화면염탐이아닌 해커가 카메라 자체를 켜서 염탐이가능한가요?
    즉 해커가 카메라로 사용자를 염탐할때 사용자가 카메라를 키면 충돌안되고 중복으로 둘다 사용되는건지 궁금해요

    사용자가 카메라를 이용안할땐 모니터링염탐이든 카메라를 몰래실행해염탐이든 가능하지만
    사용자가 카메라를 사용할때는 해커는 카메라로 따로염탐기능은 못하고 사용자가 카메라로 찍는 화면염탐만 가능한건지요?

    악성앱의경우는 백그라운드에서 카메라앱을 실행하는거잖아요 그럼 이때 동시에 사용자가 폰에있는 카메라기능이있는앱을 실행하면 실행이되는지 즉 , 동시에 카메라권한이있는앱 2개가 사용이되는지 궁금합니다

    **혹시 폰이 루팅된경우라면 동시에 실행이 가능할까요?

    sound assistant라는앱은 음악앱 2개가 동시에 재생 가능하게 해주는걸로 아는데요 카메라권한도 동시에 2개앱이 사용할수있겠끔해주는 어플개발이 가능한건지요..!?
    Sound assistant는 오디오출력권한을 2개앱이 동시에 실행시킬수 있는 이유도 궁금합니다ㅠㅠ

    악성앱을 설계할때 사용자가 카메라를 사용해도 카메라를 백그라운드에서 작동시키는게 가능한지궁금합니다..!

    읽어주셔서 감사합니다
    **아 저는 안드로이드 5.0정도 약 2014-2016년때의 버전기준으로 질문드리는겁니다..!

    악성앱도 어쨋든 앱이라는건 알지만 혹시나 루트권한을 획득하거나 일반앱과는 다른 어떤점이있어 위에여쭤본걸 가능하게할수있는지 궁금해서요..!
    읽어주셔서 감사합미다

    답글삭제

댓글 쓰기