Skip to content

[BUG] Repeating notifications not scheduling on exact time. #2

@cengiz-pz

Description

@cengiz-pz

Is there an existing issue for this?

  • I have searched the existing issues

Current Behavior

AlarmManager.setRepeating() is notoriously unreliable on modern Android for exact timing — it’s subject to batching, power optimizations, and the inexact triggering issues.

Depending on the Android version and power optimizations (Doze mode, App Standby, battery optimizations), the system may align or batch alarms to save power.

If trigger time is in the future but within the interval window, some devices will align the first trigger to the nearest multiple of the interval, which can result in the first notification being delivered at interval seconds instead of delay seconds.

Expected Behavior

First of the repeating notifications to be delivered at or near delay seconds after scheduling. The rest delivered at interval-second intervals.

Steps To Reproduce

No response

Godot Version

4.5.stable

OS Godot Is Running On

Windows 11

Plugin Version

5.0

OS Plugin Is Running On

Android 16

Anything else?

Proposed new scheduling logic:

public int schedule(Dictionary data) {
    if (!isInitialized) {
        Log.e(LOG_TAG, "schedule(): plugin is not initialized!");
        return Error.ERR_UNCONFIGURED.toNativeValue();
    }

    NotificationData notificationData = new NotificationData(data);
    Log.d(LOG_TAG, "schedule():: notification id: " + notificationData.getId());

    if (!notificationData.isValid()) {
        Log.e(LOG_TAG, "schedule(): invalid notification data object");
        return Error.ERR_INVALID_DATA.toNativeValue();
    }

    int notificationId = notificationData.getId();

    Intent intent = new Intent(activity.getApplicationContext(), NotificationReceiver.class);
    intent.putExtra(NotificationData.DATA_KEY_ID, notificationId);
    intent.putExtra(NotificationData.DATA_KEY_CHANNEL_ID, notificationData.getChannelId());
    intent.putExtra(NotificationData.DATA_KEY_TITLE, notificationData.getTitle());
    intent.putExtra(NotificationData.DATA_KEY_CONTENT, notificationData.getContent());
    intent.putExtra(NotificationData.DATA_KEY_SMALL_ICON_NAME, notificationData.getSmallIconName());

    if (notificationData.hasDeeplink()) {
        intent.putExtra(NotificationData.DATA_KEY_DEEPLINK, notificationData.getDeeplink());
    }
    if (notificationData.hasRestartAppOption()) {
        intent.putExtra(NotificationData.OPTION_KEY_RESTART_APP, true);
    }
    if (notificationData.getBadgeCount() > 0) {
        intent.putExtra(NotificationData.DATA_KEY_BADGE_COUNT, notificationData.getBadgeCount());
    }

    if (notificationData.hasInterval()) {
        intent.putExtra(NotificationData.DATA_KEY_IS_REPEAT, true);
        intent.putExtra(NotificationData.DATA_KEY_INTERVAL_SECONDS, notificationData.getInterval());
    }

    scheduleExactAlarm(activity.getApplicationContext(), notificationId, intent, notificationData.getDelay());
    return Error.OK.toNativeValue();
}

public void scheduleExactAlarm(Context context, int notificationId, Intent intent, int delaySeconds) {
    AlarmManager alarmManager = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
    long triggerTime = calculateTimeAfterDelay(delaySeconds);

    PendingIntent pendingIntent = PendingIntent.getBroadcast(
        context.getApplicationContext(),
        notificationId,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
    );

    alarmManager.setExactAndAllowWhileIdle(
        AlarmManager.RTC_WAKEUP,
        triggerTime,
        pendingIntent
    );

    Log.i(LOG_TAG, String.format(
        "Scheduled alarm for notification '%d' at %d (delay %ds).",
        notificationId,
        triggerTime,
        delaySeconds
    ));
}

Receiver:

@Override
public void onReceive(Context context, Intent intent) {
    // 1. Show the notification (existing code for building & showing)

    // 2. Self-reschedule if repeating
    boolean isRepeat = intent.getBooleanExtra(NotificationData.DATA_KEY_IS_REPEAT, false);
    if (isRepeat) {
        int notificationId = intent.getIntExtra(NotificationData.DATA_KEY_ID, -1);
        int intervalSeconds = intent.getIntExtra(NotificationData.DATA_KEY_INTERVAL_SECONDS, 0);

        if (notificationId != -1 && intervalSeconds > 0) {
            long nextTriggerTime = System.currentTimeMillis() + intervalSeconds * 1000L;

            PendingIntent pendingIntent = PendingIntent.getBroadcast(
                context.getApplicationContext(),
                notificationId,
                intent, // reuse same intent & extras
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
            );

            AlarmManager alarmManager = (AlarmManager) context.getApplicationContext().getSystemService(Context.ALARM_SERVICE);
            alarmManager.setExactAndAllowWhileIdle(
                AlarmManager.RTC_WAKEUP,
                nextTriggerTime,
                pendingIntent
            );

            Log.i("NotificationReceiver", String.format(
                "Self-rescheduled repeating notification '%d' for %d.",
                notificationId,
                nextTriggerTime
            ));
        }
    }
}

Why this is better

  • NotificationReceiver is fully independent — it doesn’t rely on MyNotificationScheduler.
  • The same logic is used for both the first and subsequent alarms.
  • Minimal coupling → easier to maintain, fewer dependencies.
  • Works with any context (foreground, background, boot receivers, etc.).
  • Avoids setRepeating() quirks while preserving exact timing for all notifications.

With this structure, the delay → first notification → fixed interval repeats will always work exactly as designed.

Concerns

Repeating alarms won't survive system restarts, which would not be production-ready for long-term schedules.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions