사용 의도
금일 종료되는 메이플스토리의 이벤트가 존재함을 메붕이들에게 알려주기 위하여, AlarmManager를 이용하여 Notification을 보내기로 하였다.
- 또한 부캠에서 그룹프로젝트를 할 때, Push Notification을 보내는 기능을 구현하는 것을 기획했는데 결국 구현까지 하지는 못 했어서, 부캠 끝나고 한 번 공부해보기로 하였다.
학습 내용
setRepeating()
- 앱이 백그라운드 상태일 때나, 꺼져있는 상태일 때에도 알림을 받기 위해서 AlarmManager를 사용하기로 하였다.
- 매일 00시가 딱 경과할 때마다 알림을 보낸다. 따라서 반복적인 알림을 예약해야 하는데, AlarmManager에서 setRepeating() 메소드가 반복 알람을 지원한다.
- 하지만 setRepeating()은 공식 문서에 보면, 드물게 정확한 시간을 요구하는 앱에서 사용하면 된다고 나와 있고, 사용해 보면 진짜로 정확하지 않은 시간에 온다.
- 그래서 setInexactRepeating()을 사용한다면 부정확한 반복 알람을 동기화하고 실행하며, 배터리 소모를 줄일 수 있다고 나온다. 그런데, 기기를 잠금해놓은 채로 있으면 알림이 오지 않는다… 이건 내가 원한 그림이 아니야!!
setExactAndAllowWhileIdle()
- 앱이 백그라운드 상태인 건 물론이고, 기기 자체가 잠금 상태일 때에도 알림이 오도록 하려면 setAndAllowWhileIdle()을 사용해야 하고, 더 정확한 시간을 요구한다면 setExactAndAllowWhileIdle()을 사용해야 한다.
- 그런데도 불구하고 알림이 정확한 시간에 오지 않는다는 것을 확인하였다. setExactAndAllowWhileIdle()는 요청된 트리거 시간과 최대한 가까운 시간에 알림을 보내도록, OS에서 판단하여 기기가 유휴 상태일 때 배터리 수명을 최적화하기 위하여 알림을 보내는 일정을 자유롭게 조정할 수 있다고 한다.
- 그럼 뭘 쓰라는 거임?
setAlarmClock()
- setAlarmClock()은 setExactAndAllowWhileIdle()와 같이 백그라운드 상태나 기기가 유휴 상태일 때에도 알림을 보내주는데, 더 정확한 시간에 보내준다고 한다.
- 근데 문제는 앱이 백그라운드 상태일 때나, 꺼져있는 상태일 때에도 반복 알림을 받아야 하는데, setExactAndAllowWhileIdle()나 setAlarmClock()은 반복 기능이 없다.
- 그럼 진짜 어쩌란거임?
BroadcastReceiver와 onReceive()
- 따라서 Service와 BroadcastReceiver를 이용한다. Service를 AlarmManager로써 사용하여 알림을 예약하고, 예약한 시간이 되면 BroadcastReceiver의 onReceive() 메소드에서 알림을 한 번 보내면서 그 다음 날 00시에 알림을 한번 더 예약하는 로직을 사용한다.
- 이러한 로직을 바탕으로 가장 정확하고, 앱이 백그라운드 상태에서도, 심지어 기기가 유휴 상태에서도 알림을 보내는 이벤트 알리미 기능을 구현하는 것을 목표로 한다.
- 앱이 켜져 있는 상태인 포그라운드뿐만 아니라, 백그라운드 상태일 때나 앱이 아예 꺼진 상태일 때에도 AlarmManager가 작동해야 하므로, 백그라운드에서도 오래 실행되는 작업을 수행할 수 있는 컴포넌트인 Service를 사용하여 푸시 알림을 예약한다.
- 사용자가 포그라운드에서 이벤트 알리미를 수신하겠다고 허용하면, 앱은 ALARM_SERVICE를 AlarmManager로써 사용하여 푸시 알림을 보낸다.
- 이 때, PendingIntent를 정의해서, 정해진 시간에 어떻게 동작할 것인지(예를 들자면 지금 우리가 하고 있는 푸시 알림을 보내는 것과 같은…)를 정의해야 한다. 엥? Intent는 들어봤는데 PendingIntent는 뭐냐?
- 사용자가 포그라운드에서 이벤트 알리미를 수신하겠다고 허용하면, 앱은 ALARM_SERVICE를 AlarmManager로써 사용하여 푸시 알림을 보낸다.
PendingIntent
- PendingIntent도 기본적으로 Intent인데, Intent와는 다르게 특정한 때에 수행할 동작을 말한다. PendingIntent는 getActivity(), getService(), getBroadcast() 메소드를 통해서 생성할 수 있는데, 특정한 때에 수행할 Intent를 미리 정의해야 한다.
- 아까 우리는 반복 알림을 사용하기 위해 BroadcastReceiver을 사용한다고 하였다. 따라서, getBroadcast()로 PendingIntent를 생성하고, 그 과정에서 BroadcastReceiver를 호출하는 Intent를 또 정의한다.
- AlarmManager의 AlarmClockInfo()로 Intent를 수행하는 시점(즉 푸시 알림을 보내는 시점)을 정한다. 우리는 매일 0시마다 푸시 알림을 보낼 것이므로, 0시 0분 0초 0밀리초로 set()을 하고 add() 메소드로 하루를 더한다. 그리고 AlarmManager의 setAlarmClock() 메소드를 호출하는 것이다. 그럼 다음 날 00시에 푸시 알림이 올 것이다.
NotificationManager
- BroadcastReceiver에서는 먼저 Notification Channel을 생성해야 한다. NotificationManager를 선언하고, createNotificationChannel()로 Notification Channel을 생성한다.
- 여기서 importance를 IMPORTANCE_HIGH로 해야 우리가 원하는 Head-up Notification 구현이 가능하다.
- 그리고 NotificationCompat.Builder로 Notification을 구현하고 이 Builder로 NotificationManager의 notify() 메소드를 호출하면 우리가 구현하고자 하는 푸시 알림을 완성한 것이고, Service로 이 푸시 알림을 띄울 수 있게 된 것이다.
- 마지막으로 onReceive() 메소드의 끝에 우리가 아까 구현한 AlarmManager로 다음 날 00시에 푸시 알림을 보내는 코드를 여기에서도 작성해준다. 이렇게 하면 매일 00시마다 알림을 보내는 이벤트 알리미 기능이 구현이 된다.
- AlarmManager로 푸시 알림을 띄우는 이벤트를 발생시킴으로써 BroadcastReceiver의 onReceive() 메소드가 호출되는 시점에서 AlarmManager로 새로운 알람을 예약하는 방식으로 반복 알림을 구현한 것이다. 이렇게 하면 거의 정확히 매일 00시마다, 앱이 백그라운드 상태이거나 꺼져 있거나, 심지어 기기가 잠금 상태일지라도 푸시 알림을 받을 수 있고 이것이 우리가 원하는 메붕이들을 위한 이벤트 알리미인 것이다.
- 근데 폰을 재부팅하면 예약해 두었던 알람이 해제된다. 그럼 어떻게 하냐?
- 그래서 BroadcastReceiver를 하나 더 생성해서, 기기가 재부팅 완료라는 Action을 수행하면 바로 AlarmManager를 생성하여 다음 날 00시에 알림을 등록하는 작업을 추가로 진행하였다.
- 실제로 산책하고 왔는데 갑자기 스마트폰이 방전되어 재부팅을 하였음에도 불구하고 00시에 알림이 제 때 온 것을 확인할 수 있었다.
PendingIntent의 cancel()
- 진짜 마지막으로 알림을 해제하는 방법은 AlarmManager의 cancel() 메소드를 호출하는 것이다.
- 아까 기록을 안 해놨었는데 PendingIntent를 생성하기 위하여 getBroadcast()나 getActivity() 등과 같은 메소드를 호출한다고 하였는데, 메소드의 패러미터에 Request Code가 필요하다.
- 이것은 PendingIntent를 구분하기 위한 식별자의 역할을 하기 때문에, 반복 알림을 예약할 때 사용했던 Request Code와 동일한 Request Code로 PendingIntent를 생성하고 cancel() 메소드의 패러미터로 한다. 그럼 예약해두었던 PendingIntent가 취소되면서 더 이상 이벤트 알리미가 작동하지 않게 된다.
- 기기가 재부팅했을 때 이벤트 알리미를 켰었는지 껐었는지는 SharedPreferences를 활용하였다.
- 아까 기록을 안 해놨었는데 PendingIntent를 생성하기 위하여 getBroadcast()나 getActivity() 등과 같은 메소드를 호출한다고 하였는데, 메소드의 패러미터에 Request Code가 필요하다.
적용 방법
BroadcastReceiver
모든 코드는 onReceive()에 작성하여 BroadcastReceiver가 호출되면서 발생하는 이벤트인 onReceive()를 정의한다.
먼저 NotificationManager를 선언하고 Channel을 생성한다.
notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(
NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH
)
)
다음으로 NotificationCompat Builder를 선언하고 푸시 알림을 구현한다.
notificationCompatBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
Builder 내에 메소드를 활용하여 푸시 알림을 구현할 수 있게 된다.
notificationCompatBuilder
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(getIcon())
.setAutoCancel(true)
.setContentIntent(fullscreenPendingIntent)
.setDefaults(Notification.DEFAULT_SOUND)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setLocalOnly(true)
.build()
- PRIORITY_HIGH로 Priority를 설정해야 Head-up Notification을 구현할 수 있으며, setContentIntent()로 푸시 알림 터치 시 어떤 동작을 할 지를 결정하고, autoCancel을 true로 설정함으로써 푸시 알림 터치 시 알림을 지운다.
- 또한 setContentTitle(), setContentText() 로 푸시 알림의 내용을 추가할 수 있다.
마지막으로 Builder로 NotificationManager의 notify() 메소드를 호출하면 푸시 알림 기능이 완성된다.
when (Build.VERSION.SDK_INT) {
!in Build.VERSION_CODES.BASE until Build.VERSION_CODES.TIRAMISU -> {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
notificationManager.notify(REQUEST_CODE, eventNotification)
}
}
else -> {
notificationManager.notify(REQUEST_CODE, eventNotification)
}
}
- 여기서 안드로이드 13 이후부터는 POST_NOTIFICATIONS 권한이 필요하므로, 권한이 허용되어 있는지를 체크하고, 그 미만 버전에서는 바로 notify()를 호출한다.
기기 재부팅 시 알림을 다시 예약하기 위해 BroadcastReceiver를 하나 더 선언한다.
class MyAlarmManagerRestarter : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if ((intent.action == "android.intent.action.BOOT_COMPLETED") && (MainApplication.mySharedPreferences.getAlarm("eventAlarm", "") == "alarm")) {
MyAlarmProvider.callAlarm()
}
}
}
AlarmManager
ALARM_SERVICE라는 System Service를 활용하여 알람 예약 기능을 사용한다.
- AlarmManager를 선언한다.
- 다음으로 PendingIntent를 선언한다.
- PendingIntent에서 정의한 내용을 언제 수행할 지 시간을 정한다.(다음 날 00시)
- 마지막으로 AlarmManager의 setAlarmClock() 메소드를 호출한다.
val alarmManager = MainApplication.myContext().getSystemService(Context.ALARM_SERVICE) as AlarmManager
val receiverIntent = Intent(MainApplication.myContext(), MyAlarmReceiver::class.java)
pendingIntent = when (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
true -> {
PendingIntent.getBroadcast(MainApplication.myContext(), 1, receiverIntent, PendingIntent.FLAG_IMMUTABLE)
}
false -> {
PendingIntent.getBroadcast(MainApplication.myContext(), 1, receiverIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
val calendar = Calendar.getInstance().apply {
timeInMillis = System.currentTimeMillis()
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
add(Calendar.DAY_OF_MONTH, 1)
}
val alarmClock = AlarmManager.AlarmClockInfo(calendar.timeInMillis, pendingIntent)
alarmManager.setAlarmClock(alarmClock, pendingIntent)
기타
앞서 선언한 2개의 BroadcastReceiver 역시 Android를 구성하는 컴포넌트이므로 AndroidManifest.xml에 등록해둔다.
<receiver
android:name=".presentation.broadcastreceiver.MyAlarmReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".presentation.broadcastreceiver.MyAlarmManagerRestarter"
android:directBootAware="true"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
결과
00시가 되니 알림이 오는 것을 확인할 수 있다.
코드
'개인 프로젝트 > 안드로이드' 카테고리의 다른 글
[메이플 캘린더] RecyclerView로 원하는 달력 View를 만들어보자 (0) | 2024.01.22 |
---|---|
[메이플 캘린더] 다크 모드에 대응해보자 (0) | 2024.01.18 |
[메이플 캘린더] 메이플 캘린더라는 앱을 만들어보았다! 그런데...? (0) | 2024.01.16 |
[개인 프로젝트] 설빙 레시피 앱 #3 (0) | 2022.03.31 |
[개인 프로젝트] 설빙 레시피 앱 #2 (0) | 2022.03.23 |