Android - AlarmManager로 알람을 등록하는 방법

JS · 20 Jun 2020

AlarmManager 통해 정해진 시간에 알람을 받을 수 있습니다. App이 실행 중이 아닐 때라도 정해진 시간에 이벤트를 받아 어떤 작업을 처리할 수 있습니다.

Alarm의 특징은 다음과 같습니다.

  • 지정된 시간에, 일정 간격마다 App이 알람 이벤트를 받도록 설정할 수 있습니다.
  • 이벤트는 인텐트를 의미하며, 보통 BroadcastReceiver로 인텐트가 전달됩니다.
  • AlarmManager가 이벤트를 보내기 때문에, 내 App이 실행 중이 아니더라도 알람을 받아 어떤 작업을 처리하도록 구현할 수 있습니다.

이 글에서 사용된 Sample은 kotlin으로 작성되었고 GitHub - AlarmManager에서 확인할 수 있습니다.

제한 사항

여러 앱이 AlarmManager를 통해 10분마다 알람을 받는다고 생각해보세요. 사용자는 앱을 종료해도 앱은 알람을 받고 다시 살아나게 됩니다. 이런 행동이 시스템을 busy하게 만들고 배터리를 빠르게 소비하게 만듭니다.

이런 이유로, 플랫폼은 짧은 시간 간격으로 알람을 못 받게 하거나, Doze mode일 때 알람을 전달하지 않으려고 합니다. 물론 필요한 경우 AlarmManager가 제공하는 API로 알람이 발생하도록 요청할 수 있지만, 정확한 시간에 알람이 발생할 필요가 없다면 이런 기능을 사용하지 않도록 권장합니다.

또한 알람을 등록할 때 비교적 정확하지 않은 시간에 알람을 발생시키도록 만들 수 있습니다. 굳이 왜 이렇게 해야하는지 이상해보일 수도 있지만, 이렇게 구현하면 시스템이 적은 리소스를 사용해서 알람을 발생시킬 수 있습니다. 따라서 가능하다면 디바이스의 배터리를 위해 이런 API들을 이용하는 것이 좋습니다.

Android developer에서는 알람이 정확하지 않을 수 있고, 여러 태스크가 동일한 시간에 알람을 설정하여 네트워크 작업의 지연이 발생할 수 있기 때문에 AlarmManager를 사용하기보다 다른 방법을 사용할 것을 권장하고 있습니다.

권장 사항

아래 권장사항은 앱을 위해서, 디바이스를 사용하는 사용자를 위해서 고려해야 하는 내용들입니다.

  • 네트워크 작업보다는 로컬 작업을 위해 AlarmManager를 사용하는 것이 좋습니다.
  • 가능하다면 절전상태일 때 알람을 받도록 설정하지 마세요. 시스템 리소스가 빨리 소모되어 사용자가 불편할 수 있습니다.
  • 가능하다면 알람을 설정할 때 정확한 시간으로 설정하지 마세요. setInexactRepeating()을 사용하면 정확한 시간에 알람을 받지 못하지만, 시스템이 다른 앱의 알람 이벤트를 보낼 때 함께 보낼 수 있습니다. 즉, 시스템이 sleep 상태에서 깨는 시간을 최소화하여 배터리 등의 시스템 자원을 적게 사용할 수 있습니다.
  • 가능하다면 Real time(1970년을 기준으로 하는 실제 시간)으로 설정하지 말고, Elapsed time(기기가 부팅된 후 경과한 시간)으로 시간을 설정하세요. Real time은 UTC 시간을 사용하기 때문에 사용자가 설정한 시간대, 언어의 영향을 받을 수 있습니다. 오동작할 가능성이 있기 때문에 가능하다면 Elapsed time을 사용하세요.

1회성 알람 등록

알람을 특정 시간에 한번만 받도록 설정할 수 있습니다. 알람을 받았을 때, 다시 알람을 등록한다면 반복적으로 알람을 받을 수 있습니다.

알람 이벤트는 브로드캐스트로 전달됩니다. 먼저 아래와 같이 BroadcastReceiver를 만들어야 합니다. 인텐트를 받으면, 로그로 출력하고 Notification을 띄우도록 구현하였습니다.

class AlarmReceiver : BroadcastReceiver() {

    companion object {
        const val TAG = "AlarmReceiver"
        const val NOTIFICATION_ID = 0
        const val PRIMARY_CHANNEL_ID = "primary_notification_channel"
    }

