Created
July 11, 2025 13:41
-
-
Save maliksaif/147813a5c212e8b9d26900c8505a48e0 to your computer and use it in GitHub Desktop.
DozeTaskScheduler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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