Skip to content

Commit ac8b748

Browse files
committed
Merge main into feature branch
Merge branch 'main' into abby/deep-linking
2 parents d0bc7a3 + acc3cff commit ac8b748

34 files changed

Lines changed: 1675 additions & 455 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# Ithaca Transit
1+
# Navi
22

3-
<p align="center"><img src="https://github.com/cuappdev/assets/blob/master/app-icons/Transit-83.5x83.5%402x.png" width=210 /></p>
3+
<p align="center"><img src="https://github.com/cuappdev/assets/blob/master/app-icons/Navi-83.5x83.5%402x.png" width=210 /></p>
44

5-
Introducing Ithaca Transit, a new end-to-end navigation service for built for the TCAT bus service. A free and open-source app, Ithaca Transit offers a diverse range of features in a beautiful, clean interface to help get you where you need to go.
5+
Introducing Navi (formerly Ithaca Transit), a new end-to-end navigation service for built for the TCAT bus service. A free and open-source app, Navi offers a diverse range of features in a beautiful, clean interface to help get you where you need to go.
66

77
## Getting Started
88

app/build.gradle.kts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ hilt {
1919

2020
android {
2121
namespace = "com.cornellappdev.transit"
22-
compileSdk = 34
22+
compileSdk = 36
2323

2424
defaultConfig {
2525
applicationId = "com.cornellappdev.transit"
2626
minSdk = 26
27-
targetSdk = 35
28-
versionCode = 9
29-
versionName = "1.0"
27+
targetSdk = 36
28+
versionCode = 10
29+
versionName = "2.0"
3030

3131
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3232
vectorDrawables {
@@ -42,15 +42,15 @@ android {
4242
signingConfig = signingConfigs.getByName("debug")
4343
}
4444
debug {
45-
buildConfigField("boolean", "ECOSYSTEM_FLAG", "false")
45+
buildConfigField("boolean", "ECOSYSTEM_FLAG", "true")
4646
}
4747
release {
4848
isMinifyEnabled = false
4949
proguardFiles(
5050
getDefaultProguardFile("proguard-android-optimize.txt"),
5151
"proguard-rules.pro"
5252
)
53-
buildConfigField("boolean", "ECOSYSTEM_FLAG", "false")
53+
buildConfigField("boolean", "ECOSYSTEM_FLAG", "true")
5454
signingConfig = signingConfigs.getByName("debug")
5555
}
5656
}

app/release/app-release.aab

12.4 MB
Binary file not shown.

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
<activity
2626
android:name=".MainActivity"
2727
android:exported="true"
28-
android:theme="@style/Theme.Transit">
28+
android:theme="@style/Theme.Transit"
29+
android:screenOrientation="portrait">
2930
<intent-filter>
3031
<action android:name="android.intent.action.MAIN" />
3132
<category android:name="android.intent.category.LAUNCHER" />
@@ -39,6 +40,9 @@
3940
<meta-data
4041
android:name="BACKEND_URL"
4142
android:value="${BACKEND_URL}" />
43+
44+
<property android:name="android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY"
45+
android:value="true" />
4246
</application>
4347

4448
</manifest>

app/src/main/java/com/cornellappdev/transit/models/Route.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.cornellappdev.transit.models
22

3-
import com.cornellappdev.transit.util.StringUtils.fromMetersToMiles
3+
import com.cornellappdev.transit.util.StringUtils.fromMetersToImperial
44
import com.google.android.gms.maps.model.LatLng
55
import com.squareup.moshi.Json
66

@@ -40,7 +40,7 @@ data class Direction(
4040
val type
4141
get() = if (directionType == "walk") DirectionType.WALK else DirectionType.DEPART
4242
val distance: String
43-
get() = distanceMeters.fromMetersToMiles()
43+
get() = distanceMeters.fromMetersToImperial()
4444
}
4545

4646

app/src/main/java/com/cornellappdev/transit/models/RouteRepository.kt

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@ import com.cornellappdev.transit.networking.EcosystemNetworkApi
77
import com.cornellappdev.transit.networking.RoutesNetworkApi
88
import com.cornellappdev.transit.util.ECOSYSTEM_FLAG
99
import com.google.android.gms.maps.model.LatLng
10-
import kotlinx.coroutines.CancellationException
1110
import kotlinx.coroutines.CoroutineScope
1211
import kotlinx.coroutines.Dispatchers
1312
import kotlinx.coroutines.flow.MutableStateFlow
1413
import kotlinx.coroutines.flow.asStateFlow
1514
import kotlinx.coroutines.launch
15+
import java.util.concurrent.atomic.AtomicLong
1616
import javax.inject.Inject
1717
import javax.inject.Singleton
18+
import kotlin.coroutines.cancellation.CancellationException
19+
20+
data class PlaceSearchState(
21+
val query: String,
22+
val response: ApiResponse<List<Place>>,
23+
)
1824

1925
/**
2026
* Repository for data related to routes
@@ -52,6 +58,9 @@ class RouteRepository @Inject constructor(
5258
private val _placeFlow: MutableStateFlow<ApiResponse<List<Place>>> =
5359
MutableStateFlow(ApiResponse.Pending)
5460

61+
private val _placeSearchStateFlow: MutableStateFlow<PlaceSearchState> =
62+
MutableStateFlow(PlaceSearchState(query = "", response = ApiResponse.Success(emptyList())))
63+
5564
private val _lastRouteFlow: MutableStateFlow<ApiResponse<RouteOptions>> =
5665
MutableStateFlow(ApiResponse.Pending)
5766

@@ -61,6 +70,9 @@ class RouteRepository @Inject constructor(
6170
private val _libraryFlow: MutableStateFlow<ApiResponse<List<Library>>> =
6271
MutableStateFlow(ApiResponse.Pending)
6372

73+
// Monotonic token used to ensure only the latest search request updates placeFlow.
74+
private val latestSearchToken = AtomicLong(0L)
75+
6476
init {
6577
fetchAllStops()
6678
if (ECOSYSTEM_FLAG) {
@@ -84,6 +96,11 @@ class RouteRepository @Inject constructor(
8496
*/
8597
val placeFlow = _placeFlow.asStateFlow()
8698

99+
/**
100+
* A StateFlow holding the last queried location tagged by query.
101+
*/
102+
val placeSearchStateFlow = _placeSearchStateFlow.asStateFlow()
103+
87104
/**
88105
* A StateFlow holding the list of all printers
89106
*/
@@ -94,6 +111,13 @@ class RouteRepository @Inject constructor(
94111
*/
95112
val libraryFlow = _libraryFlow.asStateFlow()
96113

114+
/**
115+
* Clears the currently displayed route result.
116+
*/
117+
fun clearLastRoute() {
118+
_lastRouteFlow.value = ApiResponse.Idle
119+
}
120+
97121
/**
98122
* Makes a new call to backend for all stops.
99123
*/
@@ -143,15 +167,47 @@ class RouteRepository @Inject constructor(
143167
* Makes a new call to places related to a query string.
144168
*/
145169
fun makeSearch(query: String) {
146-
_placeFlow.value = ApiResponse.Pending
170+
val normalizedQuery = query.trim()
171+
val token = latestSearchToken.incrementAndGet()
172+
173+
if (normalizedQuery.isBlank()) {
174+
if (token == latestSearchToken.get()) {
175+
_placeFlow.value = ApiResponse.Success(emptyList())
176+
_placeSearchStateFlow.value =
177+
PlaceSearchState(query = "", response = ApiResponse.Success(emptyList()))
178+
}
179+
return
180+
}
181+
182+
_placeSearchStateFlow.value = PlaceSearchState(
183+
query = normalizedQuery,
184+
response = ApiResponse.Pending
185+
)
147186
CoroutineScope(Dispatchers.IO).launch {
148187
try {
149-
val placeResponse = appleSearch(SearchQuery(query))
188+
if(token == latestSearchToken.get()){
189+
_placeFlow.value = ApiResponse.Pending
190+
}
191+
val placeResponse = appleSearch(SearchQuery(normalizedQuery))
150192
val res = placeResponse.unwrap()
151193
val totalLocations = (res.places ?: emptyList()) + (res.stops ?: (emptyList()))
152-
_placeFlow.value = ApiResponse.Success(totalLocations)
194+
if (token == latestSearchToken.get()) {
195+
_placeFlow.value = ApiResponse.Success(totalLocations)
196+
_placeSearchStateFlow.value = PlaceSearchState(
197+
query = normalizedQuery,
198+
response = ApiResponse.Success(totalLocations)
199+
)
200+
}
201+
} catch (_: CancellationException) {
202+
// Ignore cancellation; latest query owns the flow update.
153203
} catch (e: Exception) {
154-
_placeFlow.value = ApiResponse.Error
204+
if (token == latestSearchToken.get()) {
205+
_placeFlow.value = ApiResponse.Error
206+
_placeSearchStateFlow.value = PlaceSearchState(
207+
query = normalizedQuery,
208+
response = ApiResponse.Error
209+
)
210+
}
155211
}
156212
}
157213
}
@@ -194,5 +250,4 @@ class RouteRepository @Inject constructor(
194250
_lastRouteFlow.value = ApiResponse.Error
195251
}
196252
}
197-
198253
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.cornellappdev.transit.models.search
2+
3+
import com.cornellappdev.transit.models.Place
4+
import com.cornellappdev.transit.models.RouteRepository
5+
import com.cornellappdev.transit.models.ecosystem.EateryRepository
6+
import com.cornellappdev.transit.models.ecosystem.GymRepository
7+
import com.cornellappdev.transit.networking.ApiResponse
8+
import com.cornellappdev.transit.util.ecosystem.buildEcosystemSearchPlaces
9+
import com.cornellappdev.transit.util.ecosystem.mergeAndRankSearchResults
10+
import kotlinx.coroutines.CoroutineScope
11+
import kotlinx.coroutines.flow.Flow
12+
import kotlinx.coroutines.flow.SharingStarted
13+
import kotlinx.coroutines.flow.StateFlow
14+
import kotlinx.coroutines.flow.combine
15+
import kotlinx.coroutines.flow.distinctUntilChanged
16+
import kotlinx.coroutines.flow.map
17+
import kotlinx.coroutines.flow.stateIn
18+
import javax.inject.Inject
19+
import javax.inject.Singleton
20+
21+
/**
22+
* Centralized search data source that merges route search results with ecosystem places.
23+
*
24+
* UI and ViewModels can share this data pipeline to keep search behavior consistent.
25+
*/
26+
@Singleton
27+
class UnifiedSearchRepository @Inject constructor(
28+
private val routeRepository: RouteRepository,
29+
eateryRepository: EateryRepository,
30+
gymRepository: GymRepository,
31+
coroutineScope: CoroutineScope,
32+
) {
33+
34+
val ecosystemSearchPlacesFlow: StateFlow<List<Place>> =
35+
combine(
36+
routeRepository.printerFlow,
37+
routeRepository.libraryFlow,
38+
eateryRepository.eateryFlow,
39+
gymRepository.gymFlow
40+
) { printers, libraries, eateries, gyms ->
41+
buildEcosystemSearchPlaces(
42+
printers = printers,
43+
libraries = libraries,
44+
eateries = eateries,
45+
gyms = gyms
46+
)
47+
}.stateIn(
48+
scope = coroutineScope,
49+
started = SharingStarted.Eagerly,
50+
initialValue = emptyList()
51+
)
52+
53+
fun mergedSearchResults(queryFlow: Flow<String>): Flow<ApiResponse<List<Place>>> =
54+
combine(
55+
queryFlow
56+
.map { it.trim() }
57+
.distinctUntilChanged(),
58+
routeRepository.placeSearchStateFlow,
59+
ecosystemSearchPlacesFlow
60+
) { query, routeSearchState, ecosystemPlaces ->
61+
val routeSearchResults = when {
62+
query.isBlank() -> ApiResponse.Success(emptyList())
63+
routeSearchState.query.equals(query, ignoreCase = true) -> routeSearchState.response
64+
else -> ApiResponse.Pending
65+
}
66+
67+
mergeAndRankSearchResults(
68+
query = query,
69+
routeSearchResults = routeSearchResults,
70+
ecosystemPlaces = ecosystemPlaces
71+
)
72+
}
73+
}
74+
75+
76+

app/src/main/java/com/cornellappdev/transit/networking/ApiResponse.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package com.cornellappdev.transit.networking
44
* Wrapper for data returned from an API
55
*/
66
sealed class ApiResponse<out T : Any> {
7+
object Idle : ApiResponse<Nothing>()
78
object Pending : ApiResponse<Nothing>()
89
object Error : ApiResponse<Nothing>()
910
class Success<out T : Any>(val data: T) : ApiResponse<T>()

app/src/main/java/com/cornellappdev/transit/ui/NavigationController.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import com.cornellappdev.transit.ui.screens.settings.FavoritesScreen
1414
import com.cornellappdev.transit.ui.screens.settings.NotifsAndPrivacyScreen
1515
import com.cornellappdev.transit.ui.screens.settings.PrivacySettingsScreen
1616
import com.cornellappdev.transit.ui.screens.settings.SupportScreen
17-
import com.cornellappdev.transit.ui.viewmodels.HomeViewModel
1817
import com.cornellappdev.transit.ui.viewmodels.RouteViewModel
18+
import com.cornellappdev.transit.util.navigateSingleTop
1919

2020
/**
2121
* The navigation controller for the app (parent of all screens)
@@ -45,18 +45,20 @@ fun NavigationController(
4545

4646
composable("settings") {
4747
SettingsScreen (
48-
onAboutClick = {navController.navigate("about")},
49-
onSupportClick = {navController.navigate("support")},
50-
onNotificationsAndPrivacyClick = {navController.navigate("notifs_privacy")})
48+
onAboutClick = { navController.navigateSingleTop("about") },
49+
onSupportClick = { navController.navigateSingleTop("support") },
50+
onNotificationsAndPrivacyClick = { navController.navigateSingleTop("notifs_privacy") },
51+
onBackClick = { navController.popBackStack() }
52+
)
5153
}
5254

5355
composable("about") {
54-
AboutScreen()
56+
AboutScreen(onBackClick = { navController.popBackStack() })
5557
}
5658

5759
composable("notifs_privacy") {
5860
NotifsAndPrivacyScreen (
59-
onPrivacyClick = {navController.navigate("privacy_settings")}
61+
onPrivacyClick = {navController.navigateSingleTop("privacy_settings")}
6062
)
6163
}
6264

@@ -69,7 +71,7 @@ fun NavigationController(
6971
}
7072

7173
composable("support") {
72-
SupportScreen()
74+
SupportScreen(onBackClick = { navController.popBackStack() })
7375
}
7476
}
7577
}

app/src/main/java/com/cornellappdev/transit/ui/components/LoadingLocationItems.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package com.cornellappdev.transit.ui.components
22

33
import androidx.compose.foundation.lazy.LazyColumn
44
import androidx.compose.foundation.lazy.items
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.PaddingValues
57
import androidx.compose.runtime.Composable
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.unit.dp
610
import com.cornellappdev.transit.models.Place
711
import com.cornellappdev.transit.networking.ApiResponse
812

@@ -13,6 +17,10 @@ import com.cornellappdev.transit.networking.ApiResponse
1317
@Composable
1418
fun LoadingLocationItems(searchResult: ApiResponse<List<Place>>, onClick: (Place) -> Unit) {
1519
when (searchResult) {
20+
is ApiResponse.Idle -> {
21+
LocationNotFound()
22+
}
23+
1624
is ApiResponse.Error -> {
1725
LocationNotFound()
1826
}
@@ -25,15 +33,18 @@ fun LoadingLocationItems(searchResult: ApiResponse<List<Place>>, onClick: (Place
2533
if (searchResult.data.isEmpty()) {
2634
LocationNotFound()
2735
} else {
28-
LazyColumn {
36+
LazyColumn(
37+
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 6.dp),
38+
verticalArrangement = Arrangement.spacedBy(12.dp)
39+
) {
2940
items(
3041
searchResult.data
3142
) {
3243
MenuItem(
3344
type = it.type,
3445
label = it.name,
3546
sublabel = it.subLabel,
36-
onClick = { onClick(it) }
47+
onClick = { onClick(it) },
3748
)
3849
}
3950
}

0 commit comments

Comments
 (0)