이번주도 저번주도 저저번주도 디버깅과 개선의 일화들!
해결한 문제
1. 알림이 보이지 않음
: Android 13 (API 33) 부터 안드로이드 정책이 강화되어 모든 앱의 알람이 기본적으로 차단됩니다.
따라서 POST_NOTIFICATIONS 권한을 Manifest에 기재하고, 런타임으로 요청해줘야합니다.
단, targetSdk를 32로 낮춰주면 간단하게 해결될 수 있습니다.
(단, 매우 급한 상황이라면 targetSdk를 32로 낮춰주면 간단하게 해결할 수 있지만 정말 매우 급할 때에만 사용하시길..)
Android 13(API 레벨 33) 이상에서는 알림 권한을 거부할 경우 FGS(Forground Services) 작업 관리자에 포그라운드 서비스와 관련된 알림이 계속 표시되지만 알림 드로어에는 표시되지 않습니다.
[궁금해서 실험해본 것]
1. startService -> startForeground 해도 FGS 동작 변함없음
2. 알림 권한 거부해도 포그라운드 서비스는 동작. FGS 에 등록될 뿐 알림에는 안나타는 것 뿐.
3. stopForeground 하지 않아도 stopSelf로 서비스를 종료할 수 있으므로 FGS에서 없어지는건 매한가지.
이에 대한 내용은 여기에 더 기록해놨다
POST_NOTIFICATION에 대해: https://hevton.tistory.com/907
2. 알림을 onGoing 형식으로 사용하지 않고 있었다.
: Android 31 부터는 포그라운드 서비스의 노티피케이션이 기본적으로 onGoing이 해제된다. 따라서 onGoing을 하고싶으면 추가 설정을 해줘야 한다.
3. Splash 처리
: 의미상 분리하는게 맞다고 생각하여 Splash를 액티비티 탄으로 구성해왔는데,
Single Activty-Multiple Fragment 구조에서 Fragment로 구성하거나 또는 Layout 단으로 하는게 나을 수도 있겠다고 생각하여 변경했습니다. 이는 Activity와 데이터가 공유되는 초기 설정을 진행하는 것에 용이하고,
ViewModel의 init()이 완료되면 컨텐츠를 보여지게 하게끔 View 단에서 collect하여 반영해주면 전환이 간단하기 때문입니다.
4. 앱 내의 alertDialog가 순간 순간마다 등장한다.
: PreferenceDataStore 같은 경우에, Key가 다르더라도 같은 .data 파일을 이용한다면
서로 다른 Key에 대한 data가 변경되더라도 다른 Key의 Flow emit에도 영향을 미친다.
따라서 Key A에 대한 flow를 collect하고 있는데, key B에 대한 값을 변경해도 A에 대한 collect가 갱신된다는 문제가 있었다.
해결 방법은 다음과 같이 a, b 두 가지 방법이 있다.
a. 파일 분리
b. distinctUntilChanged() 이용 -> collect 시, 값이 변하지 않았다면 재 호출하지 않음.
5. 시간 재설정
TimerService로 넘겨주는 시간의 기준이 '앞으로 남은 시간' 이었는데, 이렇게 하면
TimerService 실행 도중 앱이 업데이트되는 플로우에서, 타이머를 재등록해주는 과정에서 시간 설정에 오차가 생기므로
'타이머를 등록한 시간' 으로 변경하였다.
6. startForeground() 이후 알림이 나타나기까지 시간 지연
Android 12부터는, 특별한 제한사항이 없는경우 startForeground() 실행 이후 Notification이 등록되기까지 몇 초가량 소요됩니다.
해결법 1. 노티피케이션에 FOREGROUND_SERVICE_IMMEDIATE 플래그 옵션을 줍니다.
해결법 2. NotificationManager를 이용해서 임의로 notify해놓습니다.
-> NotificationManager를 이용해서 임의로 미리 생성해도 됨. nm.notify(321, noti); startForeground(321, noti);
7. Service 실행 도중에 앱이 업데이트되어 재설치되면 문제가 발생할 수 있다
Service 실행 도중에 앱이 업데이트 된다면 어떻게 해야 할까요! Google Play Store와 사용자의 설정에 따라서
앱은 언제든지 업데이트 될 수 있습니다. 따라서 이 예외 처리에 중요도를 주어야 했습니다.
-> MY_PACKAGE_REPLACED 이용합니다
저의 상황은 이러했습니다.
서비스 실행 도중 앱이 업데이트된 상황
-> ForegroundService를 통해 등록한 알람은 남아 있음, 하지만 포그라운드 서비스는 종료됩니다.
그렇다고 해서 만약 인자를 넘겨주는 방식인 stopSelf(startId)를 사용하면, 이전에 실행한 서비스를 이후에 종료할 수 없습니다.
문제는 지금 재설치 시에 '알람은 남아 있고, 포그라운드 서비스는 종료된 상황' 에서 어떻게 마이그레이션할지에 초점을 둬야 했습니다.
먼저 이 과정에서 새로 알게된 점은, 안드로이드 스튜디오에서 앱을 덮어쓰는 방식으로 재설치되는 'Run' 은 위 브로드캐스트를 발생시키지 않는다는 점이었습니다.
Generate sigend apk 해서 직접 설치하는 것이, 이 문제를 재현시킬 수 있는 방법이었습니다.
debug용으로 앱을 만들고, 설치해 놓은 다음에
adb -s emulator-5554 install -r app-debug.apk
<receiver
android:name=".receiver.PackageReplaceReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
</intent-filter>
</receiver>
class PackageReplaceReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action.equals(Intent.ACTION_MY_PACKAGE_REPLACED, true)) {
//이벤트를 받을시 처리할 로직
Log.d("PackageReplaceReceiver", "곧바로 된다")
}
}
}
MY_PACKAGE_REPLACED 를 수신하는 리시버를 등록하고, 다시 설치하면 곧바로 Log가 찍힙니다!
내 앱의 흐름
재우기 : 알람 등록 & 포그라운드 서비스 시작
취소 : 알람 취소 & 실행중인 포그라운드 서비스 종료
완료 : 포그라운드 서비스 종료
재설치 : 취소 & 재우기
따라서 package_replace 개념을 도입하여 알람만 삭제해주는 메커니즘을 추가로 넣었고,
이전 버전과의 타이머 동작 마이그레이션 테스트까지 완료했습니다.
------------------------------------------------------------------------
이전 버전과의 타이머 동작 마이그레이션 테스트까지 완료했다. (예뮬레이터 기준)
내 기기 & 실 기기 테스트 예정
급한건아니니 충분히 더 검토 후에 퍼블리싱 할 예정!
Branch : hotfix/replaced
이전 50버전은 애드몹을 테스트로 돌려놓은 임시 브랜치가 hotfix/admob_test 이다.
즉 테스트할때
hotfix/admob_test -> hotfix/replaced 로 마이그레이션 테스트 하면 된다.
내기기 & API 33 기기 & 예뮬레이터
마이그레이션 타이머 테스트 완료
알림설정 허용하지 않은 상황에서 마이그레이션 테스트 완료
테마 적용 상황에서 마이그레이션 테스트 완료
타이머 설정하지 않은 상황에서 마이그레이션 테스트 완료
Doze모드 테스트 완료
-> '지연알림 해제' commit 버전 기준으로 테스트 한 것임.
------------------------------------------------------------------------
8. base.apk!libmonochrome_64.so
: 특정 기기에서 비정상 종료가 발생해서, 뭐가 문제인가 싶었다. 이는 내 앱의 문제가 아니라, 기기에서의 문제였다.
Android WebView를 시스템적으로 업데이트해줘야 문제를 해결할 수 있다.
https://stackoverflow.com/questions/66361472/app-crashed-with-base-apklibmonochrome-so
9. Canvas: trying to draw too large(165888000bytes) bitmap
: 저사양 기기에서 나타나는 문제. Splash 이미지를 소화하지 못해서 발생하는 문제다.
10. 간혹 있는 비정상 종료들은 대체적으로 OutOfMemory 관련이다.
: drawable을 분기해주지 않은 점이 문제가 되는 것으로 보인다. 이미지 크기들이 크기 때문.
11. 방해금지모드 On 일때, '고마워용' 누를 때 popBackStack 동작 코드 미기재
: 계속 무한루프 돈다. 사용자 경험 저하.. 이 부분 수정
12. 전면광고 제거
: 당분간은 전면광고를 제거한다..!
13. 리뷰요청
: 리뷰 요청 마케팅 도입..! 앱을 이용해주시는 분들 대상으로 리뷰 안내를 드린다!
14. BOOT_COMPLETED & startForegroundService
: 서비스를 실행하여 알람을 등록시켜놓고 휴대폰 전원을 껐다가 켜면 알람과 포그라운드 서비스는 꺼지지만,
내부적으로 사용하고 있는 '동작 중' 이라는 파일 데이터값은 그대로이기 때문에
재부팅 후 앱을 켜면 의미 없는 triggered 화면이 보여지게 될 수 있다.
또한 BOOT_COMPLETED가 언제 receive 될지 보장이 안되므로(1분 ~10분 걸리기도 한다), BOOT_COMPLETED를 통해 알람을 취소한다 한들 곧바로 취소되지 않고, 그 텀에 앱을 들어갔을 때 사용자가 어떻게 느끼게 할지 어렵다. 따라서 BOOT_COMPLETED 를 통해 취소가 아닌 재등록으로 진행하였습니다.
+ 타이머 동작 여부 체크를 파일데이터 기준으로 결정한 이유
사실 Service에 companion object를 넣어서 전역변수를 이용한 값을 타이머 동작 체크에 사용해도 딱히 문제는 없을 것 같다.
서비스가 비정상적으로 강제종료된다고 하더라도, START_REDELIVER_INTENT 를 리턴값으로 넘겨주면
시스템이 이러한 경우 서비스를 다시 재실행한다고 한들, 기존의 intent값을 그대로 넘겨주기 때문이다.
하지만 일단 앱 업데이트 시 어차피 이 파일 데이터값으로, 타이머가 triggered 되었는지 여부를 통해 재요청을 해야한다.
그렇기 때문에 파일 데이터 값은 필요합니다.
그리고 앱 재부팅 이후 BOOT_COMPLETE가 호출되기 전까지의 동작을 위해 이렇게 유지했습니다.
그렇지만 다른 목적으로, 파일 데이터 기반의 값 외에도 타이머 서비스의 전역변수도 사용하고 있습니다.
포그라운드서비스가 현재 동작하고 있지 않을 때, 포그라운드 서비스 취소를 위한 startForegroundService를 호출하면
startForeground()가 호출되지 않을텐데, 이렇게되면 문제가 될 수 있습니다. 자세한 내용은 15번에 있습니다.
현재 방안
BOOT_COMPLETED : 재시작하고, IS_RUNNING(파일데이터) 가 triggered 되어있으면 정보를 토대로 알람 & 포그라운드 재등록.
파일데이터기반의 IS_RUNNING의 의미는 isTriggered (트리거 여부)
서비스 기반의 companion object는 isForeground or isRunning (동작 여부)
15. startForegroundService 시작 후 (자세히 : https://hevton.tistory.com/908)
현재 실행중인 foreground 서비스가 있다면, startForegroundService 시작 후에 stopForeground()나 stopSelf()만을 통해 현재 진행중인 foreground를 닫아주는 역할의 호출이 가능하지만, 실행중인 foreground가 없는 상태에서 startForegroundService를 호출했다면 startForeground가 반드시 호출되어야만 한다. 그렇지 않으면 Exception이 발생합니다.
서비스 취소 관련
이는, 포그라운드 서비스에서 실행하고 있는 리소스를 정리하기 위해서 동일한 서비스에 flag를 달리 주어 exit을 위한 서비스 실행 흐름이 있었는데, startForegroundService를 동일하게 호출한 뒤에 stopForeground나 stopSelf만 호출될 상황에서 문제가 발생했습니다.
이유 : startForegroundService는, 현재 startForeground() 되어있는게 없으면 startForeground()를 호출하지 않으면 에러. (포그라운드 서비스가 동작중이지 않기 때문)
방안 1.
startForegroundService로 두되, Service 에서 stopForeground앞에 항상 startForeground를 하나 둔다.
방안 2.
포그라운드 서비스가 동작중이지 않을 예외가 있을 법한 상황에서, 서비스 중지 목적의 startForegroundService는 startService로 둔다.
현재 방안
일단 호출부는 모두 startForegroundService()로 변화한 그대로 이전처럼 유지한다. 위 예외-'서비스가 동작중이지 않은 상황에서 stop을 요청하는 경우' 는 크게 많이 발생하지 않을 것으로 보입니다.
그리고 그 예외를 다루기 위해서, Service 전역변수로 isForeground를 도입해서, 현재 포그라운드 서비스가 동작중이지 않은데 startForegroundService -> stop 목적으로 서비스 요청이 들어오면, stop 전에 startForeground를 임의로 생성하게끔 수정했습니다.
isRunning( 파일 시스템 ) -> 전반적인 타이머 동작 여부 판단. 좀 더 의미론적으론 isTriggered의 의미라고 보면 된다.
isForeground( 서비스 companion object) -> stopForeground, stopSelf 목적(취소)의 startForegroundService가 실행되었을 때, 현재 서비스가 포그라운드 상태가 아니라면 startForeground() 호출 없이는 예외가 발생될 것이므로 임의로 startForeground()를 호출해주는 역할.
실기기 테스트 (최종본)
내기기 & API 33 기기 & 예뮬레이터
기본 동작 테스트 (타이머 동작 / 취소)
- 알람 앱 포그라운드 동작, 알람 앱 백그라운드 동작 완료
- 알람 동작 후 앱 다시켜기 완료
- 알람 취소 완료
(알림설정 허용하지 않은 상황에서도 추가 확인 완료)
마이그레이션 테스트
- 마이그레이션 타이머 동작 테스트 완료
- 테마 적용 상황에서 마이그레이션 테스트 완료
- 마이그레이션 이후 알람 취소 완료
- 알림설정 허용하지 않은 상황에서 마이그레이션 테스트 완료
- 타이머 설정하지 않은 상황에서 마이그레이션 테스트 완료
재부팅 테스트
- 재부팅 이후 알람 취소 완료
- 재부팅 이후 알람 동작 완료
Doze모드 테스트
- Doze 모드 진입 후 타이머 제기능 동작 완료
-> 여기서 갑자기 일주일가량 헤매게 되었었다~~! 아래 링크 확인
긴 시간 테스트
- 45분 테스트 완료
- 1시간이상 방치 테스트 완료
'[클라이언트] > [Android Kotlin]' 카테고리의 다른 글
Paging3 + Room 이틀동안 삽질 후 성공 정리 (0) | 2023.06.24 |
---|---|
안드로이드 Alpha값 세팅 해프닝 (0) | 2023.06.11 |
당신이 잠든 사이에(앱 자동종료 타이머) - Sleep timer 앱 리뉴얼 개발일지 (1) | 2023.06.04 |
당잠사 실험..! (죽어나갔었다) (0) | 2023.06.01 |
[2023.05.21~27] startForegroundService / startForeground / startService / stopSelf / stopService 실험 정리 (1) | 2023.05.27 |