Replace ThreeTenBP with SystemClock in CallService#1623
Replace ThreeTenBP with SystemClock in CallService#1623aleksandar-apostolov merged 1 commit intodevelopfrom
Conversation
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.
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
WalkthroughThis 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorFix debouncer unit regression—
submit()expects milliseconds, not seconds.The
Debouncer.submit()method'sdelayMsparameter explicitly expects milliseconds (confirmed byhandler.postDelayed(it, delayMs)and tested behavior), but the code passesdebouncerThresholdTimeInSeconds(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:stopServiceGracefullyis a silent no-op whenstartTimeElapsedRealtimeisnull.The
?.letguard means that ifsetStartTimewas never called (e.g., a test subclass overridesonCreatewithout callingsuper, or some future refactor moves initialization), neitherinternalStopServiceGracefully()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.
SDK Size Comparison 📏
|
|




Goal
Fix
ZoneRulesExceptioncrash whenCallServiceis started by the system beforeStreamVideoBuilder.build()has run (e.g. from a push notification or process restart).Implementation
Replace
OffsetDateTime.now()/Duration.between()withSystemClock.elapsedRealtime()for measuring service uptime in the stop debouncer. This removes the ThreeTenBP dependency fromCallServiceentirely.🎨 UI Changes
N/A — no UI changes.
Testing
After this fix,
CallServiceno longer depends on ThreeTenBP initialization.Summary by CodeRabbit