Code review fixes.

This commit is contained in:
Onuray Sahin 2022-09-15 15:14:46 +03:00
parent 2763ebdd5a
commit 81cc8ab98b
19 changed files with 391 additions and 84 deletions

View file

@ -1 +0,0 @@
[Devices Management] Refactor some code to improve testability

View file

@ -3265,6 +3265,32 @@
<string name="device_manager_session_title">Session</string>
<!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM -->
<string name="device_manager_session_last_activity">Last activity %1$s</string>
<string name="device_manager_filter_bottom_sheet_title">Filter</string>
<string name="device_manager_filter_option_all_sessions">All sessions</string>
<string name="device_manager_filter_option_verified">Verified</string>
<string name="device_manager_filter_option_verified_description">Ready for secure messaging</string>
<string name="device_manager_filter_option_unverified">Unverified</string>
<string name="device_manager_filter_option_unverified_description">Not ready for secure messaging</string>
<string name="device_manager_filter_option_inactive">Inactive</string>
<plurals name="device_manager_filter_option_inactive_description">
<item quantity="one">Inactive for %1$d day or longer</item>
<item quantity="other">Inactive for %1$d days or longer</item>
</plurals>
<string name="a11y_device_manager_filter">Filter</string>
<string name="device_manager_other_sessions_recommendation_title_verified">Verified</string>
<string name="device_manager_other_sessions_recommendation_description_verified">For best security, sign out from any session that you dont recognize or use anymore.</string>
<string name="device_manager_other_sessions_recommendation_title_unverified">Unverified</string>
<string name="device_manager_other_sessions_recommendation_description_unverified">Verify your sessions for enhanced secure messaging or sign out from those you dont recognize or use anymore.</string>
<string name="device_manager_other_sessions_recommendation_title_inactive">Inactive</string>
<plurals name="device_manager_other_sessions_recommendation_description_inactive">
<item quantity="one">Consider signing out from old sessions (%1$d day or more) you dont use anymore.</item>
<item quantity="other">Consider signing out from old sessions (%1$d days or more) you dont use anymore.</item>
</plurals>
<string name="device_manager_other_sessions_no_verified_sessions_found">No verified sessions found.</string>
<string name="device_manager_other_sessions_no_unverified_sessions_found">No unverified sessions found.</string>
<string name="device_manager_other_sessions_no_inactive_sessions_found">No inactive sessions found.</string>
<string name="device_manager_other_sessions_clear_filter">Clear Filter</string>
<!-- Note to translators: %s will be replaces with selected space name -->
<string name="home_empty_space_no_rooms_title">%s\nis looking a little empty.</string>
<!-- Note to translators: for RTL languages, Spaces will be at the bottom left. Please translate "bottom-left" instead of "bottom-right". Thanks!-->
@ -3286,31 +3312,4 @@
<string name="onboarding_new_app_layout_feedback_message">Tap top right to see the option to feedback.</string>
<string name="onboarding_new_app_layout_button_try">Try it out</string>
<string name="device_manager_filter_bottom_sheet_title">Filter</string>
<string name="device_manager_filter_option_all_sessions">All session</string>
<string name="device_manager_filter_option_verified">Verified</string>
<string name="device_manager_filter_option_verified_description">Ready for secure messaging</string>
<string name="device_manager_filter_option_unverified">Unverified</string>
<string name="device_manager_filter_option_unverified_description">Not ready for secure messaging</string>
<string name="device_manager_filter_option_inactive">Inactive</string>
<plurals name="device_manager_filter_option_inactive_description">
<item quantity="one">Inactive for %1$d day or longer</item>
<item quantity="other">Inactive for %1$d days or longer</item>
</plurals>
<string name="device_manager_other_sessions_title">Other sessions</string>
<string name="a11y_device_manager_filter">Filter</string>
<string name="device_manager_other_sessions_recommendation_title_verified">Verified</string>
<string name="device_manager_other_sessions_recommendation_description_verified">For best security, sign out from any session that you dont recognize or use anymore.</string>
<string name="device_manager_other_sessions_recommendation_title_unverified">Unverified</string>
<string name="device_manager_other_sessions_recommendation_description_unverified">Verify your sessions for enhanced secure messaging or sign out from those you dont recognize or use anymore.</string>
<string name="device_manager_other_sessions_recommendation_title_inactive">Inactive</string>
<plurals name="device_manager_other_sessions_recommendation_description_inactive">
<item quantity="one">Consider signing out from old sessions (%1$d day or more) you dont use anymore.</item>
<item quantity="other">Consider signing out from old sessions (%1$d days or more) you dont use anymore.</item>
</plurals>
<string name="device_manager_other_sessions_no_verified_sessions_found">No verified sessions found.</string>
<string name="device_manager_other_sessions_no_unverified_sessions_found">No unverified sessions found.</string>
<string name="device_manager_other_sessions_no_inactive_sessions_found">No inactive sessions found.</string>
<string name="device_manager_other_sessions_clear_filter">Clear Filter</string>
</resources>

