diff --git a/changelog.d/7694.feature b/changelog.d/7694.feature new file mode 100644 index 0000000000..408925974e --- /dev/null +++ b/changelog.d/7694.feature @@ -0,0 +1 @@ +Remind unverified sessions with a banner once a week diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 58fc62b347..4158ff31bc 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2649,8 +2649,12 @@ Unencrypted Encrypted by an unverified device The authenticity of this encrypted message can\'t be guaranteed on this device. - Review where you’re logged in - Verify all your sessions to ensure your account & messages are safe + + Review where you’re logged in + + Verify all your sessions to ensure your account & messages are safe + You have unverified sessions + Review to ensure your account is safe Verify the new login accessing your account: %1$s diff --git a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index 5c497c24ec..2134c8cf2c 100644 --- a/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector-app/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -88,6 +88,9 @@ class DebugVectorFeatures( override fun isVoiceBroadcastEnabled(): Boolean = read(DebugFeatureKeys.voiceBroadcastEnabled) ?: vectorFeatures.isVoiceBroadcastEnabled() + override fun isUnverifiedSessionsAlertEnabled(): Boolean = read(DebugFeatureKeys.unverifiedSessionsAlertEnabled) + ?: vectorFeatures.isUnverifiedSessionsAlertEnabled() + fun override(value: T?, key: Preferences.Key) = updatePreferences { if (value == null) { it.remove(key) @@ -151,4 +154,5 @@ object DebugFeatureKeys { val qrCodeLoginForAllServers = booleanPreferencesKey("qr-code-login-for-all-servers") val reciprocateQrCodeLogin = booleanPreferencesKey("reciprocate-qr-code-login") val voiceBroadcastEnabled = booleanPreferencesKey("voice-broadcast-enabled") + val unverifiedSessionsAlertEnabled = booleanPreferencesKey("unverified-sessions-alert-enabled") } diff --git a/vector-config/src/main/java/im/vector/app/config/Config.kt b/vector-config/src/main/java/im/vector/app/config/Config.kt index c91987dbfd..fdc8e9f73b 100644 --- a/vector-config/src/main/java/im/vector/app/config/Config.kt +++ b/vector-config/src/main/java/im/vector/app/config/Config.kt @@ -16,6 +16,8 @@ package im.vector.app.config +import kotlin.time.Duration.Companion.days + /** * Set of flags to configure the application. */ @@ -93,4 +95,6 @@ object Config { * Can be disabled by providing Analytics.Disabled */ val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "NIGHTLY") + + val SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS = 7.days.inWholeMilliseconds // 1 Week } diff --git a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt index 95cf272abd..99abc15f81 100644 --- a/vector/src/main/java/im/vector/app/features/VectorFeatures.kt +++ b/vector/src/main/java/im/vector/app/features/VectorFeatures.kt @@ -44,6 +44,7 @@ interface VectorFeatures { fun isQrCodeLoginForAllServers(): Boolean fun isReciprocateQrCodeLogin(): Boolean fun isVoiceBroadcastEnabled(): Boolean + fun isUnverifiedSessionsAlertEnabled(): Boolean } class DefaultVectorFeatures : VectorFeatures { @@ -63,4 +64,5 @@ class DefaultVectorFeatures : VectorFeatures { override fun isQrCodeLoginForAllServers(): Boolean = false override fun isReciprocateQrCodeLogin(): Boolean = false override fun isVoiceBroadcastEnabled(): Boolean = true + override fun isUnverifiedSessionsAlertEnabled(): Boolean = true } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index e824dc1820..69abeed424 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -239,12 +239,12 @@ class HomeDetailFragment : .requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "") } unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } dismissedAction = Runnable { unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } } @@ -256,8 +256,8 @@ class HomeDetailFragment : alertManager.postVectorAlert( VerificationVectorAlert( uid = uid, - title = getString(R.string.review_logins), - description = getString(R.string.verify_other_sessions), + title = getString(R.string.review_unverified_sessions_title), + description = getString(R.string.review_unverified_sessions_description), iconId = R.drawable.ic_shield_warning ).apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) diff --git a/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt new file mode 100644 index 0000000000..5a0d4743dc --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/IsNewLoginAlertShownUseCase.kt @@ -0,0 +1,31 @@ +/* + * 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.home + +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class IsNewLoginAlertShownUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, +) { + + fun execute(deviceId: String?): Boolean { + deviceId ?: return false + + return vectorPreferences.isNewLoginAlertShownForDevice(deviceId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt index 5956646eab..ccd5a7e84b 100644 --- a/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/NewHomeDetailFragment.kt @@ -253,12 +253,12 @@ class NewHomeDetailFragment : .requestSessionVerification(vectorBaseActivity, newest.deviceId ?: "") } unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } dismissedAction = Runnable { unknownDeviceDetectorSharedViewModel.handle( - UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty()) + UnknownDeviceDetectorSharedViewModel.Action.IgnoreNewLogin(newest.deviceId?.let { listOf(it) }.orEmpty()) ) } } @@ -270,8 +270,8 @@ class NewHomeDetailFragment : alertManager.postVectorAlert( VerificationVectorAlert( uid = uid, - title = getString(R.string.review_logins), - description = getString(R.string.verify_other_sessions), + title = getString(R.string.review_unverified_sessions_title), + description = getString(R.string.review_unverified_sessions_description), iconId = R.drawable.ic_shield_warning ).apply { viewBinder = VerificationVectorAlert.ViewBinder(user, avatarRenderer) diff --git a/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt new file mode 100644 index 0000000000..d313f93043 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/SetNewLoginAlertShownUseCase.kt @@ -0,0 +1,31 @@ +/* + * 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.home + +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class SetNewLoginAlertShownUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, +) { + + fun execute(deviceIds: List) { + deviceIds.forEach { + vectorPreferences.setNewLoginAlertShownForDevice(it) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt b/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt new file mode 100644 index 0000000000..4580ac0f31 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/SetUnverifiedSessionsAlertShownUseCase.kt @@ -0,0 +1,34 @@ +/* + * 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.home + +import im.vector.app.core.time.Clock +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class SetUnverifiedSessionsAlertShownUseCase @Inject constructor( + private val vectorPreferences: VectorPreferences, + private val clock: Clock, +) { + + fun execute(deviceIds: List) { + val epochMillis = clock.epochMillis() + deviceIds.forEach { + vectorPreferences.setUnverifiedSessionsAlertLastShownMillis(it, epochMillis) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt new file mode 100644 index 0000000000..18c7ed9689 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCase.kt @@ -0,0 +1,39 @@ +/* + * 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.home + +import im.vector.app.config.Config +import im.vector.app.core.time.Clock +import im.vector.app.features.VectorFeatures +import im.vector.app.features.settings.VectorPreferences +import javax.inject.Inject + +class ShouldShowUnverifiedSessionsAlertUseCase @Inject constructor( + private val vectorFeatures: VectorFeatures, + private val vectorPreferences: VectorPreferences, + private val clock: Clock, +) { + + fun execute(deviceId: String?): Boolean { + deviceId ?: return false + + val isUnverifiedSessionsAlertEnabled = vectorFeatures.isUnverifiedSessionsAlertEnabled() + val unverifiedSessionsAlertLastShownMillis = vectorPreferences.getUnverifiedSessionsAlertLastShownMillis(deviceId) + return isUnverifiedSessionsAlertEnabled && + clock.epochMillis() - unverifiedSessionsAlertLastShownMillis >= Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt index 855c47f4bb..21c7bd6ea1 100644 --- a/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/UnknownDeviceDetectorSharedViewModel.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksViewModelFactory -import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted @@ -33,7 +32,6 @@ import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.time.Clock -import im.vector.app.features.settings.VectorPreferences import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn @@ -63,12 +61,16 @@ data class DeviceDetectionInfo( class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( @Assisted initialState: UnknownDevicesState, session: Session, - private val vectorPreferences: VectorPreferences, clock: Clock, + private val shouldShowUnverifiedSessionsAlertUseCase: ShouldShowUnverifiedSessionsAlertUseCase, + private val setUnverifiedSessionsAlertShownUseCase: SetUnverifiedSessionsAlertShownUseCase, + private val isNewLoginAlertShownUseCase: IsNewLoginAlertShownUseCase, + private val setNewLoginAlertShownUseCase: SetNewLoginAlertShownUseCase, ) : VectorViewModel(initialState) { sealed class Action : VectorViewModelAction { data class IgnoreDevice(val deviceIds: List) : Action() + data class IgnoreNewLogin(val deviceIds: List) : Action() } @AssistedFactory @@ -86,8 +88,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( } } - private val ignoredDeviceList = ArrayList() - init { val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) .firstOrNull { it.deviceId == session.sessionParams.deviceId } @@ -95,12 +95,6 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( ?: clock.epochMillis() Timber.v("## Detector - Current Session first time seen $currentSessionTs") - ignoredDeviceList.addAll( - vectorPreferences.getUnknownDeviceDismissedList().also { - Timber.v("## Detector - Remembered ignored list $it") - } - ) - combine( session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo(), @@ -114,13 +108,15 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( cryptoList.firstOrNull { info.deviceId == it.deviceId }?.isVerified?.not().orFalse() } // filter out ignored devices - .filter { !ignoredDeviceList.contains(it.deviceId) } + .filter { shouldShowUnverifiedSessionsAlertUseCase.execute(it.deviceId) } .sortedByDescending { it.lastSeenTs } .map { deviceInfo -> val deviceKnownSince = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }?.firstTimeSeenLocalTs ?: 0 + val isNew = isNewLoginAlertShownUseCase.execute(deviceInfo.deviceId).not() && deviceKnownSince > currentSessionTs + DeviceDetectionInfo( deviceInfo, - deviceKnownSince > currentSessionTs + 60_000, // short window to avoid false positive, + isNew, pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change ) } @@ -150,22 +146,11 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( override fun handle(action: Action) { when (action) { is Action.IgnoreDevice -> { - ignoredDeviceList.addAll(action.deviceIds) - // local echo - withState { state -> - state.unknownSessions.invoke()?.let { detectedSessions -> - val updated = detectedSessions.filter { !action.deviceIds.contains(it.deviceInfo.deviceId) } - setState { - copy(unknownSessions = Success(updated)) - } - } - } + setUnverifiedSessionsAlertShownUseCase.execute(action.deviceIds) + } + is Action.IgnoreNewLogin -> { + setNewLoginAlertShownUseCase.execute(action.deviceIds) } } } - - override fun onCleared() { - vectorPreferences.storeUnknownDeviceDismissedList(ignoredDeviceList) - super.onCleared() - } } 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 1a452e0426..d46b819cce 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 @@ -228,8 +228,6 @@ class VectorPreferences @Inject constructor( private const val MEDIA_SAVING_1_MONTH = 2 private const val MEDIA_SAVING_FOREVER = 3 - private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST" - private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE" private const val SETTINGS_LABS_ENABLE_LIVE_LOCATION = "SETTINGS_LABS_ENABLE_LIVE_LOCATION" @@ -246,6 +244,9 @@ class VectorPreferences @Inject constructor( // This key will be used to enable user for displaying live user info or not. const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO" + const val SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS = "SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS_" + const val SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE = "SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE_" + // Possible values for TAKE_PHOTO_VIDEO_MODE const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1 @@ -521,18 +522,6 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true) } - fun storeUnknownDeviceDismissedList(deviceIds: List) { - defaultPrefs.edit(true) { - putStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, deviceIds.toSet()) - } - } - - fun getUnknownDeviceDismissedList(): List { - return tryOrNull { - defaultPrefs.getStringSet(SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST, null)?.toList() - }.orEmpty() - } - /** * Update the notification ringtone. * @@ -1243,7 +1232,27 @@ class VectorPreferences @Inject constructor( fun setIpAddressVisibilityInDeviceManagerScreens(isVisible: Boolean) { defaultPrefs.edit { - putBoolean(VectorPreferences.SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible) + putBoolean(SETTINGS_SESSION_MANAGER_SHOW_IP_ADDRESS, isVisible) + } + } + + fun getUnverifiedSessionsAlertLastShownMillis(deviceId: String): Long { + return defaultPrefs.getLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, 0) + } + + fun setUnverifiedSessionsAlertLastShownMillis(deviceId: String, lastShownMillis: Long) { + defaultPrefs.edit { + putLong(SETTINGS_UNVERIFIED_SESSIONS_ALERT_LAST_SHOWN_MILLIS + deviceId, lastShownMillis) + } + } + + fun isNewLoginAlertShownForDevice(deviceId: String): Boolean { + return defaultPrefs.getBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, false) + } + + fun setNewLoginAlertShownForDevice(deviceId: String) { + defaultPrefs.edit { + putBoolean(SETTINGS_NEW_LOGIN_ALERT_SHOWN_FOR_DEVICE + deviceId, true) } } } diff --git a/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt new file mode 100644 index 0000000000..5d08499e32 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/ShouldShowUnverifiedSessionsAlertUseCaseTest.kt @@ -0,0 +1,75 @@ +/* + * 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.home + +import im.vector.app.config.Config +import im.vector.app.test.fakes.FakeClock +import im.vector.app.test.fakes.FakeVectorFeatures +import im.vector.app.test.fakes.FakeVectorPreferences +import org.amshove.kluent.shouldBe +import org.junit.Test + +private val AN_EPOCH = Config.SHOW_UNVERIFIED_SESSIONS_ALERT_AFTER_MILLIS.toLong() +private const val A_DEVICE_ID = "A_DEVICE_ID" + +class ShouldShowUnverifiedSessionsAlertUseCaseTest { + + private val fakeVectorFeatures = FakeVectorFeatures() + private val fakeVectorPreferences = FakeVectorPreferences() + private val fakeClock = FakeClock() + + private val shouldShowUnverifiedSessionsAlertUseCase = ShouldShowUnverifiedSessionsAlertUseCase( + vectorFeatures = fakeVectorFeatures, + vectorPreferences = fakeVectorPreferences.instance, + clock = fakeClock, + ) + + @Test + fun `given the feature is disabled then the use case returns false`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(false) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L) + + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe false + } + + @Test + fun `given the feature in enabled and there is not a saved preference then the use case returns true`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(0L) + fakeClock.givenEpoch(AN_EPOCH + 1) + + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe true + } + + @Test + fun `given the feature in enabled and last shown is a long time ago then the use case returns true`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH) + fakeClock.givenEpoch(AN_EPOCH * 2 + 1) + + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe true + } + + @Test + fun `given the feature in enabled and last shown is not a long time ago then the use case returns false`() { + fakeVectorFeatures.givenUnverifiedSessionsAlertEnabled(true) + fakeVectorPreferences.givenUnverifiedSessionsAlertLastShownMillis(AN_EPOCH) + fakeClock.givenEpoch(AN_EPOCH + 1) + + shouldShowUnverifiedSessionsAlertUseCase.execute(A_DEVICE_ID) shouldBe false + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt index d989abc214..c3c2fa684f 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeVectorFeatures.kt @@ -50,4 +50,8 @@ class FakeVectorFeatures : VectorFeatures by spyk() { fun givenVoiceBroadcast(isEnabled: Boolean) { every { isVoiceBroadcastEnabled() } returns isEnabled } + + fun givenUnverifiedSessionsAlertEnabled(isEnabled: Boolean) { + every { isUnverifiedSessionsAlertEnabled() } returns isEnabled + } } 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 d89764a77e..77df3ffc7a 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 @@ -56,4 +56,8 @@ class FakeVectorPreferences { fun givenSessionManagerShowIpAddress(showIpAddress: Boolean) { every { instance.showIpAddressInSessionManagerScreens() } returns showIpAddress } + + fun givenUnverifiedSessionsAlertLastShownMillis(lastShownMillis: Long) { + every { instance.getUnverifiedSessionsAlertLastShownMillis(any()) } returns lastShownMillis + } }