    lateinit var notificationManager: NotificationManager

    override fun onReceive(context: Context, intent: Intent) {
        Log.d(TAG, "Received intent : $intent")
        notificationManager = context.getSystemService(
                Context.NOTIFICATION_SERVICE) as NotificationManager

        createNotificationChannel()
        deliverNotification(context)
    }

    private fun deliverNotification(context: Context) {
        val contentIntent = Intent(context, MainActivity::class.java)
        val contentPendingIntent = PendingIntent.getActivity(
            context,
            NOTIFICATION_ID,
            contentIntent,
            PendingIntent.FLAG_UPDATE_CURRENT
        )
        val builder =
            NotificationCompat.Builder(context, PRIMARY_CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_alarm)
                .setContentTitle("Alert")
                .setContentText("This is repeating alarm")
                .setContentIntent(contentPendingIntent)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setAutoCancel(true)
                .setDefaults(NotificationCompat.DEFAULT_ALL)

        notificationManager.notify(NOTIFICATION_ID, builder.build())
    }

    fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationChannel = NotificationChannel(
                PRIMARY_CHANNEL_ID,
                "Stand up notification",
                NotificationManager.IMPORTANCE_HIGH
            )
            notificationChannel.enableLights(true)
            notificationChannel.lightColor = Color.RED
            notificationChannel.enableVibration(true)
            notificationChannel.description = "AlarmManager Tests"
            notificationManager.createNotificationChannel(
                    notificationChannel)
        }
    }
}

위의 코드는 리시버에 대한 구현이고, 아직 AlarmManager 관련 코드를 구현하지 않았습니다. 여기서는 Notification을 등록하는 코드가 대부분인데, Notification은 이 글의 주제를 벗어나기 때문에 간단히 설명하겠습니다.

Android Oreo 이상부터는 Notification을 띄울 때 먼저 channel을 등록해야 합니다. createNotificationChannel()는 채널을 등록하는 코드입니다. 채널이 등록되면 deliverNotification()으로 노티피케이션을 등록합니다.

Notificaiton에 대해서 자세히 알고 싶으시면 안드로이드의 다양한 Notification 종류와 구현 방법를 참고해주세요.

마지막으로 내가 만든 리시버를 다음과 같이 AndroidManifest.xml에 등록해야 합니다. exported속성을 false로 설정하면 리시버는 내 앱으로부터 전달되는 인텐트만 받을 수 있습니다. true로 설정하면 다른 앱으로부터 전달되는 인텐트도 수신하게 됩니다. 다른 앱으로부터 이벤트를 받지 않기 때문에 false로 설정하면 됩니다.

<receiver android:name=".AlarmReceiver"
    android:exported="false">

알람 등록

이제 알람을 등록하는 코드를 구현하면 됩니다.

다음은 ToggleButton을 눌렀을 때 알람을 등록하거나 취소하는 코드입니다.

val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager

val intent = Intent(this, AlarmReceiver::class.java)  // 1
val pendingIntent = PendingIntent.getBroadcast(     // 2
            this, AlarmReceiver.NOTIFICATION_ID, intent,
        PendingIntent.FLAG_UPDATE_CURRENT)

onetimeAlarmToggle.setOnCheckedChangeListener(OnCheckedChangeListener { _, isChecked ->
    val toastMessage = if (isChecked) {   // 3
        val triggerTime = (SystemClock.elapsedRealtime()  // 4
                + 60 * 1000)
        alarmManager.set(   // 5
                AlarmManager.ELAPSED_REALTIME_WAKEUP,
                triggerTime,
                pendingIntent
        )
        "Onetime Alarm On"
    } else {
        alarmManager.cancel(pendingIntent)    // 6
        "Onetime Alarm Off"
    }
    Log.d(TAG, toastMessage)
    Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show()
})

//1처럼 중요한 코드를 주석으로 표기하였고 각각에 대해서 아래에 설명하였습니다.

  1. 알람 조건이 충족되었을 때, 리시버로 전달될 인텐트를 설정합니다.
  2. AlarmManager가 인텐트를 갖고 있다가 일정 시간이 흐른 뒤에 전달하기 때문에 PendingIntent로 만들어야 합니다. PendingIntent의 requestCode 인자로 NOTIFICATION_ID를 전달하였습니다. 여러 PendingIntent를 사용한다면 requestCode를 다르게 해줘야 하는데, 이 예제에서는 하나의 알람만 등록하기 때문에 NOTIFICATION_ID를 사용하였습니다. flag는 아래에서 다시 설명합니다.
  3. ToggleButton이 눌리면 isChecked=true, 다시 눌리면 isChecked=false가 됩니다.
  4. Elapsed time을 사용하였고, 현재 시간부터 60초 뒤에 알람이 발생하도록 설정하였습니다. 시간은 ms로 설정하야 합니다.
  5. set()을 이용하여 인자를 전달합니다. ELAPSED_REALTIME_WAKEUP는 아래에 다시 설명합니다.
  6. 알람을 취소할 때는 등록한 PendingIntent를 인자로 전달합니다.

2번에서 PendingIntent를 만들 때 flag를 설정하는데, 4개의 flag가 있고 의미는 다음과 같습니다.

  • FLAG_UPDATE_CURRENT : 현재 PendingIntent를 유지하고, 대신 인텐트의 extra data는 새로 전달된 Intent로 교체
  • FLAG_CANCEL_CURRENT : 현재 인텐트가 이미 등록되어있다면 삭제하고, 다시 등록합니다.
  • FLAG_NO_CREATE : 이미 등록된 인텐트가 있다면, 아무것도 하지 않습니다.
  • FLAG_ONE_SHOT : 한번 사용되면, 그 다음에 다시 사용되지 않습니다.

5번에서 타입을 설정하였는데 다음과 같은 타입들이 있습니다.

  • ELAPSED_REALTIME : 기기가 부팅된 후 경과한 시간을 기준으로, 상대적인 시간을 사용하여 알람을 발생시킵니다. 기기가 절전모드(doze)에 있을 때는 알람을 발생시키지 않고 해제되면 발생시킵니다.
  • ELAPSED_REALTIME_WAKEUP : ELAPSED_REALTIME와 동일하지만 절전모드일 때 알람을 발생시킵니다.
  • RTC : Real Time Clock을 사용하여 알람을 발생시킵니다. 절전모드일 때는 알람을 발생시키지 않습니다.
  • RTC_WAKEUP : RTC와 동일하지만 절전모드일 때 알람을 발생시킵니다.

위의 코드를 실행해보면 결과는 다음과 같습니다.

06-20 15:19:40.031  3529  3529 D MainActivity: Onetime Alarm On
06-20 15:20:40.038  3529  3529 D AlarmReceiver: Received intent : Intent { flg=0x14 cmp=com.codechacha.alarmmanager/.AlarmReceiver (has extras) }

Android AlarmManager

좀 더 정확한 시간에 알람 발생 시키기

위의 예제에서는 AlarmManager.set() API를 사용하였습니다.

set() API는 SDK API 19 미만에서는 정확히 설정한 시간에 알람이 발생하지만, API 19 이상에서는 덜 정확한 시간에 알람이 발생하도록 변경되었습니다.

API 19 이상에서 정확한 시간에 알람을 발생시키려면 setExact()를 사용해야 합니다.

alarmManager.setExact(
        AlarmManager.ELAPSED_REALTIME_WAKEUP,
        triggerTime,
        pendingIntent
)
  • set(int type, long triggerAtMillis, PendingIntent operation)
  • setExact(int type, long triggerAtMillis, PendingIntent operation)

디바이스가 절전모드일 때도 동작하게 만들기

디바이스가 절전모드(Doze)일 때는 setExact()으로 등록된 알람은 발생하지 않습니다. 다음 API를 사용하면 절전모드에서도 알람이 발생합니다.

  • setAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation) : set()과 동일하지만 절전모드에서도 동작하는 API입니다.
  • setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation) : setExact()과 동일하지만 절전모드에서도 동작하는 API입니다.

반복 알람 등록

반복적으로 알람을 받도록 구현할 수도 있습니다.

아래는 ToggleButton을 누르면 반복 알람을 등록하거나 취소하는 예제입니다.

periodicAlarmToggle.setOnCheckedChangeListener(OnCheckedChangeListener { _, isChecked ->
    val toastMessage: String
    toastMessage = if (isChecked) {
        val repeatInterval: Long = AlarmManager.INTERVAL_FIFTEEN_MINUTES
        val triggerTime = (SystemClock.elapsedRealtime()    // 1
                + repeatInterval)
        alarmManager.setInexactRepeating(     // 2
                AlarmManager.ELAPSED_REALTIME_WAKEUP,
                triggerTime, repeatInterval,
                pendingIntent)
        "Periodic Alarm On"
    } else {
        alarmManager.cancel(pendingIntent)    // 3
        "Periodic Alarm Off"
    }
    Log.d(TAG, toastMessage)
    Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show()
})
  1. Elapsed time으로 알람이 발생하는 시간을 설정하였습니다. Interval은 INTERVAL_FIFTEEN_MINUTES을 사용하였습니다. 자세한 것은 아래에서 다시 설명합니다.
  2. setInexactRepeating()으로 알람을 등록하면, 정확하진 않지만 대체적으로 비슷한 시간에 알람을 발생시켜줍니다.
  3. 알람을 취소합니다.

1번에서 Interval은 미리 정의된 INTERVAL_FIFTEEN_MINUTES 상수를 사용하였습니다. 그 이유는 setInexactRepeating()를 사용하면 특정 시간으로 알람을 설정할 수 없고 정해진 시간만 사용할 수 있기 때문입니다.

사용할 수 있는 시간들은 다음과 같이 미리 정의되어있습니다.

  • INTERVAL_FIFTEEN_MINUTES : 15분
  • INTERVAL_HALF_HOUR : 30분
  • INTERVAL_HOUR : 1시간
  • INTERVAL_HALF_DAY : 12시간
  • INTERVAL_DAY : 1일

위의 실행해보면 결과는 다음과 같습니다.

06-20 15:52:53.611  6752  6752 D MainActivity: Periodic Alarm On
06-20 16:08:09.532  6752  6752 D AlarmReceiver: Received intent : Intent { flg=0x14 cmp=com.codechacha.alarmmanager/.AlarmReceiver (has extras) }
06-20 16:23:12.312  6752  6752 D AlarmReceiver: Received intent : Intent { flg=0x14 cmp=com.codechacha.alarmmanager/.AlarmReceiver (has extras) }
....

정확한 시간에 알람이 발생하도록 설정

정확한 시간에 알람이 울리도록 하려면 setInexactRepeating()를 사용해서는 안됩니다.

setRepeating()는 API 19 미만에서는 정확한 시간에 알람이 발생하는 것을 보장합니다. 하지만 배터리 등의 문제로 API 19 이상에서는 정확한 시간에 알람이 발생되는 것을 보장하지 않습니다. 또한, AlarmManager는 반복 알람을 등록할 때 정확한 시간을 보장하는 API를 제공하지 않고 있습니다.

setRepeating()setInexactRepeating()의 차이점은 setInexactRepeating()은 Interval로 설정할 수 있는 시간이 제한적인데 setRepeating()는 원하는 시간을 설정할 수 있습니다.

다음은 setRepeating()으로 알람을 등록하는 예제입니다.

periodicAlarmToggle.setOnCheckedChangeListener(OnCheckedChangeListener { _, isChecked ->
    val toastMessage: String
    toastMessage = if (isChecked) {
        val repeatInterval: Long = 60*1000
        val triggerTime = (SystemClock.elapsedRealtime()
                + repeatInterval)
        alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                triggerTime, repeatInterval,
                pendingIntent)
        "Exact periodic Alarm On"
    } else {
        alarmManager.cancel(pendingIntent)
        "Exact periodic Alarm Off"
    }
    Log.d(TAG, toastMessage)
    Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show()
})

위에서 말했듯이 API 19 이상에서는 정확성이 보장되지 않습니다.

만약 정확한 시간에 알람이 발생하도록 하려면, setExact()를 사용하여 원하는 시간에 1회만 알람이 울리도록 만들고, 이벤트를 받았을 때 다시 다음 시간을 설정하여 1회성 알람을 등록하도록 구현되어야 합니다.

Real time으로 알람 등록

다음은 RTC를 사용하여 반복적인 알람을 등록하는 예제입니다.

realtimePeriodicAlarmToggle.setOnCheckedChangeListener(OnCheckedChangeListener { _, isChecked ->
    val toastMessage: String
    toastMessage = if (isChecked) {
        val repeatInterval: Long = 15 * 60 * 1000   // 15 min
        val calendar: Calendar = Calendar.getInstance().apply { // 1
            timeInMillis = System.currentTimeMillis()
            set(Calendar.HOUR_OF_DAY, 20)
            set(Calendar.MINUTE, 25)
        }

        alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, // 2
                calendar.timeInMillis,
                repeatInterval,
                pendingIntent)
        "Realtime periodic Alarm On"
    } else {
        alarmManager.cancel(pendingIntent)  // 3
        "Realtime periodic Alarm Off"
    }
    Log.d(TAG, toastMessage)
    Toast.makeText(this, toastMessage, Toast.LENGTH_SHORT).show()
})
  1. Calendar 객체를 생성하여 알람이 울릴 정확한 시간을 설정합니다.
  2. setRepeating()의 인자로 RTC_WAKEUP과 calendar의 시간을 전달합니다.
  3. 알람을 취소합니다.

위의 코드를 실행하면 결과는 다음과 같습니다.

06-20 20:02:02.458  3435  3435 D MainActivity: Realtime periodic Alarm On
06-20 20:25:02.467  3435  3435 D AlarmReceiver: Received intent : Intent { flg=0x14 cmp=com.codechacha.alarmmanager/.AlarmReceiver (has extras) }
06-20 20:40:13.302  3435  3435 D AlarmReceiver: Received intent : Intent { flg=0x14 cmp=com.codechacha.alarmmanager/.AlarmReceiver (has extras) }
....

알람 취소

위의 예제들에서 이미 알람 취소 코드를 소개했습니다. 다음과 같이 cancel()을 호출할 때 등록한 PendingIntent를 인자로 전달하면 됩니다.

alarmManager.cancel(pendingIntent)

디바이스가 실행될 때 알람 등록

디바이스가 부팅될 때 알람을 등록하려면 ACTION_BOOT_COMPLETED 이벤트를 받고 알람을 등록하도록 구현해야 합니다.

먼저 ACTION_BOOT_COMPLETED를 받는 리시버를 구현해야 합니다. onReceive()에서 인텐트를 받을 때 알람을 등록하면 됩니다.

class BootReceiver : BroadcastReceiver() {
    companion object {
        const val TAG = "BootReceiver"
    }

    override fun onReceive(context: Context, intent: Intent) {
        Log.d(TAG, "Received intent : $intent")
        if (intent.action == "android.intent.action.BOOT_COMPLETED") {
            // Register alarm
        }
    }
}

그리고 App의 Manifest에 다음과 같이 리시버를 등록해야 합니다. 앱이 ACTION_BOOT_COMPLETED 이벤트를 받으려면 android.permission.RECEIVE_BOOT_COMPLETED라는 권한이 필요합니다. 이 권한도 빠짐없이 꼭 Manifest에 명시해야 합니다.

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

<application ... />
    ....
    <receiver android:name=".BootReceiver">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
    </receiver>
</application>

알람이 발생할 때 IntentService로 인텐트 전달

위의 예제들은 BroadcastReceiver로 인텐트를 전달하였지만, IntentService로도 전달할 수 있습니다.

아래와 같이 IntentService를 구현합니다.

class AlarmIntentService : IntentService(AlarmIntentService::class.java.name) {
    override fun onHandleIntent(intent: Intent?) {
        val context: Context = applicationContext
        Log.d(TAG, "Received intent: $intent")
    }

    companion object {
        const val TAG = "AlarmIntentService"
    }
}

AndroidManifest.xml에 서비스를 등록합니다.

<service android:name=".AlarmIntentService"
    android:exported="false">
</service>

마지막으로 다음과 같이 PendingIntent를 만들고 AlarmManager에 알람을 등록하면 됩니다.

intentServiceAlarmToggle.setOnCheckedChangeListener(OnCheckedChangeListener { _, isChecked ->
    val pIntentForIntentService = PendingIntent.getService(
            this,
            0,
            Intent(this, AlarmIntentService::class.java),
            PendingIntent.FLAG_UPDATE_CURRENT)
    val triggerTime = System.currentTimeMillis() + 10 * 60 * 1000
    alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)
})

다른 방식으로 Task를 스케줄링

AlarmManager를 사용하지 않고 WorkManager, JobScheduler를 이용하여 Task를 스케줄링 할 수 있습니다.

자세한 내용은 다음 두개 글을 참고하시면 좋습니다.

Sample

이 글에서 사용된 Sample은 GitHub - AlarmManager에서 확인할 수 있습니다.

참고

댓글을 보거나 쓰려면 이 버튼을 눌러주세요.
codechachaCopyright ©2019 codechacha