Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Call sharing metadata [WPB-14605] #3142

Merged
merged 2 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading