diff --git a/app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt b/app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt index a9d5dbba7c..cca20017c6 100644 --- a/app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt +++ b/app/src/androidTest/java/com/wire/android/SelfDeletionTimerTest.kt @@ -21,20 +21,43 @@ import androidx.test.platform.app.InstrumentationRegistry import com.wire.android.ui.home.conversations.SelfDeletionTimerHelper import com.wire.android.ui.home.conversations.model.ExpirationStatus import com.wire.kalium.logic.data.message.Message +import io.mockk.every +import io.mockk.mockkObject +import io.mockk.unmockkObject +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import org.junit.After +import org.junit.Before import org.junit.Test import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds class SelfDeletionTimerTest { - private val selfDeletionTimer = SelfDeletionTimerHelper( - context = InstrumentationRegistry.getInstrumentation().targetContext - ) + private val selfDeletionTimer by lazy { + SelfDeletionTimerHelper(context = InstrumentationRegistry.getInstrumentation().targetContext) + } + private val dispatcher = StandardTestDispatcher() + private fun currentTime(): Instant = Instant.fromEpochMilliseconds(dispatcher.scheduler.currentTime) + + @Before + fun setUp() { + mockkObject(SelfDeletionTimerHelper.Companion) + every { SelfDeletionTimerHelper.Companion.currentTime() } answers { currentTime() } + } + + @After + fun cleanUp() { + unmockkObject(SelfDeletionTimerHelper.Companion) + } @Test - fun givenTimeLeftIsAboveOneHour_whenGettingTheUpdateInterval_ThenIsEqualToMinutesLeftTillWholeHour() { + fun givenTimeLeftIsAboveOneHour_whenGettingTheUpdateInterval_ThenIsEqualToMinutesLeftTillWholeHour() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours + 30.minutes, @@ -47,7 +70,7 @@ class SelfDeletionTimerTest { } @Test - fun givenTimeLeftIsEqualToWholeHour_whenGettingTheUpdateInterval_ThenIsEqualToOneMinute() { + fun givenTimeLeftIsEqualToWholeHour_whenGettingTheUpdateInterval_ThenIsEqualToOneMinute() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours, @@ -60,7 +83,7 @@ class SelfDeletionTimerTest { } @Test - fun givenTimeLeftIsEqualToOneHour_whenGettingTheUpdateInterval_ThenIsEqualToOneMinute() { + fun givenTimeLeftIsEqualToOneHour_whenGettingTheUpdateInterval_ThenIsEqualToOneMinute() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.hours, @@ -73,7 +96,7 @@ class SelfDeletionTimerTest { } @Test - fun givenTimeLeftIsEqualToOneMinute_whenGettingTheUpdateInterval_ThenIsEqualToOneSeconds() { + fun givenTimeLeftIsEqualToOneMinute_whenGettingTheUpdateInterval_ThenIsEqualToOneSeconds() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.minutes, @@ -85,6 +108,19 @@ class SelfDeletionTimerTest { assert(interval == 1.seconds) } + @Test + fun givenTimeLeftIsEqualTo1Min10SecAnd900Millis_whenGettingTheUpdateInterval_ThenIsEqualTo10SecAnd900Millis() = runTest(dispatcher) { + val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( + ExpirationStatus.Expirable( + expireAfter = 1.minutes + 10.seconds + 900.milliseconds, + selfDeletionStatus = Message.ExpirationData.SelfDeletionStatus.NotStarted + ) + ) + assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) + val interval = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).updateInterval() + assert(interval == 10.seconds + 900.milliseconds) + } + @Test fun givenTimeLeftIsEqualToThirtySeconds_whenGettingTheUpdateInterval_ThenIsEqualToOneSeconds() { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( @@ -99,7 +135,7 @@ class SelfDeletionTimerTest { } @Test - fun givenTimeLeftIsEqualToFiftyDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() { + fun givenTimeLeftIsEqualToFiftyDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 50.days, @@ -107,12 +143,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "4 weeks left") } @Test - fun givenTimeLeftIsEqualToTwentySevenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() { + fun givenTimeLeftIsEqualToTwentySevenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 27.days, @@ -120,12 +156,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "4 weeks left") } @Test - fun givenTimeLeftIsEqualToTwentySevenDaysAndTwelveHours_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() { + fun givenTimeLeftIsEqualTo27DaysAnd12Hours_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 27.days + 12.hours, @@ -133,12 +169,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "4 weeks left") } @Test - fun givenTimeLeftIsEqualToTwentySevenDaysAndOneSecond_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() { + fun givenTimeLeftIsEqualTo27DaysAnd1Second_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 27.days + 1.seconds, @@ -146,12 +182,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "4 weeks left") } @Test - fun givenTimeLeftIsEqualToTwentyEightDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() { + fun givenTimeLeftIsEqualTo28Days_whenGettingThTimeLeftFormatted_ThenIsEqualToFourWeeksLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 28.days, @@ -159,12 +195,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "4 weeks left") } @Test - fun givenTimeLeftIsEqualToTwentyOneDays_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyOneLeft() { + fun givenTimeLeftIsEqualTo21Days_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyOneLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 21.days, @@ -172,12 +208,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "21 days left") } @Test - fun givenTimeLeftIsEqualToFourTeenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToFourTeenDaysLeft() { + fun givenTimeLeftIsEqualTo14Days_whenGettingThTimeLeftFormatted_ThenIsEqualToFourTeenDaysLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 14.days, @@ -185,12 +221,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "14 days left") } @Test - fun givenTimeLeftIsEqualToTwentyDays_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyDaysLeft() { + fun givenTimeLeftIsEqualTo20Days_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyDaysLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 20.days, @@ -198,12 +234,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "20 days left") } @Test - fun givenTimeLeftIsEqualToSevenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() { + fun givenTimeLeftIsEqualToSevenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 7.days, @@ -211,12 +247,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 week left") } @Test - fun givenTimeLeftIsEqualToSixDays_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() { + fun givenTimeLeftIsEqualToSixDays_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 6.days, @@ -224,12 +260,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 week left") } @Test - fun givenTimeLeftIsEqualToSixDaysAnd12Hours_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() { + fun givenTimeLeftIsEqualToSixDaysAnd12Hours_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 6.days + 12.hours, @@ -237,12 +273,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 week left") } @Test - fun givenTimeLeftIsEqualToSixDaysAndOneSecond_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() { + fun givenTimeLeftIsEqualToSixDaysAndOneSecond_whenGettingThTimeLeftFormatted_ThenIsEqualToOneWeekLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 6.days + 1.seconds, @@ -250,12 +286,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 week left") } @Test - fun givenTimeLeftIsEqualToThirteenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToThirteenDays() { + fun givenTimeLeftIsEqualToThirteenDays_whenGettingThTimeLeftFormatted_ThenIsEqualToThirteenDays() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 13.days, @@ -263,12 +299,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "13 days left") } @Test - fun givenTimeLeftIsEqualToOneDay_whenGettingThTimeLeftFormatted_ThenIsEqualToOneDayLeft() { + fun givenTimeLeftIsEqualToOneDay_whenGettingThTimeLeftFormatted_ThenIsEqualToOneDayLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.days, @@ -276,12 +312,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 day left") } @Test - fun givenTimeLeftIsEqualToTwentyFourHours_whenGettingThTimeLeftFormatted_ThenIsEqualToOneDayLeft() { + fun givenTimeLeftIsEqualToTwentyFourHours_whenGettingThTimeLeftFormatted_ThenIsEqualToOneDayLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 24.hours, @@ -289,12 +325,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 day left") } @Test - fun givenTimeLeftIsEqualToTwentyThreeHours_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyThreeHourLeft() { + fun givenTimeLeftIsEqualToTwentyThreeHours_whenGettingThTimeLeftFormatted_ThenIsEqualToTwentyThreeHourLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours, @@ -302,12 +338,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "23 hours left") } @Test - fun givenTimeLeftIsEqualToSixtyMinutes_whenGettingThTimeLeftFormatted_ThenIsEqualToOneHourLeft() { + fun givenTimeLeftIsEqualToSixtyMinutes_whenGettingThTimeLeftFormatted_ThenIsEqualToOneHourLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 60.minutes, @@ -315,12 +351,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 hour left") } @Test - fun givenTimeLeftIsEqualToOneMinute_whenGettingThTimeLeftFormatted_ThenIsEqualToOneMinuteLeft() { + fun givenTimeLeftIsEqualToOneMinute_whenGettingThTimeLeftFormatted_ThenIsEqualToOneMinuteLeft() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.minutes, @@ -328,12 +364,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 minute left") } @Test - fun givenTimeLeftIsEqualToOFiftyNineMinutes_whenGettingThTimeLeftFormatted_ThenIsEqualToFiftyNineMinutes() { + fun givenTimeLeftIsEqualToOFiftyNineMinutes_whenGettingThTimeLeftFormatted_ThenIsEqualToFiftyNineMinutes() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 59.minutes, @@ -341,12 +377,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "59 minutes left") } @Test - fun givenTimeLeftIsEqualToSixtySeconds_whenGettingThTimeLeftFormatted_ThenIsEqualToOneMinute() { + fun givenTimeLeftIsEqualToSixtySeconds_whenGettingThTimeLeftFormatted_ThenIsEqualToOneMinute() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 60.seconds, @@ -354,12 +390,12 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted() + val timeLeftLabel = (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).timeLeftFormatted assert(timeLeftLabel == "1 minute left") } @Test - fun givenTimeLeftIsEqualToOneDayAndTwelveHours_whenDecreasingTimeWithInterval_thenTimeLeftIsEqualToExpecetedTimeLeft() { + fun givenTimeLeftIs1DayAnd12Hours_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.days + 12.hours, @@ -367,17 +403,19 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - assert(selfDeletionTimer.timeLeftFormatted() == "1 day left") - - selfDeletionTimer.decreaseTimeLeft(selfDeletionTimer.updateInterval()) - assert(selfDeletionTimer.timeLeftFormatted() == "23 hours left") + with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "1 day left") + + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "23 hours left") + } } @Test - fun givenTimeLeftIsEqualToTwentyThreeHoursAndTwentyThreeMinutes_whenDecreasingTimeWithInterval_thenTimeLeftIsEqualToExpeceted() { + fun givenTimeLeftIs23HoursAnd23Minutes_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 23.hours + 23.minutes, @@ -385,16 +423,15 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - - val timeLeftLabel = selfDeletionTimer.timeLeftFormatted() - assert(timeLeftLabel == "23 hours left") + with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "23 hours left") + } } @Test - fun givenTimeLeftIsEqualToOneHourAndTwelveMinutes_whenDecreasingTimeWithInterval_thenTimeLeftIsEqualToExpecetedTimeLeft() { + fun givenTimeLeftIs1HourAnd12Minutes_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.hours + 12.minutes, @@ -402,18 +439,19 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - assert(selfDeletionTimer.timeLeftFormatted() == "1 hour left") - selfDeletionTimer.decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - assert(selfDeletionTimer.timeLeftFormatted() == "59 minutes left") + with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "1 hour left") + + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "59 minutes left") + } } @Test - fun givenTimeLeftIsEqualToOneHourAndTwentyThreeSeconds_whenDecreasingTimeWithInterval_thenTimeLeftIsEqualToExpecetedTimeLeft() { + fun givenTimeLeftIs1HourAnd23Seconds_whenRecalculatingTimeAfterIntervals_thenTimeLeftIsEqualToExpected() = runTest(dispatcher) { val selfDeletionTimer = selfDeletionTimer.fromExpirationStatus( ExpirationStatus.Expirable( expireAfter = 1.minutes + 23.seconds, @@ -421,13 +459,14 @@ class SelfDeletionTimerTest { ) ) assert(selfDeletionTimer is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) - (selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable).decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - assert(selfDeletionTimer.timeLeftFormatted() == "1 minute left") - selfDeletionTimer.decreaseTimeLeft( - selfDeletionTimer.updateInterval() - ) - assert(selfDeletionTimer.timeLeftFormatted() == "59 seconds left") + with(selfDeletionTimer as SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "1 minute left") + + advanceTimeBy(updateInterval()) + recalculateTimeLeft() + assert(selfDeletionTimer.timeLeftFormatted == "59 seconds left") + } } } diff --git a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt index 954fe2a4a7..944b629225 100644 --- a/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt +++ b/app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt @@ -22,11 +22,13 @@ import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.di.KaliumCoreLogic import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager +import com.wire.android.util.CurrentScreenManager import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.logout.LogoutReason import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.auth.LogoutCallback +import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -35,8 +37,12 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @@ -52,6 +58,7 @@ class GlobalObserversManager @Inject constructor( private val notificationManager: WireNotificationManager, private val notificationChannelsManager: NotificationChannelsManager, private val userDataStoreProvider: UserDataStoreProvider, + private val currentScreenManager: CurrentScreenManager, ) { private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.io()) @@ -65,6 +72,7 @@ class GlobalObserversManager @Inject constructor( } } scope.handleLogouts() + scope.handleDeleteEphemeralMessageEndDate() } private suspend fun setUpNotifications() { @@ -114,4 +122,21 @@ class GlobalObserversManager @Inject constructor( awaitClose { coreLogic.getGlobalScope().logoutCallbackManager.unregister(callback) } }.launchIn(this) } + + private fun CoroutineScope.handleDeleteEphemeralMessageEndDate() { + launch { + currentScreenManager.isAppVisibleFlow() + .flatMapLatest { isAppVisible -> + if (isAppVisible) { + coreLogic.getGlobalScope().session.currentSessionFlow() + .distinctUntilChanged() + .filter { it is CurrentSessionResult.Success && it.accountInfo.isValid() } + .map { (it as CurrentSessionResult.Success).accountInfo.userId } + } else { + emptyFlow() + } + } + .collect { userId -> coreLogic.getSessionScope(userId).messages.deleteEphemeralMessageEndDate() } + } + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt index 5eadf4f0cf..94f40bd33a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageExpiration.kt @@ -19,13 +19,18 @@ package com.wire.android.ui.home.conversations import android.content.Context import android.content.res.Resources +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle import com.wire.android.R import com.wire.android.ui.home.conversations.model.ExpirationStatus import com.wire.android.ui.home.conversations.model.UIMessage @@ -33,12 +38,15 @@ import com.wire.android.ui.home.conversations.model.UIMessageContent import com.wire.kalium.logic.data.message.Message import kotlinx.coroutines.delay import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.toDuration @Composable fun rememberSelfDeletionTimer(expirationStatus: ExpirationStatus): SelfDeletionTimerHelper.SelfDeletionTimerState { @@ -54,12 +62,11 @@ class SelfDeletionTimerHelper(private val context: Context) { fun fromExpirationStatus(expirationStatus: ExpirationStatus): SelfDeletionTimerState { return if (expirationStatus is ExpirationStatus.Expirable) { with(expirationStatus) { - val timeLeft = calculateTimeLeft(selfDeletionStatus, expireAfter) + val expireAt = calculateExpireAt(selfDeletionStatus, expireAfter) SelfDeletionTimerState.Expirable( context.resources, - timeLeft, expireAfter, - selfDeletionStatus is Message.ExpirationData.SelfDeletionStatus.Started + expireAt, ) } } else { @@ -67,35 +74,22 @@ class SelfDeletionTimerHelper(private val context: Context) { } } - private fun calculateTimeLeft( + private fun calculateExpireAt( selfDeletionStatus: Message.ExpirationData.SelfDeletionStatus?, - expireAfter: Duration - ): Duration { - return if (selfDeletionStatus is Message.ExpirationData.SelfDeletionStatus.Started) { - val timeElapsedSinceSelfDeletionStartDate = Clock.System.now() - selfDeletionStatus.selfDeletionStartDate - val timeLeft = expireAfter - timeElapsedSinceSelfDeletionStartDate - - /** - * time left for deletion, can be a negative value if the time difference between the self deletion start date and - * Clock.System.now() is greater then [expireAfter], we normalize it to 0 seconds - */ - if (timeLeft.isNegative()) { - ZERO - } else { - timeLeft - } - } else { - expireAfter + expireAfter: Duration, + ) = + if (selfDeletionStatus is Message.ExpirationData.SelfDeletionStatus.Started) selfDeletionStatus.selfDeletionStartDate + expireAfter + else { + val currentTime = currentTime() + currentTime + expireAfter } - } sealed class SelfDeletionTimerState { class Expirable( private val resources: Resources, - timeLeft: Duration, private val expireAfter: Duration, - val timerStarted: Boolean + private val expireAt: Instant, ) : SelfDeletionTimerState() { companion object { /** @@ -115,67 +109,69 @@ class SelfDeletionTimerHelper(private val context: Context) { private const val OPAQUE_BACKGROUND_COLOR_ALPHA_VALUE = 1F } - var timeLeft by mutableStateOf(timeLeft) - + var timeLeft by mutableStateOf(calculateTimeLeft()) + private set @Suppress("MagicNumber", "ComplexMethod") - fun timeLeftFormatted(): String = when { - timeLeft > 28.days -> - resources.getQuantityString( - R.plurals.weeks_left, - 4, - 4 - ) - // 4 weeks - timeLeft >= 27.days && timeLeft <= 28.days -> - resources.getQuantityString( - R.plurals.weeks_left, - 4, - 4 - ) - // days below 4 weeks - timeLeft <= 27.days && timeLeft > 7.days -> - resources.getQuantityString( - R.plurals.days_left, - timeLeft.inWholeDays.toInt(), - timeLeft.inWholeDays.toInt() - ) - // one week - timeLeft >= 6.days && timeLeft <= 7.days -> - resources.getQuantityString( - R.plurals.weeks_left, - 1, - 1 - ) - // days below 1 week - timeLeft < 7.days && timeLeft >= 1.days -> - resources.getQuantityString( - R.plurals.days_left, - timeLeft.inWholeDays.toInt(), - timeLeft.inWholeDays.toInt() - ) - // hours below one day - timeLeft >= 1.hours && timeLeft < 24.hours -> - resources.getQuantityString( - R.plurals.hours_left, - timeLeft.inWholeHours.toInt(), - timeLeft.inWholeHours.toInt() - ) - // minutes below hour - timeLeft >= 1.minutes && timeLeft < 60.minutes -> - resources.getQuantityString( - R.plurals.minutes_left, - timeLeft.inWholeMinutes.toInt(), - timeLeft.inWholeMinutes.toInt() - ) - // seconds below minute - timeLeft < 60.seconds -> - resources.getQuantityString( - R.plurals.seconds_left, - timeLeft.inWholeSeconds.toInt(), - timeLeft.inWholeSeconds.toInt() - ) + val timeLeftFormatted: String by derivedStateOf { + when { + timeLeft > 28.days -> + resources.getQuantityString( + R.plurals.weeks_left, + 4, + 4 + ) + // 4 weeks + timeLeft >= 27.days && timeLeft <= 28.days -> + resources.getQuantityString( + R.plurals.weeks_left, + 4, + 4 + ) + // days below 4 weeks + timeLeft <= 27.days && timeLeft > 7.days -> + resources.getQuantityString( + R.plurals.days_left, + timeLeft.inWholeDays.toInt(), + timeLeft.inWholeDays.toInt() + ) + // one week + timeLeft >= 6.days && timeLeft <= 7.days -> + resources.getQuantityString( + R.plurals.weeks_left, + 1, + 1 + ) + // days below 1 week + timeLeft < 7.days && timeLeft >= 1.days -> + resources.getQuantityString( + R.plurals.days_left, + timeLeft.inWholeDays.toInt(), + timeLeft.inWholeDays.toInt() + ) + // hours below one day + timeLeft >= 1.hours && timeLeft < 24.hours -> + resources.getQuantityString( + R.plurals.hours_left, + timeLeft.inWholeHours.toInt(), + timeLeft.inWholeHours.toInt() + ) + // minutes below hour + timeLeft >= 1.minutes && timeLeft < 60.minutes -> + resources.getQuantityString( + R.plurals.minutes_left, + timeLeft.inWholeMinutes.toInt(), + timeLeft.inWholeMinutes.toInt() + ) + // seconds below minute + timeLeft < 60.seconds -> + resources.getQuantityString( + R.plurals.seconds_left, + timeLeft.inWholeSeconds.toInt(), + timeLeft.inWholeSeconds.toInt() + ) - else -> throw IllegalStateException("Not possible state for a time left label") + else -> throw IllegalStateException("Not possible state for a time left label") + } } /** @@ -186,48 +182,41 @@ class SelfDeletionTimerHelper(private val context: Context) { * updated every second. * @return how long until the next timer update. */ - fun updateInterval(): Duration { - val timeLeftUpdateInterval = when { - timeLeft > 24.hours -> { - val timeLeftTillWholeDay = (timeLeft.inWholeMinutes % 1.days.inWholeMinutes).minutes - if (timeLeftTillWholeDay == ZERO) { - 1.days - } else { - timeLeftTillWholeDay - } - } - - timeLeft <= 24.hours && timeLeft > 1.hours -> { - val timeLeftTillWholeHour = (timeLeft.inWholeSeconds % 1.hours.inWholeSeconds).seconds - if (timeLeftTillWholeHour == ZERO) { - 1.hours - } else { - timeLeftTillWholeHour - } - } - - timeLeft <= 1.hours && timeLeft > 1.minutes -> { - val timeLeftTillWholeMinute = (timeLeft.inWholeSeconds % 1.minutes.inWholeSeconds).seconds - if (timeLeftTillWholeMinute == ZERO) { - 1.minutes - } else { - timeLeftTillWholeMinute - } - } - - timeLeft <= 1.minutes -> { - 1.seconds - } + @VisibleForTesting + internal fun updateInterval(): Duration { + fun remainingTimeToDurationUnit(durationUnit: DurationUnit): Duration { + /* + * Function toLong returns the whole part for the given duration unit and then this whole value is converted back to + * Duration and subtracted from the original duration, which gives the remaining time to the next full duration unit. + * + * For example, if the time left is "1 day and 1 hour" and durationUnit is DAYS, then toLong will return 1L + * which means "1 full day" (just like .inWholeDays) and then it will be converted back to Duration type. + * Then this "1 day" will be subtracted from the original duration, returning "1 hour" left ("1d 1h" - "1d" = "1h"). + * So in this case it's the same as `timeLeft - timeLeft.inWholeHours.hours` + * because `timeLeft.inWholeDays` is basically `timeLeft.toLong(DurationUnit.DAYS)` + * and `1L.days` is the same as `1L.toDuration(DurationUnit.DAYS)`. + */ + val timeLeftForDurationUnit = timeLeft - timeLeft.toLong(durationUnit).toDuration(durationUnit) + return if (timeLeftForDurationUnit == ZERO) 1.toDuration(durationUnit) + else timeLeftForDurationUnit + } + val timeLeftUpdateInterval = when { + timeLeft > 24.hours -> remainingTimeToDurationUnit(DurationUnit.DAYS) + timeLeft <= 24.hours && timeLeft > 1.hours -> remainingTimeToDurationUnit(DurationUnit.HOURS) + timeLeft <= 1.hours && timeLeft > 1.minutes -> remainingTimeToDurationUnit(DurationUnit.MINUTES) + timeLeft <= 1.minutes -> remainingTimeToDurationUnit(DurationUnit.SECONDS) else -> throw IllegalStateException("Not possible state for the interval") } return timeLeftUpdateInterval } - fun decreaseTimeLeft(interval: Duration) { - if (timeLeft.inWholeSeconds != 0L) timeLeft -= interval - } + // non-negative value, returns ZERO if message is already expired + private fun calculateTimeLeft(): Duration = (expireAt - currentTime()).let { if (it.isNegative()) ZERO else it } + + @VisibleForTesting + internal fun recalculateTimeLeft() { timeLeft = calculateTimeLeft() } /** * if the time elapsed ratio is between 0.50 and 0.75 @@ -266,72 +255,80 @@ class SelfDeletionTimerHelper(private val context: Context) { OPAQUE_BACKGROUND_COLOR_ALPHA_VALUE } } - } - object NotExpirable : SelfDeletionTimerState() - } -} + @Composable + fun startDeletionTimer(message: UIMessage, onStartMessageSelfDeletion: (UIMessage) -> Unit) { + when (val messageContent = message.messageContent) { + is UIMessageContent.AssetMessage -> startAssetDeletion( + onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, + downloadStatus = messageContent.downloadStatus + ) -@Composable -fun startDeletionTimer( - message: UIMessage, - expirableTimer: SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable, - onStartMessageSelfDeletion: (UIMessage) -> Unit -) { - when (val messageContent = message.messageContent) { - is UIMessageContent.AssetMessage -> startAssetDeletion( - expirableTimer = expirableTimer, - onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, - downloadStatus = messageContent.downloadStatus - ) + is UIMessageContent.AudioAssetMessage -> startAssetDeletion( + onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, + downloadStatus = messageContent.downloadStatus + ) - is UIMessageContent.AudioAssetMessage -> startAssetDeletion( - expirableTimer = expirableTimer, - onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, - downloadStatus = messageContent.downloadStatus - ) + is UIMessageContent.ImageMessage -> startAssetDeletion( + onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, + downloadStatus = messageContent.downloadStatus + ) - is UIMessageContent.ImageMessage -> startAssetDeletion( - expirableTimer = expirableTimer, - onSelfDeletingMessageRead = { onStartMessageSelfDeletion(message) }, - downloadStatus = messageContent.downloadStatus - ) + else -> startRegularDeletion(message = message, onStartMessageSelfDeletion = onStartMessageSelfDeletion) + } + } - else -> { - LaunchedEffect(Unit) { - onStartMessageSelfDeletion(message) + @Composable + private fun startAssetDeletion(onSelfDeletingMessageRead: () -> Unit, downloadStatus: Message.DownloadStatus) { + LaunchedEffect(downloadStatus) { + if (downloadStatus == Message.DownloadStatus.SAVED_EXTERNALLY + || downloadStatus == Message.DownloadStatus.SAVED_INTERNALLY + ) { + onSelfDeletingMessageRead() + } + } + LaunchedEffect(key1 = timeLeft, key2 = downloadStatus) { + if (downloadStatus == Message.DownloadStatus.SAVED_EXTERNALLY + || downloadStatus == Message.DownloadStatus.SAVED_INTERNALLY + ) { + if (timeLeft != ZERO) { + delay(updateInterval()) + recalculateTimeLeft() + } + } + } + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + recalculateTimeLeft() + } + } } - LaunchedEffect(expirableTimer.timeLeft) { - with(expirableTimer) { + + @Composable + private fun startRegularDeletion(message: UIMessage, onStartMessageSelfDeletion: (UIMessage) -> Unit) { + LaunchedEffect(Unit) { + onStartMessageSelfDeletion(message) + } + LaunchedEffect(timeLeft) { if (timeLeft != ZERO) { delay(updateInterval()) - decreaseTimeLeft(updateInterval()) + recalculateTimeLeft() + } + } + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(lifecycleOwner) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + recalculateTimeLeft() } } } } - } -} -@Composable -private fun startAssetDeletion( - expirableTimer: SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable, - onSelfDeletingMessageRead: () -> Unit, - downloadStatus: Message.DownloadStatus -) { - LaunchedEffect(downloadStatus) { - if (downloadStatus == Message.DownloadStatus.SAVED_EXTERNALLY || downloadStatus == Message.DownloadStatus.SAVED_INTERNALLY) { - onSelfDeletingMessageRead() - } + object NotExpirable : SelfDeletionTimerState() } - LaunchedEffect(expirableTimer.timeLeft, downloadStatus) { - if (downloadStatus == Message.DownloadStatus.SAVED_EXTERNALLY || downloadStatus == Message.DownloadStatus.SAVED_INTERNALLY) { - with(expirableTimer) { - if (timeLeft != ZERO) { - delay(updateInterval()) - decreaseTimeLeft(updateInterval()) - } - } - } + + companion object { + fun currentTime(): Instant = Clock.System.now() } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt index 0154d6b0e2..0d13266fdb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/MessageItem.kt @@ -125,9 +125,8 @@ fun MessageItem( !message.isPending && !message.sendingFailed ) { - startDeletionTimer( + selfDeletionTimerState.startDeletionTimer( message = message, - expirableTimer = selfDeletionTimerState, onStartMessageSelfDeletion = onSelfDeletingMessageRead ) } @@ -226,7 +225,7 @@ fun MessageItem( MessageAuthorRow(messageHeader = message.header) } if (selfDeletionTimerState is SelfDeletionTimerHelper.SelfDeletionTimerState.Expirable) { - MessageExpireLabel(messageContent, selfDeletionTimerState.timeLeftFormatted()) + MessageExpireLabel(messageContent, selfDeletionTimerState.timeLeftFormatted) // if the message is marked as deleted and is [SelfDeletionTimer.SelfDeletionTimerState.Expirable] // the deletion responsibility belongs to the receiver, therefore we need to wait for the receiver diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt index 69dbac5789..5e2686e992 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/SystemMessageItem.kt @@ -97,9 +97,8 @@ fun SystemMessageItem( !message.isPending && !message.sendingFailed ) { - startDeletionTimer( + selfDeletionTimerState.startDeletionTimer( message = message, - expirableTimer = selfDeletionTimerState, onStartMessageSelfDeletion = onSelfDeletingMessageRead ) } diff --git a/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt b/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt index 3e75aac6c0..ecb71639f6 100644 --- a/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/GlobalObserversManagerTest.kt @@ -24,10 +24,18 @@ import com.wire.android.datastore.UserDataStoreProvider import com.wire.android.framework.TestUser import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager +import com.wire.android.util.CurrentScreenManager +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.auth.PersistentWebSocketStatus +import com.wire.kalium.logic.data.logout.LogoutReason import com.wire.kalium.logic.data.team.Team import com.wire.kalium.logic.data.user.SelfUser +import com.wire.kalium.logic.feature.UserSessionScope +import com.wire.kalium.logic.feature.auth.LogoutCallbackManager +import com.wire.kalium.logic.feature.message.MessageScope +import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSocketConnectionStatusUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -35,6 +43,7 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -87,6 +96,56 @@ class GlobalObserversManagerTest { } } + @Test + fun `given app visible and valid session, when handling ephemeral messages, then call deleteEphemeralMessageEndDate`() { + val (arrangement, manager) = Arrangement() + .withCurrentSessionFlow(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.SELF_USER.id))) + .withAppVisibleFlow(true) + .arrange() + manager.observe() + coVerify(exactly = 1) { arrangement.messageScope.deleteEphemeralMessageEndDate() } + } + + @Test + fun `given app not visible and valid session, when handling ephemeral messages, then do not call deleteEphemeralMessageEndDate`() { + val (arrangement, manager) = Arrangement() + .withCurrentSessionFlow(CurrentSessionResult.Success(AccountInfo.Valid(TestUser.SELF_USER.id))) + .withAppVisibleFlow(false) + .arrange() + manager.observe() + coVerify(exactly = 0) { arrangement.messageScope.deleteEphemeralMessageEndDate() } + } + + @Test + fun `given app visible and invalid session, when handling ephemeral messages, then do not call deleteEphemeralMessageEndDate`() { + val (arrangement, manager) = Arrangement() + .withCurrentSessionFlow(CurrentSessionResult.Success(AccountInfo.Invalid(TestUser.SELF_USER.id, LogoutReason.DELETED_ACCOUNT))) + .withAppVisibleFlow(true) + .arrange() + manager.observe() + coVerify(exactly = 0) { arrangement.messageScope.deleteEphemeralMessageEndDate() } + } + + @Test + fun `given app visible and no session, when handling ephemeral messages, then do not call deleteEphemeralMessageEndDate`() { + val (arrangement, manager) = Arrangement() + .withCurrentSessionFlow(CurrentSessionResult.Failure.SessionNotFound) + .withAppVisibleFlow(true) + .arrange() + manager.observe() + coVerify(exactly = 0) { arrangement.messageScope.deleteEphemeralMessageEndDate() } + } + + @Test + fun `given app visible and session failure, when handling ephemeral messages, then do not call deleteEphemeralMessageEndDate`() { + val (arrangement, manager) = Arrangement() + .withCurrentSessionFlow(CurrentSessionResult.Failure.Generic(CoreFailure.Unknown(RuntimeException("error")))) + .withAppVisibleFlow(true) + .arrange() + manager.observe() + coVerify(exactly = 0) { arrangement.messageScope.deleteEphemeralMessageEndDate() } + } + private class Arrangement { @MockK @@ -101,6 +160,18 @@ class GlobalObserversManagerTest { @MockK lateinit var userDataStoreProvider: UserDataStoreProvider + @MockK + lateinit var currentScreenManager: CurrentScreenManager + + @MockK + lateinit var logoutCallbackManager: LogoutCallbackManager + + @MockK + lateinit var userSessionScope: UserSessionScope + + @MockK + lateinit var messageScope: MessageScope + private val manager by lazy { GlobalObserversManager( dispatcherProvider = TestDispatcherProvider(), @@ -108,6 +179,7 @@ class GlobalObserversManagerTest { notificationChannelsManager = notificationChannelsManager, notificationManager = notificationManager, userDataStoreProvider = userDataStoreProvider, + currentScreenManager = currentScreenManager, ) } @@ -118,6 +190,14 @@ class GlobalObserversManagerTest { // Default empty values mockUri() every { notificationChannelsManager.createUserNotificationChannels(any()) } returns Unit + every { coreLogic.getGlobalScope().logoutCallbackManager } returns logoutCallbackManager + every { coreLogic.getSessionScope(any()) } returns userSessionScope + every { userSessionScope.messages } returns messageScope + coEvery { messageScope.deleteEphemeralMessageEndDate() } returns Unit + withPersistentWebSocketConnectionStatuses(emptyList()) + withValidAccounts(emptyList()) + withCurrentSessionFlow(CurrentSessionResult.Failure.SessionNotFound) + withAppVisibleFlow(true) } fun withValidAccounts(list: List>): Arrangement = apply { @@ -129,6 +209,14 @@ class GlobalObserversManagerTest { ObservePersistentWebSocketConnectionStatusUseCase.Result.Success(flowOf(list)) } + fun withCurrentSessionFlow(result: CurrentSessionResult): Arrangement = apply { + coEvery { coreLogic.getGlobalScope().session.currentSessionFlow() } returns flowOf(result) + } + + fun withAppVisibleFlow(isVisible: Boolean) = apply { + coEvery { currentScreenManager.isAppVisibleFlow() } returns MutableStateFlow(isVisible) + } + fun arrange() = this to manager } }