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 62928caa75..df12d49da6 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2653,8 +2653,12 @@ Encrypted by an unverified device Encrypted by a deleted 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 a9bae626a4..9102e938f8 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. */ @@ -95,4 +97,6 @@ object Config { val NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "NIGHTLY") val ER_NIGHTLY_ANALYTICS_CONFIG = RELEASE_ANALYTICS_CONFIG.copy(sentryEnvironment = "element-r") val ER_DEBUG_ANALYTICS_CONFIG = DEBUG_ANALYTICS_CONFIG.copy(sentryEnvironment = "element-r") + + 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 e56405638c..ef80b1b0d1 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.flow @@ -61,14 +59,19 @@ data class DeviceDetectionInfo( val currentSessionTrust: Boolean ) -class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted initialState: UnknownDevicesState, - session: Session, - private val vectorPreferences: VectorPreferences, - private val clock: Clock,) : - VectorViewModel(initialState) { +class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor( + @Assisted initialState: UnknownDevicesState, + session: Session, + 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,37 +89,35 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted } } - private val ignoredDeviceList = ArrayList() - init { - ignoredDeviceList.addAll( - vectorPreferences.getUnknownDeviceDismissedList().also { - Timber.v("## Detector - Remembered ignored list $it") - } - ) + val currentSessionTs = session.cryptoService().getCryptoDeviceInfo(session.myUserId) + .firstOrNull { it.deviceId == session.sessionParams.deviceId } + ?.firstTimeSeenLocalTs + ?: clock.epochMillis() + Timber.v("## Detector - Current Session first time seen $currentSessionTs") combine( session.flow().liveUserCryptoDevices(session.myUserId), session.flow().liveMyDevicesInfo(), session.flow().liveCrossSigningPrivateKeys(), - session.firstTimeDeviceSeen(), - ) { cryptoList, infoList, pInfo, firstTimeDeviceSeen -> + ) { cryptoList, infoList, pInfo -> // Timber.v("## Detector trigger ${cryptoList.map { "${it.deviceId} ${it.trustLevel}" }}") // Timber.v("## Detector trigger canCrossSign ${pInfo.get().selfSigned != null}") - Timber.v("## Detector - Current Session first time seen $firstTimeDeviceSeen") infoList .filter { info -> // filter verified session, by checking the crypto device info 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 > firstTimeDeviceSeen + 60_000, // short window to avoid false positive, + isNew, pInfo.getOrNull()?.selfSigned != null // adding this to pass distinct when cross sign change ) } @@ -148,30 +149,11 @@ class UnknownDeviceDetectorSharedViewModel @AssistedInject constructor(@Assisted 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() - } - - private fun Session.firstTimeDeviceSeen() = flow { - val value = cryptoService().getCryptoDeviceInfoList(myUserId) - .firstOrNull { it.deviceId == sessionParams.deviceId } - ?.firstTimeSeenLocalTs - ?: clock.epochMillis() - emit(value) - } } 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 f8f78332c0..696895d705 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. * @@ -1249,7 +1238,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 + } }