Skip to content

Comments

Replace ThreeTenBP with SystemClock in CallService#1623

Merged
aleksandar-apostolov merged 1 commit intodevelopfrom
fix/threetenbp-init-callservice
Feb 21, 2026
Merged

Replace ThreeTenBP with SystemClock in CallService#1623
aleksandar-apostolov merged 1 commit intodevelopfrom
fix/threetenbp-init-callservice

Conversation

@aleksandar-apostolov
Copy link
Contributor

@aleksandar-apostolov aleksandar-apostolov commented Feb 20, 2026

Goal

Fix ZoneRulesException crash when CallService is started by the system before StreamVideoBuilder.build() has run (e.g. from a push notification or process restart).

Implementation

Replace OffsetDateTime.now() / Duration.between() with SystemClock.elapsedRealtime() for measuring service uptime in the stop debouncer. This removes the ThreeTenBP dependency from CallService entirely.

🎨 UI Changes

N/A — no UI changes.

Testing

After this fix, CallService no longer depends on ThreeTenBP initialization.

Summary by CodeRabbit

  • Refactor
    • Improved internal timing mechanisms for call service management with more reliable and efficient time tracking calculations.

When the system launches CallService (e.g. from a push notification
or process restart), AndroidThreeTen may not be initialized since
StreamVideoBuilder.build() hasn't run yet. This causes a
ZoneRulesException crash on OffsetDateTime.now() in onCreate.

Since the only purpose is measuring elapsed service uptime for the
stop debouncer, replace OffsetDateTime with SystemClock.elapsedRealtime()
which is monotonic, requires no initialization, and is unaffected by
wall clock changes.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 20, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled.

🎉 Great job! This PR is ready for review.

@aleksandar-apostolov aleksandar-apostolov changed the title fix(core): replace ThreeTenBP with SystemClock in CallService Replace ThreeTenBP with SystemClock in CallService Feb 20, 2026
@aleksandar-apostolov aleksandar-apostolov marked this pull request as ready for review February 20, 2026 14:41
@aleksandar-apostolov aleksandar-apostolov requested a review from a team as a code owner February 20, 2026 14:41
@aleksandar-apostolov aleksandar-apostolov added the pr:bug Fixes a bug label Feb 20, 2026
Copy link
Contributor

@rahul-lohra rahul-lohra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Walkthrough

This pull request migrates the notification service's time tracking from OffsetDateTime-based tracking to SystemClock.elapsedRealtime() milliseconds. The change updates the call service implementation, state controller public API, and the service state data model to use elapsed realtime measurements instead of absolute datetime objects.

Changes

Cohort / File(s) Summary
Service State Model
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/models/ServiceStateSnapshot.kt
Replaced startTime: OffsetDateTime? property with startTimeElapsedRealtime: Long? in the data class; removed OffsetDateTime import.
Service State Controller
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/controllers/ServiceStateController.kt
Updated public API to replace startTime: OffsetDateTime? getter with startTimeElapsedRealtime: Long?; changed setStartTime(time: OffsetDateTime) to setStartTime(elapsedRealtime: Long) for accepting millisecond elapsed realtime values.
Call Service Implementation
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt
Switched time initialization from OffsetDateTime.now() to SystemClock.elapsedRealtime(); updated duration calculations in stopServiceGracefully to use TimeUnit.MILLISECONDS.toSeconds() for converting elapsed milliseconds to seconds; added SystemClock and TimeUnit imports.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 Tick-tock, the service now keeps better time,
No more DateTime, just elapsed sublime!
SystemClock whispers its steady beat,
Making our notifications precise and neat! 🕐✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: replacing ThreeTenBP with SystemClock in CallService, which directly addresses the root cause of the ZoneRulesException crash described in the PR.
Description check ✅ Passed The PR description includes all required sections: Goal clearly states the problem and rationale, Implementation details the technical approach, UI Changes acknowledges N/A status, and Testing explains the fix impact.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/threetenbp-init-callservice

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt (1)

618-631: ⚠️ Potential issue | 🟠 Major

Fix debouncer unit regression—submit() expects milliseconds, not seconds.

The Debouncer.submit() method's delayMs parameter explicitly expects milliseconds (confirmed by handler.postDelayed(it, delayMs) and tested behavior), but the code passes debouncerThresholdTimeInSeconds (value: 2L) instead of milliseconds (2_000L). This causes a 2 ms delay instead of 2 000 ms, silently breaking the notification-dismissal workaround for young services (< 2 seconds) that the design intentionally preserves.

