Skip to content

Commit

Permalink
feat(android.NearbyTransitPage): leave / rejoin vehicles channel afte…
Browse files Browse the repository at this point in the history
…r backgrounding (#579)

* feat(android.NearbyTransitPage): leave / rejoin vehicles channel after backgrounding

* feat(android.ContentView): Stop / start socket after backgrounding

* WIP

* refactor(StopDetailsFilter): RouteDirection interface

* test(subscribeToVehicles): add test for connecting / disconnecting

* test: backgrounding / restoring

* cleanup: remove debug logs

* fix(subscribeToVehiclesTest): waitUntil so not flaky

* fix(ContentViewTests): set contentVM
  • Loading branch information
KaylaBrady authored Dec 17, 2024
1 parent f8aaf69 commit a264d30
Show file tree
Hide file tree
Showing 13 changed files with 350 additions and 54 deletions.
1 change: 1 addition & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ dependencies {
implementation(libs.mapbox.turf)
implementation(libs.okhttp)
implementation(libs.playServices.location)
implementation(libs.androidx.lifecycle.runtime.testing)
debugImplementation(platform(libs.compose.bom))
debugImplementation(libs.compose.ui.test.manifest)
debugImplementation(libs.compose.ui.tooling)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.rule.GrantPermissionRule
import com.mbta.tid.mbta_app.android.location.MockFusedLocationProviderClient
import com.mbta.tid.mbta_app.android.util.LocalActivity
Expand Down Expand Up @@ -55,4 +58,45 @@ class ContentViewTests : KoinTest {
composeTestRule.onNodeWithText("Nearby").performClick()
composeTestRule.onNodeWithText("Nearby Transit").assertIsDisplayed()
}

@Test
fun testSocketClosedOnPause() {
val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.RESUMED)
var onAttachCount = 0
var onDetatchCount = 0

val koinApplication = koinApplication {
modules(
repositoriesModule(MockRepositories.buildWithDefaults()),
MainApplication.koinViewModelModule,
module {
single<PhoenixSocket> {
MockPhoenixSocket({ onAttachCount += 1 }, { onDetatchCount += 1 })
}
}
)
}

composeTestRule.setContent {
KoinContext(koinApplication.koin) {
CompositionLocalProvider(
LocalActivity provides (LocalContext.current as Activity),
LocalLocationClient provides MockFusedLocationProviderClient(),
LocalLifecycleOwner provides lifecycleOwner
) {
ContentView()
}
}
}

composeTestRule.waitUntil { onAttachCount == 1 && onDetatchCount == 0 }

composeTestRule.runOnIdle { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) }

composeTestRule.waitUntil { onAttachCount == 1 && onDetatchCount == 1 }

composeTestRule.runOnIdle { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) }

composeTestRule.waitUntil { onAttachCount == 2 && onDetatchCount == 1 }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.mbta.tid.mbta_app.android.nearbyTransit

import com.mbta.tid.mbta_app.model.ObjectCollectionBuilder
import com.mbta.tid.mbta_app.model.PatternsByStop
import com.mbta.tid.mbta_app.model.StopDetailsDepartures
import com.mbta.tid.mbta_app.model.StopDetailsFilter
import kotlin.test.assertEquals
import kotlin.test.assertNull
import org.junit.Test

