Refactors AppStateHandler into interface implementation pattern

This commit is contained in:
ericdecanini 2022-07-08 10:05:05 +01:00
parent 4dccff4d78
commit e4c8c88cee
10 changed files with 219 additions and 140 deletions

View File

@ -1,11 +1,11 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@ -17,134 +17,25 @@
package im.vector.app
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.asFlow
import arrow.core.Option
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.utils.BehaviorDataSource
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import kotlinx.coroutines.flow.Flow
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.sync.SyncRequestState
import javax.inject.Inject
import javax.inject.Singleton
/**
* This class handles the global app state.
* It is required that this class is added as an observer to ProcessLifecycleOwner.get().lifecycle in [VectorApplication]
*/
@Singleton
class AppStateHandler @Inject constructor(
private val sessionDataSource: ActiveSessionDataSource,
private val uiStateRepository: UiStateRepository,
private val activeSessionHolder: ActiveSessionHolder,
private val analyticsTracker: AnalyticsTracker
) : DefaultLifecycleObserver {
interface AppStateHandler : DefaultLifecycleObserver {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomSummary>>(Option.empty())
val selectedSpaceFlow = selectedSpaceDataSource.stream()
private val spaceBackstack = ArrayDeque<String?>()
fun getCurrentSpace(): RoomSummary? {
return selectedSpaceDataSource.currentValue?.orNull()?.let { spaceSummary ->
activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(spaceSummary.roomId)
}
}
fun getCurrentSpace(): RoomSummary?
fun setCurrentSpace(
spaceId: String?,
session: Session? = null,
persistNow: Boolean = false,
isForwardNavigation: Boolean = true,
) {
val activeSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
val currentSpace = selectedSpaceDataSource.currentValue?.orNull()
val spaceSummary = spaceId?.let { activeSession.getRoomSummary(spaceId) }
val sameSpaceSelected = currentSpace != null && spaceId == currentSpace.roomId
)
if (sameSpaceSelected) {
return
}
fun getSpaceBackstack(): ArrayDeque<String?>
if (isForwardNavigation) {
spaceBackstack.addLast(currentSpace?.roomId)
}
fun getSelectedSpaceFlow(): Flow<Option<RoomSummary>>
if (persistNow) {
uiStateRepository.storeSelectedSpace(spaceSummary?.roomId, activeSession.sessionId)
}
if (spaceSummary == null) {
selectedSpaceDataSource.post(Option.empty())
} else {
selectedSpaceDataSource.post(Option.just(spaceSummary))
}
if (spaceId != null) {
activeSession.coroutineScope.launch(Dispatchers.IO) {
tryOrNull {
activeSession.getRoom(spaceId)?.membershipService()?.loadRoomMembersIfNeeded()
}
}
}
}
private fun observeActiveSession() {
sessionDataSource.stream()
.distinctUntilChanged()
.onEach {
// sessionDataSource could already return a session while activeSession holder still returns null
it.orNull()?.let { session ->
setCurrentSpace(uiStateRepository.getSelectedSpace(session.sessionId), session)
observeSyncStatus(session)
}
}
.launchIn(coroutineScope)
}
private fun observeSyncStatus(session: Session) {
session.syncService().getSyncRequestStateLive()
.asFlow()
.filterIsInstance<SyncRequestState.IncrementalSyncDone>()
.map { session.spaceService().getRootSpaceSummaries().size }
.distinctUntilChanged()
.onEach { spacesNumber ->
analyticsTracker.updateUserProperties(UserProperties(numSpaces = spacesNumber))
}.launchIn(session.coroutineScope)
}
fun getSpaceBackstack() = spaceBackstack
fun safeActiveSpaceId(): String? {
return selectedSpaceDataSource.currentValue?.orNull()?.roomId
}
override fun onResume(owner: LifecycleOwner) {
observeActiveSession()
}
override fun onPause(owner: LifecycleOwner) {
coroutineScope.coroutineContext.cancelChildren()
val session = activeSessionHolder.getSafeActiveSession() ?: return
uiStateRepository.storeSelectedSpace(selectedSpaceDataSource.currentValue?.orNull()?.roomId, session.sessionId)
}
fun getSafeActiveSpaceId(): String?
}

View File

