Skip to content

Bug: Exponential timer drain in AlarmChallengeController due to orphaned async loops #901

@vibhutomer

Description

@vibhutomer

📱 Description & Context

In alarm_challenge_controller.dart, the wake-up challenge timer is managed by the _startTimer() method. This method uses an asynchronous for loop with await Future.delayed() to slowly decrement the progress.value over 15 seconds.

However, a critical concurrency bug occurs when the user interacts with the challenge (e.g., shaking the phone or answering a math question). These actions trigger restartTimer(), which resets the progress to 1.0 and immediately calls _startTimer() again.

Because the previous asynchronous for loop is never explicitly cancelled or broken out of, a completely new loop is spawned alongside the old one.

⚠️ Actual Behavior (The Bug)

If a user shakes the phone 5 times, there will be 5 separate async loops running concurrently in the background. All of these loops are fighting to decrement the exact same progress.value simultaneously. This causes the progress bar to drain exponentially faster with every interaction, instantly dropping to zero and unfairly forcing the user to fail the challenge.

✨ Expected Behavior

When restartTimer() is invoked, any previously running instances of the _startTimer() loop must be terminated before the new loop begins. Only one asynchronous timer loop should be actively decrementing the progress value at any given time.

🔄 Steps to Reproduce

  1. Set an alarm with the "Shake" or "Math" challenge enabled.
  2. Wait for the alarm to ring and trigger the challenge screen.
  3. Rapidly shake the device (or quickly enter math digits) multiple times.
  4. Observe the progress bar at the bottom of the screen. Instead of resetting and draining slowly over 15 seconds, it will accelerate and drain in a fraction of a second.

💡 Proposed Solution

Introduce a cancellation token pattern (or a session ID integer) at the controller scope.

  • Increment the session ID inside restartTimer().
  • Inside the _startTimer() loop, check if the local loop's ID matches the global session ID. If it does not, explicitly break out of the orphaned loop.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions