diff --git a/changelog.d/7043.wip b/changelog.d/7043.wip deleted file mode 100644 index 3c9b7731bf..0000000000 --- a/changelog.d/7043.wip +++ /dev/null @@ -1 +0,0 @@ -[Devices Management] Refactor some code to improve testability diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index cff1aaea1b..2731ba8837 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3265,6 +3265,32 @@ Session Last activity %1$s + Filter + All sessions + Verified + Ready for secure messaging + Unverified + Not ready for secure messaging + Inactive + + Inactive for %1$d day or longer + Inactive for %1$d days or longer + + Filter + Verified + For best security, sign out from any session that you don’t recognize or use anymore. + Unverified + Verify your sessions for enhanced secure messaging or sign out from those you don’t recognize or use anymore. + Inactive + + Consider signing out from old sessions (%1$d day or more) you don’t use anymore. + Consider signing out from old sessions (%1$d days or more) you don’t use anymore. + + No verified sessions found. + No unverified sessions found. + No inactive sessions found. + Clear Filter + %s\nis looking a little empty. @@ -3286,31 +3312,4 @@ Tap top right to see the option to feedback. Try it out - Filter - All session - Verified - Ready for secure messaging - Unverified - Not ready for secure messaging - Inactive - - Inactive for %1$d day or longer - Inactive for %1$d days or longer - - Other sessions - Filter - Verified - For best security, sign out from any session that you don’t recognize or use anymore. - Unverified - Verify your sessions for enhanced secure messaging or sign out from those you don’t recognize or use anymore. - Inactive - - Consider signing out from old sessions (%1$d day or more) you don’t use anymore. - Consider signing out from old sessions (%1$d days or more) you don’t use anymore. - - No verified sessions found. - No unverified sessions found. - No inactive sessions found. - Clear Filter - diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 8bcfd4e422..21016077a1 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -88,6 +88,7 @@ import im.vector.app.features.settings.account.deactivation.DeactivateAccountVie import im.vector.app.features.settings.crosssigning.CrossSigningSettingsViewModel import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheetViewModel import im.vector.app.features.settings.devices.DevicesViewModel +import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewModel import im.vector.app.features.settings.devices.v2.overview.SessionOverviewViewModel import im.vector.app.features.settings.devtools.AccountDataViewModel import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel @@ -641,4 +642,9 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(SessionOverviewViewModel::class) fun sessionOverviewViewModelFactory(factory: SessionOverviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(OtherSessionsViewModel::class) + fun otherSessionsViewModelFactory(factory: OtherSessionsViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt index 3c459ca992..8c7718bfcf 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt @@ -17,10 +17,8 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.platform.VectorViewModelAction -import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() - data class FilterDevices(val filterType: DeviceManagerFilterType) : DevicesAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index 4bdadda815..99afc33a8a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -26,6 +26,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.utils.PublishDataSource +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.lib.core.utils.flow.throttleFirst import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -94,7 +95,10 @@ class DevicesViewModel @AssistedInject constructor( } private fun observeDevices() { - getDeviceFullInfoListUseCase.execute() + getDeviceFullInfoListUseCase.execute( + filterType = DeviceManagerFilterType.ALL_SESSIONS, + excludeCurrentDevice = false + ) .execute { async -> if (async is Success) { val deviceFullInfoList = async.invoke() @@ -144,19 +148,9 @@ class DevicesViewModel @AssistedInject constructor( override fun handle(action: DevicesAction) { when (action) { is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() - is DevicesAction.FilterDevices -> handleFilterDevices(action) } } - private fun handleFilterDevices(action: DevicesAction.FilterDevices) { - setState { - copy( - currentFilter = action.filterType - ) - } - queryRefreshDevicesList() - } - private fun handleMarkAsManuallyVerifiedAction() { // TODO implement when needed } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt index 5ca3e71a06..3fc061daa4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewState.kt @@ -19,8 +19,6 @@ package im.vector.app.features.settings.devices.v2 import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized -import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType -import org.matrix.android.sdk.api.extensions.orFalse data class DevicesViewState( val currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo = CurrentSessionCrossSigningInfo(), @@ -28,17 +26,4 @@ data class DevicesViewState( val unverifiedSessionsCount: Int = 0, val inactiveSessionsCount: Int = 0, val isLoading: Boolean = false, - val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS, -) : MavericksState { - - fun List?.filteredDevices(): List? { - return this?.filter { - when (currentFilter) { - DeviceManagerFilterType.ALL_SESSIONS -> true - DeviceManagerFilterType.VERIFIED -> it.cryptoDeviceInfo?.isVerified.orFalse() - DeviceManagerFilterType.UNVERIFIED -> !it.cryptoDeviceInfo?.isVerified.orFalse() - DeviceManagerFilterType.INACTIVE -> it.isInactive - } - } - } -} +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt index da2cf25f39..3c0d3a5e56 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/GetDeviceFullInfoListUseCase.kt @@ -17,6 +17,8 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -32,16 +34,23 @@ class GetDeviceFullInfoListUseCase @Inject constructor( private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, + private val filterDevicesUseCase: FilterDevicesUseCase, ) { - fun execute(): Flow> { + fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow> { return activeSessionHolder.getSafeActiveSession()?.let { session -> val deviceFullInfoFlow = combine( getCurrentSessionCrossSigningInfoUseCase.execute(), session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo() ) { currentSessionCrossSigningInfo, cryptoList, infoList -> - convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList) + val deviceFullInfoList = convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList) + val excludedDeviceIds = if (excludeCurrentDevice) { + listOf(currentSessionCrossSigningInfo.deviceId) + } else { + emptyList() + } + filterDevicesUseCase.execute(deviceFullInfoList, filterType, excludedDeviceIds) } deviceFullInfoFlow.distinctUntilChanged() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterBottomSheet.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterBottomSheet.kt index 4ab5acd496..28c7045a82 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/DeviceManagerFilterBottomSheet.kt @@ -50,7 +50,7 @@ class DeviceManagerFilterBottomSheet : VectorBaseBottomSheetDialogFragment onFilterTypeChanged(checkedId) } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt new file mode 100644 index 0000000000..e0bb567dc6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/filter/FilterDevicesUseCase.kt @@ -0,0 +1,41 @@ +/* + * 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.features.settings.devices.v2.filter + +import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import org.matrix.android.sdk.api.extensions.orFalse +import javax.inject.Inject + +class FilterDevicesUseCase @Inject constructor() { + + fun execute( + devices: List, + filterType: DeviceManagerFilterType, + excludedDeviceIds: List = emptyList(), + ): List { + return devices + .filter { + when (filterType) { + DeviceManagerFilterType.ALL_SESSIONS -> true + DeviceManagerFilterType.VERIFIED -> it.cryptoDeviceInfo?.isVerified.orFalse() + DeviceManagerFilterType.UNVERIFIED -> !it.cryptoDeviceInfo?.isVerified.orFalse() + DeviceManagerFilterType.INACTIVE -> it.isInactive + } + } + .filter { it.deviceInfo.deviceId !in excludedDeviceIds } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt index 8e8de69fac..6f6956c885 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt @@ -20,9 +20,12 @@ import android.content.Context import android.util.AttributeSet import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.OnModelBuildFinishedListener import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.databinding.ViewOtherSessionsBinding @@ -44,18 +47,32 @@ class OtherSessionsView @JvmOverloads constructor( @Inject lateinit var otherSessionsController: OtherSessionsController private val views: ViewOtherSessionsBinding - private val recyclerViewDataObserver: RecyclerView.AdapterDataObserver + private lateinit var recyclerViewDataObserver: RecyclerView.AdapterDataObserver + private lateinit var stateRestorer: LayoutManagerStateRestorer + private var modelBuildListener: OnModelBuildFinishedListener? = null + var callback: Callback? = null init { inflate(context, R.layout.view_other_sessions, this) views = ViewOtherSessionsBinding.bind(this) - otherSessionsController.callback = this + configureOtherSessionsRecyclerView() views.otherSessionsViewAllButton.setOnClickListener { callback?.onViewAllOtherSessionsClicked() } + } + + private fun configureOtherSessionsRecyclerView() { + views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = false) + + val layoutManager = LinearLayoutManager(context) + stateRestorer = LayoutManagerStateRestorer(layoutManager) + views.otherSessionsRecyclerView.layoutManager = layoutManager + layoutManager.recycleChildrenOnDetach = true + modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } + otherSessionsController.addModelBuildListener(modelBuildListener) recyclerViewDataObserver = object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { @@ -64,10 +81,11 @@ class OtherSessionsView @JvmOverloads constructor( } } otherSessionsController.adapter.registerAdapterDataObserver(recyclerViewDataObserver) + + otherSessionsController.callback = this } fun render(devices: List, totalNumberOfDevices: Int, showViewAll: Boolean) { - views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = true) if (showViewAll) { views.otherSessionsViewAllButton.isVisible = true views.otherSessionsViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, totalNumberOfDevices) @@ -78,6 +96,8 @@ class OtherSessionsView @JvmOverloads constructor( } override fun onDetachedFromWindow() { + otherSessionsController.removeModelBuildListener(modelBuildListener) + modelBuildListener = null otherSessionsController.callback = null otherSessionsController.adapter.unregisterAdapterDataObserver(recyclerViewDataObserver) views.otherSessionsRecyclerView.cleanup() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt new file mode 100644 index 0000000000..7164ecc866 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt @@ -0,0 +1,24 @@ +/* + * 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.features.settings.devices.v2.othersessions + +import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType + +sealed class OtherSessionsAction : VectorViewModelAction { + data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index b582d7952c..81ea5f4b89 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -32,9 +32,6 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.FragmentOtherSessionsBinding import im.vector.app.features.settings.devices.v2.DeviceFullInfo -import im.vector.app.features.settings.devices.v2.DevicesAction -import im.vector.app.features.settings.devices.v2.DevicesViewModel -import im.vector.app.features.settings.devices.v2.VectorSettingsDevicesViewNavigator import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.list.OtherSessionsView @@ -48,9 +45,11 @@ class OtherSessionsFragment : VectorBaseBottomSheetDialogFragment.ResultListener, OtherSessionsView.Callback { - private val viewModel: DevicesViewModel by fragmentViewModel() + private val viewModel: OtherSessionsViewModel by fragmentViewModel() + @Inject lateinit var colorProvider: ColorProvider - @Inject lateinit var viewNavigator: VectorSettingsDevicesViewNavigator + + @Inject lateinit var viewNavigator: OtherSessionsViewNavigator override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding { return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false) @@ -59,9 +58,19 @@ class OtherSessionsFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupToolbar(views.otherSessionsToolbar).allowBack() + observeViewEvents() initFilterView() } + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + is OtherSessionsViewEvents.Loading -> showLoading(it.message) + is OtherSessionsViewEvents.Failure -> showFailure(it.throwable) + } + } + } + private fun initFilterView() { views.otherSessionsFilterFrameLayout.debouncedClicks { withState(viewModel) { state -> @@ -72,7 +81,7 @@ class OtherSessionsFragment : } views.otherSessionsClearFilterButton.debouncedClicks { - viewModel.handle(DevicesAction.FilterDevices(DeviceManagerFilterType.ALL_SESSIONS)) + viewModel.handle(OtherSessionsAction.FilterDevices(DeviceManagerFilterType.ALL_SESSIONS)) } views.deviceListOtherSessions.callback = this @@ -80,18 +89,13 @@ class OtherSessionsFragment : override fun onBottomSheetResult(resultCode: Int, data: Any?) { if (resultCode == RESULT_OK && data != null && data is DeviceManagerFilterType) { - viewModel.handle(DevicesAction.FilterDevices(data)) + viewModel.handle(OtherSessionsAction.FilterDevices(data)) } } override fun invalidate() = withState(viewModel) { state -> if (state.devices is Success) { - with(state) { - val devices = state.devices() - ?.filter { it.deviceInfo.deviceId != state.currentSessionCrossSigningInfo.deviceId } - ?.filteredDevices() - renderDevices(devices, state.currentFilter) - } + renderDevices(state.devices(), state.currentFilter) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt index c72dc30a93..5a7d1fa910 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt @@ -28,7 +28,7 @@ import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.databinding.ViewOtherSessionSecurityRecommendationBinding @AndroidEntryPoint -class OtherSessionsSecurityRecommendationView @JvmOverloads constructor( +class OtherSessionsSecurityRecommendationView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -84,13 +84,14 @@ class OtherSessionsSecurityRecommendationView @JvmOverloads constructor( private fun setDescription(description: String?) { val learnMore = context.getString(R.string.action_learn_more) - val stringBuilder = StringBuilder() - stringBuilder.append(description) - stringBuilder.append(" ") - stringBuilder.append(learnMore) + val formattedDescription = buildString { + append(description) + append(" ") + append(learnMore) + } views.recommendationDescriptionTextView.setTextWithColoredPart( - fullText = stringBuilder.toString(), + fullText = formattedDescription, coloredPart = learnMore, underline = false ) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt new file mode 100644 index 0000000000..95f9c72b33 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewEvents.kt @@ -0,0 +1,24 @@ +/* + * 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.features.settings.devices.v2.othersessions + +import im.vector.app.core.platform.VectorViewEvents + +sealed class OtherSessionsViewEvents : VectorViewEvents { + data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents() + data class Failure(val throwable: Throwable) : OtherSessionsViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt new file mode 100644 index 0000000000..4a7f911ff4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModel.kt @@ -0,0 +1,135 @@ +/* + * 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.features.settings.devices.v2.othersessions + +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.Success +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.utils.PublishDataSource +import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase +import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.lib.core.utils.flow.throttleFirst +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.matrix.android.sdk.api.session.crypto.verification.VerificationService +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction +import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState +import kotlin.time.Duration.Companion.seconds + +class OtherSessionsViewModel @AssistedInject constructor( + @Assisted initialState: OtherSessionsViewState, + private val activeSessionHolder: ActiveSessionHolder, + private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, + private val refreshDevicesUseCase: RefreshDevicesUseCase, +) : VectorViewModel(initialState), VerificationService.Listener { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: OtherSessionsViewState): OtherSessionsViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + private var observeDevicesJob: Job? = null + + private val refreshSource = PublishDataSource() + private val refreshThrottleDelayMs = 4.seconds.inWholeMilliseconds + + init { + observeDevices(initialState.currentFilter) + addVerificationListener() + observeRefreshSource() + } + + override fun onCleared() { + removeVerificationListener() + super.onCleared() + } + + private fun observeDevices(currentFilter: DeviceManagerFilterType) { + observeDevicesJob?.cancel() + observeDevicesJob = getDeviceFullInfoListUseCase.execute( + filterType = currentFilter, + excludeCurrentDevice = true + ) + .execute { async -> + if (async is Success) { + copy( + devices = async, + ) + } else { + copy( + devices = async + ) + } + } + } + + private fun addVerificationListener() { + activeSessionHolder.getSafeActiveSession() + ?.cryptoService() + ?.verificationService() + ?.addListener(this) + } + + private fun removeVerificationListener() { + activeSessionHolder.getSafeActiveSession() + ?.cryptoService() + ?.verificationService() + ?.removeListener(this) + } + + private fun observeRefreshSource() { + refreshSource.stream() + .throttleFirst(refreshThrottleDelayMs) + .onEach { refreshDevicesUseCase.execute() } + .launchIn(viewModelScope) + } + + override fun transactionUpdated(tx: VerificationTransaction) { + if (tx.state == VerificationTxState.Verified) { + queryRefreshDevicesList() + } + } + + private fun queryRefreshDevicesList() { + refreshSource.post(Unit) + } + + override fun handle(action: OtherSessionsAction) { + when (action) { + is OtherSessionsAction.FilterDevices -> handleFilterDevices(action) + } + } + + private fun handleFilterDevices(action: OtherSessionsAction.FilterDevices) { + setState { + copy( + currentFilter = action.filterType + ) + } + observeDevices(action.filterType) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigator.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigator.kt new file mode 100644 index 0000000000..ef1895d0ae --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewNavigator.kt @@ -0,0 +1,28 @@ +/* + * 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.features.settings.devices.v2.othersessions + +import android.content.Context +import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity +import javax.inject.Inject + +class OtherSessionsViewNavigator @Inject constructor() { + + fun navigateToSessionOverview(context: Context, deviceId: String) { + context.startActivity(SessionOverviewActivity.newIntent(context, deviceId)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt new file mode 100644 index 0000000000..d03cba03f9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewState.kt @@ -0,0 +1,28 @@ +/* + * 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.features.settings.devices.v2.othersessions + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.Uninitialized +import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType + +data class OtherSessionsViewState( + val devices: Async> = Uninitialized, + val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS, +) : MavericksState diff --git a/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml index 73e1971820..a7987e70b5 100644 --- a/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml +++ b/vector/src/main/res/layout/bottom_sheet_device_manager_filter.xml @@ -47,6 +47,7 @@ android:text="@string/device_manager_filter_option_verified" /> + app:title="@string/settings_sessions_other_title">