Halloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PCHalloween DealsAmazon USOctober picks are moving - compare before checkoutAmazon US: current deals, useful picks and tech finds.See Picks×
FREEDOM251

Freedom251 › Blog › AlarmManager

Build Reliable Scheduled App Blocks in Kotlin With Day Masks and Boundary Flows

By Freedom251 Team · · 6 min read

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a weekday bitmask to select the day a block starts, treat an overnight interval as continuing into the next calendar day, and have a collected Kotlin Flow wait for the next boundary before emitting updated state. That Flow is suitable while the app process is active; it is not a system alarm and cannot guarantee work after the process stops.

Define the schedule rules before writing code

A bitmask is only meaningful when its weekday mapping and interval policy are stable. The example below assigns Monday to bit 0 through Sunday to bit 6. A set bit means a block starts on that weekday. Persist that mapping as part of the schedule format; changing it later can reinterpret saved masks.

As an Amazon Associate I earn from qualifying purchases.

This implementation uses a half-open interval: the start is included and the end is excluded. Equal start and end times mean an empty interval, not a full-day block. If a product needs different behavior, change the policy deliberately and test it rather than relying on accidental time comparisons.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Represent weekdays with a bitmask

import java.time.DayOfWeek
import java.time.LocalTime
import java.time.ZonedDateTime

private fun DayOfWeek.bit(): Int = 1 shl (value - 1)

data class DailyBlock(
    val daysMask: Int,
    val start: LocalTime,
    val end: LocalTime,
) {
    fun startsOn(day: DayOfWeek): Boolean =
        (daysMask and day.bit()) != 0
}

DayOfWeek.value numbers Monday as 1 and Sunday as 7, so this mapping produces Monday = 1, Tuesday = 2, and Sunday = 64. For example, a Monday-and-Friday mask is 1 or 16, or 17. The platform does not prescribe a weekday bit assignment or persistence format; this is an application choice that should be documented and covered by serialization tests.

Evaluate regular and overnight windows

For a same-day window, the current weekday’s bit controls whether the interval is active. For an overnight window, its early-morning portion belongs to the previous calendar day’s selected start. For example, a Monday 22:00–02:00 block is active Monday at 23:00 and Tuesday at 01:00, but not Tuesday at 22:00 unless Tuesday is also selected.

fun DailyBlock.isActiveAt(now: ZonedDateTime): Boolean {
    if (start == end) return false

    val time = now.toLocalTime()
    return if (start < end) {
        startsOn(now.dayOfWeek) && time >= start && time < end
    } else {
        val selectedStartDay =
            (time >= start && startsOn(now.dayOfWeek)) ||
            (time < end && startsOn(now.minusDays(1).dayOfWeek))
        selectedStartDay
    }
}

The comparisons implement the stated inclusive-start, exclusive-end rule: a block is active exactly at its start and inactive exactly at its end. The code uses the supplied ZonedDateTime‘s local date and time. It does not define product policy for timezone changes or daylight-saving gaps and overlaps; decide whether a schedule follows the device’s current zone or a fixed zone, then test those transitions explicitly.

Find the next schedule boundary

A boundary-driven Flow needs the next start or end instant, not a loop that polls every second. Construct candidate boundaries from selected start days, retain only future instants, and take the earliest one. Passing a zone-aware time avoids treating a local clock time as a universal instant.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import java.time.Instant
import java.time.temporal.ChronoUnit

fun DailyBlock.nextBoundaryAfter(now: ZonedDateTime): Instant? {
    if (start == end || daysMask == 0) return null

    return (0L..8L).asSequence()
        .map { offset -> now.toLocalDate().plusDays(offset) }
        .filter { date -> startsOn(date.dayOfWeek) }
        .flatMap { date ->
            sequenceOf(
                date.atTime(start).atZone(now.zone).toInstant(),
                date.atTime(end).let { endDateTime ->
                    if (start > end) endDateTime.plusDays(1) else endDateTime
                }.atZone(now.zone).toInstant(),
            )
        }
        .filter { it > now.toInstant() }
        .minOrNull()
}

The search spans today through eight days ahead so that a selected weekday can be found even when the current day is just past its boundary. Each enabled start day contributes a start and an end instant; for an overnight window, the end is placed on the following date. If no day is enabled, or the interval is empty under this policy, there is no boundary.

