본문 바로가기

Android

Immortal Service, 죽지 않는 서비스 만들기

 

 

2019/06/17 - [Android] - ClipboardManager, Clipboard 상태 모니터링에서 ClipboardService를 구현하였습니다.

마지막 부분에 던져놓은 질문을 해결해 볼까 합니다.

 

- 서비스 주체 앱이 메모리에서 내려갈 경우에 클립보드 모니터링을 어떻게 하는가?

- 서비스 주체 앱이 항상 살아있다고 보장할 수 없는데 이 때는 어떻게 클립보드 모니터링을 할 것인가?

 

위 부분을 해결하기 위해 나름대로 찾아본 방법들에 대해 정리를 해보겠습니다.

 


1. startForeground를 이용하는 방법

(※참고 : Android 죽지 않는 서비스 사용하기)

 

Recent app에서 Task kill을 하더라도 서비스를 죽지 않게 하기 위해서 startForeground를 사용합니다.

안드로이드 4.3 버전 이상부터는 startForeground 사용 시 Notification으로 Service가 동작하고 있음을 명시해 주어야 합니다.

 

또한, 핸드폰을 재부팅 했을 때 App을 실행하지 않더라도 Clipboard 상태변화를 감지해야 하므로 BOOT_COMPLETE BR을 받아 Service를 실행해야 합니다.

 

정리하자면, 요구사항은 아래와 같습니다.

 

- AndroidManifest.xml : BOOT_COMPLETED 권한 추가 및 BR 등록

- BootCompletedReceiver  : BOOT_COMPLETE BR 수신 시 ClipboardService를 실행

- ClipboardService : startForeground 로 서비스 실행

 

 

 

 

 

 

위와 같이 구현 시, 원하는 대로 Task kill 하더라도, 앱을 재부팅하더라도 clipboard에서 copy시 

ClipboardService에서 구현한 대로 copy한 내용이 toast로 뜨게 됩니다.

 

하지만, 또 하나의 문제점이 있습니다.

 

- startForeground 로 하면 Notification 이 항상 보인다!

 

 

카카오톡에서는 앱을 실행하고 있지 않더라도 계좌번호 복사를 하면 Push가 오지만,

카카오톡에 대한 Service Notification 은 보이지 않습니다.

 

 

2. startForeground로 생성된 Notification을 Hide하는 방법

 

약간의 꼼수이지만, notification을 startForeground 와 동일한 id로 생성하여 cancel 해 주면

startForeground를 하고도 notification을 hide할 수 있습니다.

 

 

 

 

중간에 Thread를 준 이유는, startForeground로 생성된 notification 이 아래에 생성한 emptyNoti보다 

이 후에 생기는 타이밍 이슈가 있어 임의로 sleep을 주었습니다.

 

O OS 단말에서는 원하는 수준으로 notification이 보이지 않고 service가 죽지않고 clipboard 모니터링을 할 수 있었는데, P OS와 M OS 특정 단말에서 empty로 만든 notification이 보여지고 있었습니다.

카카오는 어떻게 이렇게 깔끔하게 notification을 hide하고 있는걸까요

 

카카오가 어떻게 하고 있는지, 계좌번호 복사->notification 호출 되는 로그를 확인해보았습니다.

task kill을 하고도 clipboard service가 동작하는 것을 확인 하기 위해 task kill을 하였습니다.

바로 MessengerService가 restart 됨을 알 수 있습니다.

 

I ActivityManager: Killing 8160:com.kakao.talk/u0a212 (adj 1001): remove task 
W ActivityManager: Scheduling restart of crashed service com.kakao.talk/.service.MessengerService in 1000ms 
I ActivityManager: Start proc 9268:com.kakao.talk/u0a212 for service com.kakao.talk/.service.MessengerService
D EdgeLightingManager: showForNotification : isInteractive=true, isHeadUp=true, color=0, sbn = 
StatusBarNotification(pkg=com.kakao.talk user=UserHandle{0} id=49152 tag=null key=0|com.kakao.talk|49152|null|
10212: Notification(channel=clipboard pri=1 contentView=null vibrate=null sound=null defaults=0x0 flags=0x110 
color=0xff4d3e36 category=reminder number=0 vis=PRIVATE semFlags=0x0 semPriority=0 semMissedCount=0))
D StatusBar: addNotification key=0|com.kakao.talk|49152|null|10212 fullscreen:false

 

그리고 위 처럼 showForNotification 로그가 보입니다.

카카오에서도 무엇인가를 noti로 띄우고 있다는 것입니다. (channel=clipboard !!!)

카카오톡 설정을 보면 알림 유형에 [클립보드] 가 있는 것을 볼 수 있습니다.

아니 카카오도, Notification을 띄우고 있으면서 꼼수(!?)로 가리고 있는건가?!

 

I notification_cancel: [10212,1558,com.kakao.talk,2,217781550141718,0,0,1088,8,NULL] 
I notification_cancel: [10212,1558,com.kakao.talk,1,NULL,0,0,1088,8,NULL]

 

위와 같이 notification을 cancel해 주는 로그도 보입니다..

카카오도 뭔가 나이스한 방법을 쓰고 있는게 아니라 notification을 cancel하는 꼼수를 써주고 있는 것인가?!!?

 

의문에 빠지고 있는데 또 다른 로그를 보았습니다..

 

I notification_enqueue: [10212,1558,com.kakao.talk,49152,NULL,0,Notification(channel=clipboard pri=1 
contentView=null vibrate=null sound=null defaults=0x0 flags=0x110 color=0xff4d3e36 category=reminder 
number=0 vis=PRIVATE semFlags=0x0 semPriority=0 semMissedCount=0),0,NULL]

 

위와 같이 카카오에서 clipboard notification_enque 된 flags값을 보면 0x110입니다.

Notification의 Flag값을 보면 110은 FLAG_LOCAL_ONLY, FLAG_AUTO_CANCEL 의 조합입니다.

 

startForeground를 사용한 앱의 로그를 보면 notification enque 값이 아래와 같습니다.

 

I notification_enqueue: [10252,13618,com.example.clipboardwatcherapp,1,NULL,0,Notification(channel=
clipboard pri=0 contentView=null vibrate=null sound=null defaults=0x0 flags=0x40 color=0x00000000 
category=reminder vis=PRIVATE semFlags=0x0 semPriority=0 semMissedCount=0),0,NULL]

 

0x40은 바로 FLAG_FOREGROUND_SERVICE 입니다.

카카오에서 foreground service로 돌리고 있다면 flags값이 0x40이거나 0x40이 더해진 값이어야 할텐데 말이죠.

 

또 이벤트 로그를 보면 예상과는 다르게 foreground Service가 아닙니다.

 * ServiceRecord{b71c61b u0 com.kakao.talk/.service.MessengerService}
    intent={cmp=com.kakao.talk/.service.MessengerService}
    packageName=com.kakao.talk
    processName=com.kakao.talk
    baseDir=/data/app/com.kakao.talk-B5x3-BZMyYRyhbeSqYxZVw==/base.apk
    dataDir=/data/user/0/com.kakao.talk
    app=ProcessRecord{df398a9d0 22677:com.kakao.talk/u0a228}
    createTime=-1m29s424ms startingBgTimeout=--
    lastActivity=-1m28s804ms restartTime=-1m29s423ms createdFromFg=true
    startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=2

 

foreground service로 만든 테스트 앱은 아래와 같이 isForeground=true로 명시되어 있고,

이 앱에서 사용하는 foregroundNoti에 대한 정보도 나오고 있습니다.

 

  * ServiceRecord{d5cb2a9 u0 com.example.clipboardwatcherapp/.ClipboardService} 
    intent={cmp=com.example.clipboardwatcherapp/.ClipboardService} 
    packageName=com.example.clipboardwatcherapp 
    processName=com.example.clipboardwatcherapp 
    baseDir=/data/app/com.example.clipboardwatcherapp-l4FDMshBKfhTRSxc2DREsQ==/base.apk 
    dataDir=/data/user/0/com.example.clipboardwatcherapp 
    app=ProcessRecord{ef7547dd0 30391:com.example.clipboardwatcherapp/u0a251} 
    isForeground=true foregroundId=123 foregroundNoti=Notification(channel=ClipboardChannelId pri=0 
contentView=null vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE 
semFlags=0x0 semPriority=0 semMissedCount=0)
 
    createTime=-8m50s203ms startingBgTimeout=-- 
    lastActivity=-8m50s203ms restartTime=-8m50s203ms createdFromFg=true 
    startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

 

