본문 바로가기
프로그래밍/App 개발

[Android] 반복알림(notification) 구현

by 엽기토기 2022. 6. 13.
반응형

반복 알람 구현



1. AlarmManager 사용 (❌)

처음에 이것저것 구현해봤는데 잘 안되었었음. 안울리거나.. 늦게 울리거나... 반복이 안된다거나...

(밑에 서술하겠지만, 결국 Alarm Manager로 돌아왔다.)

 

?

2. WorkManager 사용 (❌)

첨에 잘안되어서 알아보니,

테스트로 1분간격으로 해서 안된거였다. 최소 15분이라고 한다.

암튼 15분으로 맞춰서 테스트 해보니 아~~주 정확하진 않지만 오차 범위 1분내외로 잘 울린다. 

리마인더 설정하는 부분

1
2
3
4
5
6
7
8
9
val workRequest = PeriodicWorkRequestBuilder<NotificationWorker>(15,TimeUnit.MINUTES)
                .setInitialDelay(
                    morningReminderTimeCal.timeInMillis - Calendar.getInstance().timeInMillis,
                    TimeUnit.MILLISECONDS
                )
                .addTag("morning_reminder")
                .build()
 
            WorkManager.getInstance(this).enqueueUniquePeriodicWork("morning_reminder", ExistingPeriodicWorkPolicy.REPLACE, workRequest)
cs

Worker 부분

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class NotificationWorker(
    context: Context,
    params: WorkerParameters
) : Worker(context, params) {
    private val notificationManager: NotificationManager =
        context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 
    override fun doWork(): Result {
        createNotificationChannel()
        fireReminder(applicationContext)
 
        //TODO Notification Worker Success
        return Result.success()
    }
 
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(
               "notificationChannel ID",
                "notificationChannel name",
                NotificationManager.IMPORTANCE_HIGH
            )
            notificationChannel.apply {
                enableVibration(true)
                description = "notificationChannel description"
            }
            notificationManager.createNotificationChannel(notificationChannel)
        }
    }
 
    //TODO Reminder ID 별로 Notification 나누기
    private fun fireReminder(context: Context) {
        val id = MORNING_NOTIFICATION_ID
 
        val contentIntent = Intent(context, MainActivity::class.java)
        val contentPendingIntent = PendingIntent.getActivity(
            context,
            id,
            contentIntent,
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )
        val builder =
           NotificationCompat.Builder(context, "notificationChannel ID")
                .setSmallIcon(R.mipmap.ic_launcher_round)
                .setContentTitle("리마인더")
                .setContentText("안녕 나는 알람이야")
                .setContentIntent(contentPendingIntent)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
 
        notificationManager.notify(id, builder.build())
    }
 
    companion object {
        const val TAG = "tag"
        const val MORNING_NOTIFICATION_ID = -1
        const val EVENING_NOTIFICATION_ID = -2
    }
}
 
cs

(WorkManager 관련해서는 많은 글이 있으므로 설명x)

하지만 이 방법도 문제가 있었다. Doze 상태에서 알림이 즉시 울리지 않고, 지연되어 핸드폰 화면을 킬 때 쌓인게 한 방에 오는 현상이 발견되었다.


3. AlamManager 를 '잘' 사용해보기 (✅)

#setExactAndAllowWhileIdle #BatteryOptimizations

 

- AlarmReceiver (* 중요)

반복알림으로 setRepeating()을 사용하면 안울리거나 늦게 올리는 문제가 있음.

setRepeating() 대신 setExactAndAllowWhileIdle() 을 사용하고,

알람이 울리면 AlarmReceiver 이 호출되는데, 여기서 intent에 설정한 Interval 만큼 다시 알람을 설정.


코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
 
 
@AndroidEntryPoint
class AlarmReceiver : BroadcastReceiver() {
    @Inject
    lateinit var reminderRepository: ReminderRepository
 
    lateinit var notificationManager: NotificationManager
 
