0

I’m having issues with my Android app’s notification system and scheduled background tasks.

On some devices (such as the Google Pixel 8a), notifications are not received, the database doesn’t update automatically every few hours, or notifications arrive very late when the phone is locked or idle.

On other devices (for example, Samsung phones), everything works perfectly:

  • Scheduled notifications are delivered at the correct time.
  • The database updates approximately every 4 hours as intended.

App context

When the user adds an anime, the app schedules a notification for the release time of the next episode.

I also have a WorkManager task that runs every 4 hours to:

  1. Check if any release times have changed.
  2. Update the firebase database if needed.
  3. Reschedule notifications accordingly.

Everything works fine on some devices, but on others (like the Pixel 8a), the system seems to restrict or delay background tasks due to Doze mode or more aggressive battery optimization policies.

Question

Is there a more reliable way to ensure periodic background work and scheduled notifications run correctly even on devices with stricter Doze or battery restrictions?

I’m currently using WorkManager. Should I consider alternatives or additional configurations such as setExactAndAllowWhileIdle(), a ForegroundService, or any other recommended approach?

If possible, could someone provide a basic implementation example showing how to:

Run a reliable background task that executes even under Doze mode, and Schedule a notification that will fire exactly at a given time (even if the device is idle)?

I show how I have it mounted:

@HiltAndroidApp
class NotakuApplication : Application(), Configuration.Provider {

    @Inject
    lateinit var workerFactory: HiltWorkerFactory

    override val workManagerConfiguration: Configuration
        get() = Configuration.Builder()
            .setWorkerFactory(workerFactory)
            .setMinimumLoggingLevel(Log.ERROR)
            .build()

    override fun onCreate() {
        super.onCreate()
        launchPeriodicSyncWorker()
        launchImmediateSyncWorker()
    }

    private fun launchPeriodicSyncWorker() {
        val workRequest = PeriodicWorkRequestBuilder<AnimeSyncAndReminderWorker>(
            4, TimeUnit.HOURS
        )
            .setInitialDelay(1, TimeUnit.MINUTES)
            .build()

        WorkManager.getInstance(this).enqueueUniquePeriodicWork(
            "anime_sync_and_reminder_work",
            ExistingPeriodicWorkPolicy.KEEP,
            workRequest
        )
    }

    private fun launchImmediateSyncWorker() {
        val oneTimeWork = OneTimeWorkRequestBuilder<AnimeSyncAndReminderWorker>().build()
        WorkManager.getInstance(this)
            .enqueueUniqueWork(
                "immediate_anime_sync_and_reminder_work",
                ExistingWorkPolicy.REPLACE,
                oneTimeWork
            )
    }
}
@HiltWorker
class ReminderWorker @AssistedInject constructor(
    @Assisted context: Context,
    @Assisted workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    override suspend fun doWork(): Result {
        val title = inputData.getString("title") ?: return Result.failure()
        val episodeNumber = inputData.getInt("episodeNumber", 0)
        val episodeDateMillis = inputData.getLong("episodeDateMillis", System.currentTimeMillis())

        showNotification(applicationContext, title, episodeNumber, episodeDateMillis)
        return Result.success()
    }

    companion object {
        fun showNotification(context: Context, title: String, episodeNumber: Int, episodeDateMillis: Long) {
            val formatter = DateTimeFormatter.ofPattern("HH:mm")
                .withLocale(Locale.getDefault())
                .withZone(ZoneId.systemDefault())
            val formattedTime = formatter.format(Instant.ofEpochMilli(episodeDateMillis))

            val message = "¡El episodio $episodeNumber de $title se estrenará a las $formattedTime!"

            val channelId = "notaku_channel"
            val notificationManager = context.getSystemService(NotificationManager::class.java)

            if (notificationManager.getNotificationChannel(channelId) == null) {
                val channel = NotificationChannel(
                    channelId,
                    "Notaku Notifications",
                    NotificationManager.IMPORTANCE_DEFAULT
                )
                notificationManager.createNotificationChannel(channel)
            }

            val builder = NotificationCompat.Builder(context, channelId)
                .setSmallIcon(R.drawable.notaku_logo_black)
                .setContentTitle(title)
                .setContentText(message)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                .setAutoCancel(true)

            notificationManager.notify(System.currentTimeMillis().toInt(), builder.build())
        }
    }
}
@HiltWorker
class AnimeSyncAndReminderWorker @AssistedInject constructor(
    @Assisted context: Context,
    @Assisted workerParams: WorkerParameters,
    private val syncUserTrackedAnimesUseCase: SyncUserTrackedAnimesUseCase,
    private val getTrackedAnimesUseCase: GetTrackedAnimesUseCase,
    private val getUserPreferencesUseCase: GetUserPreferencesUseCase
) : CoroutineWorker(context, workerParams) {

    override suspend fun doWork(): Result {
        return try {
            syncUserTrackedAnimesUseCase.execute()
            scheduleEpisodeReminders(getTrackedAnimesUseCase.execute())

            Result.success()
        } catch (e: Exception) {
            e.printStackTrace()
            Result.retry()
        }
    }

    private suspend fun scheduleEpisodeReminders(animes: List<FavoriteAnimeModel>) {
        val context = applicationContext
        val workManager = WorkManager.getInstance(context)
        val reminderMinutes = getUserReminderMinutes()
        val now = System.currentTimeMillis()

        for (anime in animes) {
            val episodeDateMillis = parseDateToMillis(anime.episodeDate) ?: continue
            val delayMillis = episodeDateMillis - now - reminderMinutes * 60_000
            val workName = "reminder_${anime.title}_${anime.episodeNumber}"
            val data = workDataOf(
                "title" to anime.title,
                "episodeNumber" to anime.episodeNumber,
                "episodeDateMillis" to episodeDateMillis
            )

            val existing = workManager.getWorkInfosForUniqueWork(workName).get()
            val alreadyScheduled = existing.any {
                it.state == WorkInfo.State.ENQUEUED &&
                        it.outputData.getLong("episodeDateMillis", 0) == episodeDateMillis
            }

            if (alreadyScheduled) {
                Log.d(TagHandler.NOTAKU_INFO, "🔁 Already scheduled ${anime.title} - skipping")
                continue
            }

            if (delayMillis > 0) {
                val workRequest = OneTimeWorkRequestBuilder<ReminderWorker>()
                    .setInitialDelay(delayMillis, TimeUnit.MILLISECONDS)
                    .setInputData(data)
                    .build()

                workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, workRequest)

            }
        }
    }

    private fun parseDateToMillis(dateString: String): Long? {
        return try {
            val odt = OffsetDateTime.parse(dateString)
            odt.toInstant().toEpochMilli()
        } catch (e: Exception) {
            Log.e(TagHandler.NOTAKU_ERROR, "Error parsing date: $dateString", e)
            null
        }
    }

    private suspend fun getUserReminderMinutes(): Int {
        return getUserPreferencesUseCase()
            .firstOrNull()
            ?.notificationTime ?: 30
    }
}
  <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

  <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="androidx.work.WorkManagerInitializer"
                android:value="androidx.startup"
                tools:node="remove" />
        </provider>

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.