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">