    override fun onReceive(context: Context, intent: Intent) {
        notificationManager = context.getSystemService(
            Context.NOTIFICATION_SERVICE
        ) as NotificationManager
 
        createNotificationChannel(intent)
        fireReminder(context, intent)
    }
 
    private fun createNotificationChannel(intent: Intent) {
        val id = intent.getIntExtra("id"0)
        val type = intent.getStringExtra("type")
 
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(
                "notificationChannel_$id",
                "$type",
                NotificationManager.IMPORTANCE_HIGH
            )
            notificationChannel.run {
                enableVibration(true)
                description = "notification"
            }
            notificationManager.createNotificationChannel(notificationChannel)
        }
    }
 
    private fun fireReminder(context: Context, intent: Intent) {
        val id = intent.getIntExtra(ID, 0)
        val title = intent.getStringExtra(TITLE) ?: ""
        val type = intent.getStringExtra(TYPE) ?: ""
        val content = intent.getStringExtra(CONTENT) ?: ""
        val isRepeat = intent.getBooleanExtra(REPEAT, false)
        val dateTime = try {
            intent.getSerializableExtra(TIME) as LocalDateTime
        } catch (e: NullPointerException) {
            LocalDateTime.now()
        }
        val idLocal = intent.getStringExtra(ITEM_ID_LOCAL) ?: ""
        val idItemLocal = intent.getStringExtra(ITEM_ID_ITEM_LOCAL) ?: ""
 
        Log.d(TAG_NOTIFICATION, "\n")
        Log.d(
            TAG_NOTIFICATION,
            "--------------------------------------------------------------------------------------------------"
        )
        Log.d(
            TAG_NOTIFICATION,
            "NOTIFICATION FIRED! | id : $id | type : $type | time:$dateTime | text : $title | content : $content | isRepeat : $isRepeat |"
        )
        Log.d(
            TAG_NOTIFICATION,
            "--------------------------------------------------------------------------------------------------\n"
        )
        Log.d(TAG_NOTIFICATION, "\n")
 
        val contentIntent = Intent(context, MainActivity::class.java)
        val contentPendingIntent = PendingIntent.getActivity(
            context,
            id,
            contentIntent,
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )
        val builder =
            NotificationCompat.Builder(context, "notificationChannel_$id")
                .setSmallIcon(R.mipmap.ic_launcher_round)
                .setContentTitle(title)
                .setContentText(content)
                .setContentIntent(contentPendingIntent)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setAutoCancel(true)
                .setStyle(NotificationCompat.BigTextStyle().bigText(content))
                .setDefaults(NotificationCompat.DEFAULT_ALL)
 
        notificationManager.notify(id, builder.build())
 
        if (!isRepeat) return
 
        val interval = intent.getIntExtra(INTERVAL, 1)
 
        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val nextIntent = Intent(context, AlarmReceiver::class.java)
        nextIntent.putExtra(ID, id)
        nextIntent.putExtra(TITLE, title)
        nextIntent.putExtra(TYPE, type)
        nextIntent.putExtra(CONTENT, content)
        nextIntent.putExtra(REPEAT, true)
        nextIntent.putExtra(INTERVAL, interval)
        nextIntent.putExtra(ITEM_ID_LOCAL, idLocal)
        nextIntent.putExtra(ITEM_ID_ITEM_LOCAL, idItemLocal)
 
        val pendingIntent = PendingIntent.getBroadcast(
            context, id, nextIntent,
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )
 
        val nextDate = dateTime.plusDays(interval.toLong())
 
        alarmManager.setExactAndAllowWhileIdle(
            AlarmManager.RTC_WAKEUP,
            nextDate.getMilliSeconds(),
            pendingIntent
        )
        Log.d(
            TAG_NOTIFICATION,
            "SET: | id : $id | type : habit | time: $nextDate | content : $content | interval: $interval | isRepeat : true |"
        )
 
        CoroutineScope(Dispatchers.IO).launch {
            reminderRepository.insertOrUpdateItem(
                Reminder(
                    id = id,
                    itemIdLocal = idLocal,
                    itemIdItemLocal = idItemLocal,
                    type = ReminderType.from(type),
                    isEnabled = true,
                    dateTime = nextDate.toDefaultFullDateTimeString(),
                    title = title,
                    contents = content,
                    deleted = false,
                    isRepeat = isRepeat
                )
            )
        }
    }
}
 
