Skip to content

Commit

Permalink
feat: Call sharing metadata [WPB-14605] (#3142)
Browse files Browse the repository at this point in the history
* feat: Hold call sharing metadata [WPB-14605]

* Adjust names
  • Loading branch information
m-zagorski authored Dec 4, 2024
1 parent 346034a commit 06c284a
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.user.OtherUserMinimized
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.data.user.type.UserType
import kotlinx.datetime.Instant

data class CallMetadataProfile(
val data: Map<ConversationId, CallMetadata>
Expand All @@ -46,7 +47,8 @@ data class CallMetadata(
val maxParticipants: Int = 0, // Was used for tracking
val protocol: Conversation.ProtocolInfo,
val activeSpeakers: Map<UserId, List<String>> = mapOf(),
val users: List<OtherUserMinimized> = listOf()
val users: List<OtherUserMinimized> = listOf(),
val screenShareMetadata: CallScreenSharingMetadata = CallScreenSharingMetadata()
) {
fun getFullParticipants(): List<Participant> = participants.map { participant ->
val user = users.firstOrNull { it.id == participant.userId }
Expand All @@ -66,3 +68,14 @@ data class CallMetadata(
)
}
}

/**
* [activeScreenShares] - map of user ids that share screen with the start timestamp
* [completedScreenShareDurationInMillis] - total time of already ended screen shares in milliseconds
* [uniqueSharingUsers] - set of users that were sharing a screen at least once
*/
data class CallScreenSharingMetadata(
val activeScreenShares: Map<QualifiedID, Instant> = emptyMap(),
val completedScreenShareDurationInMillis: Long = 0L,
val uniqueSharingUsers: Set<String> = emptySet()
)
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.FederatedIdMapper
import com.wire.kalium.logic.data.id.GroupID
import com.wire.kalium.logic.data.id.QualifiedClientID
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.id.QualifiedIdMapper
import com.wire.kalium.logic.data.id.SubconversationId
import com.wire.kalium.logic.data.id.toCrypto
Expand Down Expand Up @@ -441,6 +442,8 @@ internal class CallDataSource(

val currentParticipantIds = call.participants.map { it.userId }.toSet()
val newParticipantIds = participants.map { it.userId }.toSet()
val sharingScreenParticipantIds = participants.filter { it.isSharingScreen }
.map { participant -> participant.id }

val updatedUsers = call.users.toMutableList()

Expand All @@ -456,7 +459,11 @@ internal class CallDataSource(
this[conversationId] = call.copy(
participants = participants,
maxParticipants = max(call.maxParticipants, participants.size + 1),
users = updatedUsers
users = updatedUsers,
screenShareMetadata = updateScreenSharingMetadata(
metadata = call.screenShareMetadata,
usersCurrentlySharingScreen = sharingScreenParticipantIds
)
)
}

Expand All @@ -479,6 +486,43 @@ internal class CallDataSource(
}
}

/**
* Manages call sharing metadata for analytical purposes by tracking the following:
* - **Active Screen Shares**: Maintains a record of currently active screen shares with their start times (local to the device).
* - **Completed Screen Share Duration**: Accumulates the total duration of screen shares that have already ended.
* - **Unique Sharing Users**: Keeps a unique list of all users who have shared their screen during the call.
*
* To update the metadata, the following steps are performed:
* 1. **Calculate Ended Screen Share Time**: Determine the total time for users who stopped sharing since the last update.
* 2. **Update Active Shares**: Filter out inactive shares and add any new ones, associating them with the current start time.
* 3. **Track Unique Users**: Append ids to current set in order to keep track of unique users.
*/
private fun updateScreenSharingMetadata(
metadata: CallScreenSharingMetadata,
usersCurrentlySharingScreen: List<QualifiedID>
): CallScreenSharingMetadata {
val now = DateTimeUtil.currentInstant()

val alreadyEndedScreenSharesTimeInMillis = metadata.activeScreenShares
.filterKeys { id -> id !in usersCurrentlySharingScreen }
.values
.sumOf { startTime -> DateTimeUtil.calculateMillisDifference(startTime, now) }

val updatedShares = metadata.activeScreenShares
.filterKeys { id -> id in usersCurrentlySharingScreen }
.plus(
usersCurrentlySharingScreen
.filterNot { id -> metadata.activeScreenShares.containsKey(id) }
.associateWith { now }
)

return metadata.copy(
activeScreenShares = updatedShares,
completedScreenShareDurationInMillis = metadata.completedScreenShareDurationInMillis + alreadyEndedScreenSharesTimeInMillis,
uniqueSharingUsers = metadata.uniqueSharingUsers.plus(usersCurrentlySharingScreen.map { id -> id.toString() })
)
}

private fun clearStaleParticipantTimeout(participant: ParticipantMinimized) {
callingLogger.i("Clear stale participant timer")
val qualifiedClient = QualifiedClientID(ClientId(participant.clientId), participant.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.wire.kalium.cryptography.CryptoQualifiedClientId
import com.wire.kalium.cryptography.MLSClient
import com.wire.kalium.logic.StorageFailure
import com.wire.kalium.logic.data.call.CallRepositoryTest.Arrangement.Companion.callerId
import com.wire.kalium.logic.data.call.CallRepositoryTest.Arrangement.Companion.participant
import com.wire.kalium.logic.data.call.mapper.CallMapperImpl
import com.wire.kalium.logic.data.client.MLSClientProvider
import com.wire.kalium.logic.data.conversation.ClientId
Expand Down Expand Up @@ -84,6 +85,7 @@ import kotlinx.datetime.Clock
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.time.DurationUnit
import kotlin.time.toDuration
Expand Down Expand Up @@ -1509,6 +1511,158 @@ class CallRepositoryTest {
)
}

@Test
fun givenCallWithParticipantsNotSharingScreen_whenOneStartsToShare_thenSharingMetadataHasProperValues() = runTest {
// given
val otherParticipant = participant.copy(id = QualifiedID("anotherParticipantId", "participantDomain"))
val participantsList = listOf(participant, otherParticipant)
val (_, callRepository) = Arrangement()
.givenGetKnownUserMinimizedSucceeds()
.arrange()
callRepository.updateCallMetadataProfileFlow(
callMetadataProfile = CallMetadataProfile(
data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList))
)
)

// when
callRepository.updateCallParticipants(
Arrangement.conversationId,
listOf(participant, otherParticipant.copy(isSharingScreen = true))
)
val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId]

// then
assertNotNull(callMetadata)
assertEquals(0L, callMetadata.screenShareMetadata.completedScreenShareDurationInMillis)
assertTrue(callMetadata.screenShareMetadata.activeScreenShares.containsKey(otherParticipant.id))
}

