Add ActiveRoomsHolder to manage the active rooms for a session (#4758)

This commit is contained in:
Jorge Martin Espinosa 2025-05-26 13:03:55 +02:00 committed by GitHub
parent 630e1d19c0
commit 9b9d75aa5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 240 additions and 30 deletions

View file

@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.api.AppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope
@ -62,6 +63,7 @@ class CallScreenPresenter @AssistedInject constructor(
private val activeCallManager: ActiveCallManager,
private val languageTagProvider: LanguageTagProvider,
private val appForegroundStateService: AppForegroundStateService,
private val activeRoomsHolder: ActiveRoomsHolder,
private val appCoroutineScope: CoroutineScope,
) : Presenter<CallScreenState> {
@AssistedFactory
@ -241,8 +243,10 @@ class CallScreenPresenter @AssistedInject constructor(
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
if (!notifiedCallStart) {
getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
?.onSuccess { notifiedCallStart = true }
val activeRoomForSession = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
val sendCallNotificationResult = activeRoomForSession?.sendCallNotificationIfNeeded()
?: getJoinedRoom(roomId)?.use { it.sendCallNotificationIfNeeded() }
sendCallNotificationResult?.onSuccess { notifiedCallStart = true }
}
}

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject
@ -24,6 +25,7 @@ class DefaultCallWidgetProvider @Inject constructor(
private val matrixClientsProvider: MatrixClientProvider,
private val appPreferencesStore: AppPreferencesStore,
private val callWidgetSettingsProvider: CallWidgetSettingsProvider,
private val activeRoomsHolder: ActiveRoomsHolder,
) : CallWidgetProvider {
override suspend fun getWidget(
sessionId: SessionId,
@ -33,7 +35,9 @@ class DefaultCallWidgetProvider @Inject constructor(
theme: String?,
): Result<CallWidgetProvider.GetWidgetResult> = runCatching {
val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow()
val room = matrixClient.getJoinedRoom(roomId) ?: error("Room not found")
val room = activeRoomsHolder.getActiveRoomMatching(sessionId, roomId)
?: matrixClient.getJoinedRoom(roomId)
?: error("Room not found")
val customBaseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull()
val baseUrl = customBaseUrl ?: EMBEDDED_CALL_WIDGET_BASE_URL

View file

@ -32,6 +32,7 @@ import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.analytics.test.FakeScreenTracker
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.services.toolbox.api.systemclock.SystemClock
import io.element.android.tests.testutils.WarmUpRule
@ -367,6 +368,7 @@ import kotlin.time.Duration.Companion.seconds
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
screenTracker: ScreenTracker = FakeScreenTracker(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
): CallScreenPresenter {
val userAgentProvider = object : UserAgentProvider {
override fun provide(): String {
@ -387,6 +389,7 @@ import kotlin.time.Duration.Companion.seconds
languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService,
appCoroutineScope = backgroundScope,
activeRoomsHolder = activeRoomsHolder,
)
}
}

View file

@ -15,11 +15,13 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.matrix.test.widget.FakeCallWidgetSettingsProvider
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -77,6 +79,23 @@ class DefaultCallWidgetProviderTest {
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").getOrNull()).isNotNull()
}
@Test
fun `getWidget - reuses the active room if possible`() = runTest {
val client = FakeMatrixClient().apply {
// No room from the client
givenGetRoomResult(A_ROOM_ID, null)
}
val activeRoomsHolder = ActiveRoomsHolder().apply {
// A current active room with the same room id
addRoom(FakeJoinedRoom(baseRoom = FakeBaseRoom(roomId = A_ROOM_ID)))
}
val provider = createProvider(
matrixClientProvider = FakeMatrixClientProvider { Result.success(client) },
activeRoomsHolder = activeRoomsHolder
)
assertThat(provider.getWidget(A_SESSION_ID, A_ROOM_ID, "clientId", "languageTag", "theme").isFailure).isTrue()
}
@Test
fun `getWidget - will use a custom base url if it exists`() = runTest {
val room = FakeJoinedRoom(
@ -104,9 +123,11 @@ class DefaultCallWidgetProviderTest {
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
callWidgetSettingsProvider: CallWidgetSettingsProvider = FakeCallWidgetSettingsProvider(),
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
) = DefaultCallWidgetProvider(
matrixClientsProvider = matrixClientProvider,
appPreferencesStore = appPreferencesStore,
callWidgetSettingsProvider = callWidgetSettingsProvider,
activeRoomsHolder = activeRoomsHolder,
)
}

View file

@ -78,6 +78,7 @@ dependencies {
implementation(projects.features.roomlist.api)
implementation(projects.services.analytics.api)
implementation(projects.services.analytics.compose)
implementation(projects.services.appnavstate.api)
implementation(projects.services.toolbox.api)
implementation(libs.datetime)
implementation(libs.coil.compose)

View file

@ -18,6 +18,7 @@ import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.api.PushService
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import javax.inject.Inject
@ -37,8 +38,11 @@ class DefaultClearCacheUseCase @Inject constructor(
private val ftueService: FtueService,
private val pushService: PushService,
private val seenInvitesStore: SeenInvitesStore,
private val activeRoomsHolder: ActiveRoomsHolder,
) : ClearCacheUseCase {
override suspend fun invoke() = withContext(coroutineDispatchers.io) {
// Active rooms should be disposed of before clearing the cache
activeRoomsHolder.clear(matrixClient.sessionId)
// Clear Matrix cache
matrixClient.clearCache()
// Clear Coil cache

View file

@ -15,8 +15,11 @@ import io.element.android.features.invite.test.InMemorySeenInvitesStore
import io.element.android.features.preferences.impl.DefaultCacheService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.libraries.push.test.FakePushService
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers
@ -31,8 +34,10 @@ import org.robolectric.RobolectricTestRunner
class DefaultClearCacheUseCaseTest {
@Test
fun `execute clear cache should do all the expected tasks`() = runTest {
val activeRoomsHolder = ActiveRoomsHolder().apply { addRoom(FakeJoinedRoom()) }
val clearCacheLambda = lambdaRecorder<Unit> { }
val matrixClient = FakeMatrixClient(
sessionId = A_SESSION_ID,
clearCacheLambda = clearCacheLambda,
)
val defaultCacheService = DefaultCacheService()
@ -55,6 +60,7 @@ class DefaultClearCacheUseCaseTest {
ftueService = ftueService,
pushService = pushService,
seenInvitesStore = seenInvitesStore,
activeRoomsHolder = activeRoomsHolder,
)
defaultCacheService.clearedCacheEventFlow.test {
sut.invoke()
@ -64,6 +70,7 @@ class DefaultClearCacheUseCaseTest {
.with(value(matrixClient.sessionId), value(false))
assertThat(awaitItem()).isEqualTo(matrixClient.sessionId)
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull()
}
}
}

View file

@ -37,6 +37,7 @@ dependencies {
implementation(projects.libraries.roomselect.api)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.testtags)
implementation(projects.services.appnavstate.api)
api(libs.statemachine)
api(projects.features.share.api)

View file

@ -20,9 +20,11 @@ import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -33,6 +35,7 @@ class SharePresenter @AssistedInject constructor(
private val matrixClient: MatrixClient,
private val mediaPreProcessor: MediaPreProcessor,
private val sessionPreferencesStore: SessionPreferencesStore,
private val activeRoomsHolder: ActiveRoomsHolder,
) : Presenter<ShareState> {
@AssistedFactory
interface Factory {
@ -59,6 +62,12 @@ class SharePresenter @AssistedInject constructor(
)
}
private suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom? {
return activeRoomsHolder.getActiveRoom(matrixClient.sessionId)
?.takeIf { it.roomId == roomId }
?: matrixClient.getJoinedRoom(roomId)
}
private fun CoroutineScope.share(
intent: Intent,
roomIds: List<RoomId>,
@ -72,7 +81,7 @@ class SharePresenter @AssistedInject constructor(
} else {
roomIds
.map { roomId ->
val room = matrixClient.getJoinedRoom(roomId) ?: return@map false
val room = getJoinedRoom(roomId) ?: return@map false
val mediaSender = MediaSender(
preProcessor = mediaPreProcessor,
room = room,
@ -86,7 +95,11 @@ class SharePresenter @AssistedInject constructor(
).isSuccess
}
.all { it }
.also { room.destroy() }
.also {
if (activeRoomsHolder.getActiveRoomMatching(matrixClient.sessionId, roomId) == null) {
room.destroy()
}
}
}
.all { it }
}
@ -94,7 +107,7 @@ class SharePresenter @AssistedInject constructor(
onPlainText = { text ->
roomIds
.map { roomId ->
matrixClient.getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
getJoinedRoom(roomId)?.liveTimeline?.sendMessage(
body = text,
htmlBody = null,
intentionalMentions = emptyList(),

View file

@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.services.appnavstate.api.ActiveRoomsHolder
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.test.TestScope
@ -163,7 +164,8 @@ class SharePresenterTest {
intent: Intent = Intent(),
shareIntentHandler: ShareIntentHandler = FakeShareIntentHandler(),
matrixClient: MatrixClient = FakeMatrixClient(),
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor()
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
activeRoomsHolder: ActiveRoomsHolder = ActiveRoomsHolder(),
): SharePresenter {
return SharePresenter(
intent = intent,
@ -171,7 +173,8 @@ class SharePresenterTest {
shareIntentHandler = shareIntentHandler,
matrixClient = matrixClient,
mediaPreProcessor = mediaPreProcessor,
InMemorySessionPreferencesStore(),
sessionPreferencesStore = InMemorySessionPreferencesStore(),
activeRoomsHolder = activeRoomsHolder,
)
}
}