cs

- Reminder ViewModel

private val reminderViewModel by viewModels<ReminderViewModel>()

필요한 곳에서 reminder viewmodel 을 사용하여 알람을 설정.

 

*key point!

- reminderRepository 를 inject (reminder DB 에 저장)

=> 재부팅되면 알람이 해제되므로, 리마인더를 설정할 때 db에 넣어놓고 재부팅시 BootReceiver 가 호출되면 여기서 reminder DB 에서 알람 정보를 꺼내와서 알람을 재설정.

- intent 에 알림 정보 저장.

- ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 로 배터리 관리 해제.

fun checkBatteryOptimizationsAndGetPermission(context: Context) {
    if (!isIgnoringBatteryOptimizations(context)) {
        val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
        intent.data = Uri.parse("package:${context.packageName}")
        context.startActivity(intent)
    }
}

 


코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@HiltViewModel
class ReminderViewModel @Inject constructor(
    private val reminderRepository: ReminderRepository
) : ViewModel() {
    fun setMorningReminder(        context: Context,
        isEnabled: Boolean,
        morningQuote: Quote = Quote(0""""0)
    ) {
        DreamforaApplication.checkBatteryOptimizationsAndGetPermission(context)
 
        setPreference(NotificationConstants.MORNING_REMINDER_ENABLED_KEY, isEnabled)
 
        val uniqueId = NotificationConstants.MORNING_NOTIFICATION_ID.hashCode()
        val dateTime = updateToAfterDateIfBeforeDate(
            getPreference(
                NotificationConstants.MORNING_REMINDER_TIME_KEY,
                getDefaultMorningReminderTime()
            ).toLocalDateTime()
        )
        val dateTimeFullTimeString = dateTime.toDefaultFullDateTimeString()
        val title = "Quote of the day - ${morningQuote.author}"
        val contents = morningQuote.description
        val type = ReminderType.MORNING
        val isRepeat = true
 
        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val intent = Intent(context, AlarmReceiver::class.java)
        intent.putExtra(NotificationConstants.ID, uniqueId)
        intent.putExtra(NotificationConstants.TITLE, title)
        intent.putExtra(NotificationConstants.TYPE, type.value)
        intent.putExtra(NotificationConstants.TIME, dateTime)
        intent.putExtra(NotificationConstants.CONTENT, contents)
        intent.putExtra(NotificationConstants.INTERVAL, 1)
        intent.putExtra(NotificationConstants.REPEAT, true)
 
        val pendingIntent = PendingIntent.getBroadcast(
            context, uniqueId, intent,
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )
 
        cancelExistReminder(
            itemIdLocal = "",
            itemIdItemLocal = "",
            pendingIntent,
            alarmManager,
            uniqueId,
            type,
            dateTimeFullTimeString,
            title,
            contents,
            isRepeat
        )
 
        if (!isEnabled) return
 
        alarmManager.setExactAndAllowWhileIdle(
            AlarmManager.RTC_WAKEUP,
            dateTime.getMilliSeconds(),
            pendingIntent
        )
        Log.d(
            TAG_NOTIFICATION,
            "SET: | id : $uniqueId | type : morning | time: $dateTime | isRepeat : true |"
        )
 
        CoroutineScope(Dispatchers.IO).launch {
            reminderRepository.insertOrUpdateItem(
                Reminder(
                    id = uniqueId,
                    itemIdLocal = "",
                    itemIdItemLocal = "",
                    type = type,
                    isEnabled = true,
                    dateTime = dateTimeFullTimeString,
                    title = title,
                    contents = contents,
                    deleted = false,
                    isRepeat = isRepeat
                )
            )
        }
    }}
 
cs

 

반응형