Merge pull request #7546 from vector-im/feature/ons/toggle_ip_address_visibility

Toggle IP address visibility (PSG-860)
This commit is contained in:
Onuray Sahin 2022-11-22 22:41:21 +03:00 committed by GitHub
commit 0957b38329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 309 additions and 18 deletions

1
changelog.d/7546.feature Normal file
View File

@ -0,0 +1 @@
[Device Manager] Toggle IP address visibility

View File

@ -3356,6 +3356,8 @@
<item quantity="one">Sign out of %1$d session</item>
<item quantity="other">Sign out of %1$d sessions</item>
</plurals>
<string name="device_manager_other_sessions_show_ip_address">Show IP address</string>
<string name="device_manager_other_sessions_hide_ip_address">Hide IP address</string>
<string name="device_manager_session_overview_signout">Sign out of this session</string>
<string name="device_manager_session_details_title">Session details</string>
<string name="device_manager_session_details_description">Application, device, and activity information.</string>

View File

@ -57,5 +57,7 @@
<!-- Level 1: Legals -->
<!-- Level 3: Security & Privacy, Sessions -->
<bool name="settings_session_manager_show_ip_address">false</bool>
</resources>

View File

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

View File

@ -29,4 +29,5 @@ sealed class DevicesAction : VectorViewModelAction {
object VerifyCurrentSession : DevicesAction()
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
object MultiSignoutOtherSessions : DevicesAction()
object ToggleIpAddressVisibility : DevicesAction()
}

View File