도대체 카카오는 어떻게 서비스를 죽지 않게 하면서 notification에 노출도 되지 않는걸까요 ㅠ_ㅠ

라는 의문을 가지고 있을 때 서비스가 종료되면 알람으로 서비스를 다시 실행하는 방법을 보았습니다.

(※참고 : Android Immortal Service(죽지않는 서비스) 구현하기)

 

 

3. Alarm을 이용한 Immortal Service 구현

 

1. App을 실행했으면 foreground이므로 MainActivity에서는 startService로 CustomService 실행

2. CustomService의 onDestroy에서 1초 뒤에 Alarm을 실행

3. AlarmReceiver에서는 O OS이상이면 RestartService를 foregroundService로 실행, O OS 이하면 CustomService를 startService로 실행

4. RestartService는 foregroundService로 실행하고, CustomService를 startService로 깨운 후, stopForeground로 서비스를 종료

 

파일 순서는 MainActivity -> ClipboardService -> AlarmReceiver -> RestartReceiver 로 보는 것이 편합니다.

 

 

 

 

위와 같이 했더니 O OS 이상에서 notification 없이 service가 죽지 않고 계속 살아서 clipboard를 monitoring 할 수 있었습니다 :)

하나하나 잘 알려주신 forest71 님께 감사드립니다.

 

ActivityManager : Stopping service due to app idle: u0a253 -1m0s73ms com.example.clipboardwatcherapp/.ClipboardService
AlarmManagerEXT : setExact (T:0)20190618T130057 com.example.clipboardwatcherapp (1560830457157:6921324/6921324/0,d744e32:26463978) from 10253:22003
SamsungAlarmManager : setExact Intent (T:0/F:0/AC:false) 20190618T130057 (O:1560830457157/E:6921324/ME:6921324/RI:0) nowELAPSED=6920334 - CU:10253/CP:22003/op:PendingIntent{a74ccef: PendingIntentRecord{d744e32 com.example.clipboardwatcherapp broadcastIntent}}
SamsungAlarmManager : Sending to uid : 10253 nowELAPSED=6921324, action=null alarm=Alarm{a0643fc type 0 when 1560830457157 com.example.clipboardwatcherapp} allowWhileIdle=false
ApplicationPolicy : isStatusBarNotificationAllowedAsUser: packageName = com.example.clipboardwatcherapp,userId = 0
ApplicationPolicy : isStatusBarNotificationAllowedAsUser: packageName = com.example.clipboardwatcherapp,userId = 0
NotificationService : Cannot find enqueued record for key: 0|com.example.clipboardwatcherapp|2000|null|10253 MARs
PolicyManager : onAlertToastWindowStarted pkgName = com.example.clipboardwatcherapp, userId = 0 EdgeLighting
PolicyManager : isAcceptableApplication: pkg=com.example.clipboardwatcherapp , range=512 , includeAllApp=false , userId=0 , infoRange=0 , infoCategory=0

 

하-지만 결국 돌고 돌아, alarm을 써서 구현한 로그를 보면 위 처럼 Service가 destory되고 Alarm이 등록되는 로그들을 볼 수 있으나, kakaotalk은 그런 로그가 보이지 않습니다. ㅠㅠ

 

로그를 다시 한 번 뜯어보겠습니다.

kakaotalk을 process 종료 시키면 아래와 같은 로그가 나옵니다.

 