class NearbyTransitTabViewModelTest {

@Test
fun testSetStopDetailsFilter() {
val vm = NearbyTransitTabViewModel()
val newFilter = StopDetailsFilter("route_1", 1)

assertNull(vm.stopDetailsFilter.value)

vm.setStopDetailsFilter(newFilter)
assertEquals(newFilter, vm.stopDetailsFilter.value)
}

@Test
fun testSetStopDetailsDepartures() {
val vm = NearbyTransitTabViewModel()
val objectCollectionBuilder = ObjectCollectionBuilder()
val route = objectCollectionBuilder.route {}
val stop = objectCollectionBuilder.stop {}

val departures = StopDetailsDepartures(listOf(PatternsByStop(route, stop, emptyList())))

assertNull(vm.stopDetailsDepartures.value)

vm.setStopDetailsDepartures(departures)
assertEquals(departures, vm.stopDetailsDepartures.value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.mbta.tid.mbta_app.android.state

import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.runComposeUiTest
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.testing.TestLifecycleOwner
import com.mbta.tid.mbta_app.model.ObjectCollectionBuilder
import com.mbta.tid.mbta_app.model.StopDetailsFilter
import com.mbta.tid.mbta_app.model.Vehicle
import com.mbta.tid.mbta_app.model.response.VehiclesStreamDataResponse
import com.mbta.tid.mbta_app.repositories.MockVehiclesRepository
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test

@OptIn(ExperimentalTestApi::class)
class SubscribeToVehiclesTest {
@get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()

// Test structure based on lifecycle tests:
// https://github.com/androidx/androidx/blob/0b709f31b71110fe671ed8b4c03f96ad30a0cd37/lifecycle/lifecycle-runtime-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/compose/LifecycleEffectTest.kt#L247
@Test
fun testSubscribeToVehicles() = runComposeUiTest {
val vehicle =
ObjectCollectionBuilder().vehicle { currentStatus = Vehicle.CurrentStatus.StoppedAt }
var connectProps: Pair<String, Int>? = null

val vehiclesRepo =
MockVehiclesRepository(
VehiclesStreamDataResponse(mapOf(vehicle.id to vehicle)),
onConnect = { routeId, directionId -> connectProps = Pair(routeId, directionId) }
)

var vehicles: List<Vehicle> = emptyList()

var stateFilter = mutableStateOf(StopDetailsFilter("route_1", 1))

setContent {
var filter by remember { stateFilter }
vehicles = subscribeToVehicles(filter, vehiclesRepo)
}

waitUntil { connectProps == Pair("route_1", 1) }
waitUntil { listOf(vehicle) == vehicles }

runOnUiThread { stateFilter.value = StopDetailsFilter("route_2", 1) }
waitUntil { connectProps == Pair("route_2", 1) }
}

@Test
fun testDisconnectsOnPause() = runComposeUiTest {
val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.RESUMED)

val vehicle =
ObjectCollectionBuilder().vehicle { currentStatus = Vehicle.CurrentStatus.StoppedAt }
var connectCount = 0
var disconnectCount = 0

val vehiclesRepo =
MockVehiclesRepository(
VehiclesStreamDataResponse(mapOf(vehicle.id to vehicle)),
onConnect = { _, _ -> connectCount += 1 },
onDisconnect = { disconnectCount += 1 }
)

var vehicles: List<Vehicle> = emptyList()

var stateFilter = mutableStateOf(StopDetailsFilter("route_1", 1))

setContent {
CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
var filter by remember { stateFilter }
vehicles = subscribeToVehicles(filter, vehiclesRepo)
}
}

waitUntil { connectCount == 1 }
// Disconnect called before connecting
assertEquals(1, disconnectCount)

runOnIdle { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) }

waitUntil { disconnectCount == 2 }
assertEquals(2, disconnectCount)
assertEquals(1, connectCount)

runOnIdle { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) }

waitUntil { disconnectCount == 3 }
assertEquals(2, connectCount)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
Expand Down Expand Up @@ -62,10 +62,10 @@ fun ContentView(
rememberBottomSheetScaffoldState(bottomSheetState = rememberStandardBottomSheetState())
var navBarVisible by remember { mutableStateOf(true) }

DisposableEffect(null) {
LifecycleResumeEffect(null) {
socket.attach()
(socket as? PhoenixSocketWrapper)?.attachLogging()
onDispose { socket.detach() }
onPauseOrDispose { socket.detach() }
}

if (!pendingOnboarding.isNullOrEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ fun HomeMapView(
val stopSourceData = viewModel.stopSourceData.collectAsState(initial = null).value
val globalResponse = viewModel.globalResponse.collectAsState(initial = null).value
val railRouteLineData = viewModel.railRouteLineData.collectAsState(initial = null).value

val now = timer(updateInterval = 10.seconds)
val globalMapData = remember(now) { viewModel.globalMapData(now) }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.mbta.tid.mbta_app.android.nearbyTransit

import androidx.lifecycle.ViewModel
import com.mbta.tid.mbta_app.model.StopDetailsDepartures
import com.mbta.tid.mbta_app.model.StopDetailsFilter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class NearbyTransitTabViewModel : ViewModel() {

private val _stopDetailsFilter = MutableStateFlow<StopDetailsFilter?>(null)
val stopDetailsFilter: StateFlow<StopDetailsFilter?> = _stopDetailsFilter

private val _stopDetailsDepartures = MutableStateFlow<StopDetailsDepartures?>(null)
val stopDetailsDepartures: StateFlow<StopDetailsDepartures?> = _stopDetailsDepartures

fun setStopDetailsFilter(filter: StopDetailsFilter?) {
_stopDetailsFilter.value = filter
}

fun setStopDetailsDepartures(departures: StopDetailsDepartures?) {
_stopDetailsDepartures.value = departures
}
}
Loading

0 comments on commit a264d30

Please sign in to comment.