From 2ecd4ecaf52c0363f1faa82ed37eed488b6b6526 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 18 Sep 2025 09:18:36 +0200 Subject: [PATCH] review + tests --- features/call/impl/build.gradle.kts | 1 + .../notifications/CallNotificationData.kt | 1 + .../call/impl/utils/ActiveCallManager.kt | 4 +- .../call/DefaultElementCallEntryPointTest.kt | 1 + .../RingingCallNotificationCreatorTest.kt | 1 + .../utils/DefaultActiveCallManagerTest.kt | 81 +++++++++++++++++++ 6 files changed, 88 insertions(+), 1 deletion(-) diff --git a/features/call/impl/build.gradle.kts b/features/call/impl/build.gradle.kts index 0c9ae5a4b0..7d6cc745d0 100644 --- a/features/call/impl/build.gradle.kts +++ b/features/call/impl/build.gradle.kts @@ -100,6 +100,7 @@ dependencies { testImplementation(projects.libraries.push.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.appnavstate.test) + testImplementation(projects.services.toolbox.test) testImplementation(projects.tests.testutils) testImplementation(libs.androidx.compose.ui.test.junit) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt index 8ebb8b8080..0bfbfb0411 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt @@ -26,5 +26,6 @@ data class CallNotificationData( val notificationChannelId: String, val timestamp: Long, val textContent: String?, + // Expiration timestamp in millis since epoch val expirationTimestamp: Long, ) : Parcelable diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt index b609bfcbce..4006ede376 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.push.api.notifications.ForegroundServiceType import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler import io.element.android.services.appnavstate.api.AppForegroundStateService +import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -98,6 +99,7 @@ class DefaultActiveCallManager( private val defaultCurrentCallService: DefaultCurrentCallService, private val appForegroundStateService: AppForegroundStateService, private val imageLoaderHolder: ImageLoaderHolder, + private val systemClock: SystemClock, ) : ActiveCallManager { private val tag = "DefaultActiveCallManager" private var timedOutCallJob: Job? = null @@ -120,7 +122,7 @@ class DefaultActiveCallManager( mutex.withLock { val ringDuration = min( - notificationData.expirationTimestamp - System.currentTimeMillis(), + notificationData.expirationTimestamp - systemClock.epochMillis(), ElementCallConfig.RINGING_CALL_DURATION_SECONDS * 1000L ) diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt index 86d9b80d65..7d0f15b540 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt @@ -59,6 +59,7 @@ class DefaultElementCallEntryPointTest { senderName = "senderName", avatarUrl = "avatarUrl", timestamp = 0, + expirationTimestamp = 0, notificationChannelId = "notificationChannelId", textContent = "textContent", ) diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt index f36267a353..0af482fc49 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt @@ -73,6 +73,7 @@ class RingingCallNotificationCreatorTest { roomAvatarUrl = "https://example.com/avatar.jpg", notificationChannelId = "channelId", timestamp = 0L, + expirationTimestamp = 20L, textContent = "textContent", ) diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt index d842126c67..3d1c35df4d 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -39,6 +39,8 @@ import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolde import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader import io.element.android.services.appnavstate.test.FakeAppForegroundStateService +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.plantTestTimber @@ -368,6 +370,83 @@ class DefaultActiveCallManagerTest { assertThat(manager.activeCall.value).isNotNull() } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `IncomingCall - rings no longer than expiration time`() = runTest { + setupShadowPowerManager() + val notificationManagerCompat = mockk(relaxed = true) + val clock = FakeSystemClock() + val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat, systemClock = clock) + + assertThat(manager.activeWakeLock?.isHeld).isFalse() + assertThat(manager.activeCall.value).isNull() + + val eventTimestamp = A_FAKE_TIMESTAMP + // The call should not ring more than 30 seconds after the initial event was sent + val expirationTimestamp = eventTimestamp + 30_000 + + val callNotificationData = aCallNotificationData( + timestamp = eventTimestamp, + expirationTimestamp = expirationTimestamp, + ) + + // suppose it took 10s to be notified + clock.epochMillisResult = eventTimestamp + 10_000 + manager.registerIncomingCall(callNotificationData) + + assertThat(manager.activeCall.value).isEqualTo( + ActiveCall( + callType = CallType.RoomCall( + sessionId = callNotificationData.sessionId, + roomId = callNotificationData.roomId, + ), + callState = CallState.Ringing(callNotificationData) + ) + ) + + runCurrent() + + assertThat(manager.activeWakeLock?.isHeld).isTrue() + verify { notificationManagerCompat.notify(notificationId, any()) } + + // advance by 21s it should have stopped ringing + advanceTimeBy(21_000) + runCurrent() + + verify { notificationManagerCompat.cancel(any()) } + } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `IncomingCall - ignore expired ring lifetime`() = runTest { + setupShadowPowerManager() + val notificationManagerCompat = mockk(relaxed = true) + val clock = FakeSystemClock() + val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat, systemClock = clock) + + assertThat(manager.activeWakeLock?.isHeld).isFalse() + assertThat(manager.activeCall.value).isNull() + + val eventTimestamp = A_FAKE_TIMESTAMP + // The call should not ring more than 30 seconds after the initial event was sent + val expirationTimestamp = eventTimestamp + 30_000 + + val callNotificationData = aCallNotificationData( + timestamp = eventTimestamp, + expirationTimestamp = expirationTimestamp, + ) + + // suppose it took 35s to be notified + clock.epochMillisResult = eventTimestamp + 35_000 + manager.registerIncomingCall(callNotificationData) + + assertThat(manager.activeCall.value).isNull() + + runCurrent() + + assertThat(manager.activeWakeLock?.isHeld).isFalse() + verify(exactly = 0) { notificationManagerCompat.notify(notificationId, any()) } + } + private fun setupShadowPowerManager() { shadowOf(InstrumentationRegistry.getInstrumentation().targetContext.getSystemService()).apply { setIsWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK, true) @@ -378,6 +457,7 @@ class DefaultActiveCallManagerTest { matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(), onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(), notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true), + systemClock: FakeSystemClock = FakeSystemClock(), ) = DefaultActiveCallManager( context = InstrumentationRegistry.getInstrumentation().targetContext, coroutineScope = backgroundScope, @@ -393,5 +473,6 @@ class DefaultActiveCallManagerTest { defaultCurrentCallService = DefaultCurrentCallService(), appForegroundStateService = FakeAppForegroundStateService(), imageLoaderHolder = FakeImageLoaderHolder(), + systemClock = systemClock, ) }