Local-time conversion across daylight-saving transitions requires a product decision: a local time can be skipped or occur twice. Java’s zone rules resolve such conversions, but the resolution may not match the intended blocking policy. Add explicit tests for the zones and transition behavior the app supports.

Emit state and wait until the next boundary

Kotlin Flow is an asynchronous stream. A cold Flow begins its upstream work when collected, and each collector runs a separate upstream instance. This pattern emits the current state, computes the next boundary, and suspends with a cancellable delay before reevaluating.

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.time.Duration
import java.time.ZonedDateTime

fun DailyBlock.activeFlow(
    clock: () -> ZonedDateTime,
): Flow<Boolean> = flow {
    while (true) {
        val now = clock()
        emit(isActiveAt(now))

        val next = nextBoundaryAfter(now) ?: return@flow
        val millis = Duration.between(now.toInstant(), next).toMillis()
        delay(millis.coerceAtLeast(1L))
    }
}

Provide a clock that reads the current zoned wall time on each iteration, rather than capturing one timestamp when constructing the Flow. The delay is an in-process wait: Kotlin documents that delay waits at least the requested duration without blocking a thread and is cancellable. It may resume later than the target, so recomputing the state after wake-up is essential. Collect in a lifecycle-appropriate scope; cancellation of that collection stops the timer.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

This simple loop does not itself react immediately to a manual clock or timezone change while suspended. If those changes must take effect promptly, observe the relevant system change in the owner and cancel/restart the collection, or use a scheduling mechanism designed for the required lifetime and behavior.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Choose an Android timing mechanism by required lifetime

A Flow with delay is appropriate when the app needs to refresh an in-memory state while its process is running. It should not be used as a promise to execute while the app is stopped or the device is asleep. Android describes AlarmManager alarms as time-based operations outside the lifetime of an application.

Approach Process must remain alive? Device wake behavior Precision and ownership Restart and time semantics
Collected Flow with delay Yes; collection and process must remain active. Does not independently wake a sleeping device. Convenient for active-app state; cancellable with its coroutine scope and may run late. Recreate collection after restart. Base the calculation on wall-clock time for time-of-day schedules.
Handler timing APIs Yes; intended for work during app lifetime. Not a substitute for an alarm that must wake the device. Useful for in-process scheduling, with lifecycle ownership handled by the app. App must restore its own in-process work; choose timing semantics appropriate to the task.
AlarmManager No; alarms operate outside app lifetime. Alarm guidance covers operation while the device sleeps; select the alarm behavior that fits the requirement. Prefer inexact alarms for most cases. Exact alarms are for genuinely precise, user-facing behavior and have access requirements and battery trade-offs. Alarms are cleared on reboot and must be restored. Use RTC for a particular wall-clock time or elapsed-time alarms for a duration.

See Android’s alarm scheduling guidance and AlarmManager reference for current constraints, exact-alarm access, and alarm types. Do not request exact-alarm access merely to make a foreground Flow appear more punctual.

Handle reboot and clock meaning deliberately

Alarms do not survive device reboot, so an app that depends on a persistent schedule must restore or reschedule them after boot according to its permissions and product requirements. The relevant Android guidance is in the AlarmManager reference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use wall-clock/RTC semantics when the event means “at 9:00 AM,” because the target is a civil time. Use elapsed time when the requirement means “after 30 minutes.” Kotlin’s time-measurement guidance notes that Android’s System.nanoTime() does not count deep-sleep time, so a monotonic duration measurement and a wall-clock appointment are not interchangeable.

Test the boundaries and policy

  • Verify each weekday maps to exactly one stable bit, including Monday and Sunday, and round-trip persisted masks.
  • Test a same-day block before its start, exactly at start, just before end, exactly at end, and after end.
  • Test an overnight block on its selected start day and the next morning, including the exclusive end instant.
  • Test unselected weekdays, an empty mask, and equal start/end times according to the chosen policy.
  • Test next-boundary selection when the current time is just before and just after a boundary, and when the next enabled day is a week away.
  • Test Flow cancellation and verify a collected flow emits a fresh state after waking late.
  • For supported zones, test daylight-saving gaps, repeated local times, timezone changes, device clock changes, and restoration after reboot.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

AlarmManagerAndroidCoroutinesKotlinKotlin Flow

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.