From 32dd8804f29d615e87650550323a8a3f961ef569 Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Tue, 15 Oct 2024 08:22:08 -0600 Subject: [PATCH] feat(iOS): forget predictions if backgrounded for over ten minutes (#461) * feat: forget predictions if backgrounded for over ten minutes * fix the tests * fix a test that was working locally --------- Co-authored-by: Kayla Brady <31781298+KaylaBrady@users.noreply.github.com> --- .../nearbyTransit/NearbyTransitPageTest.kt | 2 ++ .../nearbyTransit/NearbyTransitViewTest.kt | 2 ++ .../stopDetails/StopDetailsViewTest.kt | 2 ++ .../util/SubscribeToPredictionsTest.kt | 2 ++ .../NearbyTransit/NearbyTransitView.swift | 9 ++++- .../Pages/StopDetails/StopDetailsPage.swift | 19 ++++++++--- .../Pages/TripDetails/TripDetailsPage.swift | 21 ++++++++---- .../TripDetails/TripDetailsPageTests.swift | 4 +++ .../mbta_app/endToEnd/EndToEndRepositories.kt | 4 +++ .../repositories/PredictionsRepository.kt | 9 +++++ .../repositories/TripPredictionsRepository.kt | 9 +++++ .../PredictionsRepositoryTests.kt | 34 +++++++++++++++++++ 12 files changed, 105 insertions(+), 12 deletions(-) diff --git a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/nearbyTransit/NearbyTransitPageTest.kt b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/nearbyTransit/NearbyTransitPageTest.kt index 043b6827b..13c1cec63 100644 --- a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/nearbyTransit/NearbyTransitPageTest.kt +++ b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/nearbyTransit/NearbyTransitPageTest.kt @@ -195,6 +195,8 @@ class NearbyTransitPageTest : KoinTest { override var lastUpdated: Instant? = null + override fun shouldForgetPredictions(predictionCount: Int) = false + override fun disconnect() { /* no-op */ } diff --git a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/nearbyTransit/NearbyTransitViewTest.kt b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/nearbyTransit/NearbyTransitViewTest.kt index 2a5ed4700..199fa233b 100644 --- a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/nearbyTransit/NearbyTransitViewTest.kt +++ b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/nearbyTransit/NearbyTransitViewTest.kt @@ -189,6 +189,8 @@ class NearbyTransitViewTest : KoinTest { override var lastUpdated: Instant? = null + override fun shouldForgetPredictions(predictionCount: Int) = false + override fun disconnect() { /* no-op */ } diff --git a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/stopDetails/StopDetailsViewTest.kt b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/stopDetails/StopDetailsViewTest.kt index 0889a5f13..3dc169b64 100644 --- a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/stopDetails/StopDetailsViewTest.kt +++ b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/stopDetails/StopDetailsViewTest.kt @@ -135,6 +135,8 @@ class StopDetailsViewTest { override var lastUpdated: Instant? = null + override fun shouldForgetPredictions(predictionCount: Int) = false + override fun disconnect() { /* no-op */ } diff --git a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/util/SubscribeToPredictionsTest.kt b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/util/SubscribeToPredictionsTest.kt index 2b9864f06..0cf397918 100644 --- a/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/util/SubscribeToPredictionsTest.kt +++ b/androidApp/src/androidTest/java/com/mbta/tid/mbta_app/android/util/SubscribeToPredictionsTest.kt @@ -59,6 +59,8 @@ class SubscribeToPredictionsTest { override var lastUpdated: Instant? = null + override fun shouldForgetPredictions(predictionCount: Int) = false + override fun disconnect() { check(isConnected) { "called disconnect when not connected" } isConnected = false diff --git a/iosApp/iosApp/Pages/NearbyTransit/NearbyTransitView.swift b/iosApp/iosApp/Pages/NearbyTransit/NearbyTransitView.swift index b2c788d51..3a227ca66 100644 --- a/iosApp/iosApp/Pages/NearbyTransit/NearbyTransitView.swift +++ b/iosApp/iosApp/Pages/NearbyTransit/NearbyTransitView.swift @@ -88,7 +88,14 @@ struct NearbyTransitView: View { } } .withScenePhaseHandlers( - onActive: { joinPredictions(state.nearbyByRouteAndStop?.stopIds()) }, + onActive: { + if let predictionsByStop, + predictionsRepository + .shouldForgetPredictions(predictionCount: predictionsByStop.predictionQuantity()) { + self.predictionsByStop = nil + } + joinPredictions(state.nearbyByRouteAndStop?.stopIds()) + }, onInactive: leavePredictions, onBackground: { leavePredictions() diff --git a/iosApp/iosApp/Pages/StopDetails/StopDetailsPage.swift b/iosApp/iosApp/Pages/StopDetails/StopDetailsPage.swift index f4a30c8fd..3e1b9b5cd 100644 --- a/iosApp/iosApp/Pages/StopDetails/StopDetailsPage.swift +++ b/iosApp/iosApp/Pages/StopDetails/StopDetailsPage.swift @@ -106,11 +106,20 @@ struct StopDetailsPage: View { .onDisappear { leavePredictions() } - .withScenePhaseHandlers(onActive: { joinPredictions(stop) }, - onInactive: leavePredictions, - onBackground: { leavePredictions() - isReturningFromBackground = true - }) + .withScenePhaseHandlers( + onActive: { + if let predictionsByStop, + predictionsRepository + .shouldForgetPredictions(predictionCount: predictionsByStop.predictionQuantity()) { + self.predictionsByStop = nil + } + joinPredictions(stop) + }, + onInactive: leavePredictions, + onBackground: { leavePredictions() + isReturningFromBackground = true + } + ) } } diff --git a/iosApp/iosApp/Pages/TripDetails/TripDetailsPage.swift b/iosApp/iosApp/Pages/TripDetails/TripDetailsPage.swift index a4855596f..38601b21a 100644 --- a/iosApp/iosApp/Pages/TripDetails/TripDetailsPage.swift +++ b/iosApp/iosApp/Pages/TripDetails/TripDetailsPage.swift @@ -130,12 +130,21 @@ struct TripDetailsPage: View { joinVehicle(vehicleId: vehicleId) } .onReceive(inspection.notice) { inspection.visit(self, $0) } - .withScenePhaseHandlers(onActive: joinRealtime, - onInactive: leaveRealtime, - onBackground: { - leaveRealtime() - isReturningFromBackground = true - }) + .withScenePhaseHandlers( + onActive: { + if let tripPredictions, + tripPredictionsRepository + .shouldForgetPredictions(predictionCount: tripPredictions.predictionQuantity()) { + self.tripPredictions = nil + } + joinRealtime() + }, + onInactive: leaveRealtime, + onBackground: { + leaveRealtime() + isReturningFromBackground = true + } + ) } private func loadEverything() { diff --git a/iosApp/iosAppTests/Pages/TripDetails/TripDetailsPageTests.swift b/iosApp/iosAppTests/Pages/TripDetails/TripDetailsPageTests.swift index 807dade76..55abaee1f 100644 --- a/iosApp/iosAppTests/Pages/TripDetails/TripDetailsPageTests.swift +++ b/iosApp/iosAppTests/Pages/TripDetails/TripDetailsPageTests.swift @@ -655,6 +655,10 @@ final class TripDetailsPageTests: XCTestCase { var lastUpdated: Instant? + func shouldForgetPredictions(predictionCount _: Int32) -> Bool { + false + } + func disconnect() { onDisconnect?() } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/endToEnd/EndToEndRepositories.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/endToEnd/EndToEndRepositories.kt index be5cee66e..d706c0f80 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/endToEnd/EndToEndRepositories.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/endToEnd/EndToEndRepositories.kt @@ -133,6 +133,8 @@ fun endToEndModule(): Module { override var lastUpdated: Instant? = null + override fun shouldForgetPredictions(predictionCount: Int) = false + override fun connectV2( stopIds: List, onJoin: (ApiResult) -> Unit, @@ -199,6 +201,8 @@ fun endToEndModule(): Module { override var lastUpdated: Instant? = null + override fun shouldForgetPredictions(predictionCount: Int) = false + override fun disconnect() { TODO("Not yet implemented") } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/PredictionsRepository.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/PredictionsRepository.kt index 5ae0f8a11..013fddb27 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/PredictionsRepository.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/PredictionsRepository.kt @@ -10,6 +10,7 @@ import com.mbta.tid.mbta_app.network.PhoenixMessage import com.mbta.tid.mbta_app.network.PhoenixPushStatus import com.mbta.tid.mbta_app.network.PhoenixSocket import com.mbta.tid.mbta_app.phoenix.PredictionsForStopsChannel +import kotlin.time.Duration.Companion.minutes import kotlinx.datetime.Clock import kotlinx.datetime.Instant import org.koin.core.component.KoinComponent @@ -28,6 +29,8 @@ interface IPredictionsRepository { var lastUpdated: Instant? + fun shouldForgetPredictions(predictionCount: Int): Boolean + fun disconnect() } @@ -38,6 +41,10 @@ class PredictionsRepository(private val socket: PhoenixSocket) : override var lastUpdated: Instant? = null + override fun shouldForgetPredictions(predictionCount: Int) = + (Clock.System.now() - (lastUpdated ?: Instant.DISTANT_FUTURE)) > 10.minutes && + predictionCount > 0 + override fun connect( stopIds: List, onReceive: (ApiResult) -> Unit @@ -221,6 +228,8 @@ class MockPredictionsRepository( override var lastUpdated: Instant? = null + override fun shouldForgetPredictions(predictionCount: Int) = false + override fun disconnect() { onDisconnect() } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/TripPredictionsRepository.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/TripPredictionsRepository.kt index 90cce9842..dc06582e9 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/TripPredictionsRepository.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/TripPredictionsRepository.kt @@ -8,6 +8,7 @@ import com.mbta.tid.mbta_app.network.PhoenixMessage import com.mbta.tid.mbta_app.network.PhoenixPushStatus import com.mbta.tid.mbta_app.network.PhoenixSocket import com.mbta.tid.mbta_app.phoenix.PredictionsForStopsChannel +import kotlin.time.Duration.Companion.minutes import kotlinx.datetime.Clock import kotlinx.datetime.Instant import org.koin.core.component.KoinComponent @@ -17,6 +18,8 @@ interface ITripPredictionsRepository { var lastUpdated: Instant? + fun shouldForgetPredictions(predictionCount: Int): Boolean + fun disconnect() } @@ -27,6 +30,10 @@ class TripPredictionsRepository(private val socket: PhoenixSocket) : override var lastUpdated: Instant? = null + override fun shouldForgetPredictions(predictionCount: Int) = + (Clock.System.now() - (lastUpdated ?: Instant.DISTANT_FUTURE)) > 10.minutes && + predictionCount > 0 + override fun connect( tripId: String, onReceive: (ApiResult) -> Unit @@ -89,6 +96,8 @@ class MockTripPredictionsRepository : ITripPredictionsRepository { override var lastUpdated: Instant? = null + override fun shouldForgetPredictions(predictionCount: Int) = false + override fun disconnect() { /* no-op */ } diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/PredictionsRepositoryTests.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/PredictionsRepositoryTests.kt index ac2beab9d..398055b75 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/PredictionsRepositoryTests.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/repositories/PredictionsRepositoryTests.kt @@ -18,9 +18,14 @@ import dev.mokkery.mock import dev.mokkery.verify import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.minutes +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import org.koin.test.KoinTest class PredictionsRepositoryTests : KoinTest { @@ -416,4 +421,33 @@ class PredictionsRepositoryTests : KoinTest { } ) } + + @Test + fun `shouldForgetPredictions false when never updated`() { + val predictionsRepo = PredictionsRepository(mock(MockMode.autofill)) + predictionsRepo.lastUpdated = null + // there will not, in practice, be ten predictions and no last updated time + assertFalse(predictionsRepo.shouldForgetPredictions(10)) + } + + @Test + fun `shouldForgetPredictions false when no predictions`() { + val predictionsRepo = PredictionsRepository(mock(MockMode.autofill)) + predictionsRepo.lastUpdated = Instant.DISTANT_PAST + assertFalse(predictionsRepo.shouldForgetPredictions(0)) + } + + @Test + fun `shouldForgetPredictions false when within ten minutes`() { + val predictionsRepo = PredictionsRepository(mock(MockMode.autofill)) + predictionsRepo.lastUpdated = Clock.System.now() - 9.9.minutes + assertFalse(predictionsRepo.shouldForgetPredictions(10)) + } + + @Test + fun `shouldForgetPredictions true when old and nonempty`() { + val predictionsRepo = PredictionsRepository(mock(MockMode.autofill)) + predictionsRepo.lastUpdated = Clock.System.now() - 10.1.minutes + assertTrue(predictionsRepo.shouldForgetPredictions(10)) + } }