@ -0,0 +1,149 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.asFlow
import arrow.core.Option
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.utils.BehaviorDataSource
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.session.coroutineScope
import im.vector.app.features.ui.UiStateRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.getRoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.sync.SyncRequestState
import javax.inject.Inject
import javax.inject.Singleton
/**
* This class handles the global app state.
* It is required that this class is added as an observer to ProcessLifecycleOwner.get().lifecycle in [VectorApplication]
*/
@Singleton
class AppStateHandlerImpl @Inject constructor(
private val sessionDataSource: ActiveSessionDataSource,
private val uiStateRepository: UiStateRepository,
private val activeSessionHolder: ActiveSessionHolder,
private val analyticsTracker: AnalyticsTracker
) : AppStateHandler {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val selectedSpaceDataSource = BehaviorDataSource<Option<RoomSummary>>(Option.empty())
private val selectedSpaceFlow = selectedSpaceDataSource.stream()
private val spaceBackstack = ArrayDeque<String?>()
override fun getCurrentSpace(): RoomSummary? {
return selectedSpaceDataSource.currentValue?.orNull()?.let { spaceSummary ->
activeSessionHolder.getSafeActiveSession()?.roomService()?.getRoomSummary(spaceSummary.roomId)
}
}
override fun setCurrentSpace(
spaceId: String?,
session: Session?,
persistNow: Boolean,
isForwardNavigation: Boolean,
) {
val activeSession = session ?: activeSessionHolder.getSafeActiveSession() ?: return
val currentSpace = selectedSpaceDataSource.currentValue?.orNull()
val spaceSummary = spaceId?.let { activeSession.getRoomSummary(spaceId) }
val sameSpaceSelected = currentSpace != null && spaceId == currentSpace.roomId
if (sameSpaceSelected) {
return
}
if (isForwardNavigation) {
spaceBackstack.addLast(currentSpace?.roomId)
}
if (persistNow) {
uiStateRepository.storeSelectedSpace(spaceSummary?.roomId, activeSession.sessionId)
}
if (spaceSummary == null) {
selectedSpaceDataSource.post(Option.empty())
} else {
selectedSpaceDataSource.post(Option.just(spaceSummary))
}
if (spaceId != null) {
activeSession.coroutineScope.launch(Dispatchers.IO) {
tryOrNull {
activeSession.getRoom(spaceId)?.membershipService()?.loadRoomMembersIfNeeded()
}
}
}
}
private fun observeActiveSession() {
sessionDataSource.stream()
.distinctUntilChanged()
.onEach {
// sessionDataSource could already return a session while activeSession holder still returns null
it.orNull()?.let { session ->
setCurrentSpace(uiStateRepository.getSelectedSpace(session.sessionId), session)
observeSyncStatus(session)
}
}
.launchIn(coroutineScope)
}
private fun observeSyncStatus(session: Session) {
session.syncService().getSyncRequestStateLive()
.asFlow()
.filterIsInstance<SyncRequestState.IncrementalSyncDone>()
.map { session.spaceService().getRootSpaceSummaries().size }
.distinctUntilChanged()
.onEach { spacesNumber ->
analyticsTracker.updateUserProperties(UserProperties(numSpaces = spacesNumber))
}.launchIn(session.coroutineScope)
}
override fun getSpaceBackstack() = spaceBackstack
override fun getSelectedSpaceFlow() = selectedSpaceFlow
override fun getSafeActiveSpaceId(): String? {
return selectedSpaceDataSource.currentValue?.orNull()?.roomId
}
override fun onResume(owner: LifecycleOwner) {
observeActiveSession()
}
override fun onPause(owner: LifecycleOwner) {
coroutineScope.coroutineContext.cancelChildren()
val session = activeSessionHolder.getSafeActiveSession() ?: return
uiStateRepository.storeSelectedSpace(selectedSpaceDataSource.currentValue?.orNull()?.roomId, session.sessionId)
}
}

View File

