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> <string name="device_manager_session_title">Session</string>
<!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM --> <!-- 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_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 --> <!-- 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> <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!--> <!-- 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_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="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> </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.crosssigning.CrossSigningSettingsViewModel
import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheetViewModel import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheetViewModel
import im.vector.app.features.settings.devices.DevicesViewModel 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.devices.v2.overview.SessionOverviewViewModel
import im.vector.app.features.settings.devtools.AccountDataViewModel import im.vector.app.features.settings.devtools.AccountDataViewModel
import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel
@ -641,4 +642,9 @@ interface MavericksViewModelModule {
@IntoMap @IntoMap
@MavericksViewModelKey(SessionOverviewViewModel::class) @MavericksViewModelKey(SessionOverviewViewModel::class)
fun sessionOverviewViewModelFactory(factory: SessionOverviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *> 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 package im.vector.app.features.settings.devices.v2
import im.vector.app.core.platform.VectorViewModelAction 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 import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
sealed class DevicesAction : VectorViewModelAction { sealed class DevicesAction : VectorViewModelAction {
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() 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.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.PublishDataSource 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 im.vector.lib.core.utils.flow.throttleFirst
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -94,7 +95,10 @@ class DevicesViewModel @AssistedInject constructor(
} }
private fun observeDevices() { private fun observeDevices() {
getDeviceFullInfoListUseCase.execute() getDeviceFullInfoListUseCase.execute(
filterType = DeviceManagerFilterType.ALL_SESSIONS,
excludeCurrentDevice = false
)
.execute { async -> .execute { async ->
if (async is Success) { if (async is Success) {
val deviceFullInfoList = async.invoke() val deviceFullInfoList = async.invoke()
@ -144,19 +148,9 @@ class DevicesViewModel @AssistedInject constructor(
override fun handle(action: DevicesAction) { override fun handle(action: DevicesAction) {
when (action) { when (action) {
is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction()
is DevicesAction.FilterDevices -> handleFilterDevices(action)
} }
} }
private fun handleFilterDevices(action: DevicesAction.FilterDevices) {
setState {
copy(
currentFilter = action.filterType
)
}
queryRefreshDevicesList()
}
private fun handleMarkAsManuallyVerifiedAction() { private fun handleMarkAsManuallyVerifiedAction() {
// TODO implement when needed // 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.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized 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( data class DevicesViewState(
val currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo = CurrentSessionCrossSigningInfo(), val currentSessionCrossSigningInfo: CurrentSessionCrossSigningInfo = CurrentSessionCrossSigningInfo(),
@ -28,17 +26,4 @@ data class DevicesViewState(
val unverifiedSessionsCount: Int = 0, val unverifiedSessionsCount: Int = 0,
val inactiveSessionsCount: Int = 0, val inactiveSessionsCount: Int = 0,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS, ) : MavericksState
) : 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
}
}
}
}

View File

@ -17,6 +17,8 @@
package im.vector.app.features.settings.devices.v2 package im.vector.app.features.settings.devices.v2
import im.vector.app.core.di.ActiveSessionHolder 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 im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -32,16 +34,23 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase, private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase,
private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase, private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, 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 -> return activeSessionHolder.getSafeActiveSession()?.let { session ->
val deviceFullInfoFlow = combine( val deviceFullInfoFlow = combine(
getCurrentSessionCrossSigningInfoUseCase.execute(), getCurrentSessionCrossSigningInfoUseCase.execute(),
session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveUserCryptoDevices(session.myUserId),
session.flow().liveMyDevicesInfo() session.flow().liveMyDevicesInfo()
) { currentSessionCrossSigningInfo, cryptoList, infoList -> ) { 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() deviceFullInfoFlow.distinctUntilChanged()

View File

@ -50,7 +50,7 @@ class DeviceManagerFilterBottomSheet : VectorBaseBottomSheetDialogFragment<Botto
} }
private fun initFilterRadioGroup() { private fun initFilterRadioGroup() {
views.filterOptionInactiveRadioButtonDescription.text = resources.getQuantityString( views.filterOptionInactiveTextView.text = resources.getQuantityString(
R.plurals.device_manager_filter_option_inactive_description, R.plurals.device_manager_filter_option_inactive_description,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
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.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 -> views.filterOptionsRadioGroup.setOnCheckedChangeListener { _, checkedId ->
onFilterTypeChanged(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 android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.OnModelBuildFinishedListener
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R 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.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.databinding.ViewOtherSessionsBinding import im.vector.app.databinding.ViewOtherSessionsBinding
@ -44,18 +47,32 @@ class OtherSessionsView @JvmOverloads constructor(
@Inject lateinit var otherSessionsController: OtherSessionsController @Inject lateinit var otherSessionsController: OtherSessionsController
private val views: ViewOtherSessionsBinding 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 var callback: Callback? = null
init { init {
inflate(context, R.layout.view_other_sessions, this) inflate(context, R.layout.view_other_sessions, this)
views = ViewOtherSessionsBinding.bind(this) views = ViewOtherSessionsBinding.bind(this)
otherSessionsController.callback = this configureOtherSessionsRecyclerView()
views.otherSessionsViewAllButton.setOnClickListener { views.otherSessionsViewAllButton.setOnClickListener {
callback?.onViewAllOtherSessionsClicked() 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() { recyclerViewDataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
@ -64,10 +81,11 @@ class OtherSessionsView @JvmOverloads constructor(
} }
} }
otherSessionsController.adapter.registerAdapterDataObserver(recyclerViewDataObserver) otherSessionsController.adapter.registerAdapterDataObserver(recyclerViewDataObserver)
otherSessionsController.callback = this
} }
fun render(devices: List<DeviceFullInfo>, totalNumberOfDevices: Int, showViewAll: Boolean) { fun render(devices: List<DeviceFullInfo>, totalNumberOfDevices: Int, showViewAll: Boolean) {
views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = true)
if (showViewAll) { if (showViewAll) {
views.otherSessionsViewAllButton.isVisible = true views.otherSessionsViewAllButton.isVisible = true
views.otherSessionsViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, totalNumberOfDevices) 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() { override fun onDetachedFromWindow() {
otherSessionsController.removeModelBuildListener(modelBuildListener)
modelBuildListener = null
otherSessionsController.callback = null otherSessionsController.callback = null
otherSessionsController.adapter.unregisterAdapterDataObserver(recyclerViewDataObserver) otherSessionsController.adapter.unregisterAdapterDataObserver(recyclerViewDataObserver)
views.otherSessionsRecyclerView.cleanup() 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.core.resources.ColorProvider
import im.vector.app.databinding.FragmentOtherSessionsBinding import im.vector.app.databinding.FragmentOtherSessionsBinding
import im.vector.app.features.settings.devices.v2.DeviceFullInfo 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.DeviceManagerFilterBottomSheet
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.OtherSessionsView
@ -48,9 +45,11 @@ class OtherSessionsFragment :
VectorBaseBottomSheetDialogFragment.ResultListener, VectorBaseBottomSheetDialogFragment.ResultListener,
OtherSessionsView.Callback { OtherSessionsView.Callback {
private val viewModel: DevicesViewModel by fragmentViewModel() private val viewModel: OtherSessionsViewModel by fragmentViewModel()
@Inject lateinit var colorProvider: ColorProvider @Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var viewNavigator: VectorSettingsDevicesViewNavigator
@Inject lateinit var viewNavigator: OtherSessionsViewNavigator
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding {
return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false) return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false)
@ -59,9 +58,19 @@ class OtherSessionsFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupToolbar(views.otherSessionsToolbar).allowBack() setupToolbar(views.otherSessionsToolbar).allowBack()
observeViewEvents()
initFilterView() initFilterView()
} }
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is OtherSessionsViewEvents.Loading -> showLoading(it.message)
is OtherSessionsViewEvents.Failure -> showFailure(it.throwable)
}
}
}
private fun initFilterView() { private fun initFilterView() {
views.otherSessionsFilterFrameLayout.debouncedClicks { views.otherSessionsFilterFrameLayout.debouncedClicks {
withState(viewModel) { state -> withState(viewModel) { state ->
@ -72,7 +81,7 @@ class OtherSessionsFragment :
} }
views.otherSessionsClearFilterButton.debouncedClicks { views.otherSessionsClearFilterButton.debouncedClicks {
viewModel.handle(DevicesAction.FilterDevices(DeviceManagerFilterType.ALL_SESSIONS)) viewModel.handle(OtherSessionsAction.FilterDevices(DeviceManagerFilterType.ALL_SESSIONS))
} }
views.deviceListOtherSessions.callback = this views.deviceListOtherSessions.callback = this
@ -80,18 +89,13 @@ class OtherSessionsFragment :
override fun onBottomSheetResult(resultCode: Int, data: Any?) { override fun onBottomSheetResult(resultCode: Int, data: Any?) {
if (resultCode == RESULT_OK && data != null && data is DeviceManagerFilterType) { 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 -> override fun invalidate() = withState(viewModel) { state ->
if (state.devices is Success) { if (state.devices is Success) {
with(state) { renderDevices(state.devices(), state.currentFilter)
val devices = state.devices()
?.filter { it.deviceInfo.deviceId != state.currentSessionCrossSigningInfo.deviceId }
?.filteredDevices()
renderDevices(devices, state.currentFilter)
}
} }
} }

View File

@ -28,7 +28,7 @@ import im.vector.app.core.extensions.setTextWithColoredPart
import im.vector.app.databinding.ViewOtherSessionSecurityRecommendationBinding import im.vector.app.databinding.ViewOtherSessionSecurityRecommendationBinding
@AndroidEntryPoint @AndroidEntryPoint
class OtherSessionsSecurityRecommendationView @JvmOverloads constructor( class OtherSessionsSecurityRecommendationView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
@ -84,13 +84,14 @@ class OtherSessionsSecurityRecommendationView @JvmOverloads constructor(
private fun setDescription(description: String?) { private fun setDescription(description: String?) {
val learnMore = context.getString(R.string.action_learn_more) val learnMore = context.getString(R.string.action_learn_more)
val stringBuilder = StringBuilder() val formattedDescription = buildString {
stringBuilder.append(description) append(description)
stringBuilder.append(" ") append(" ")
stringBuilder.append(learnMore) append(learnMore)
}
views.recommendationDescriptionTextView.setTextWithColoredPart( views.recommendationDescriptionTextView.setTextWithColoredPart(
fullText = stringBuilder.toString(), fullText = formattedDescription,
coloredPart = learnMore, coloredPart = learnMore,
underline = false 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" /> android:text="@string/device_manager_filter_option_verified" />
<TextView <TextView
android:id="@+id/filterOptionVerifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -63,6 +64,7 @@
android:text="@string/device_manager_filter_option_unverified" /> android:text="@string/device_manager_filter_option_unverified" />
<TextView <TextView
android:id="@+id/filterOptionUnverifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -79,7 +81,7 @@
android:text="@string/device_manager_filter_option_inactive" /> android:text="@string/device_manager_filter_option_inactive" />
<TextView <TextView
android:id="@+id/filterOptionInactiveRadioButtonDescription" android:id="@+id/filterOptionInactiveTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement" style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

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