Required fix
-            val debouncerThresholdTimeInSeconds = SERVICE_DESTROY_THRESHOLD_TIME_MS / 1_000
             logger.d { "[stopServiceGracefully] differenceInSeconds: $differenceInSeconds" }
             if (differenceInSeconds >= debouncerThresholdTimeInSeconds) {
                 internalStopServiceGracefully()
             } else {
-                debouncer.submit(debouncerThresholdTimeInSeconds) {
+                debouncer.submit(SERVICE_DESTROY_THRESHOLD_TIME_MS) {
                     internalStopServiceGracefully()
                 }
             }

Also fix the condition to use seconds directly: if (differenceInSeconds >= SERVICE_DESTROY_THRESHOLD_TIME_MS / 1_000) to avoid the misleading variable name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt`
around lines 618 - 631, The debouncer is being given seconds instead of
milliseconds causing a tiny delay; inside the
serviceStateController.startTimeElapsedRealtime block, keep computing
differenceInSeconds = TimeUnit.MILLISECONDS.toSeconds(elapsedMs) and use the
seconds comparison with SERVICE_DESTROY_THRESHOLD_TIME_MS / 1_000 in the if
(differenceInSeconds >= SERVICE_DESTROY_THRESHOLD_TIME_MS / 1_000) check, but
when scheduling debouncer.submit(...) pass milliseconds (use
SERVICE_DESTROY_THRESHOLD_TIME_MS or a new debouncerThresholdTimeInMs variable)
rather than debouncerThresholdTimeInSeconds so debouncer.submit(delayMs)
receives milliseconds and calls internalStopServiceGracefully() after the
correct delay.
🧹 Nitpick comments (1)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt (1)

618-618: stopServiceGracefully is a silent no-op when startTimeElapsedRealtime is null.

The ?.let guard means that if setStartTime was never called (e.g., a test subclass overrides onCreate without calling super, or some future refactor moves initialization), neither internalStopServiceGracefully() nor the debouncer fires — the service never stops. Consider adding a defensive fallback:

🛡️ Proposed defensive fallback
-        serviceStateController.startTimeElapsedRealtime?.let { startTime ->
-            val elapsedMs = SystemClock.elapsedRealtime() - startTime
-            val differenceInSeconds = TimeUnit.MILLISECONDS.toSeconds(elapsedMs)
-            val debouncerThresholdTimeInSeconds = SERVICE_DESTROY_THRESHOLD_TIME_MS / 1_000
-            logger.d { "[stopServiceGracefully] differenceInSeconds: $differenceInSeconds" }
-            if (differenceInSeconds >= debouncerThresholdTimeInSeconds) {
-                internalStopServiceGracefully()
-            } else {
-                debouncer.submit(debouncerThresholdTimeInSeconds) {
-                    internalStopServiceGracefully()
-                }
-            }
-        }
+        val startTime = serviceStateController.startTimeElapsedRealtime
+        if (startTime == null) {
+            logger.w { "[stopServiceGracefully] startTime is null, stopping immediately" }
+            internalStopServiceGracefully()
+            return
+        }
+        val elapsedMs = SystemClock.elapsedRealtime() - startTime
+        val differenceInSeconds = TimeUnit.MILLISECONDS.toSeconds(elapsedMs)
+        val debouncerThresholdTimeInSeconds = SERVICE_DESTROY_THRESHOLD_TIME_MS / 1_000
+        logger.d { "[stopServiceGracefully] differenceInSeconds: $differenceInSeconds" }
+        if (differenceInSeconds >= debouncerThresholdTimeInSeconds) {
+            internalStopServiceGracefully()
+        } else {
+            debouncer.submit(debouncerThresholdTimeInSeconds) {
+                internalStopServiceGracefully()
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt`
at line 618, stopServiceGracefully currently uses a safe-call with
serviceStateController.startTimeElapsedRealtime?.let, causing a silent no-op if
startTimeElapsedRealtime is null; update stopServiceGracefully to defensively
handle the null case by calling internalStopServiceGracefully() (and firing the
debouncer) when startTimeElapsedRealtime is null or missing, and ensure any
fallback logs a warning so missing setStartTime (or overridden onCreate) is
visible; locate the logic around stopServiceGracefully,
serviceStateController.startTimeElapsedRealtime, internalStopServiceGracefully,
and setStartTime to add the null-branch behavior and a concise warning log.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt`:
- Around line 618-631: The debouncer is being given seconds instead of
milliseconds causing a tiny delay; inside the
serviceStateController.startTimeElapsedRealtime block, keep computing
differenceInSeconds = TimeUnit.MILLISECONDS.toSeconds(elapsedMs) and use the
seconds comparison with SERVICE_DESTROY_THRESHOLD_TIME_MS / 1_000 in the if
(differenceInSeconds >= SERVICE_DESTROY_THRESHOLD_TIME_MS / 1_000) check, but
when scheduling debouncer.submit(...) pass milliseconds (use
SERVICE_DESTROY_THRESHOLD_TIME_MS or a new debouncerThresholdTimeInMs variable)
rather than debouncerThresholdTimeInSeconds so debouncer.submit(delayMs)
receives milliseconds and calls internalStopServiceGracefully() after the
correct delay.

---

Nitpick comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt`:
- Line 618: stopServiceGracefully currently uses a safe-call with
serviceStateController.startTimeElapsedRealtime?.let, causing a silent no-op if
startTimeElapsedRealtime is null; update stopServiceGracefully to defensively
handle the null case by calling internalStopServiceGracefully() (and firing the
debouncer) when startTimeElapsedRealtime is null or missing, and ensure any
fallback logs a warning so missing setStartTime (or overridden onCreate) is
visible; locate the logic around stopServiceGracefully,
serviceStateController.startTimeElapsedRealtime, internalStopServiceGracefully,
and setStartTime to add the null-branch behavior and a concise warning log.

@github-actions
Copy link
Contributor

SDK Size Comparison 📏

SDK Before After Difference Status
stream-video-android-core 12.00 MB 12.00 MB 0.00 MB 🟢
stream-video-android-ui-xml 5.68 MB 5.68 MB 0.00 MB 🟢
stream-video-android-ui-compose 6.28 MB 6.28 MB 0.00 MB 🟢

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
B Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@aleksandar-apostolov aleksandar-apostolov merged commit fffac9c into develop Feb 21, 2026
14 of 18 checks passed
@aleksandar-apostolov aleksandar-apostolov deleted the fix/threetenbp-init-callservice branch February 21, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:bug Fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants