ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 알람 기능 구현하기(AlarmManager)
    Android 2025. 4. 27. 22:10

    안녕하세요! 오늘은 알람 울리는 기능에 대해서 한번 알아보고자 합니다 🙌

    알람 기능을 구현하기 위해서는 안드로이드 4대 컴포넌트 중에 액티비티, 브로드캐스트 리시버, 서비스 가 필요합니다. 하나씩 짚어보겠습니다!

    1. AlarmManager 에서 BroadcastReceiver  등록

    // AlarmSettingFragment
    val alarmManager = requireContext().getSystemService(Context.ALARM_SERVICE) as AlarmManager
    if(alarmManager.canScheduleExactAlarms()) {
        val intent = Intent(requireContext(), AlarmReceiver::class.java)
        val pendingIntent = PendingIntent.getBroadcast(requireContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
        val goesOffTime = calendar.timeInMillis
        alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, goesOffTime, pendingIntent)
    } else {
        val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
            data = Uri.parse("package:${requireContext().packageName}")
        }
        requireContext().startActivity(intent)
    }

     

    가장 먼저 AlarmManager 가 필요한데요. 필요한 시점은 사용자가 알람을 추가했을 때로 볼 수 있기에 프래그먼트에 작성했습니다. 알람 매니저를 통해 브로드캐스트를 시스템에 등록해줍니다. 저는 알람이 울리고 싶은 시간을 goesOffTime 이라는 변수에 담았습니다. Intent 가 아닌 pendingIntent 를 사용하는 이유는 알람을 바로 실행하는 것이 아니라 특정 시간에 알람을 울리는 것이기 때문에 "지연"이 필요합니다. 만약에, 알람에 대한 권한이 없으면 else 문을 통해 사용자를 설정 화면으로 유도합니다.

     

    2. BroadcastReceiver 에서 Service 실행

    class AlarmReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            val alarmService = Intent(context, AlarmService::class.java)
            context?.startForegroundService(alarmService)
        }
    }

     

    BroadcastReceiver 에서는 Service 를 실행시킵니다. 처음엔 서비스를 사용하지 않고 BroadcastReceiver 내에서 알림음 소리를 설정하여 알림을 띄워보았는데, 알림음은 반복 재생이 되지 않고, 액티비티 생명주기 관리에 까다로움이 있었습니다. 따라서, 백그라운드 역할을 하는 Service 내에서 알림과 소리를 처리하는 방향으로 진행했습니다. 이때, startForegroundService 를 이용하여 서비스를 실행시킵니다. 

     

    3. Service 에서 소리 재생(MediaPlayer) + 알림 띄우기

    class AlarmService : Service() {
    
        private var player: MediaPlayer? = null
    
        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        
            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            val channelId = "alarm_channel_id"
            val channel = NotificationChannel(channelId, "Alarm Channel", NotificationManager.IMPORTANCE_HIGH)
            notificationManager.createNotificationChannel(channel)
            val intent = Intent(this, AlarmRingsActivity::class.java).apply {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) 
            }
            val pendingIntent = PendingIntent.getActivity(
                this,
                0,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            ) 
            val notification = NotificationCompat.Builder(this, channelId)
                .setSmallIcon(R.drawable.ic_alarm_alert)
                .setContentTitle("알람이 울려요!")
                .setContentText("알람이 울러요! 눌러서 확인해보세요!")
                .setAutoCancel(true)
                .setContentIntent(pendingIntent)
                .build()
    
            startForeground(1001, notification) 
    
            val soundUri = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_ALARM)
            player = MediaPlayer.create(this, soundUri).apply {
                isLooping = true
                start()
            }
    
            return START_STICKY
        }
    
    
        override fun onDestroy() {
            super.onDestroy()
            player?.stop()
            player?.release()
            player = null
        }
    
        override fun onBind(intent: Intent): IBinder? = null
    }

     

    1. onCreate 와 onStartCommand 중 어디에 작성해야할까?

    서비스도 안드로이드 컴포넌트이기 때문에 생명주기를 갖습니다. 이 중에 onCreate 와 onStartCommand 가 나뉘어져 있는데요. onCreate 같은 경우에는 서비스가 처음 생성될 때 한번만 호출되는 대신, onStartCommand 같은 경우에는 서비스가 불릴 때마다 호출됩니다. 알람 기능 같은 경우에는 사용자가 단 한개의 알람이 아닌 여러 개의 알람을 추가하는 상황도 고려해야하기문에 onStartCommand 에 작성하는게 적절합니다.

     

    2. ForegroundService 임을 명시해야 한다 → Notification 를 띄우고 startForeground 를 호출한다!

    안드로이드에서는 백그라운드에 오래 실행되는 작업을 허용하지 않습니다. 알람이 울렸을 때 사용자가 알림을 터치하기 전까지는 MediaPlayer 소리를 계속 재생해야하는데 어떻게 해야될까요? 장시간으로 백그라운드를 실행하려면 ForegroundService 를 사용해야 하고, 이를 시스템에 명시해야 합니다. 이를 위해서는 두 가지가 필요한데요. 첫 번째로, 알림(Notification) 을 반드시 띄워야합니다. 두 번째로, Service 안에서는 startForeground(notificationId, notification)를 호출해서 자신이 ForegroundService임을 명시해야 합니다. 그렇지 않으면, 시스템에서는 5초 안에 앱을 강제 종료하게 됩니다.

     

    3. 소리를 재생한다 (MediaPlayer)

    서비스가 실행되면서 알림을 띄우고 동시에 MediaPlayer 를 통해 소리를 재생합니다.  알림을 통해서도 알림음을 재생할 순 있지만, 알림을 클릭한 후 화면을 이동한 상태에서도 소리가 재생되어야 합니다. 사용자가 직접 알람을 종료하기 전까지는 말이죠. 그러면, 소리 제어를 알림이 아닌 MediaPlayer를 통해서 해야합니다. 또한, 알람을 끄는 상황을 고려하기 위해 서비스의 생명주기 중 onDestory 에서 MediaPlayer 를 해제합니다.

     

    이후에 사용자는 소리를 듣고 알람을 끄기 위해 알림을 클릭하게 되는데요. 앱의 Activity 로 이동하게 됩니다.

     

    4. Activity 에서 알람 소리 멈추기 (Service 중단하기)

    class AlarmRingsActivity : AppCompatActivity() {
    
        lateinit var binding: ActivityAlarmRingsBinding
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityAlarmRingsBinding.inflate(layoutInflater)
            setContentView(binding.root)
            initListeners()
        }
    
        private fun initListeners() = with(binding) {
            tvStopAlarm.setOnClickListener {
                Toast.makeText(this@AlarmRingsActivity, "알람이 종료되었습니다!", Toast.LENGTH_SHORT).show()
                val serviceIntent = Intent(this@AlarmRingsActivity, AlarmService::class.java)
                stopService(serviceIntent)
                finish()
            }
            // 뒤로가기 버튼(백버튼) 막기
            onBackPressedDispatcher.addCallback(
                this@AlarmRingsActivity,
                object : OnBackPressedCallback(true) {
                    override fun handleOnBackPressed() {
                        Toast.makeText(
                            this@AlarmRingsActivity, 
                            "뒤로 가기로 앱을 종료할 수 없습니다!", 
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
            )
        }
    }

     

    액티비티에서는 특정 뷰를 클릭했을 때, stopService 메소드를 호출하면서 백그라운드에 동작 중인 서비스를 중단합니다. stopService 가 호출되면, 서비스의 생명주기가 소멸되면서 onDestory 메소드가 호출됩니다. 이때, MediaPlayer 을 해제함으로써 알림음을 끌 수 있습니다. 또한, 뒤로가기 버튼을 override 메소드로 제어하여 앱의 종료를 막을 수도 있습니다.

     

    구조 도식화

    알람 플로우

     

    지금까지 진행한 부분을 다이어그램으로 구조 도식화를 해보았는데요. 이용자한테 보이는 부분은 초록색으로 표시했습니다. 지금까지 구현한 내용을 시연 영상으로 보겠습니다!

     

    시연 영상

    알람 등록

     

     

     

    다음에는 반복 알람 기능이나 요일, 알람 끄기, 미루기 기능 부분을 추가적으로 공부하여 포스팅해보겠습니다! 읽어주셔서 감사합니다🙌

    'Android' 카테고리의 다른 글

    카카오 로그인 기능 구현하기  (7) 2025.05.11
    하단 내비게이션 바(BottomNavigationView)  (2) 2025.05.04
    서버와 통신하기(retrofit)  (0) 2025.03.16
    외부 앱 접속 막기  (0) 2025.03.03
    핸드폰에 설치된 앱 불러오기  (0) 2025.02.25
Designed by Tistory.