diff --git a/changelog.d/7546.feature b/changelog.d/7546.feature new file mode 100644 index 0000000000..94450082c9 --- /dev/null +++ b/changelog.d/7546.feature @@ -0,0 +1 @@ +[Device Manager] Toggle IP address visibility diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 0e2b2bef94..f1d5bfbcad 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3356,6 +3356,8 @@ Sign out of %1$d session Sign out of %1$d sessions + Show IP address + Hide IP address Sign out of this session Session details Application, device, and activity information. diff --git a/vector-config/src/main/res/values/config-settings.xml b/vector-config/src/main/res/values/config-settings.xml index 504c587b8d..ad9c16c214 100755 --- a/vector-config/src/main/res/values/config-settings.xml +++ b/vector-config/src/main/res/values/config-settings.xml @@ -57,5 +57,7 @@ + + false diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 9f40a7cede..447038d768 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -209,6 +209,9 @@ class VectorPreferences @Inject constructor( private const val SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG = "SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG" const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG" + // New Session Manager + const val SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS = "SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS" + // other const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY" private const val SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY" @@ -1228,4 +1231,14 @@ class VectorPreferences @Inject constructor( return vectorFeatures.isVoiceBroadcastEnabled() && defaultPrefs.getBoolean(SETTINGS_LABS_VOICE_BROADCAST_KEY, getDefault(R.bool.settings_labs_enable_voice_broadcast_default)) } + + fun showIpAddressInSessionManagerScreens(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, getDefault(R.bool.settings_session_manager_show_ip_address)) + } + + fun setIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) { + defaultPrefs.edit { + putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible) + } + } } 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 21cbb86e94..6f002359c8 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 @@ -29,4 +29,5 @@ sealed class DevicesAction : VectorViewModelAction { object VerifyCurrentSession : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() object MultiSignoutOtherSessions : DevicesAction() + object ToggleIpAddressVisibility : 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 cd97795b69..f42d5af398 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 @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2 +import android.content.SharedPreferences import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted @@ -25,6 +26,7 @@ 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.features.auth.PendingAuthHandler +import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded @@ -49,7 +51,12 @@ class DevicesViewModel @AssistedInject constructor( private val interceptSignoutFlowResponseUseCase: InterceptSignoutFlowResponseUseCase, private val pendingAuthHandler: PendingAuthHandler, refreshDevicesUseCase: RefreshDevicesUseCase, -) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase) { + private val vectorPreferences: VectorPreferences, + private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase, +) : VectorSessionsListViewModel(initialState, activeSessionHolder, refreshDevicesUseCase), + SharedPreferences.OnSharedPreferenceChangeListener { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -63,6 +70,28 @@ class DevicesViewModel @AssistedInject constructor( observeDevices() refreshDevicesOnCryptoDevicesChange() refreshDeviceList() + refreshIpAddressVisibility() + observePreferences() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + refreshIpAddressVisibility() + } + + private fun observePreferences() { + vectorPreferences.subscribeToChanges(this) + } + + override fun onCleared() { + vectorPreferences.unsubscribeToChanges(this) + super.onCleared() + } + + private fun refreshIpAddressVisibility() { + val shouldShowIpAddress = vectorPreferences.showIpAddressInSessionManagerScreens() + setState { + copy(isShowingIpAddress = shouldShowIpAddress) + } } private fun observeCurrentSessionCrossSigningInfo() { @@ -112,9 +141,14 @@ class DevicesViewModel @AssistedInject constructor( is DevicesAction.VerifyCurrentSession -> handleVerifyCurrentSessionAction() is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() DevicesAction.MultiSignoutOtherSessions -> handleMultiSignoutOtherSessions() + DevicesAction.ToggleIpAddressVisibility -> handleToggleIpAddressVisibility() } } + private fun handleToggleIpAddressVisibility() { + toggleIpAddressVisibilityUseCase.execute() + } + private fun handleVerifyCurrentSessionAction() { viewModelScope.launch { val currentSessionCanBeVerified = checkIfCurrentSessionCanBeVerifiedUseCase.execute() 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 e8bed35e24..e0531c34dc 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 @@ -27,4 +27,5 @@ data class DevicesViewState( val unverifiedSessionsCount: Int = 0, val inactiveSessionsCount: Int = 0, val isLoading: Boolean = false, + val isShowingIpAddress: Boolean = false, ) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt new file mode 100644 index 0000000000..1e1dc19c96 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/ToggleIpAddressVisibilityUseCase.kt @@ -0,0 +1,30 @@ +/* + * 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 + +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class ToggleIpAddressVisibilityUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, +) { + + fun execute() { + val currentVisibility = vectorPreferences.showIpAddressInSessionManagerScreens() + vectorPreferences.setIpAddressVisibilityInDeviceManagerScreens(!currentVisibility) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 3a3c3463fb..b27d8a7270 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -146,11 +146,19 @@ class VectorSettingsDevicesFragment : confirmMultiSignoutOtherSessions() true } + R.id.otherSessionsHeaderToggleIpAddress -> { + handleToggleIpAddressVisibility() + true + } else -> false } } } + private fun handleToggleIpAddressVisibility() { + viewModel.handle(DevicesAction.ToggleIpAddressVisibility) + } + private fun confirmMultiSignoutOtherSessions() { activity?.let { buildConfirmSignoutDialogUseCase.execute(it, this::multiSignoutOtherSessions) @@ -240,7 +248,7 @@ class VectorSettingsDevicesFragment : renderSecurityRecommendations(state.inactiveSessionsCount, state.unverifiedSessionsCount, isCurrentSessionVerified) renderCurrentDevice(currentDeviceInfo) - renderOtherSessionsView(otherDevices) + renderOtherSessionsView(otherDevices, state.isShowingIpAddress) } else { hideSecurityRecommendations() hideCurrentSessionView() @@ -297,7 +305,7 @@ class VectorSettingsDevicesFragment : hideInactiveSessionsRecommendation() } - private fun renderOtherSessionsView(otherDevices: List?) { + private fun renderOtherSessionsView(otherDevices: List?, isShowingIpAddress: Boolean) { if (otherDevices.isNullOrEmpty()) { hideOtherSessionsView() } else { @@ -308,12 +316,18 @@ class VectorSettingsDevicesFragment : multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) multiSignoutItem.setTextColor(color) views.deviceListOtherSessions.isVisible = true + val devices = if (isShowingIpAddress) otherDevices else otherDevices.map { it.copy(deviceInfo = it.deviceInfo.copy(lastSeenIp = null)) } views.deviceListOtherSessions.render( - devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER), - totalNumberOfDevices = otherDevices.size, - showViewAll = otherDevices.size > NUMBER_OF_OTHER_DEVICES_TO_RENDER + devices = devices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER), + totalNumberOfDevices = devices.size, + showViewAll = devices.size > NUMBER_OF_OTHER_DEVICES_TO_RENDER ) - } + views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderToggleIpAddress).title = if (isShowingIpAddress) { + stringProvider.getString(R.string.device_manager_other_sessions_hide_ip_address) + } else { + stringProvider.getString(R.string.device_manager_other_sessions_show_ip_address) + } + } } private fun hideOtherSessionsView() { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt index de1cd33d35..9d9cb15c28 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt @@ -29,6 +29,7 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.onClick +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider @@ -69,6 +70,9 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la @EpoxyAttribute var selected: Boolean = false + @EpoxyAttribute + var ipAddress: String? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var clickListener: ClickListener? = null @@ -100,6 +104,7 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la holder.otherSessionDescriptionTextView.setTextColor(it) } holder.otherSessionDescriptionTextView.setCompoundDrawablesWithIntrinsicBounds(sessionDescriptionDrawable, null, null, null) + holder.otherSessionIpAddressTextView.setTextOrHide(ipAddress) holder.otherSessionItemBackgroundView.isSelected = selected } @@ -108,6 +113,7 @@ abstract class OtherSessionItem : VectorEpoxyModel(R.la val otherSessionVerificationStatusImageView by bind(R.id.otherSessionVerificationStatusImageView) val otherSessionNameTextView by bind(R.id.otherSessionNameTextView) val otherSessionDescriptionTextView by bind(R.id.otherSessionDescriptionTextView) + val otherSessionIpAddressTextView by bind(R.id.otherSessionIpAddressTextView) val otherSessionItemBackgroundView by bind(R.id.otherSessionItemBackground) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt index 8d70552101..5e2549f42a 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt @@ -72,6 +72,7 @@ class OtherSessionsController @Inject constructor( sessionDescription(description) sessionDescriptionDrawable(descriptionDrawable) sessionDescriptionColor(descriptionColor) + ipAddress(device.deviceInfo.lastSeenIp) stringProvider(host.stringProvider) colorProvider(host.colorProvider) drawableProvider(host.drawableProvider) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt index 3d9c3a8f37..c6044d04a4 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoView.kt @@ -76,6 +76,7 @@ class SessionInfoView @JvmOverloads constructor( sessionInfoViewState.deviceFullInfo.isInactive, sessionInfoViewState.deviceFullInfo.deviceInfo, sessionInfoViewState.isLastSeenDetailsVisible, + sessionInfoViewState.isShowingIpAddress, dateFormatter, drawableProvider, colorProvider, @@ -157,6 +158,7 @@ class SessionInfoView @JvmOverloads constructor( isInactive: Boolean, deviceInfo: DeviceInfo, isLastSeenDetailsVisible: Boolean, + isShowingIpAddress: Boolean, dateFormatter: VectorDateFormatter, drawableProvider: DrawableProvider, colorProvider: ColorProvider, @@ -186,7 +188,7 @@ class SessionInfoView @JvmOverloads constructor( } else { views.sessionInfoLastActivityTextView.isGone = true } - views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible }) + views.sessionInfoLastIPAddressTextView.setTextOrHide(deviceInfo.lastSeenIp?.takeIf { isLastSeenDetailsVisible && isShowingIpAddress }) } private fun renderDetailsButton(isDetailsButtonVisible: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt index 287bb956f5..5d3c4b4f4b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionInfoViewState.kt @@ -25,4 +25,5 @@ data class SessionInfoViewState( val isDetailsButtonVisible: Boolean = true, val isLearnMoreLinkVisible: Boolean = false, val isLastSeenDetailsVisible: Boolean = false, + val isShowingIpAddress: Boolean = false, ) 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 index 24d2a08bdc..bdad65ca43 100644 --- 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 @@ -33,4 +33,5 @@ sealed class OtherSessionsAction : VectorViewModelAction { object SelectAll : OtherSessionsAction() object DeselectAll : OtherSessionsAction() object MultiSignout : OtherSessionsAction() + object ToggleIpAddressVisibility : 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 74a78b2415..87330b087a 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 @@ -85,6 +85,12 @@ class OtherSessionsFragment : menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse() + menu.findItem(R.id.otherSessionsToggleIpAddress).isVisible = !isSelectModeEnabled + menu.findItem(R.id.otherSessionsToggleIpAddress).title = if (state.isShowingIpAddress) { + getString(R.string.device_manager_other_sessions_hide_ip_address) + } else { + getString(R.string.device_manager_other_sessions_show_ip_address) + } updateMultiSignoutMenuItem(menu, state) } } @@ -130,10 +136,18 @@ class OtherSessionsFragment : confirmMultiSignout() true } + R.id.otherSessionsToggleIpAddress -> { + toggleIpAddressVisibility() + true + } else -> false } } + private fun toggleIpAddressVisibility() { + viewModel.handle(OtherSessionsAction.ToggleIpAddressVisibility) + } + private fun confirmMultiSignout() { activity?.let { buildConfirmSignoutDialogUseCase.execute(it, this::multiSignout) @@ -213,7 +227,7 @@ class OtherSessionsFragment : updateLoading(state.isLoading) if (state.devices is Success) { val devices = state.devices.invoke() - renderDevices(devices, state.currentFilter) + renderDevices(devices, state.currentFilter, state.isShowingIpAddress) updateToolbar(devices, state.isSelectModeEnabled) } } @@ -237,7 +251,7 @@ class OtherSessionsFragment : toolbar?.title = title } - private fun renderDevices(devices: List, currentFilter: DeviceManagerFilterType) { + private fun renderDevices(devices: List, currentFilter: DeviceManagerFilterType, isShowingIpAddress: Boolean) { views.otherSessionsFilterBadgeImageView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS views.otherSessionsSecurityRecommendationView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS views.deviceListHeaderOtherSessions.isVisible = currentFilter == DeviceManagerFilterType.ALL_SESSIONS @@ -299,7 +313,8 @@ class OtherSessionsFragment : } else { views.deviceListOtherSessions.isVisible = true views.otherSessionsNotFoundLayout.isVisible = false - views.deviceListOtherSessions.render(devices = devices, totalNumberOfDevices = devices.size, showViewAll = false) + val mappedDevices = if (isShowingIpAddress) devices else devices.map { it.copy(deviceInfo = it.deviceInfo.copy(lastSeenIp = null)) } + views.deviceListOtherSessions.render(devices = mappedDevices, totalNumberOfDevices = mappedDevices.size, showViewAll = false) } } 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 index 9b4c26ee4f..a5282e7ba2 100644 --- 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 @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.othersessions +import android.content.SharedPreferences import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted @@ -25,8 +26,10 @@ 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.features.auth.PendingAuthHandler +import im.vector.app.features.settings.VectorPreferences 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.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthNeeded @@ -42,10 +45,12 @@ class OtherSessionsViewModel @AssistedInject constructor( private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase, private val pendingAuthHandler: PendingAuthHandler, - refreshDevicesUseCase: RefreshDevicesUseCase + refreshDevicesUseCase: RefreshDevicesUseCase, + private val vectorPreferences: VectorPreferences, + private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase -) { +), SharedPreferences.OnSharedPreferenceChangeListener { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -58,6 +63,28 @@ class OtherSessionsViewModel @AssistedInject constructor( init { observeDevices(initialState.currentFilter) + refreshIpAddressVisibility() + observePreferences() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + refreshIpAddressVisibility() + } + + private fun observePreferences() { + vectorPreferences.subscribeToChanges(this) + } + + override fun onCleared() { + vectorPreferences.unsubscribeToChanges(this) + super.onCleared() + } + + private fun refreshIpAddressVisibility() { + val shouldShowIpAddress = vectorPreferences.showIpAddressInSessionManagerScreens() + setState { + copy(isShowingIpAddress = shouldShowIpAddress) + } } private fun observeDevices(currentFilter: DeviceManagerFilterType) { @@ -85,9 +112,14 @@ class OtherSessionsViewModel @AssistedInject constructor( OtherSessionsAction.DeselectAll -> handleDeselectAll() OtherSessionsAction.SelectAll -> handleSelectAll() OtherSessionsAction.MultiSignout -> handleMultiSignout() + OtherSessionsAction.ToggleIpAddressVisibility -> handleToggleIpAddressVisibility() } } + private fun handleToggleIpAddressVisibility() { + toggleIpAddressVisibilityUseCase.execute() + } + private fun handleFilterDevices(action: OtherSessionsAction.FilterDevices) { setState { copy( 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 index c0b50fded8..f4dd3640ee 100644 --- 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 @@ -28,6 +28,7 @@ data class OtherSessionsViewState( val excludeCurrentDevice: Boolean = false, val isSelectModeEnabled: Boolean = false, val isLoading: Boolean = false, + val isShowingIpAddress: Boolean = false, ) : MavericksState { constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice) diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt index 9a92d5b629..2b6c40eead 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewAction.kt @@ -29,4 +29,5 @@ sealed class SessionOverviewAction : VectorViewModelAction { val deviceId: String, val enabled: Boolean, ) : SessionOverviewAction() + object ToggleIpAddressVisibility : SessionOverviewAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index d722cda7a1..be60b3b805 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices.v2.overview import android.app.Activity import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup @@ -156,16 +157,34 @@ class SessionOverviewFragment : override fun getMenuRes() = R.menu.menu_session_overview + override fun handlePrepareMenu(menu: Menu) { + withState(viewModel) { state -> + menu.findItem(R.id.sessionOverviewToggleIpAddress).title = if (state.isShowingIpAddress) { + getString(R.string.device_manager_other_sessions_hide_ip_address) + } else { + getString(R.string.device_manager_other_sessions_show_ip_address) + } + } + } + override fun handleMenuItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.sessionOverviewRename -> { goToRenameSession() true } + R.id.sessionOverviewToggleIpAddress -> { + toggleIpAddressVisibility() + true + } else -> false } } + private fun toggleIpAddressVisibility() { + viewModel.handle(SessionOverviewAction.ToggleIpAddressVisibility) + } + private fun goToRenameSession() = withState(viewModel) { state -> viewNavigator.goToRenameSession(requireContext(), state.deviceId) } @@ -206,6 +225,7 @@ class SessionOverviewFragment : isDetailsButtonVisible = false, isLearnMoreLinkVisible = deviceInfo.roomEncryptionTrustLevel != RoomEncryptionTrustLevel.Default, isLastSeenDetailsVisible = !isCurrentSession, + isShowingIpAddress = viewState.isShowingIpAddress, ) views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider, stringProvider) views.sessionOverviewInfo.onLearnMoreClickListener = { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt index a56872e648..472e0a4269 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.settings.devices.v2.overview +import android.content.SharedPreferences import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.Success import dagger.assisted.Assisted @@ -25,7 +26,9 @@ 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.features.auth.PendingAuthHandler +import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel import im.vector.app.features.settings.devices.v2.notification.GetNotificationsStatusUseCase import im.vector.app.features.settings.devices.v2.notification.TogglePushNotificationUseCase @@ -54,9 +57,11 @@ class SessionOverviewViewModel @AssistedInject constructor( private val togglePushNotificationUseCase: TogglePushNotificationUseCase, private val getNotificationsStatusUseCase: GetNotificationsStatusUseCase, refreshDevicesUseCase: RefreshDevicesUseCase, + private val vectorPreferences: VectorPreferences, + private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase, ) : VectorSessionsListViewModel( initialState, activeSessionHolder, refreshDevicesUseCase -) { +), SharedPreferences.OnSharedPreferenceChangeListener { companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() @@ -70,6 +75,27 @@ class SessionOverviewViewModel @AssistedInject constructor( observeSessionInfo(initialState.deviceId) observeCurrentSessionInfo() observeNotificationsStatus(initialState.deviceId) + refreshIpAddressVisibility() + observePreferences() + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + refreshIpAddressVisibility() + } + + private fun observePreferences() { + vectorPreferences.subscribeToChanges(this) + } + + override fun onCleared() { + vectorPreferences.unsubscribeToChanges(this) + super.onCleared() + } + private fun refreshIpAddressVisibility() { + val shouldShowIpAddress = vectorPreferences.showIpAddressInSessionManagerScreens() + setState { + copy(isShowingIpAddress = shouldShowIpAddress) + } } private fun refreshPushers() { @@ -111,9 +137,14 @@ class SessionOverviewViewModel @AssistedInject constructor( is SessionOverviewAction.PasswordAuthDone -> handlePasswordAuthDone(action) SessionOverviewAction.ReAuthCancelled -> handleReAuthCancelled() is SessionOverviewAction.TogglePushNotifications -> handleTogglePusherAction(action) + SessionOverviewAction.ToggleIpAddressVisibility -> handleToggleIpAddressVisibility() } } + private fun handleToggleIpAddressVisibility() { + toggleIpAddressVisibilityUseCase.execute() + } + private fun handleVerifySessionAction() = withState { viewState -> if (viewState.deviceInfo.invoke()?.isCurrentDevice.orFalse()) { handleVerifyCurrentSession() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt index 019dd2d724..0f66605f98 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewState.kt @@ -28,6 +28,7 @@ data class SessionOverviewViewState( val deviceInfo: Async = Uninitialized, val isLoading: Boolean = false, val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED, + val isShowingIpAddress: Boolean = false, ) : MavericksState { constructor(args: SessionOverviewArgs) : this( deviceId = args.deviceId diff --git a/vector/src/main/res/layout/item_other_session.xml b/vector/src/main/res/layout/item_other_session.xml index f514cea56b..a6205e7d50 100644 --- a/vector/src/main/res/layout/item_other_session.xml +++ b/vector/src/main/res/layout/item_other_session.xml @@ -13,7 +13,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:background="@drawable/bg_other_session" - app:layout_constraintBottom_toBottomOf="@id/otherSessionVerificationStatusImageView" + app:layout_constraintBottom_toBottomOf="@id/otherSessionSeparator" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -53,11 +53,12 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="8dp" + android:layout_marginTop="8dp" android:ellipsize="end" android:lines="1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/otherSessionDeviceTypeImageView" - app:layout_constraintTop_toTopOf="@id/otherSessionDeviceTypeImageView" + app:layout_constraintTop_toTopOf="@id/otherSessionItemBackground" tools:text="Element Mobile: Android" /> + + + app:layout_constraintTop_toBottomOf="@id/otherSessionIpAddressTextView" /> diff --git a/vector/src/main/res/menu/menu_other_sessions.xml b/vector/src/main/res/menu/menu_other_sessions.xml index 7893575dde..98f9dd8256 100644 --- a/vector/src/main/res/menu/menu_other_sessions.xml +++ b/vector/src/main/res/menu/menu_other_sessions.xml @@ -9,6 +9,11 @@ android:title="@string/device_manager_other_sessions_select" app:showAsAction="withText|never" /> + + + + + + () private val fakePendingAuthHandler = FakePendingAuthHandler() private val fakeRefreshDevicesUseCase = mockk(relaxUnitFun = true) + private val fakeVectorPreferences = FakeVectorPreferences() + private val toggleIpAddressVisibilityUseCase = mockk() private fun createViewModel(): DevicesViewModel { return DevicesViewModel( @@ -85,6 +90,8 @@ class DevicesViewModelTest { interceptSignoutFlowResponseUseCase = fakeInterceptSignoutFlowResponseUseCase, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, + vectorPreferences = fakeVectorPreferences.instance, + toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase, ) } @@ -97,6 +104,7 @@ class DevicesViewModelTest { givenVerificationService() givenCurrentSessionCrossSigningInfo() givenDeviceFullInfoList(deviceId1 = A_DEVICE_ID_1, deviceId2 = A_DEVICE_ID_2) + fakeVectorPreferences.givenSessionManagerShowIpAddress(false) } private fun givenVerificationService(): FakeVerificationService { @@ -343,6 +351,33 @@ class DevicesViewModelTest { } } + @Test + fun `given the viewModel when initializing it then view state of ip address visibility is false`() { + // When + val viewModelTest = createViewModel().test() + + // Then + viewModelTest.assertLatestState { it.isShowingIpAddress == false } + viewModelTest.finish() + } + + @Test + fun `given the viewModel when toggleIpAddressVisibility action is triggered then view state and preference change accordingly`() { + // When + val viewModel = createViewModel() + val viewModelTest = viewModel.test() + every { toggleIpAddressVisibilityUseCase.execute() } just runs + every { fakeVectorPreferences.instance.setIpAddressVisibilityInDeviceManagerScreens(true) } just runs + every { fakeVectorPreferences.instance.showIpAddressInSessionManagerScreens() } returns true + + viewModel.handle(DevicesAction.ToggleIpAddressVisibility) + viewModel.onSharedPreferenceChanged(null, null) + + // Then + viewModelTest.assertLatestState { it.isShowingIpAddress == true } + viewModelTest.finish() + } + private fun givenCurrentSessionCrossSigningInfo(): CurrentSessionCrossSigningInfo { val currentSessionCrossSigningInfo = mockk() every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt index 1e8c511c42..82f40d911d 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsViewModelTest.kt @@ -22,10 +22,12 @@ import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.features.settings.devices.v2.DeviceFullInfo 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.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.test.fakes.FakeActiveSessionHolder import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase +import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.fixtures.aDeviceFullInfo import im.vector.app.test.test @@ -66,6 +68,8 @@ class OtherSessionsViewModelTest { private val fakeRefreshDevicesUseCase = mockk(relaxed = true) private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase() private val fakePendingAuthHandler = FakePendingAuthHandler() + private val fakeVectorPreferences = FakeVectorPreferences() + private val toggleIpAddressVisibilityUseCase = mockk() private fun createViewModel(viewState: OtherSessionsViewState = OtherSessionsViewState(defaultArgs)) = OtherSessionsViewModel( @@ -75,6 +79,8 @@ class OtherSessionsViewModelTest { signoutSessionsUseCase = fakeSignoutSessionsUseCase.instance, pendingAuthHandler = fakePendingAuthHandler.instance, refreshDevicesUseCase = fakeRefreshDevicesUseCase, + vectorPreferences = fakeVectorPreferences.instance, + toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase, ) @Before @@ -84,6 +90,7 @@ class OtherSessionsViewModelTest { every { SystemClock.elapsedRealtime() } returns 1234 givenVerificationService() + fakeVectorPreferences.givenSessionManagerShowIpAddress(false) } private fun givenVerificationService(): FakeVerificationService { diff --git a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt index 1a57b76020..287bdd159c 100644 --- a/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewViewModelTest.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.Success import com.airbnb.mvrx.test.MavericksTestRule import im.vector.app.features.settings.devices.v2.DeviceFullInfo import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase +import im.vector.app.features.settings.devices.v2.ToggleIpAddressVisibilityUseCase import im.vector.app.features.settings.devices.v2.notification.NotificationsStatus import im.vector.app.features.settings.devices.v2.signout.InterceptSignoutFlowResponseUseCase import im.vector.app.features.settings.devices.v2.verification.CheckIfCurrentSessionCanBeVerifiedUseCase @@ -30,6 +31,7 @@ import im.vector.app.test.fakes.FakeGetNotificationsStatusUseCase import im.vector.app.test.fakes.FakePendingAuthHandler import im.vector.app.test.fakes.FakeSignoutSessionsUseCase import im.vector.app.test.fakes.FakeTogglePushNotificationUseCase +import im.vector.app.test.fakes.FakeVectorPreferences import im.vector.app.test.fakes.FakeVerificationService import im.vector.app.test.test import im.vector.app.test.testDispatcher @@ -77,6 +79,8 @@ class SessionOverviewViewModelTest { private val togglePushNotificationUseCase = FakeTogglePushNotificationUseCase() private val fakeGetNotificationsStatusUseCase = FakeGetNotificationsStatusUseCase() private val notificationsStatus = NotificationsStatus.ENABLED + private val fakeVectorPreferences = FakeVectorPreferences() + private val toggleIpAddressVisibilityUseCase = mockk() private fun createViewModel() = SessionOverviewViewModel( initialState = SessionOverviewViewState(args), @@ -89,6 +93,8 @@ class SessionOverviewViewModelTest { refreshDevicesUseCase = refreshDevicesUseCase, togglePushNotificationUseCase = togglePushNotificationUseCase.instance, getNotificationsStatusUseCase = fakeGetNotificationsStatusUseCase.instance, + vectorPreferences = fakeVectorPreferences.instance, + toggleIpAddressVisibilityUseCase = toggleIpAddressVisibilityUseCase, ) @Before @@ -103,6 +109,7 @@ class SessionOverviewViewModelTest { A_SESSION_ID_1, notificationsStatus ) + fakeVectorPreferences.givenSessionManagerShowIpAddress(false) } private fun givenVerificationService(): FakeVerificationService { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt index 4baa7e2b90..d89764a77e 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorPreferences.kt @@ -52,4 +52,8 @@ class FakeVectorPreferences { fun verifySetNotificationEnabledForDevice(enabled: Boolean, inverse: Boolean = false) { verify(inverse = inverse) { instance.setNotificationEnabledForDevice(enabled) } } + + fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) { + every { instance.showIpAddressInSessionManagerScreens() } returns showIpAddress + } }