From c83cd2d245fb809c99a875b77c5f554fd9953be3 Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Fri, 20 Feb 2026 15:40:08 +0100 Subject: [PATCH] fix(core): replace ThreeTenBP with SystemClock in CallService 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. --- .../notifications/internal/service/CallService.kt | 14 ++++++-------- .../service/controllers/ServiceStateController.kt | 9 ++++----- .../service/models/ServiceStateSnapshot.kt | 3 +-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt index 9678cd71aa..288f53048e 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt @@ -21,6 +21,7 @@ import android.app.Notification import android.app.Service import android.content.Intent import android.os.IBinder +import android.os.SystemClock import androidx.core.app.NotificationManagerCompat import androidx.media.session.MediaButtonReceiver import io.getstream.log.taggedLogger @@ -53,9 +54,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import org.threeten.bp.Duration -import org.threeten.bp.OffsetDateTime -import kotlin.math.absoluteValue +import java.util.concurrent.TimeUnit /** * A foreground service that is running when there is an active call. @@ -117,7 +116,7 @@ internal open class CallService : Service() { override fun onCreate() { super.onCreate() - serviceStateController.setStartTime(OffsetDateTime.now()) + serviceStateController.setStartTime(SystemClock.elapsedRealtime()) } private fun shouldStopService(intent: Intent?): Boolean { @@ -616,11 +615,10 @@ internal open class CallService : Service() { logger.w { "[stopServiceGracefully] source: $source" } } - serviceStateController.startTime?.let { startTime -> + serviceStateController.startTimeElapsedRealtime?.let { startTime -> - val currentTime = OffsetDateTime.now() - val duration = Duration.between(startTime, currentTime) - val differenceInSeconds = duration.seconds.absoluteValue + 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) { diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/controllers/ServiceStateController.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/controllers/ServiceStateController.kt index 801f0d493e..ada0dab6e5 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/controllers/ServiceStateController.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/controllers/ServiceStateController.kt @@ -27,7 +27,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import org.threeten.bp.OffsetDateTime internal class ServiceStateController { private val _state = MutableStateFlow(ServiceStateSnapshot()) @@ -42,8 +41,8 @@ internal class ServiceStateController { val soundPlayer: CallSoundAndVibrationPlayer? get() = state.value.soundPlayer - val startTime: OffsetDateTime? - get() = state.value.startTime + val startTimeElapsedRealtime: Long? + get() = state.value.startTimeElapsedRealtime fun setCurrentCallId(callId: StreamCallId) { _state.update { it.copy(currentCallId = callId) } @@ -57,8 +56,8 @@ internal class ServiceStateController { _state.update { it.copy(soundPlayer = player) } } - fun setStartTime(time: OffsetDateTime) { - _state.update { it.copy(startTime = time) } + fun setStartTime(elapsedRealtime: Long) { + _state.update { it.copy(startTimeElapsedRealtime = elapsedRealtime) } } fun registerToggleCameraBroadcastReceiver( diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/models/ServiceStateSnapshot.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/models/ServiceStateSnapshot.kt index 04d4397901..ed3b147cc8 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/models/ServiceStateSnapshot.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/models/ServiceStateSnapshot.kt @@ -19,7 +19,6 @@ package io.getstream.video.android.core.notifications.internal.service.models import io.getstream.video.android.core.notifications.internal.receivers.ToggleCameraBroadcastReceiver import io.getstream.video.android.core.sounds.CallSoundAndVibrationPlayer import io.getstream.video.android.model.StreamCallId -import org.threeten.bp.OffsetDateTime internal data class ServiceStateSnapshot( val currentCallId: StreamCallId? = null, @@ -27,5 +26,5 @@ internal data class ServiceStateSnapshot( val soundPlayer: CallSoundAndVibrationPlayer? = null, val toggleCameraBroadcastReceiver: ToggleCameraBroadcastReceiver? = null, val isReceiverRegistered: Boolean = false, - val startTime: OffsetDateTime? = null, + val startTimeElapsedRealtime: Long? = null, )