Search code examples
androidmvvmalarmmanagerobserver-patternandroid-livedata

Using AlarmManager inside Observer causes observe() to be called only once


I've implemented a MVVM architecture for an alarm app with a ToggleButton that activates the alarm when it's toggled on and deactivates the alarm when it's toggled off. Clicking the ToggleButton calls a function in the ViewModel that, based on the ToggleButton's toggle status, either activates or deactivates the alarm by setting a value to LiveData with the alarm information.

In the Fragment, I observe the LiveData from the ViewModel and set the alarm by dispatching PendingIntent with AlarmManager or cancel the alarm by cancelling the PendingIntent and AlarmManager. The problem is that when I launch the app for the first time, the first click on the ToggleButton triggers the Observer in the Fragment, but after that the Observer simply refuses to trigger even after the LiveData value is changed.

What I've found out is that removing the AlarmManager-related functions (setExactAndAllowWhileIdle() / cancel()) makes the Observer be triggered every time, but adding those functions makes the Observer not react to any more LiveData changes.

I think it's better explained by code and logs.

AlarmViewModel.kt

private val newToast: MutableLiveData<SingleEvent<String>> = MutableLiveData()
private val activateEvent: MutableLiveData<SingleEvent<AlarmData>> = MutableLiveData()
private val deactivateEvent: MutableLiveData<SingleEvent<AlarmData>> = MutableLiveData()
...
private fun activateAlarm(alarmData: AlarmData) {
    newToast.value = SingleEvent("Alarm has been set!")
    activateEvent.value = SingleEvent(alarmData)
}

private fun deactivateAlarm(alarmData: AlarmData) {
    newToast.value = SingleEvent("Alarm has been cleared")
    deactivateEvent.value = SingleEvent(alarmData)
}
...
fun observeNewToast(): LiveData<SingleEvent<String>> = newToast
fun observeActivateEvent(): LiveData<SingleEvent<AlarmData>> = activateEvent
fun observeDeactivateEvent(): LiveData<SingleEvent<AlarmData>> = deactivateEvent

AlarmFragment.kt

