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