Merge develop into the PreviewParam rename

The 174 commits develop gained since this branch was cut touch preview data
in three ways, and each is resolved so the flipped konsist rule still holds
across the merged tree.

Two screens the branch had renamed were deleted upstream by "Remove the
account provider search screen and homeserver resolver": the
changeaccountprovider and searchaccountprovider packages. Their deletion
wins, so ChangeAccountProviderStatePreviewParam, SearchAccountProviderStatePreviewParam
and the two views that used them go with them rather than coming back
under the new name.

Two preview classes develop added still carried the old suffix and are
renamed here, along with the `@PreviewParameter` reference in each view:
GlobalSearchStateProvider -> GlobalSearchStatePreviewParam and
SendFailureDialogStateProvider -> SendFailureDialogStatePreviewParam.

Everything else merged textually. No production class extending
PreviewParameterProvider is left with a `Provider` name, and no reference
to any of the 194 renamed names survives outside the CHANGES.md entry that
records a past pull request title.
This commit is contained in:
hayaksi1
2026-08-24 10:55:40 +03:00
397 changed files with 6635 additions and 4221 deletions
+6
View File
@@ -61,17 +61,23 @@ dependencies {
testCommonDependencies(libs)
testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test)
testImplementation(projects.libraries.accountselect.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.matrixmedia.test)
testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.features.announcement.test)
testImplementation(projects.features.forward.test)
testImplementation(projects.features.messages.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.preferences.test)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.signedout.test)
testImplementation(projects.services.apperror.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test)
@@ -51,7 +51,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
@@ -157,7 +157,7 @@ class LoggedInFlowNode(
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Placeholder,
@@ -335,11 +335,11 @@ class LoggedInFlowNode(
}
NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) {
override fun navigateToRoom(roomId: RoomId, eventId: EventId?, joinedRoom: JoinedRoom?) {
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom),
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom, eventId = eventId),
clearBackstack = false,
)
}
@@ -685,7 +685,7 @@ class LoggedInFlowNode(
val currentMapTilerConfig = LocalMapTilerConfig.current
val updatedMapTilerConfig by produceState(currentMapTilerConfig) {
value = remoteEnterpriseConfigProvider.get(matrixClient.sessionId).dataOrNull()?.mapTilerConfig ?: currentMapTilerConfig
value = customMapTilerConfigProvider.get().getOrNull() ?: currentMapTilerConfig
}
ElementThemeApp(
@@ -32,7 +32,6 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.room.RoomFlowNode
@@ -40,6 +39,7 @@ import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView
import io.element.android.appnav.session.MatrixSessionCache
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
@@ -109,6 +109,13 @@ class RootFlowNode(
buildContext = buildContext,
plugins = plugins
) {
/**
* Login params coming from a launch or new [Intent], waiting to be consumed by the not logged in flow.
* Kept here so that the root nav target can be computed from both the logged in state and the pending
* login params, whatever the order in which the intent and the first nav state emission are processed.
*/
private var pendingLoginParams: LoginParams? = null
override fun onBuilt() {
analyticsColdStartWatcher.start()
appCoroutineScope.launch {
@@ -152,7 +159,7 @@ class RootFlowNode(
}
}
LoggedInState.NotLoggedIn -> {
switchToNotLoggedInFlow(null)
switchToNotLoggedInFlow(pendingLoginParams)
}
}
}
@@ -194,15 +201,18 @@ class RootFlowNode(
}
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
pendingLoginParams = null
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
}
private fun switchToNotLoggedInFlow(params: LoginParams?) {
Timber.d("switchToNotLoggedInFlow, hasLoginParams=${params != null}")
matrixSessionCache.removeAll()
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
}
private fun switchToSignedOutFlow(sessionId: SessionId) {
pendingLoginParams = null
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
}
@@ -319,6 +329,7 @@ class RootFlowNode(
}
override fun onDone() {
pendingLoginParams = null
backstack.pop()
}
}
@@ -418,6 +429,7 @@ class RootFlowNode(
Timber.w("Login link ignored, multi account is disabled")
}
} else {
pendingLoginParams = params
switchToNotLoggedInFlow(params)
}
} else {
@@ -76,6 +76,7 @@ class IntentResolver(
}
private fun Intent.canBeIgnored(): Boolean {
if (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) return true
return action == Intent.ACTION_MAIN &&
categories?.contains(Intent.CATEGORY_LAUNCHER) == true
}
@@ -11,7 +11,7 @@ package io.element.android.appnav.root
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap
import dev.zacsweers.metro.Inject
import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.appnav.session.MatrixSessionCache
import io.element.android.features.preferences.api.CacheService
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.di
package io.element.android.appnav.session
import androidx.annotation.VisibleForTesting
import com.bumble.appyx.core.state.MutableSavedStateMap
@@ -115,7 +115,7 @@ class MatrixSessionCache(
private fun onNewMatrixClient(matrixClient: MatrixClient) {
val syncOrchestrator = syncOrchestratorFactory.create(
syncService = matrixClient.syncService,
matrixClient = matrixClient,
sessionCoroutineScope = matrixClient.sessionCoroutineScope,
)
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.di
package io.element.android.appnav.session
import androidx.annotation.VisibleForTesting
import dev.zacsweers.metro.Assisted
@@ -16,7 +16,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.recordTransaction
@@ -37,7 +37,7 @@ import kotlin.time.Duration.Companion.seconds
@AssistedInject
class SyncOrchestrator(
@Assisted private val syncService: SyncService,
@Assisted private val matrixClient: MatrixClient,
@Assisted sessionCoroutineScope: CoroutineScope,
private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor,
@@ -47,13 +47,15 @@ class SyncOrchestrator(
@AssistedFactory
interface Factory {
fun create(
syncService: SyncService,
matrixClient: MatrixClient,
sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator
}
private val tag = "SyncOrchestrator"
private val syncService = matrixClient.syncService
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
private val started = AtomicBoolean(false)
@@ -94,7 +96,7 @@ class SyncOrchestrator(
appForegroundStateService.isInCall,
appForegroundStateService.isSyncingNotificationEvent,
appForegroundStateService.hasRingingCall,
appForegroundStateService.isSharingLiveLocation
appForegroundStateService.isSharingLiveLocation,
)
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
combine(
@@ -103,6 +105,11 @@ class SyncOrchestrator(
networkMonitor.connectivity,
isAppActiveFlow,
) { syncState, networkState, isAppActive ->
if (matrixClient.isShuttingDown) {
Timber.tag(tag).d("Matrix client is shutting down, no need to start the sync service again")
return@combine SyncStateAction.NoOp
}
val isNetworkAvailable = networkState == NetworkStatus.Connected
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
@@ -0,0 +1,189 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.appnav
import android.content.Intent
import android.net.Uri
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.AncestryInfo
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.node
import com.bumble.appyx.navmodel.backstack.activeElement
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.session.FakeSyncOrchestratorFactory
import io.element.android.appnav.session.MatrixSessionCache
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.test.FakeLoginEntryPoint
import io.element.android.features.login.test.FakeLoginIntentResolver
import io.element.android.features.login.test.accesscontrol.FakeAccountProviderAccessControl
import io.element.android.features.preferences.test.FakeCacheService
import io.element.android.features.rageshake.test.FakeBugReportEntryPoint
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
import io.element.android.features.share.test.FakeShareIntentHandler
import io.element.android.features.signedout.test.FakeSignedOutEntryPoint
import io.element.android.libraries.accountselect.test.FakeAccountSelectEntryPoint
import io.element.android.libraries.architecture.AssistedNodeFactory
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.FakeSdkMetadata
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
import io.element.android.libraries.oauth.test.FakeOAuthActionFlow
import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver
import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.analytics.test.watchers.FakeAnalyticsColdStartWatcher
import io.element.android.services.apperror.test.FakeAppErrorStateService
import io.element.android.tests.testutils.node.FakeNodeFactoriesBindings
import io.element.android.tests.testutils.node.FakeParentNode
import io.element.android.tests.testutils.presenter.NotUsedPresenter
import io.element.android.tests.testutils.robolectric.RobolectricTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
private const val A_LOGIN_LINK = "https://mobile.element.io/element/?account_provider=example.com&login_hint=mxid:@alice:example.com"
private val A_LOGIN_PARAMS = LoginParams(
accountProvider = "example.com",
loginHint = "mxid:@alice:example.com",
)
private val A_LOGIN_ENTRY_POINT_PARAMS = LoginEntryPoint.Params(
accountProvider = A_LOGIN_PARAMS.accountProvider,
loginHint = A_LOGIN_PARAMS.loginHint,
)
class RootFlowNodeTest : RobolectricTest() {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun `given no session, when a login link is handled before the nav state is observed, then the login params are kept`() = runTest {
var loginEntryPointParams: LoginEntryPoint.Params? = null
val rootFlowNode = createRootFlowNode(
loginEntryPoint = FakeLoginEntryPoint { buildContext, params ->
loginEntryPointParams = params
node(buildContext) {}
},
)
rootFlowNode.parentNodeTestHelper()
// The intent is handled first, this is what happens when the deep link cold starts the app.
rootFlowNode.handleIntent(aLoginIntent())
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
// Then the first nav state emission lands, it must not override the login params.
runCurrent()
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
assertThat(loginEntryPointParams).isEqualTo(A_LOGIN_ENTRY_POINT_PARAMS)
}
@Test
fun `given no session, when a login link is handled after the nav state is observed, then the login params are applied`() = runTest {
var loginEntryPointParams: LoginEntryPoint.Params? = null
val rootFlowNode = createRootFlowNode(
loginEntryPoint = FakeLoginEntryPoint { buildContext, params ->
loginEntryPointParams = params
node(buildContext) {}
},
)
rootFlowNode.parentNodeTestHelper()
// The nav state is observed first, this is what happens when the app is already running.
runCurrent()
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(null))
rootFlowNode.handleIntent(aLoginIntent())
runCurrent()
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
assertThat(loginEntryPointParams).isEqualTo(A_LOGIN_ENTRY_POINT_PARAMS)
}
private fun aLoginIntent() = Intent(Intent.ACTION_VIEW, Uri.parse(A_LOGIN_LINK))
private fun TestScope.createRootFlowNode(
loginEntryPoint: LoginEntryPoint,
sessionStore: SessionStore = InMemorySessionStore(),
): RootFlowNode {
val matrixSessionCache = MatrixSessionCache(
authenticationService = FakeMatrixAuthenticationService(),
syncOrchestratorFactory = FakeSyncOrchestratorFactory(),
analyticsService = FakeAnalyticsService(),
)
val parentNode = FakeParentNode(
graph = FakeNodeFactoriesBindings(
mapOf(
NotLoggedInFlowNode::class to AssistedNodeFactory { buildContext, plugins ->
NotLoggedInFlowNode(
buildContext = buildContext,
plugins = plugins,
loginEntryPoint = loginEntryPoint,
imageLoaderHolder = FakeImageLoaderHolder(),
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
)
}
)
)
)
return RootFlowNode(
buildContext = BuildContext(
ancestryInfo = AncestryInfo.Child(anchor = parentNode),
savedStateMap = null,
customisations = NodeCustomisationDirectoryImpl(),
),
plugins = emptyList(),
sessionStore = sessionStore,
accountProviderAccessControl = FakeAccountProviderAccessControl { true },
navStateFlowFactory = RootNavStateFlowFactory(
sessionStore = sessionStore,
cacheService = FakeCacheService(),
matrixSessionCache = matrixSessionCache,
imageLoaderHolder = FakeImageLoaderHolder(),
sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(),
),
matrixSessionCache = matrixSessionCache,
presenter = RootPresenter(
crashDetectionPresenter = NotUsedPresenter(),
rageshakeDetectionPresenter = NotUsedPresenter(),
appErrorStateService = FakeAppErrorStateService(),
analyticsService = FakeAnalyticsService(),
sdkMetadata = FakeSdkMetadata("sha"),
),
bugReportEntryPoint = FakeBugReportEntryPoint(),
signedOutEntryPoint = FakeSignedOutEntryPoint(),
accountSelectEntryPoint = FakeAccountSelectEntryPoint(),
intentResolver = IntentResolver(
deeplinkParser = { null },
loginIntentResolver = FakeLoginIntentResolver { A_LOGIN_PARAMS },
oAuthIntentResolver = FakeOAuthIntentResolver { null },
permalinkParser = FakePermalinkParser(),
shareIntentHandler = FakeShareIntentHandler(),
),
oAuthActionFlow = FakeOAuthActionFlow(),
featureFlagService = FakeFeatureFlagService(),
announcementService = FakeAnnouncementService(),
analyticsService = FakeAnalyticsService(),
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
appCoroutineScope = backgroundScope,
)
}
}
@@ -8,10 +8,11 @@
package io.element.android.appnav
import io.element.android.appnav.di.SyncOrchestrator
import io.element.android.appnav.session.SyncOrchestrator
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
@@ -40,7 +41,7 @@ class SyncOrchestratorTest {
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
)
@@ -60,7 +61,7 @@ class SyncOrchestratorTest {
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
)
@@ -85,7 +86,7 @@ class SyncOrchestratorTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -117,7 +118,7 @@ class SyncOrchestratorTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -165,7 +166,7 @@ class SyncOrchestratorTest {
initialIsSyncingNotificationEventValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -208,7 +209,7 @@ class SyncOrchestratorTest {
initialIsSyncingNotificationEventValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -252,7 +253,7 @@ class SyncOrchestratorTest {
initialHasRingingCall = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -296,7 +297,7 @@ class SyncOrchestratorTest {
initialIsInCallValue = true,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -339,7 +340,7 @@ class SyncOrchestratorTest {
initialIsInCallValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -369,7 +370,7 @@ class SyncOrchestratorTest {
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val syncOrchestrator = createSyncOrchestrator(
syncService = syncService,
matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
)
@@ -381,12 +382,44 @@ class SyncOrchestratorTest {
startSyncRecorder.assertions().isNeverCalled()
}
@Test
fun `when the client is shutting down, the orchestrator does nothing`() = runTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val appForegroundStateService = FakeAppForegroundStateService(
initialForegroundValue = false,
initialIsSyncingNotificationEventValue = false,
initialIsInCallValue = false,
)
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
startSyncLambda = startSyncRecorder
}
val syncOrchestrator = createSyncOrchestrator(
matrixClient = FakeMatrixClient(syncService = syncService, isShuttingDownResult = { true }),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
// We start observing
syncOrchestrator.observeStates()
// These should still not trigger a sync, sync the client is shutting down
networkMonitor.givenNetworkBlocked(false)
appForegroundStateService.updateIsInCallState(true)
appForegroundStateService.isInForeground.value = true
advanceTimeBy(10.seconds)
startSyncRecorder.assertions().isNeverCalled()
}
private fun TestScope.createSyncOrchestrator(
syncService: FakeSyncService = FakeSyncService(),
matrixClient: FakeMatrixClient = FakeMatrixClient(),
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
) = SyncOrchestrator(
syncService = syncService,
matrixClient = matrixClient,
sessionCoroutineScope = backgroundScope,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
@@ -45,6 +45,24 @@ class IntentResolverTest : RobolectricTest() {
assertThat(result).isNull()
}
@Test
fun `resolve intent replayed from the history should return null`() {
val sut = createIntentResolver(
deeplinkParserResult = DeeplinkData.Room(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
threadId = null,
eventId = null,
)
)
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
action = Intent.ACTION_VIEW
addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)
}
val result = sut.resolve(intent)
assertThat(result).isNull()
}
@Test
fun `test resolve navigation intent root`() {
val sut = createIntentResolver(
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.session
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.CoroutineScope
class FakeSyncOrchestratorFactory : SyncOrchestrator.Factory {
override fun create(
matrixClient: MatrixClient,
sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator = lambdaError()
}
@@ -6,12 +6,12 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.di
package io.element.android.appnav.session
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.MatrixClient
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.auth.FakeMatrixAuthenticationService
@@ -138,11 +138,11 @@ class MatrixSessionCacheTest {
return object : SyncOrchestrator.Factory {
override fun create(
syncService: SyncService,
matrixClient: MatrixClient,
sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator {
return SyncOrchestrator(
syncService = syncService,
matrixClient = matrixClient,
sessionCoroutineScope = sessionCoroutineScope,
appForegroundStateService = FakeAppForegroundStateService(),
networkMonitor = FakeNetworkMonitor(),