UserEvent            : Source child:TASK, packageHash=764003014, componentHash=-1800665996, pageIdx=0, pkg=com.kakao.talk 
WindowManager   : Remove Window{2e0443d u0 com.kakao.talk/com.kakao.talk.activity.TaskRootActivity}: mSurfaceController=null mAnimatingExit=false ...
WindowManager   : remove win from mFocusExcludedWindows Window{2e0443d u0 com.kakao.talk/com.kakao.talk.activity.TaskRootActivity} ...
SurfaceFlinger       : id=2415 Removed 2e0443d com.kakao.talk/com.kakao.talk.activity.TaskRootActivity#0 (0/49) 
Layer                  : id=2415 onRemoved 2e0443d com.kakao.talk/com.kakao.talk.activity.TaskRootActivity#0  
WindowManager   : Cancelling animation restarting=false, leash=null, surface=Surface(name=AppWindowToken{4fac682 token=Token{7934dcd ...
SurfaceFlinger       : id=2413 Removed AppWindowToken{4fac682 token=Token{7934dcd ActivityRecord{1258764 u0 com.kakao.talk/.activity.TaskRootActivity.
Layer                  : id=2413 onRemoved AppWindowToken{4fac682 token=Token{7934dcd ActivityRecord{1258764 u0 com.kakao.talk/.activity....
WindowManager   : Remove Window{891e739 u0 com.kakao.talk/com.kakao.talk.activity.main.MainTabFragmentActivity}: mSurfaceController=null ...
WindowManager   : remove win from mFocusExcludedWindows Window{891e739 u0 com.kakao.talk/com.kakao.talk.activity.main.MainTabFragmentActivity}...
SurfaceFlinger       : id=2417 Removed 891e739 com.kakao.talk/com.kakao.talk.activity.main.MainTabFragmentActivity#0 (0/47) 
WindowManager   : Cancelling animation restarting=false, leash=null, surface=Surface(name=AppWindowToken{9bef1e7 token=Token{ecb23a6 ...
Layer                  : id=2417 onRemoved 891e739 com.kakao.talk/com.kakao.talk.activity.main.MainTabFragmentActivity#0  
SurfaceFlinger       : Attempting to destroy on removed layer: AppWindowToken{9bef1e7 token=Token{ecb23a6 ActivityRecord{34c1901 u0 com.kakao.talk...
ActivityManager    : Killing 29444:com.kakao.talk/u0a228 (adj 1001remove task 
Layer                  : id=2414 onRemoved AppWindowToken{9bef1e7 token=Token{ecb23a6 ActivityRecord{34c1901 u0 com.kakao.talk/.activity.main...
ActivityManager    : Scheduling restart of crashed service com.kakao.talk/.service.MessengerService in 16000ms 

 

그리고 위에서 만든 alarm을 이용한 Immortal Service 로그를 비교하면 아래와 같이 alarm 관련한 로그가 있습니다.

 

AlarmManagerEXT          : setExact (T:0)20190618T141042 com.example.clipboardwatcherapp (1560834642773:11106962/11106962/0,15a0578:26463978)...
SamsungAlarmManager   : setExact Intent (T:0/F:0/AC:false) 20190618T141042 (O:1560834642773/E:11106962/ME:11106962/RI:0) nowELAPSED=11105966 -...
ActivityManager             : Killing 30498:com.example.clipboardwatcherapp/u0a253 (adj 1001remove task 
SamsungAlarmManager   : Sending to uid : 10253 nowELAPSED=11106965, action=null alarm=Alarm{9ba4833 type 0 when 1560834642773 com.example....
StorageManagerService   : getExternalStorageMountMode : final mountMode=1, uid : 10253, packageName : com.example.clipboardwatcherapp 
ApplicationPolicy            : isApplicationExternalStorageWhitelisted:com.example.clipboardwatcherapp user:0 
ActivityManager             : package  com.example.clipboardwatcherapp, user - 0 is SDcard whitelisted 
ApplicationPolicy            : isApplicationExternalStorageBlacklisted:com.example.clipboardwatcherapp user:0 
ApplicationPolicy             : isApplicationExternalStorageBlacklisted:com.example.clipboardwatcherapp user:0 
ActivityManager              : Start proc 30589:com.example.clipboardwatcherapp/u0a253 for broadcast com.example.clipboardwatcherapp/.AlarmReceiver 
ActivityManager              : DSS on for com.example.clipboardwatcherapp and scale is 1.0 
ApplicationPolicy             : isStatusBarNotificationAllowedAsUser: packageName = com.example.clipboardwatcherapp,userId = 0 
ApplicationPolicy             : isStatusBarNotificationAllowedAsUser: packageName = com.example.clipboardwatcherapp,userId = 0 
NotificationService           : Cannot find enqueued record for key: 0|com.example.clipboardwatcherapp|2000|null|10253 

 

결국 카카오톡도 위와 같은 alarm을 이용한 것 같지는 않아보입니다.

무엇보다도 alarm을 이용해서 계속 서비스를 깨우게 되면, 배터리 이슈가 있어 카카오가 이 방법을 썼을 것 같지는 않습니다.

 

과연 foreground를 써야만 원하는 동작이 나오는 건가, 그냥 service만 쓰면 어떻게 되는지로 돌아가보겠습니다.

 

 

4. 그냥 backgroundService를 사용하면?

 

결국 RestartService를 사용하지 않고 MainActivity에서 ClipboardService를 startService로 실행하고,

Activity onDestroy에서 stopService를 하지 않고, ClipboardService의 onDestroy시에도 removePrimaryClipChangedListener를 제거하고 테스트 해보았습니다.

 

Recent에서 Process kill하더라도 서비스가 계속 동작하고 Clipboard 모니터링이 되어 Toast가 뜨고 있습니다.

하지만 일정 시간이 지나면 Service가 종료되어 Clipboard Monitoring이 되지 않습니다.

 

그렇다면 로그는 어떤지 다시 비교해보겠습니다.

모든 로그가 동일하고 아래 부분만 달랐습니다.

 

ActivityManager : Stopping service due to app idle: u0a253 -32s443ms com.example.clipboardwatcherapp/.ClipboardService
ActivityManager : Service done with onDestroy, but executeNesting=2: ServiceRecord{9100633 u0 com.example.clipboardwatcherapp/.ClipboardService}

 

도대체 kakao는 backgroundService이면서 어떻게 clipboard service를 유지하는 걸까요..

위 방법 말고 다른 방법 아시는 분 있으실까요?...

 

추가로 알게되면 업데이트 하도록 하겠습니다. ㅠ_ㅠ

 

(+) Recent Task에서 Process 종료 시 비교

// 1. 카카오톡
I am_finish_activity: [0,191038805,635,com.kakao.talk/.activity.main.MainTabFragmentActivity,remove-task]
I am_destroy_activity: [0,191038805,635,com.kakao.talk/.activity.main.MainTabFragmentActivity,finish-imm:finishActivityLocked]
I am_on_destroy_called: [0,com.kakao.talk.activity.main.MainTabFragmentActivity,performDestroy]
I am_kill : [0,10108,com.kakao.talk,1001,remove task] I am_proc_died: [0,10108,com.kakao.talk,1001,9,338,1422]
I am_schedule_service_restart: [0,com.kakao.talk/.service.MessengerService,4000]
I am_proc_start: [0,10331,10228,com.kakao.talk,service,com.kakao.talk/.service.MessengerService
I am_proc_bound: [0,10331,com.kakao.talk]
I notification_cancel: [10228,10331,com.kakao.talk,2016,NULL,0,0,1088,8,NULL]
I notification_cancel: [10228,10331,com.kakao.talk,5,NULL,0,0,1088,8,NULL]
I power_partial_wake_state: [501,df:com.kakao.talk]
I notification_cancel: [10228,11233,com.kakao.talk,2016,NULL,0,0,1088,8,NULL]
I notification_cancel: [10228,11233,com.kakao.talk,5,NULL,0,0,1088,8,NULL]

// 2. 알람을 이용해 Immortal service 구현 한 앱
I am_finish_activity: [0,182832395,636,com.example.clipboardwatcherapp/.MainActivity,remove-task]
I am_destroy_activity: [0,182832395,636,com.example.clipboardwatcherapp/.MainActivity,finish-imm:finishActivityLocked]
I am_on_destroy_called: [0,com.example.clipboardwatcherapp.MainActivity,performDestroy]
I am_kill : [0,10021,com.example.clipboardwatcherapp,1001,remove task]
// am_schedule_service_restart 가 없음
I am_proc_died: [0,10021,com.example.clipboardwatcherapp,1001,18,337,1425]
I am_proc_start: [0,10494,10253,com.example.clipboardwatcherapp,broadcast,com.example.clipboardwatcherapp/.AlarmReceiver]
I am_proc_bound: [0,10494,com.example.clipboardwatcherapp]
// 카카오톡에서는 notification_enqueue가 없음 (foreground로 돌릴때 생성되는 notification)
I notification_enqueue: [10253,10494,com.example.clipboardwatcherapp,2000,NULL,0,Notification(channel=clipboard pri=0 contentView=null
vibrate=null sound=null defaults=0x0 flags=0x40 color=0x00000000 vis=PRIVATE semFlags=0x0 semPriority=0 semMissedCount=0),0,NULL]
I notification_cancel: [1000,5967,com.example.clipboardwatcherapp,2000,NULL,0,0,0,8,NULL]
I sysui_multi_action: [757,128,758,5,759,8,793,19,794,0,795,19,796,2000,806,com.example.clipboardwatcherapp,857,clipboard,858,2,947,0]
I notification_canceled: [0|com.example.clipboardwatcherapp|2000|null|10253,8,19,19,0,-1,-1,NULL]

// 3. Sticky Service 만 사용한 앱
I am_finish_activity: [0,108411868,637,com.example.clipboardwatcherapp/.MainActivity,remove-task]
I am_destroy_activity: [0,108411868,637,com.example.clipboardwatcherapp/.MainActivity,finish-imm:finishActivityLocked]
I am_on_destroy_called: [0,com.example.clipboardwatcherapp.MainActivity,performDestroy]
I am_kill : [0,10866,com.example.clipboardwatcherapp,1001,remove task]
I am_proc_died: [0,10866,com.example.clipboardwatcherapp,1001,9,350,1394]
I am_schedule_service_restart: [0,com.example.clipboardwatcherapp/.ClipboardService,1000]
I am_proc_start: [0,11034,10253,com.example.clipboardwatcherapp,service,com.example.clipboardwatcherapp/.ClipboardService]
I am_proc_bound: [0,11034,com.example.clipboardwatcherapp]
// 카카오톡에서는 idle_service 로그가 없음.
I am_stop_idle_service: [10253,com.example.clipboardwatcherapp/.ClipboardService]

 

 

(+) 추가

 

5. Q OS에서는 어차피 Clipboard 모니터링을 못한다.    

 

Android Q privacy: Changes to data and identifiers에 따르면 Q OS 부터 app이 focus를 가져야지만 clipboard 모니터링을 할 수 있습니다.

Access to clipboard data

Unless your app is the default input method editor (IME) or is the app that currently has focus, your app cannot access clipboard data.

 

background에서 clipboard 모니터링을 하려면 READ_CLIPBOARD_IN_BACKGROUND 권한이 필요하며 protectionLevel이 signature이므로 제조사 앱에서만 가능합니다.

 

<permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND" android:protectionLevel="signature"/>

 

 

6. 메신저의 특성을 이용한 Immortal Service

 

마지막으로, 카카오톡 로그를 보다보면 아래와 같은 사실들을 알 수 있었습니다.

 

1. foreground Service는 아니다.

2. Alarm Scheduling으로 깨우는 것도 아니다.

 

추가적으로, 카카오톡에서 '메시지 알림'을 off 후 clipboard notification 이 동작하는 지 확인을 해보면,

메시지 알림 off 후 제대로 동작하지 않습니다.

 

즉, 카카오톡은 메신저의 특성 상 메시지 알림 등이 올 때마다 app이 깨어나기 때문에 service가 죽지않고 살아있지 않을까 싶습니다. (FCM push를 사용하면 강제로 app을 깨우기 때문에. --> FCM에 대해서도 추후 더 알아봐야 하겠습니다.)

 

확신을 할 수는 없지만 background service이면서 시간이 지나도 service가 죽지 않고 동작하는 방법이 많지 않네요 ㅠㅠ

'Android' 카테고리의 다른 글

ClipboardManager, Clipboard 상태 모니터링  (0) 2019.06.17