@ -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<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState, activeSessionHolder, refreshDevicesUseCase) {
private val vectorPreferences: VectorPreferences,
private val toggleIpAddressVisibilityUseCase: ToggleIpAddressVisibilityUseCase,
) : VectorSessionsListViewModel<DevicesViewState,
DevicesAction,
DevicesViewEvent>(initialState, activeSessionHolder, refreshDevicesUseCase),
SharedPreferences.OnSharedPreferenceChangeListener {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<DevicesViewModel, DevicesViewState> {
@ -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()

View File

@ -27,4 +27,5 @@ data class DevicesViewState(
val unverifiedSessionsCount: Int = 0,
val inactiveSessionsCount: Int = 0,
val isLoading: Boolean = false,
val isShowingIpAddress: Boolean = false,
) : MavericksState

View File

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

View File

@ -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<DeviceFullInfo>?) {
private fun renderOtherSessionsView(otherDevices: List<DeviceFullInfo>?, 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() {

View File

@ -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<OtherSessionItem.Holder>(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<OtherSessionItem.Holder>(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<OtherSessionItem.Holder>(R.la
val otherSessionVerificationStatusImageView by bind<ShieldImageView>(R.id.otherSessionVerificationStatusImageView)
val otherSessionNameTextView by bind<TextView>(R.id.otherSessionNameTextView)
val otherSessionDescriptionTextView by bind<TextView>(R.id.otherSessionDescriptionTextView)
val otherSessionIpAddressTextView by bind<TextView>(R.id.otherSessionIpAddressTextView)
val otherSessionItemBackgroundView by bind<View>(R.id.otherSessionItemBackground)
}
}

View File

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

View File

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

View File

@ -25,4 +25,5 @@ data class SessionInfoViewState(
val isDetailsButtonVisible: Boolean = true,
val isLearnMoreLinkVisible: Boolean = false,
val isLastSeenDetailsVisible: Boolean = false,
val isShowingIpAddress: Boolean = false,
)

View File

@ -33,4 +33,5 @@ sealed class OtherSessionsAction : VectorViewModelAction {
object SelectAll : OtherSessionsAction()
object DeselectAll : OtherSessionsAction()
object MultiSignout : OtherSessionsAction()
object ToggleIpAddressVisibility : OtherSessionsAction()
}

View File

@ -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<DeviceFullInfo>, currentFilter: DeviceManagerFilterType) {
private fun renderDevices(devices: List<DeviceFullInfo>, 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)
}
}

View File

@ -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<OtherSessionsViewState, OtherSessionsAction, OtherSessionsViewEvents>(
initialState, activeSessionHolder, refreshDevicesUseCase
) {
), SharedPreferences.OnSharedPreferenceChangeListener {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<OtherSessionsViewModel, OtherSessionsViewState> {
@ -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(

View File

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

View File

@ -29,4 +29,5 @@ sealed class SessionOverviewAction : VectorViewModelAction {
val deviceId: String,
val enabled: Boolean,
) : SessionOverviewAction()
object ToggleIpAddressVisibility : SessionOverviewAction()
}

View File

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

View File

@ -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<SessionOverviewViewState, SessionOverviewAction, SessionOverviewViewEvent>(
initialState, activeSessionHolder, refreshDevicesUseCase
) {
), SharedPreferences.OnSharedPreferenceChangeListener {
companion object : MavericksViewModelFactory<SessionOverviewViewModel, SessionOverviewViewState> 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()

View File

@ -28,6 +28,7 @@ data class SessionOverviewViewState(
val deviceInfo: Async<DeviceFullInfo> = Uninitialized,
val isLoading: Boolean = false,
val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED,
val isShowingIpAddress: Boolean = false,
) : MavericksState {
constructor(args: SessionOverviewArgs) : this(
deviceId = args.deviceId

View File

@ -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" />
<TextView
@ -67,19 +68,31 @@
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:drawablePadding="8dp"
app:layout_constraintBottom_toBottomOf="@id/otherSessionDeviceTypeImageView"
app:layout_constraintEnd_toEndOf="@id/otherSessionNameTextView"
app:layout_constraintStart_toStartOf="@id/otherSessionNameTextView"
app:layout_constraintTop_toBottomOf="@id/otherSessionNameTextView"
tools:text="@string/device_manager_verification_status_verified" />
<TextView
android:id="@+id/otherSessionIpAddressTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:drawablePadding="8dp"
app:layout_constraintEnd_toEndOf="@id/otherSessionNameTextView"
app:layout_constraintStart_toStartOf="@id/otherSessionNameTextView"
app:layout_constraintTop_toBottomOf="@id/otherSessionDescriptionTextView"
tools:text="0.0.0.0" />
<View
android:id="@+id/otherSessionSeparator"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="?vctr_content_quinary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/otherSessionNameTextView"
app:layout_constraintTop_toBottomOf="@id/otherSessionItemBackground" />
app:layout_constraintTop_toBottomOf="@id/otherSessionIpAddressTextView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,6 +9,11 @@
android:title="@string/device_manager_other_sessions_select"
app:showAsAction="withText|never" />
<item
android:id="@+id/otherSessionsToggleIpAddress"
android:title="@string/device_manager_other_sessions_show_ip_address"
app:showAsAction="withText|never" />
<item
android:id="@+id/otherSessionsMultiSignout"
android:title="@plurals/device_manager_other_sessions_multi_signout_all"

View File

@ -4,6 +4,11 @@
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AlwaysShowAction">
<item
android:id="@+id/otherSessionsHeaderToggleIpAddress"
android:title="@string/device_manager_other_sessions_show_ip_address"
app:showAsAction="withText|never" />
<item
android:id="@+id/otherSessionsHeaderMultiSignout"
android:title="@plurals/device_manager_other_sessions_multi_signout_all"

View File

@ -4,6 +4,11 @@
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AlwaysShowAction">
<item
android:id="@+id/sessionOverviewToggleIpAddress"
android:title="@string/device_manager_other_sessions_show_ip_address"
app:showAsAction="withText|never" />
<item
android:id="@+id/sessionOverviewRename"
android:title="@string/device_manager_session_rename"

View File

@ -29,15 +29,18 @@ import im.vector.app.features.settings.devices.v2.verification.GetCurrentSession
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.test
import im.vector.app.test.testDispatcher
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkAll
import io.mockk.verify
import io.mockk.verifyAll
@ -72,6 +75,8 @@ class DevicesViewModelTest {
private val fakeInterceptSignoutFlowResponseUseCase = mockk<InterceptSignoutFlowResponseUseCase>()
private val fakePendingAuthHandler = FakePendingAuthHandler()
private val fakeRefreshDevicesUseCase = mockk<RefreshDevicesUseCase>(relaxUnitFun = true)
private val fakeVectorPreferences = FakeVectorPreferences()
private val toggleIpAddressVisibilityUseCase = mockk<ToggleIpAddressVisibilityUseCase>()
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<CurrentSessionCrossSigningInfo>()
every { currentSessionCrossSigningInfo.deviceId } returns A_CURRENT_DEVICE_ID

View File

@ -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<RefreshDevicesUseCase>(relaxed = true)
private val fakeSignoutSessionsUseCase = FakeSignoutSessionsUseCase()
private val fakePendingAuthHandler = FakePendingAuthHandler()
private val fakeVectorPreferences = FakeVectorPreferences()
private val toggleIpAddressVisibilityUseCase = mockk<ToggleIpAddressVisibilityUseCase>()
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 {

View File

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

View File

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