Skip to content

Commit 3422b7d

Browse files
authored
Merge branch 'develop' into feature/bma/addCommentOnSdkDep
2 parents b26fa73 + 692362e commit 3422b7d

File tree

43 files changed

+307
-80
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+307
-80
lines changed

features/call/api/src/main/kotlin/io/element/android/features/call/api/ElementCallEntryPoint.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface ElementCallEntryPoint {
2929
* @param senderName The name of the sender of the event that started the call.
3030
* @param avatarUrl The avatar url of the room or DM.
3131
* @param timestamp The timestamp of the event that started the call.
32+
* @param expirationTimestamp The timestamp at which the call should stop ringing.
3233
* @param notificationChannelId The id of the notification channel to use for the call notification.
3334
* @param textContent The text content of the notification. If null the default content from the system will be used.
3435
*/
@@ -40,6 +41,7 @@ interface ElementCallEntryPoint {
4041
senderName: String?,
4142
avatarUrl: String?,
4243
timestamp: Long,
44+
expirationTimestamp: Long,
4345
notificationChannelId: String,
4446
textContent: String?,
4547
)

features/call/impl/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ dependencies {
100100
testImplementation(projects.libraries.push.test)
101101
testImplementation(projects.services.analytics.test)
102102
testImplementation(projects.services.appnavstate.test)
103+
testImplementation(projects.services.toolbox.test)
103104
testImplementation(projects.tests.testutils)
104105
testImplementation(libs.androidx.compose.ui.test.junit)
105106
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/DefaultElementCallEntryPoint.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class DefaultElementCallEntryPoint(
4343
senderName: String?,
4444
avatarUrl: String?,
4545
timestamp: Long,
46+
expirationTimestamp: Long,
4647
notificationChannelId: String,
4748
textContent: String?,
4849
) {
@@ -55,6 +56,7 @@ class DefaultElementCallEntryPoint(
5556
senderName = senderName,
5657
avatarUrl = avatarUrl,
5758
timestamp = timestamp,
59+
expirationTimestamp = expirationTimestamp,
5860
notificationChannelId = notificationChannelId,
5961
textContent = textContent,
6062
)

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/CallNotificationData.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,6 @@ data class CallNotificationData(
2626
val notificationChannelId: String,
2727
val timestamp: Long,
2828
val textContent: String?,
29+
// Expiration timestamp in millis since epoch
30+
val expirationTimestamp: Long,
2931
) : Parcelable

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class RingingCallNotificationCreator(
6464
roomAvatarUrl: String?,
6565
notificationChannelId: String,
6666
timestamp: Long,
67+
expirationTimestamp: Long,
6768
textContent: String?,
6869
): Notification? {
6970
val matrixClient = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
@@ -88,6 +89,7 @@ class RingingCallNotificationCreator(
8889
notificationChannelId = notificationChannelId,
8990
timestamp = timestamp,
9091
textContent = textContent,
92+
expirationTimestamp = expirationTimestamp,
9193
)
9294

9395
val declineIntent = PendingIntentCompat.getBroadcast(

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ internal fun IncomingCallScreenPreview() = ElementPreview {
176176
notificationChannelId = "incoming_call",
177177
timestamp = 0L,
178178
textContent = null,
179+
expirationTimestamp = 1000L,
179180
),
180181
onAnswer = {},
181182
onCancel = {},

features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import io.element.android.libraries.push.api.notifications.ForegroundServiceType
3434
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
3535
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
3636
import io.element.android.services.appnavstate.api.AppForegroundStateService
37+
import io.element.android.services.toolbox.api.systemclock.SystemClock
3738
import kotlinx.coroutines.CoroutineScope
3839
import kotlinx.coroutines.ExperimentalCoroutinesApi
3940
import kotlinx.coroutines.Job
@@ -53,7 +54,7 @@ import kotlinx.coroutines.launch
5354
import kotlinx.coroutines.sync.Mutex
5455
import kotlinx.coroutines.sync.withLock
5556
import timber.log.Timber
56-
import kotlin.time.Duration.Companion.seconds
57+
import kotlin.math.min
5758

5859
/**
5960
* Manages the active call state.
@@ -98,6 +99,7 @@ class DefaultActiveCallManager(
9899
private val defaultCurrentCallService: DefaultCurrentCallService,
99100
private val appForegroundStateService: AppForegroundStateService,
100101
private val imageLoaderHolder: ImageLoaderHolder,
102+
private val systemClock: SystemClock,
101103
) : ActiveCallManager {
102104
private val tag = "DefaultActiveCallManager"
103105
private var timedOutCallJob: Job? = null
@@ -118,8 +120,20 @@ class DefaultActiveCallManager(
118120

119121
override suspend fun registerIncomingCall(notificationData: CallNotificationData) {
120122
mutex.withLock {
123+
val ringDuration =
124+
min(
125+
notificationData.expirationTimestamp - systemClock.epochMillis(),
126+
ElementCallConfig.RINGING_CALL_DURATION_SECONDS * 1000L
127+
)
128+
129+
if (ringDuration < 0) {
130+
// Should already have stopped ringing, ignore.
131+
Timber.tag(tag).d("Received timed-out incoming ringing call for room id: ${notificationData.roomId}, cancel ringing")
132+
return
133+
}
134+
121135
appForegroundStateService.updateHasRingingCall(true)
122-
Timber.tag(tag).d("Received incoming call for room id: ${notificationData.roomId}")
136+
Timber.tag(tag).d("Received incoming call for room id: ${notificationData.roomId}, ringDuration(ms): $ringDuration")
123137
if (activeCall.value != null) {
124138
displayMissedCallNotification(notificationData)
125139
Timber.tag(tag).w("Already have an active call, ignoring incoming call: $notificationData")
@@ -138,14 +152,14 @@ class DefaultActiveCallManager(
138152
showIncomingCallNotification(notificationData)
139153

140154
// Wait for the ringing call to time out
141-
delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds)
155+
delay(timeMillis = ringDuration)
142156
incomingCallTimedOut(displayMissedCallNotification = true)
143157
}
144158

145159
// Acquire a wake lock to keep the device awake during the incoming call, so we can process the room info data
146160
if (activeWakeLock?.isHeld == false) {
147161
Timber.tag(tag).d("Acquiring partial wakelock")
148-
activeWakeLock.acquire(ElementCallConfig.RINGING_CALL_DURATION_SECONDS * 1000L)
162+
activeWakeLock.acquire(ringDuration)
149163
}
150164
}
151165
}
@@ -236,6 +250,7 @@ class DefaultActiveCallManager(
236250
notificationChannelId = notificationData.notificationChannelId,
237251
timestamp = notificationData.timestamp,
238252
textContent = notificationData.textContent,
253+
expirationTimestamp = notificationData.expirationTimestamp,
239254
) ?: return
240255
runCatchingExceptions {
241256
notificationManagerCompat.notify(

features/call/impl/src/test/kotlin/io/element/android/features/call/DefaultElementCallEntryPointTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class DefaultElementCallEntryPointTest {
5959
senderName = "senderName",
6060
avatarUrl = "avatarUrl",
6161
timestamp = 0,
62+
expirationTimestamp = 0,
6263
notificationChannelId = "notificationChannelId",
6364
textContent = "textContent",
6465
)

features/call/impl/src/test/kotlin/io/element/android/features/call/notifications/RingingCallNotificationCreatorTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class RingingCallNotificationCreatorTest {
7373
roomAvatarUrl = "https://example.com/avatar.jpg",
7474
notificationChannelId = "channelId",
7575
timestamp = 0L,
76+
expirationTimestamp = 20L,
7677
textContent = "textContent",
7778
)
7879

features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolde
3939
import io.element.android.libraries.push.test.notifications.FakeOnMissedCallNotificationHandler
4040
import io.element.android.libraries.push.test.notifications.push.FakeNotificationBitmapLoader
4141
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
42+
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
43+
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
4244
import io.element.android.tests.testutils.lambda.lambdaRecorder
4345
import io.element.android.tests.testutils.lambda.value
4446
import io.element.android.tests.testutils.plantTestTimber
@@ -368,6 +370,83 @@ class DefaultActiveCallManagerTest {
368370
assertThat(manager.activeCall.value).isNotNull()
369371
}
370372

373+
@OptIn(ExperimentalCoroutinesApi::class)
374+
@Test
375+
fun `IncomingCall - rings no longer than expiration time`() = runTest {
376+
setupShadowPowerManager()
377+
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
378+
val clock = FakeSystemClock()
379+
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat, systemClock = clock)
380+
381+
assertThat(manager.activeWakeLock?.isHeld).isFalse()
382+
assertThat(manager.activeCall.value).isNull()
383+
384+
val eventTimestamp = A_FAKE_TIMESTAMP
385+
// The call should not ring more than 30 seconds after the initial event was sent
386+
val expirationTimestamp = eventTimestamp + 30_000
387+
388+
val callNotificationData = aCallNotificationData(
389+
timestamp = eventTimestamp,
390+
expirationTimestamp = expirationTimestamp,
391+
)
392+
393+
// suppose it took 10s to be notified
394+
clock.epochMillisResult = eventTimestamp + 10_000
395+
manager.registerIncomingCall(callNotificationData)
396+
397+
assertThat(manager.activeCall.value).isEqualTo(
398+
ActiveCall(
399+
callType = CallType.RoomCall(
400+
sessionId = callNotificationData.sessionId,
401+
roomId = callNotificationData.roomId,
402+
),
403+
callState = CallState.Ringing(callNotificationData)
404+
)
405+
)
406+
407+
runCurrent()
408+
409+
assertThat(manager.activeWakeLock?.isHeld).isTrue()
410+
verify { notificationManagerCompat.notify(notificationId, any()) }
411+
412+
// advance by 21s it should have stopped ringing
413+
advanceTimeBy(21_000)
414+
runCurrent()
415+
416+
verify { notificationManagerCompat.cancel(any()) }
417+
}
418+
@OptIn(ExperimentalCoroutinesApi::class)
419+
@Test
420+
fun `IncomingCall - ignore expired ring lifetime`() = runTest {
421+
setupShadowPowerManager()
422+
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
423+
val clock = FakeSystemClock()
424+
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat, systemClock = clock)
425+
426+
assertThat(manager.activeWakeLock?.isHeld).isFalse()
427+
assertThat(manager.activeCall.value).isNull()
428+
429+
val eventTimestamp = A_FAKE_TIMESTAMP
430+
// The call should not ring more than 30 seconds after the initial event was sent
431+
val expirationTimestamp = eventTimestamp + 30_000
432+
433+
val callNotificationData = aCallNotificationData(
434+
timestamp = eventTimestamp,
435+
expirationTimestamp = expirationTimestamp,
436+
)
437+
438+
// suppose it took 35s to be notified
439+
clock.epochMillisResult = eventTimestamp + 35_000
440+
manager.registerIncomingCall(callNotificationData)
441+
442+
assertThat(manager.activeCall.value).isNull()
443+
444+
runCurrent()
445+
446+
assertThat(manager.activeWakeLock?.isHeld).isFalse()
447+
verify(exactly = 0) { notificationManagerCompat.notify(notificationId, any()) }
448+
}
449+
371450
private fun setupShadowPowerManager() {
372451
shadowOf(InstrumentationRegistry.getInstrumentation().targetContext.getSystemService<PowerManager>()).apply {
373452
setIsWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK, true)
@@ -378,6 +457,7 @@ class DefaultActiveCallManagerTest {
378457
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
379458
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
380459
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
460+
systemClock: FakeSystemClock = FakeSystemClock(),
381461
) = DefaultActiveCallManager(
382462
context = InstrumentationRegistry.getInstrumentation().targetContext,
383463
coroutineScope = backgroundScope,
@@ -393,5 +473,6 @@ class DefaultActiveCallManagerTest {
393473
defaultCurrentCallService = DefaultCurrentCallService(),
394474
appForegroundStateService = FakeAppForegroundStateService(),
395475
imageLoaderHolder = FakeImageLoaderHolder(),
476+
systemClock = systemClock,
396477
)
397478
}

0 commit comments

Comments
 (0)