Skip to content

Commit

Permalink
refactor: port iOS location architecture to Android (#568)
Browse files Browse the repository at this point in the history
* refactor: port iOS location architecture to Android

* fix collectAsState with mapped flow

* grant location permission and provide locals

* wait until done loading

* split object declarations into separate classes

* resolve a TODO
  • Loading branch information
boringcactus authored Dec 6, 2024
1 parent 301e33b commit 8f0ba13
Show file tree
Hide file tree
Showing 20 changed files with 744 additions and 115 deletions.
3 changes: 3 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,16 @@ dependencies {
implementation(libs.mapbox.compose)
implementation(libs.mapbox.turf)
implementation(libs.okhttp)
implementation(libs.playServices.location)
debugImplementation(platform(libs.compose.bom))
debugImplementation(libs.compose.ui.test.manifest)
debugImplementation(libs.compose.ui.tooling)
testImplementation(libs.junit)
testImplementation(libs.koin.test)
implementation(libs.koin.junit4)
androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.androidx.test.monitor)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.compose.ui.test.junit4)
androidTestImplementation(libs.ktor.client.mock)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.mbta.tid.mbta_app.android

import android.app.Activity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
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.test.rule.GrantPermissionRule
import com.mbta.tid.mbta_app.android.location.MockFusedLocationProviderClient
import com.mbta.tid.mbta_app.android.util.LocalActivity
import com.mbta.tid.mbta_app.android.util.LocalLocationClient
import com.mbta.tid.mbta_app.dependencyInjection.repositoriesModule
import com.mbta.tid.mbta_app.network.MockPhoenixSocket
import com.mbta.tid.mbta_app.network.PhoenixSocket
Expand All @@ -16,6 +23,9 @@ import org.koin.test.KoinTest

class ContentViewTests : KoinTest {

@get:Rule
val runtimePermissionRule =
GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION)
@get:Rule val composeTestRule = createComposeRule()

val koinApplication = koinApplication {
Expand All @@ -27,7 +37,16 @@ class ContentViewTests : KoinTest {

@Test
fun testSwitchingTabs() {
composeTestRule.setContent { KoinContext(koinApplication.koin) { ContentView() } }
composeTestRule.setContent {
KoinContext(koinApplication.koin) {
CompositionLocalProvider(
LocalActivity provides (LocalContext.current as Activity),
LocalLocationClient provides MockFusedLocationProviderClient()
) {
ContentView()
}
}
}

composeTestRule.onNodeWithText("More").performClick()
composeTestRule.onNodeWithText("MBTA Go").assertIsDisplayed()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.mbta.tid.mbta_app.android.location

import android.app.PendingIntent
import android.location.Location
import android.os.Looper
import com.google.android.gms.common.api.Api
import com.google.android.gms.common.api.internal.ApiKey
import com.google.android.gms.location.CurrentLocationRequest
import com.google.android.gms.location.DeviceOrientationListener
import com.google.android.gms.location.DeviceOrientationRequest
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LastLocationRequest
import com.google.android.gms.location.LocationAvailability
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationListener
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.tasks.CancellationToken
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import java.util.concurrent.Executor

class MockFusedLocationProviderClient : FusedLocationProviderClient {
override fun getApiKey(): ApiKey<Api.ApiOptions.NoOptions> {
TODO("Not yet implemented")
}

override fun getLastLocation(): Task<Location> = Tasks.forCanceled()

override fun getLastLocation(p0: LastLocationRequest): Task<Location> {
TODO("Not yet implemented")
}

override fun getCurrentLocation(p0: Int, p1: CancellationToken?): Task<Location> {
TODO("Not yet implemented")
}

override fun getCurrentLocation(
p0: CurrentLocationRequest,
p1: CancellationToken?
): Task<Location> {
TODO("Not yet implemented")
}

override fun getLocationAvailability(): Task<LocationAvailability> {
TODO("Not yet implemented")
}

override fun requestLocationUpdates(
p0: LocationRequest,
p1: Executor,
p2: LocationListener
): Task<Void> {
TODO("Not yet implemented")
}

override fun requestLocationUpdates(
p0: LocationRequest,
p1: LocationListener,
p2: Looper?
): Task<Void> = Tasks.forCanceled()

override fun requestLocationUpdates(
p0: LocationRequest,
p1: LocationCallback,
p2: Looper?
): Task<Void> {
TODO("Not yet implemented")
}

override fun requestLocationUpdates(
p0: LocationRequest,
p1: Executor,
p2: LocationCallback
): Task<Void> {
TODO("Not yet implemented")
}

override fun requestLocationUpdates(p0: LocationRequest, p1: PendingIntent): Task<Void> {
TODO("Not yet implemented")
}

override fun removeLocationUpdates(p0: LocationListener): Task<Void> = Tasks.forCanceled()

override fun removeLocationUpdates(p0: LocationCallback): Task<Void> {
TODO("Not yet implemented")
}

override fun removeLocationUpdates(p0: PendingIntent): Task<Void> {
TODO("Not yet implemented")
}

override fun flushLocations(): Task<Void> {
TODO("Not yet implemented")
}

override fun setMockMode(p0: Boolean): Task<Void> {
TODO("Not yet implemented")
}

override fun setMockLocation(p0: Location): Task<Void> {
TODO("Not yet implemented")
}

@Deprecated("Deprecated in Java")
override fun requestDeviceOrientationUpdates(
p0: DeviceOrientationRequest,
p1: Executor,
p2: DeviceOrientationListener
): Task<Void> {
TODO("Not yet implemented")
}

@Deprecated("Deprecated in Java")
override fun requestDeviceOrientationUpdates(
p0: DeviceOrientationRequest,
p1: DeviceOrientationListener,
p2: Looper?
): Task<Void> {
TODO("Not yet implemented")
}

@Deprecated("Deprecated in Java")
override fun removeDeviceOrientationUpdates(p0: DeviceOrientationListener): Task<Void> {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.mbta.tid.mbta_app.android.location

import android.location.Location
import kotlinx.coroutines.flow.MutableStateFlow

class MockLocationDataManager(location: Location) : LocationDataManager() {
override val currentLocation = MutableStateFlow(location)
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
package com.mbta.tid.mbta_app.android.nearbyTransit

import android.app.Activity
import android.location.Location
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraState
import com.mapbox.maps.EdgeInsets
import androidx.test.rule.GrantPermissionRule
import com.mapbox.maps.MapboxExperimental
import com.mapbox.maps.extension.compose.animation.viewport.MapViewportState
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
import com.mbta.tid.mbta_app.android.location.MockFusedLocationProviderClient
import com.mbta.tid.mbta_app.android.location.MockLocationDataManager
import com.mbta.tid.mbta_app.android.location.ViewportProvider
import com.mbta.tid.mbta_app.android.pages.NearbyTransit
import com.mbta.tid.mbta_app.android.pages.NearbyTransitPage
import com.mbta.tid.mbta_app.android.util.LocalActivity
import com.mbta.tid.mbta_app.android.util.LocalLocationClient
import com.mbta.tid.mbta_app.model.Coordinate
import com.mbta.tid.mbta_app.model.LocationType
import com.mbta.tid.mbta_app.model.NearbyStaticData
Expand Down Expand Up @@ -233,40 +242,41 @@ class NearbyTransitPageTest : KoinTest {
)
}

@get:Rule
val runtimePermissionRule =
GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION)
@get:Rule val composeTestRule = createComposeRule()

@OptIn(ExperimentalTestApi::class)
@Test
fun testNearbyTransitPageDisplaysCorrectly() {
composeTestRule.setContent {
KoinContext(koinApplication.koin) {
NearbyTransitPage(
Modifier,
NearbyTransit(
alertData = AlertsStreamDataResponse(builder.alerts),
globalResponse = globalResponse,
targetLocation = Position(0.0, 0.0),
lastNearbyTransitLocation = Position(0.0, 0.0),
mapCenter = Position(0.0, 0.0),
mapViewportState =
MapViewportState(
CameraState(
Point.fromLngLat(0.0, 0.0),
EdgeInsets(0.0, 0.0, 0.0, 0.0),
1.0,
0.0,
0.0
)
),
scaffoldState = rememberBottomSheetScaffoldState(),
),
false,
{},
{},
bottomBar = {}
)
CompositionLocalProvider(
LocalActivity provides (LocalContext.current as Activity),
LocalLocationClient provides MockFusedLocationProviderClient()
) {
NearbyTransitPage(
Modifier,
NearbyTransit(
alertData = AlertsStreamDataResponse(builder.alerts),
globalResponse = globalResponse,
lastNearbyTransitLocation = Position(0.0, 0.0),
scaffoldState = rememberBottomSheetScaffoldState(),
locationDataManager = MockLocationDataManager(Location("mock")),
viewportProvider = ViewportProvider(rememberMapViewportState()),
),
false,
{},
{},
bottomBar = {}
)
}
}
}

composeTestRule.waitUntilDoesNotExist(hasText("Loading..."))

composeTestRule.onNodeWithText("Nearby transit").assertIsDisplayed()

composeTestRule.onNodeWithText("Green Line Long Name").assertExists()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,63 +7,50 @@ 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.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.mapbox.geojson.Point
import com.mapbox.maps.MapboxExperimental
import com.mapbox.maps.extension.compose.animation.viewport.rememberMapViewportState
import com.mbta.tid.mbta_app.android.component.BottomNavBar
import com.mbta.tid.mbta_app.android.location.ViewportProvider
import com.mbta.tid.mbta_app.android.location.rememberLocationDataManager
import com.mbta.tid.mbta_app.android.pages.MorePage
import com.mbta.tid.mbta_app.android.pages.NearbyTransit
import com.mbta.tid.mbta_app.android.pages.NearbyTransitPage
import com.mbta.tid.mbta_app.android.phoenix.PhoenixSocketWrapper
import com.mbta.tid.mbta_app.android.state.getGlobalData
import com.mbta.tid.mbta_app.android.state.subscribeToAlerts
import com.mbta.tid.mbta_app.android.util.toPosition
import com.mbta.tid.mbta_app.model.response.AlertsStreamDataResponse
import com.mbta.tid.mbta_app.network.PhoenixSocket
import io.github.dellisd.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import org.koin.compose.koinInject

@OptIn(MapboxExperimental::class, ExperimentalMaterial3Api::class, FlowPreview::class)
@OptIn(MapboxExperimental::class, ExperimentalMaterial3Api::class)
@Composable
fun ContentView(
socket: PhoenixSocket = koinInject(),
) {
val navController = rememberNavController()
var alertData: AlertsStreamDataResponse? = subscribeToAlerts()
val globalResponse = getGlobalData()
val locationDataManager = rememberLocationDataManager()
val mapViewportState = rememberMapViewportState {
setCameraOptions {
center(Point.fromLngLat(-71.062424, 42.356395))
zoom(13.25)
center(ViewportProvider.Companion.Defaults.center)
zoom(ViewportProvider.Companion.Defaults.zoom)
pitch(0.0)
bearing(0.0)
transitionToFollowPuckState()
}
}
val rawMapCenterFlow = snapshotFlow { mapViewportState.cameraState.center }
val mapCenterFlow =
remember(rawMapCenterFlow) {
rawMapCenterFlow.debounce(0.25.seconds).map { it.toPosition() }
}
val mapCenter by
mapCenterFlow.collectAsState(
initial = Position(longitude = -71.062424, latitude = 42.356395)
)
val viewportProvider = remember { ViewportProvider(mapViewportState) }
val lastNearbyTransitLocation by remember { mutableStateOf<Position?>(null) }
val scaffoldState =
rememberBottomSheetScaffoldState(bottomSheetState = rememberStandardBottomSheetState())
Expand All @@ -82,11 +69,10 @@ fun ContentView(
NearbyTransit(
alertData = alertData,
globalResponse = globalResponse,
targetLocation = mapCenter,
mapCenter = mapCenter,
lastNearbyTransitLocation = lastNearbyTransitLocation,
scaffoldState = scaffoldState,
mapViewportState = mapViewportState,
locationDataManager = locationDataManager,
viewportProvider = viewportProvider,
),
navBarVisible = navBarVisible,
showNavBar = { navBarVisible = true },
Expand Down
Loading

0 comments on commit 8f0ba13

Please sign in to comment.