From e4c8c88cee73c38c82d1db71151b5e9b0648e330 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Fri, 8 Jul 2022 10:05:05 +0100 Subject: [PATCH] Refactors AppStateHandler into interface implementation pattern --- .../java/im/vector/app/AppStateHandler.kt | 127 ++------------- .../java/im/vector/app/AppStateHandlerImpl.kt | 149 ++++++++++++++++++ .../app/features/home/HomeDetailViewModel.kt | 4 +- .../home/room/list/RoomListSectionBuilder.kt | 6 +- .../home/room/list/RoomListViewModel.kt | 2 +- .../createroom/CreateRoomViewModel.kt | 2 +- .../app/features/spaces/SpaceListViewModel.kt | 3 +- .../app/features/spaces/SpaceMenuViewModel.kt | 2 +- .../leave/SpaceLeaveAdvancedViewModel.kt | 24 +-- .../java/im/vector/app/AppStateHandlerTest.kt | 40 +++++ 10 files changed, 219 insertions(+), 140 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/AppStateHandlerImpl.kt create mode 100644 vector/src/test/java/im/vector/app/AppStateHandlerTest.kt diff --git a/vector/src/main/java/im/vector/app/AppStateHandler.kt b/vector/src/main/java/im/vector/app/AppStateHandler.kt index a4e10f776e..ebc9d36c1d 100644 --- a/vector/src/main/java/im/vector/app/AppStateHandler.kt +++ b/vector/src/main/java/im/vector/app/AppStateHandler.kt @@ -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.empty()) - - val selectedSpaceFlow = selectedSpaceDataSource.stream() - - private val spaceBackstack = ArrayDeque() - - 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 - if (isForwardNavigation) { - spaceBackstack.addLast(currentSpace?.roomId) - } + fun getSelectedSpaceFlow(): Flow> - 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() - .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? } diff --git a/vector/src/main/java/im/vector/app/AppStateHandlerImpl.kt b/vector/src/main/java/im/vector/app/AppStateHandlerImpl.kt new file mode 100644 index 0000000000..55a2a1a8bb --- /dev/null +++ b/vector/src/main/java/im/vector/app/AppStateHandlerImpl.kt @@ -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.empty()) + private val selectedSpaceFlow = selectedSpaceDataSource.stream() + private val spaceBackstack = ArrayDeque() + + 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() + .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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt index 357d2b7c69..f16df17ead 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewModel.kt @@ -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( diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt index 42634c237a..3e9498891a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListSectionBuilder.kt @@ -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()) ) } ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 44d61f9ed2..6a4d46dd05 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -148,7 +148,7 @@ class RoomListViewModel @AssistedInject constructor( private val roomListSectionBuilder = RoomListSectionBuilder( session, stringProvider, - appStateHandler, + appStateHandlerImpl, viewModelScope, autoAcceptInvites, { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index e068d21b36..121007cf2a 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt index 3b2fb31b74..a72c6cc0c2 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListViewModel.kt @@ -85,8 +85,7 @@ class SpaceListViewModel @AssistedInject constructor( } observeSpaceSummaries() -// observeSelectionState() - appStateHandler.selectedSpaceFlow + appStateHandler.getSelectedSpaceFlow() .distinctUntilChanged() .setOnEach { selectedSpaceOption -> copy( diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt index 7b53b09187..e50fa5540c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt index 7413386709..4b6657fc47 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt @@ -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( diff --git a/vector/src/test/java/im/vector/app/AppStateHandlerTest.kt b/vector/src/test/java/im/vector/app/AppStateHandlerTest.kt new file mode 100644 index 0000000000..ac839ac806 --- /dev/null +++ b/vector/src/test/java/im/vector/app/AppStateHandlerTest.kt @@ -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, + ) + + + +}