Refactors AppStateHandler into interface implementation pattern
This commit is contained in:
parent
4dccff4d78
commit
e4c8c88cee
@ -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?
|
||||
}
|
||||
|
149
vector/src/main/java/im/vector/app/AppStateHandlerImpl.kt
Normal file
149
vector/src/main/java/im/vector/app/AppStateHandlerImpl.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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())
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -148,7 +148,7 @@ class RoomListViewModel @AssistedInject constructor(
|
||||
private val roomListSectionBuilder = RoomListSectionBuilder(
|
||||
session,
|
||||
stringProvider,
|
||||
appStateHandler,
|
||||
appStateHandlerImpl,
|
||||
viewModelScope,
|
||||
autoAcceptInvites,
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -85,8 +85,7 @@ class SpaceListViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
observeSpaceSummaries()
|
||||
// observeSelectionState()
|
||||
appStateHandler.selectedSpaceFlow
|
||||
appStateHandler.getSelectedSpaceFlow()
|
||||
.distinctUntilChanged()
|
||||
.setOnEach { selectedSpaceOption ->
|
||||
copy(
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(
|
||||
|
40
vector/src/test/java/im/vector/app/AppStateHandlerTest.kt
Normal file
40
vector/src/test/java/im/vector/app/AppStateHandlerTest.kt
Normal 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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user