View file

@ -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<*, *>
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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<DeviceFullInfo>?.filteredDevices(): List<DeviceFullInfo>? {
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

View file

@ -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<List<DeviceFullInfo>> {
fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow<List<DeviceFullInfo>> {
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()

View file

@ -50,7 +50,7 @@ class DeviceManagerFilterBottomSheet : VectorBaseBottomSheetDialogFragment<Botto
}
private fun initFilterRadioGroup() {
views.filterOptionInactiveRadioButtonDescription.text = resources.getQuantityString(
views.filterOptionInactiveTextView.text = resources.getQuantityString(
R.plurals.device_manager_filter_option_inactive_description,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
@ -64,6 +64,16 @@ class DeviceManagerFilterBottomSheet : VectorBaseBottomSheetDialogFragment<Botto
}
views.filterOptionsRadioGroup.check(radioButtonId)
views.filterOptionVerifiedTextView.debouncedClicks {
views.filterOptionsRadioGroup.check(R.id.filterOptionVerifiedRadioButton)
}
views.filterOptionUnverifiedTextView.debouncedClicks {
views.filterOptionsRadioGroup.check(R.id.filterOptionUnverifiedRadioButton)
}
views.filterOptionInactiveTextView.debouncedClicks {
views.filterOptionsRadioGroup.check(R.id.filterOptionInactiveRadioButton)
}
views.filterOptionsRadioGroup.setOnCheckedChangeListener { _, checkedId ->
onFilterTypeChanged(checkedId)
}

View file

@ -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<DeviceFullInfo>,
filterType: DeviceManagerFilterType,
excludedDeviceIds: List<String> = emptyList(),
): List<DeviceFullInfo> {
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 }
}
}

View file

@ -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<DeviceFullInfo>, 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()

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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
) {

View file

@ -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()
}

View file

@ -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<OtherSessionsViewState, OtherSessionsAction, OtherSessionsViewEvents>(initialState), VerificationService.Listener {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<OtherSessionsViewModel, OtherSessionsViewState> {
override fun create(initialState: OtherSessionsViewState): OtherSessionsViewModel
}
companion object : MavericksViewModelFactory<OtherSessionsViewModel, OtherSessionsViewState> by hiltMavericksViewModelFactory()
private var observeDevicesJob: Job? = null
private val refreshSource = PublishDataSource<Unit>()
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)
}
}

View file

@ -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))
}
}

View file

@ -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<List<DeviceFullInfo>> = Uninitialized,
val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS,
) : MavericksState

View file

@ -47,6 +47,7 @@
android:text="@string/device_manager_filter_option_verified" />
<TextView
android:id="@+id/filterOptionVerifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -63,6 +64,7 @@
android:text="@string/device_manager_filter_option_unverified" />
<TextView
android:id="@+id/filterOptionUnverifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -79,7 +81,7 @@
android:text="@string/device_manager_filter_option_inactive" />
<TextView
android:id="@+id/filterOptionInactiveRadioButtonDescription"
android:id="@+id/filterOptionInactiveTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -18,7 +18,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_back_24dp"
app:title="@string/device_manager_other_sessions_title">
app:title="@string/settings_sessions_other_title">
<FrameLayout
android:id="@+id/otherSessionsFilterFrameLayout"