@ -207,7 +207,7 @@ class HomeDetailViewModel @AssistedInject constructor(
}
private fun observeRoomGroupingMethod() {
appStateHandler.selectedSpaceFlow
appStateHandler.getSelectedSpaceFlow()
.setOnEach {
copy(
selectedSpace = it.orNull()
@ -216,7 +216,7 @@ class HomeDetailViewModel @AssistedInject constructor(
}
private fun observeRoomSummaries() {
appStateHandler.selectedSpaceFlow.distinctUntilChanged().flatMapLatest {
appStateHandler.getSelectedSpaceFlow().distinctUntilChanged().flatMapLatest {
// we use it as a trigger to all changes in room, but do not really load
// the actual models
session.roomService().getPagedRoomSummariesLive(

View File

@ -359,7 +359,7 @@ class RoomListSectionBuilder(
query: (RoomSummaryQueryParams.Builder) -> Unit
) {
withQueryParams(query) { roomQueryParams ->
val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
val updatedQueryParams = roomQueryParams.process(spaceFilterStrategy, appStateHandler.getSafeActiveSpaceId())
val liveQueryParams = MutableStateFlow(updatedQueryParams)
val itemCountFlow = liveQueryParams
.flatMapLatest {
@ -370,7 +370,7 @@ class RoomListSectionBuilder(
val name = stringProvider.getString(nameRes)
val filteredPagedRoomSummariesLive = session.roomService().getFilteredPagedRoomSummariesLive(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId()),
roomQueryParams.process(spaceFilterStrategy, appStateHandler.getSafeActiveSpaceId()),
pagedListConfig
)
when (spaceFilterStrategy) {
@ -417,7 +417,7 @@ class RoomListSectionBuilder(
RoomAggregateNotificationCount(it.size, it.size)
} else {
session.roomService().getNotificationCountForRooms(
roomQueryParams.process(spaceFilterStrategy, appStateHandler.safeActiveSpaceId())
roomQueryParams.process(spaceFilterStrategy, appStateHandler.getSafeActiveSpaceId())
)
}
)

View File

@ -148,7 +148,7 @@ class RoomListViewModel @AssistedInject constructor(
private val roomListSectionBuilder = RoomListSectionBuilder(
session,
stringProvider,
appStateHandler,
appStateHandlerImpl,
viewModelScope,
autoAcceptInvites,
{

View File

@ -73,7 +73,7 @@ class CreateRoomViewModel @AssistedInject constructor(
initHomeServerName()
initAdminE2eByDefault()
val parentSpaceId = initialState.parentSpaceId ?: appStateHandler.safeActiveSpaceId()
val parentSpaceId = initialState.parentSpaceId ?: appStateHandler.getSafeActiveSpaceId()
val restrictedSupport = session.homeServerCapabilitiesService().getHomeServerCapabilities()
.isFeatureSupported(HomeServerCapabilities.ROOM_CAP_RESTRICTED)

View File

@ -85,8 +85,7 @@ class SpaceListViewModel @AssistedInject constructor(
}
observeSpaceSummaries()
// observeSelectionState()
appStateHandler.selectedSpaceFlow
appStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.setOnEach { selectedSpaceOption ->
copy(

View File

@ -73,7 +73,7 @@ class SpaceMenuViewModel @AssistedInject constructor(
it.getOrNull()?.let {
if (it.membership == Membership.LEAVE) {
setState { copy(leavingState = Success(Unit)) }
if (appStateHandler.safeActiveSpaceId() == initialState.spaceId) {
if (appStateHandler.getSafeActiveSpaceId() == initialState.spaceId) {
// switch to home?
appStateHandler.setCurrentSpace(null, session)
}

View File

@ -75,19 +75,19 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
}
setState { copy(spaceSummary = spaceSummary) }
session.getRoom(initialState.spaceId)?.let { room ->
room.flow().liveRoomSummary()
.unwrap()
.onEach {
if (it.membership == Membership.LEAVE) {
setState { copy(leaveState = Success(Unit)) }
if (appStateHandler.safeActiveSpaceId() == initialState.spaceId) {
// switch to home?
appStateHandler.setCurrentSpace(null, session)
}
session.getRoom(initialState.spaceId)
?.flow()
?.liveRoomSummary()
?.unwrap()
?.onEach {
if (it.membership == Membership.LEAVE) {
setState { copy(leaveState = Success(Unit)) }
if (appStateHandler.getSafeActiveSpaceId() == initialState.spaceId) {
// switch to home?
appStateHandler.setCurrentSpace(null, session)
}
}.launchIn(viewModelScope)
}
}
}?.launchIn(viewModelScope)
viewModelScope.launch {
val children = session.roomService().getRoomSummaries(

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.ui.UiStateRepository
import io.mockk.mockk
internal class AppStateHandlerTest {
private val sessionDataSource: ActiveSessionDataSource = mockk()
private val uiStateRepository: UiStateRepository = mockk()
private val activeSessionHolder: ActiveSessionHolder = mockk()
private val analyticsTracker: AnalyticsTracker = mockk()
private val appStateHandlerImpl = AppStateHandlerImpl(
sessionDataSource,
uiStateRepository,
activeSessionHolder,
analyticsTracker,
)
}