diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index ee74856..7572717 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -25,8 +25,8 @@ android {
applicationId = "com.cornellappdev.transit"
minSdk = 26
targetSdk = 36
- versionCode = 10
- versionName = "2.0"
+ versionCode = 11
+ versionName = "2.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fc31d95..1d4e1a9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,11 @@
+
+
+
+
+
-
diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt
index cc8ddb8..193be0a 100644
--- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt
+++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/DetailedPlaceSheetContent.kt
@@ -24,6 +24,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -40,6 +41,7 @@ import com.cornellappdev.transit.ui.theme.SecondaryText
import com.cornellappdev.transit.ui.theme.Style
import com.cornellappdev.transit.ui.theme.TransitBlue
import com.cornellappdev.transit.util.BOTTOM_SHEET_MAX_HEIGHT_PERCENT
+import com.cornellappdev.transit.util.IntentUtils.openDeepLink
@Composable
fun DetailedPlaceSheetContent(
@@ -51,6 +53,8 @@ fun DetailedPlaceSheetContent(
modifier: Modifier = Modifier,
distanceStringToPlace: (Double?, Double?) -> String
) {
+ val context = LocalContext.current
+
Column(
modifier = modifier
.fillMaxWidth()
@@ -98,6 +102,9 @@ fun DetailedPlaceSheetContent(
onFavoriteClick = {
onFavoriteStarClick(ecosystemPlace.toPlace())
},
+ onDeepLinkClick = {
+ context.openDeepLink("com.cornellappdev.android.eatery")
+ },
distanceString = distanceStringToPlace(
ecosystemPlace.latitude,
ecosystemPlace.longitude
@@ -122,6 +129,9 @@ fun DetailedPlaceSheetContent(
onFavoriteClick = {
onFavoriteStarClick(ecosystemPlace.toPlace())
},
+ onDeepLinkClick = {
+ context.openDeepLink("com.cornellappdev.uplift")
+ },
distanceString = distanceStringToPlace(
ecosystemPlace.latitude,
ecosystemPlace.longitude
diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt
index de6a68e..7000afc 100644
--- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt
+++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/EateryDetailsContent.kt
@@ -1,5 +1,8 @@
package com.cornellappdev.transit.ui.components.home
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -10,9 +13,11 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -28,12 +33,14 @@ import com.cornellappdev.transit.util.StringUtils.createDeepLink
import com.cornellappdev.transit.util.TimeUtils.isOpenAnnotatedStringFromOperatingHours
import com.cornellappdev.transit.util.TimeUtils.rotateOperatingHours
import com.cornellappdev.transit.util.getAboutContent
+import androidx.core.net.toUri
@Composable
fun EateryDetailsContent(
eatery: Eatery,
isFavorite: Boolean,
onFavoriteClick: () -> Unit,
+ onDeepLinkClick: () -> Unit,
distanceString: String,
) {
Column(
@@ -88,12 +95,16 @@ fun EateryDetailsContent(
val (annotatedString, inlineContent) =
stringResource(R.string.view_menu).createDeepLink(R.drawable.eaterylink)
- Text(
- text = annotatedString,
- inlineContent = inlineContent,
- style = Style.heading2,
- color = TransitBlue
- )
+ TextButton(
+ onClick = onDeepLinkClick
+ ) {
+ Text(
+ text = annotatedString,
+ inlineContent = inlineContent,
+ style = Style.heading2,
+ color = TransitBlue,
+ )
+ }
Spacer(modifier = Modifier.height(24.dp))
diff --git a/app/src/main/java/com/cornellappdev/transit/ui/components/home/GymDetailsContent.kt b/app/src/main/java/com/cornellappdev/transit/ui/components/home/GymDetailsContent.kt
index 91da2ab..519dfcc 100644
--- a/app/src/main/java/com/cornellappdev/transit/ui/components/home/GymDetailsContent.kt
+++ b/app/src/main/java/com/cornellappdev/transit/ui/components/home/GymDetailsContent.kt
@@ -1,5 +1,8 @@
package com.cornellappdev.transit.ui.components.home
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -10,12 +13,15 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.core.net.toUri
import com.cornellappdev.transit.R
import com.cornellappdev.transit.models.ecosystem.UpliftGym
import com.cornellappdev.transit.ui.theme.DividerGray
@@ -39,6 +45,7 @@ fun GymDetailsContent(
gym: UpliftGym,
isFavorite: Boolean,
onFavoriteClick: () -> Unit,
+ onDeepLinkClick: () -> Unit,
distanceString: String
) {
val isOpen = getOpenStatus(gym.operatingHours()).isOpen
@@ -97,12 +104,16 @@ fun GymDetailsContent(
val (annotatedString, inlineContent) =
stringResource(R.string.view_gym).createDeepLink(R.drawable.upliftlink)
- Text(
- text = annotatedString,
- inlineContent = inlineContent,
- style = Style.heading2,
- color = TransitBlue
- )
+ TextButton(
+ onClick = onDeepLinkClick
+ ) {
+ Text(
+ text = annotatedString,
+ inlineContent = inlineContent,
+ style = Style.heading2,
+ color = TransitBlue,
+ )
+ }
Spacer(modifier = Modifier.height(24.dp))
diff --git a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt
index e922de7..de3982d 100644
--- a/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt
+++ b/app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt
@@ -56,11 +56,18 @@ class HomeViewModel @Inject constructor(
) : ViewModel() {
val libraryCardsFlow: StateFlow>> =
- routeRepository.libraryFlow.map { response ->
+ combine(
+ routeRepository.libraryFlow,
+ locationRepository.currentLocation
+ ) { response, location ->
+ val usersLocation = location?.let { LatLng(it.latitude, it.longitude) }
when (val filteredResponse = response.withExcludedLibrariesRemoved()) {
is ApiResponse.Success -> {
+ val sortedLibraries = filteredResponse.data.sortedBy {
+ numericalDistanceToPlace(it.latitude, it.longitude, usersLocation)
+ }
ApiResponse.Success(
- filteredResponse.data
+ sortedLibraries
.map { it.toLibraryCardUiState() }
)
}
@@ -119,6 +126,9 @@ class HomeViewModel @Inject constructor(
gymRepository.gymFlow,
locationRepository.currentLocation
) { printers, libraries, eateries, gyms, location ->
+ val userLocation = location?.let { LatLng(it.latitude, it.longitude) }
+
+
StaticPlaces(
printers = sortApiResponse(
response = if (printers is ApiResponse.Success) {
@@ -127,23 +137,27 @@ class HomeViewModel @Inject constructor(
printers
},
getLatitude = { it.latitude },
- getLongitude = { it.longitude }
+ getLongitude = { it.longitude },
+ userLocation = userLocation
),
libraries = sortApiResponse(
- response = libraries,
+ response = libraries.withExcludedLibrariesRemoved(),
getLatitude = { it.latitude },
- getLongitude = { it.longitude }
- ).withExcludedLibrariesRemoved(),
+ getLongitude = { it.longitude },
+ userLocation = userLocation
+ ),
eateries = sortApiResponse(
response = eateries,
getLatitude = { it.latitude },
getLongitude = { it.longitude },
+ userLocation = userLocation,
getIsOpen = { TimeUtils.getOpenStatus(it.operatingHours()).isOpen }
),
gyms = sortApiResponse(
response = gyms,
getLatitude = { it.latitude },
getLongitude = { it.longitude },
+ userLocation = userLocation,
getIsOpen = { TimeUtils.getOpenStatus(it.operatingHours()).isOpen }
)
)
@@ -513,14 +527,14 @@ class HomeViewModel @Inject constructor(
/**
* Returns a numerical distance from a location to the current location if both exist, otherwise returns Double.MAX_VALUE
*/
- fun numericalDistanceToPlace(latitude: Double?, longitude: Double?): Double {
- val currentLocationSnapshot = currentLocation.value
- return if (currentLocationSnapshot != null && latitude != null && longitude != null) {
+ fun numericalDistanceToPlace(
+ latitude: Double?,
+ longitude: Double?,
+ userLocation: LatLng?
+ ): Double {
+ return if (userLocation != null && latitude != null && longitude != null) {
calculateDistance(
- LatLng(
- currentLocationSnapshot.latitude,
- currentLocationSnapshot.longitude
- ), LatLng(latitude, longitude)
+ userLocation, LatLng(latitude, longitude)
)
} else {
Double.MAX_VALUE
@@ -534,15 +548,16 @@ class HomeViewModel @Inject constructor(
response: ApiResponse>,
getLatitude: (T) -> Double?,
getLongitude: (T) -> Double?,
+ userLocation: LatLng?,
getIsOpen: ((T) -> Boolean)? = null
): ApiResponse> {
if (response is ApiResponse.Success) {
val sortedData = response.data.sortedWith(
if (getIsOpen != null) {
compareByDescending { getIsOpen(it) }
- .thenBy { numericalDistanceToPlace(getLatitude(it),getLongitude(it)) }
+ .thenBy { numericalDistanceToPlace(getLatitude(it),getLongitude(it), userLocation) }
} else {
- compareBy { numericalDistanceToPlace(getLatitude(it),getLongitude(it)) }
+ compareBy { numericalDistanceToPlace(getLatitude(it),getLongitude(it), userLocation) }
}
)
return ApiResponse.Success(sortedData)
diff --git a/app/src/main/java/com/cornellappdev/transit/util/IntentUtils.kt b/app/src/main/java/com/cornellappdev/transit/util/IntentUtils.kt
new file mode 100644
index 0000000..e557aa7
--- /dev/null
+++ b/app/src/main/java/com/cornellappdev/transit/util/IntentUtils.kt
@@ -0,0 +1,32 @@
+package com.cornellappdev.transit.util
+
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import androidx.core.net.toUri
+
+object IntentUtils {
+ fun Context.openDeepLink(packageName: String) {
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
+
+ if (launchIntent != null) {
+ startActivity(launchIntent)
+ } else {
+ val playStoreIntent = Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri())
+ .setPackage("com.android.vending")
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ try {
+ startActivity(playStoreIntent)
+ } catch (e: ActivityNotFoundException) {
+ val webStoreIntent = Intent(Intent.ACTION_VIEW, "https://play.google.com/store/apps/details?id=$packageName".toUri())
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ try {
+ startActivity(webStoreIntent)
+ } catch (e2: ActivityNotFoundException) {
+ Log.e("IntentUtils","no handler for play store web URL" ,e2)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/cornellappdev/transit/util/RouteOptionsDisplayProcessor.kt b/app/src/main/java/com/cornellappdev/transit/util/RouteOptionsDisplayProcessor.kt
index 307f33e..8b03d06 100644
--- a/app/src/main/java/com/cornellappdev/transit/util/RouteOptionsDisplayProcessor.kt
+++ b/app/src/main/java/com/cornellappdev/transit/util/RouteOptionsDisplayProcessor.kt
@@ -258,13 +258,13 @@ private fun Route.effectiveArrivalInstantOrNull(
return endInstant.plusSeconds(delaySeconds.toLong())
}
-/** True when route arrives on or before provided cutoff (including grace already applied by caller). */
+/** True when route arrives on or before provided cutoff (strict, no grace period). */
private fun Route.arrivesBy(
- cutoffWithGrace: Instant,
+ cutoff: Instant,
diagnostics: RouteProcessingDiagnostics? = null,
): Boolean {
val arrivalInstant = effectiveArrivalInstantOrNull(diagnostics) ?: return false
- return !arrivalInstant.isAfter(cutoffWithGrace)
+ return !arrivalInstant.isAfter(cutoff)
}
/** Comparator for Arrive By ordering: latest departure first, then shorter distance. */
@@ -333,7 +333,7 @@ private fun compareByEffectiveLeaveTime(
/**
* Section-level Arrive By filtering and ordering.
- * Keeps routes that arrive by cutoff (with grace), then sorts by latest departure first.
+ * Keeps routes that arrive by cutoff, then sorts by latest departure first.
*/
private fun List?.filterAndSortRoutesForArriveBy(
cutoff: Instant,
@@ -341,10 +341,8 @@ private fun List?.filterAndSortRoutesForArriveBy(
): List? {
if (this == null) return null
- val cutoffWithGrace = cutoff.plus(Duration.ofMinutes(ARRIVE_BY_CUTOFF_GRACE_MINUTES))
-
return this
- .filter { route -> route.arrivesBy(cutoffWithGrace, diagnostics) }
+ .filter { route -> route.arrivesBy(cutoff, diagnostics) }
.sortedWith(::compareArriveByRoutes)
}
diff --git a/app/src/main/java/com/cornellappdev/transit/util/TransitConstants.kt b/app/src/main/java/com/cornellappdev/transit/util/TransitConstants.kt
index 54ba13f..a49ccfd 100644
--- a/app/src/main/java/com/cornellappdev/transit/util/TransitConstants.kt
+++ b/app/src/main/java/com/cornellappdev/transit/util/TransitConstants.kt
@@ -22,7 +22,6 @@ const val LEAVE_CUTOFF_HORIZON_MINUTES = 45L
const val LEAVE_CUTOFF_GRACE_MINUTES = 2L
-const val ARRIVE_BY_CUTOFF_GRACE_MINUTES = 2L
// Hide transit options when walking arrives at the same time or sooner (+ tie buffer).
const val WALKING_TRANSIT_TIE_MINUTES = 1L