@Inject
lateinit var alarmManager: AlarmManager
private val viewModel: SingleAlarmViewModel by lazy {
    ViewModelProviders.of(activity!!).get(AlarmViewModel::class.java)
}
private lateinit var newToastObserver: Observer<SingleEvent<String>>
private lateinit var activateEventObserver: Observer<SingleEvent<AlarmData>>
private lateinit var deactivateEventObserver: Observer<SingleEvent<AlarmData>>
...
override fun onResume() {
    super.onResume()

    newToastObserver = Observer {
        it?.getContentIfNotHandled()?.let { toastText ->
            info("observeNewToast() -> message: $toastText")
            Toast.makeText(activity, toastText, Toast.LENGTH_SHORT).show()
        }
    }

    activateEventObserver = Observer {
        it?.getContentIfNotHandled()?.let { alarmData ->
            info("activateEventObserver -> $alarmData")

            val idInteger = alarmData.timeInMillis.toInt()
            val alarmTime = alarmData.timeInMillis

            val activateAlarmIntent = Intent(activity, AlarmReceiver::class.java)
            activateAlarmIntent.putExtra("message", alarmData.toString())
            val pendingIntent = PendingIntent.getBroadcast(
                activity,
                idInteger,
                activateAlarmIntent,
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            AlarmManagerCompat.setExactAndAllowWhileIdle(
                alarmManager,
                AlarmManager.RTC_WAKEUP,
                alarmTime,
                pendingIntent
            )
        }
    }

    deactivateEventObserver = Observer {
        it?.getContentIfNotHandled()?.let { alarmData ->
            info("deactivateEventObserver -> $alarmData")

            val idInteger = alarmData.timeInMillis.toInt()

            val deactivateAlarmIntent = Intent(activity, AlarmReceiver::class.java)
            deactivateAlarmIntent.putExtra("message", alarmData.toString())
            val pendingIntent = PendingIntent.getBroadcast(
                activity,
                idInteger,
                deactivateAlarmIntent,
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            pendingIntent.cancel()
            alarmManager.cancel(pendingIntent)
        }
    }
    viewModel.observeNewToast().observe(activity!!, newToastObserver)
    viewModel.observeActivateEvent().observe(activity!!, activateEventObserver)
    viewModel.observeDeactivateEvent().observe(activity!!, deactivateEventObserver)
}

override fun onPause() {
    super.onPause()
    viewModel.apply {
        observeNewToast().removeObserver(newToastObserver)
        observeActivateEvent().removeObserver(activateEventObserver)
        observeDeactivateEvent().removeObserver(deactivateEventObserver)
    }
}

Launching the app like this, activateEventObserver and deactivateEventObserver are observed only once and then stop being observed, as shown in the logs below.

02-20 18:38:23.707 23671-23671/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been cleared
02-20 18:38:23.722 23671-23671/com.aly.alarm I/AlarmFragment: deactivateEventObserver -> AlarmData(id=1, timeInMillis=1550730780000, isToggledOn=true)
02-20 18:38:28.547 23671-23671/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been set!
02-20 18:38:28.557 23671-23671/com.aly.alarm I/AlarmFragment: activateEventObserver -> AlarmData(id=1, timeInMillis=1550730780000, isToggledOn=true)
02-20 18:38:34.647 23671-23671/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been cleared
02-20 18:38:35.357 23671-23671/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been set!
02-20 18:38:37.472 23671-23671/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been cleared
02-20 18:38:38.112 23671-23671/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been set!
02-20 18:38:38.962 23671-23671/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been cleared
02-20 18:38:39.377 23671-23671/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been set!

But if I remove the AlarmManager related code, the Observers are called all the time, as expected.

Modified AlarmFragment.kt

@Inject
lateinit var alarmManager: AlarmManager
private val viewModel: SingleAlarmViewModel by lazy {
    ViewModelProviders.of(activity!!).get(AlarmViewModel::class.java)
}
private lateinit var newToastObserver: Observer<SingleEvent<String>>
private lateinit var activateEventObserver: Observer<SingleEvent<AlarmData>>
private lateinit var deactivateEventObserver: Observer<SingleEvent<AlarmData>>
...
override fun onResume() {
    super.onResume()

    newToastObserver = Observer {
        it?.getContentIfNotHandled()?.let { toastText ->
            info("observeNewToast() -> message: $toastText")
            Toast.makeText(activity, toastText, Toast.LENGTH_SHORT).show()
        }
    }

    activateEventObserver = Observer {
        it?.getContentIfNotHandled()?.let { alarmData ->
            info("activateEventObserver -> $alarmData")

            val idInteger = alarmData.timeInMillis.toInt()
            val alarmTime = alarmData.timeInMillis

            val activateAlarmIntent = Intent(activity, AlarmReceiver::class.java)
            activateAlarmIntent.putExtra("message", alarmData.toString())
            val pendingIntent = PendingIntent.getBroadcast(
                activity,
                idInteger,
                activateAlarmIntent,
                PendingIntent.FLAG_UPDATE_CURRENT
            )

            // AlarmManager code commented out
            // AlarmManagerCompat.setExactAndAllowWhileIdle(
            //     alarmManager,
            //     AlarmManager.RTC_WAKEUP,
            //     alarmTime,
            //     pendingIntent
            // )
        }
    }

    deactivateEventObserver = Observer {
        it?.getContentIfNotHandled()?.let { alarmData ->
            info("deactivateEventObserver -> $alarmData")

            val idInteger = alarmData.timeInMillis.toInt()

            val deactivateAlarmIntent = Intent(activity, AlarmReceiver::class.java)
            deactivateAlarmIntent.putExtra("message", alarmData.toString())
            val pendingIntent = PendingIntent.getBroadcast(
                activity,
                idInteger,
                deactivateAlarmIntent,
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            pendingIntent.cancel()

            // AlarmManager code commented out
            // alarmManager.cancel(pendingIntent)
        }
    }
    viewModel.observeNewToast().observe(activity!!, newToastObserver)
    viewModel.observeActivateEvent().observe(activity!!, activateEventObserver)
    viewModel.observeDeactivateEvent().observe(activity!!, deactivateEventObserver)
}

override fun onPause() {
    super.onPause()
    viewModel.apply {
        observeNewToast().removeObserver(newToastObserver)
        observeActivateEvent().removeObserver(activateEventObserver)
        observeDeactivateEvent().removeObserver(deactivateEventObserver)
    }
}

Logs when launching app like this (Observer observed as expected):

02-20 18:44:18.207 24565-24565/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been cleared
02-20 18:44:18.222 24565-24565/com.aly.alarm I/AlarmFragment: deactivateEventObserver -> AlarmData(id=1, timeInMillis=1550730780000, isToggledOn=true)
02-20 18:44:18.367 24565-24565/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been set!
02-20 18:44:18.382 24565-24565/com.aly.alarm I/AlarmFragment: activateEventObserver -> AlarmData(id=1, timeInMillis=1550730780000, isToggledOn=true)
02-20 18:44:21.277 24565-24565/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been cleared
02-20 18:44:21.297 24565-24565/com.aly.alarm I/AlarmFragment: deactivateEventObserver -> AlarmData(id=1, timeInMillis=1550730780000, isToggledOn=true)
02-20 18:44:22.177 24565-24565/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been set!
02-20 18:44:22.197 24565-24565/com.aly.alarm I/AlarmFragment: activateEventObserver -> AlarmData(id=1, timeInMillis=1550730780000, isToggledOn=true)
02-20 18:44:22.842 24565-24565/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been cleared
02-20 18:44:22.857 24565-24565/com.aly.alarm I/AlarmFragment: deactivateEventObserver -> AlarmData(id=1, timeInMillis=1550730780000, isToggledOn=true)
02-20 18:44:23.447 24565-24565/com.aly.alarm I/AlarmFragment: observeNewToast() -> message: Alarm has been set!

I've tried setting the ViewModel lifecycle owner to the parent Activity, and setting it back to Fragment, but it didn't make a difference. Deleting the AlarmManger-related code makes the observer work, but since I'm trying to set the alarm via the observer, this was frustrating me a lot. Why is the AlarmManager making the Observer to stop being observed after the first time? Is it being unregistered somewhere?

Thanks in advance.


Solution

  • It turns out it was an altogether different mistake by me.. I had mistakenly forgotten to inject the AlarmManager to this particular Fragment that I was supposed to inject via Dagger. Thanks for any contributions!