@Test
fun givenCallWithParticipantsNotSharingScreen_whenTwoStartsAndOneStops_thenSharingMetadataHasProperValues() = runTest {
// given
val (_, callRepository) = Arrangement()
.arrange()
val secondParticipant = participant.copy(id = QualifiedID("secondParticipantId", "participantDomain"))
val thirdParticipant = participant.copy(id = QualifiedID("thirdParticipantId", "participantDomain"))
val participantsList = listOf(participant, secondParticipant, thirdParticipant)
callRepository.updateCallMetadataProfileFlow(
callMetadataProfile = CallMetadataProfile(
data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList))
)
)

// when
callRepository.updateCallParticipants(
Arrangement.conversationId,
listOf(participant, secondParticipant.copy(isSharingScreen = true), thirdParticipant.copy(isSharingScreen = true))
)
callRepository.updateCallParticipants(
Arrangement.conversationId,
listOf(participant, secondParticipant, thirdParticipant.copy(isSharingScreen = true))
)
val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId]

// then
assertNotNull(callMetadata)
assertTrue(callMetadata.screenShareMetadata.activeScreenShares.size == 1)
assertTrue(callMetadata.screenShareMetadata.activeScreenShares.containsKey(thirdParticipant.id))
}

@Test
fun givenCallWithParticipantsSharingScreen_whenOneStopsToShare_thenSharingMetadataHasProperValues() = runTest {
// given
val (_, callRepository) = Arrangement()
.arrange()
val otherParticipant = participant.copy(id = QualifiedID("anotherParticipantId", "participantDomain"))
val participantsList = listOf(participant, otherParticipant)
callRepository.updateCallMetadataProfileFlow(
callMetadataProfile = CallMetadataProfile(
data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList))
)
)
callRepository.updateCallParticipants(
Arrangement.conversationId,
listOf(participant, otherParticipant.copy(isSharingScreen = true))
)

// when
callRepository.updateCallParticipants(
Arrangement.conversationId,
listOf(participant, otherParticipant.copy(isSharingScreen = false))
)
val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId]

// then
assertNotNull(callMetadata)
assertTrue(callMetadata.screenShareMetadata.activeScreenShares.isEmpty())
}

@Test
fun givenCallWithParticipantsSharingScreen_whenTheSameParticipantIsSharingMultipleTime_thenSharingMetadataHasUserIdOnlyOnce() =
runTest {
// given
val (_, callRepository) = Arrangement()
.arrange()
val otherParticipant = participant.copy(id = QualifiedID("anotherParticipantId", "participantDomain"))
val participantsList = listOf(participant, otherParticipant)
callRepository.updateCallMetadataProfileFlow(
callMetadataProfile = CallMetadataProfile(
data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList))
)
)

// when
callRepository.updateCallParticipants(
Arrangement.conversationId,
listOf(participant, otherParticipant.copy(isSharingScreen = true))
)
callRepository.updateCallParticipants(
Arrangement.conversationId,
listOf(participant, otherParticipant.copy(isSharingScreen = false))
)
callRepository.updateCallParticipants(
Arrangement.conversationId,
listOf(participant, otherParticipant.copy(isSharingScreen = true))
)
val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId]

// then
assertNotNull(callMetadata)
assertTrue(callMetadata.screenShareMetadata.uniqueSharingUsers.size == 1)
assertTrue(callMetadata.screenShareMetadata.uniqueSharingUsers.contains(otherParticipant.id.toString()))
}

@Test
fun givenCallWithParticipantsSharingScreen_whenTwoParticipantsAreSharing_thenSharingMetadataHasBothOfUsersIds() = runTest {
// given
val (_, callRepository) = Arrangement()
.arrange()
val secondParticipant = participant.copy(id = QualifiedID("secondParticipantId", "participantDomain"))
val thirdParticipant = participant.copy(id = QualifiedID("thirdParticipantId", "participantDomain"))
val participantsList = listOf(participant, secondParticipant, thirdParticipant)
callRepository.updateCallMetadataProfileFlow(
callMetadataProfile = CallMetadataProfile(
data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList))
)
)

// when
callRepository.updateCallParticipants(
Arrangement.conversationId,
listOf(participant, secondParticipant.copy(isSharingScreen = true), thirdParticipant.copy(isSharingScreen = true))
)
val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId]

// then
assertNotNull(callMetadata)
assertTrue(callMetadata.screenShareMetadata.uniqueSharingUsers.size == 2)
assertEquals(
setOf(secondParticipant.id.toString(), thirdParticipant.id.toString()),
callMetadata.screenShareMetadata.uniqueSharingUsers
)
}

private fun provideCall(id: ConversationId, status: CallStatus) = Call(
conversationId = id,
status = status,
Expand Down

0 comments on commit 06c284a

Please sign in to comment.