Skip to content

Instantly share code, notes, and snippets.

@maliksaif
Created July 11, 2025 13:41
Show Gist options
  • Select an option

  • Save maliksaif/147813a5c212e8b9d26900c8505a48e0 to your computer and use it in GitHub Desktop.

Select an option

Save maliksaif/147813a5c212e8b9d26900c8505a48e0 to your computer and use it in GitHub Desktop.
DozeTaskScheduler
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.SparseArray
import androidx.core.content.getSystemService
import androidx.core.util.forEach
import com.compose.playground.lifecycle.ApplicationLifecycleTracker
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.compose.playground.time.ElapsedTimeSinceBoot
import com.compose.playground.time.Time
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
private const val INTENT_EXTRA_TASK_ID = "InexactWhileInDozeTasksScheduler.taskId"
private val BACKGROUND_SCHEDULER_PRECISION = 100L.toDuration(DurationUnit.MILLISECONDS)
private val EXACT_PRECISION = Duration.ZERO
internal class InexactWhileInDozeTasksScheduler(
private val time: Time,
private val applicationContext: Context,
private val applicationLifecycleTracker: ApplicationLifecycleTracker
) : TasksScheduler {
private val backgroundScope = CoroutineScope(Dispatchers.Unconfined)
private val tasks = SparseArray<ScheduledTaskDescription>()
private val foregroundScheduler = Handler(Looper.getMainLooper())
private val backgroundScheduler: AlarmManager = applicationContext.getSystemService()!!
init {
backgroundScope.launch(start = CoroutineStart.UNDISPATCHED) {
applicationLifecycleTracker
.isApplicationStarted()
.collect {
tasks.forEach { _, taskDescription ->
executeOrSchedule(taskDescription, EXACT_PRECISION)
}
}
}
}
@Synchronized
override fun scheduleTask(time: ElapsedTimeSinceBoot, taskId: Int, task: () -> Unit) {
cancelTask(taskId)
val taskDescription = ScheduledTaskDescription(
taskId = taskId,
task = {
cancelTask(taskId)
task()
},
time = time,
pendingIntent = PendingIntent.getBroadcast(
applicationContext,
taskId,
Intent(applicationContext, TimeoutReceiver::class.java).apply {
putExtra(INTENT_EXTRA_TASK_ID, taskId)
},
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
tasks.put(taskId, taskDescription)
executeOrSchedule(taskDescription, EXACT_PRECISION)
}
@Synchronized
override fun cancelTask(taskId: Int) {
tasks[taskId]?.let { taskDescription ->
clearSchedule(taskDescription)
tasks.remove(taskId)
}
}
private fun clearSchedule(taskDescription: ScheduledTaskDescription) {
foregroundScheduler.removeCallbacks(taskDescription.task)
backgroundScheduler.cancel(taskDescription.pendingIntent)
}
@Synchronized
private fun executeOrSchedule(
taskDescription: ScheduledTaskDescription,
timePrecision: Duration
) {
val now = time.elapsedTimeSinceBoot
if (now + timePrecision >= taskDescription.time) {
taskDescription.task.run()
return
}
clearSchedule(taskDescription)
if (applicationLifecycleTracker.isApplicationStarted().value) {
val delay = taskDescription.time - now
foregroundScheduler.postDelayed(
taskDescription.task,
delay.toLong(DurationUnit.MILLISECONDS)
)
} else {
backgroundScheduler.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
taskDescription.time.toLong(DurationUnit.MILLISECONDS),
taskDescription.pendingIntent
)
}
}
private data class ScheduledTaskDescription(
val taskId: Int,
val task: Runnable,
val time: ElapsedTimeSinceBoot,
val pendingIntent: PendingIntent
)
class TimeoutReceiver : BroadcastReceiver(), KoinComponent {
override fun onReceive(context: Context?, intent: Intent?) {
val taskId = intent?.getIntExtra(INTENT_EXTRA_TASK_ID, -1) ?: -1
if (taskId != -1) {
val tasksScheduler = get<InexactWhileInDozeTasksScheduler>()
val task = tasksScheduler.tasks[taskId]
if (task != null) {
tasksScheduler.executeOrSchedule(task, BACKGROUND_SCHEDULER_PRECISION)
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment