Skip to content

Commit d1e2a3b

Browse files
Merge pull request #145 from cuappdev/abby/deep-linking
Deep-Linking (PARTIAL)
2 parents acc3cff + f994c10 commit d1e2a3b

9 files changed

Lines changed: 118 additions & 38 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ android {
2525
applicationId = "com.cornellappdev.transit"
2626
minSdk = 26
2727
targetSdk = 36
28-
versionCode = 10
29-
versionName = "2.0"
28+
versionCode = 11
29+
versionName = "2.1"
3030

3131
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3232
vectorDrawables {

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
77
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
88

9+
<queries>
10+
<package android:name="com.cornellappdev.android.eatery" />
11+
<package android:name="com.cornellappdev.uplift" />
12+
</queries>
13+
914
<application
1015
android:name=".TransitApplication"
1116
android:allowBackup="true"
@@ -24,7 +29,6 @@
2429
android:screenOrientation="portrait">
2530
<intent-filter>
2631
<action android:name="android.intent.action.MAIN" />
27-
2832
<category android:name="android.intent.category.LAUNCHER" />
2933
</intent-filter>
3034

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import androidx.compose.ui.Alignment
2424
import androidx.compose.ui.Modifier
2525
import androidx.compose.ui.graphics.Color
2626
import androidx.compose.ui.graphics.vector.ImageVector
27+
import androidx.compose.ui.platform.LocalContext
2728
import androidx.compose.ui.res.vectorResource
2829
import androidx.compose.ui.tooling.preview.Preview
2930
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -40,6 +41,7 @@ import com.cornellappdev.transit.ui.theme.SecondaryText
4041
import com.cornellappdev.transit.ui.theme.Style
4142
import com.cornellappdev.transit.ui.theme.TransitBlue
4243
import com.cornellappdev.transit.util.BOTTOM_SHEET_MAX_HEIGHT_PERCENT
44+
import com.cornellappdev.transit.util.IntentUtils.openDeepLink
4345

4446
@Composable
4547
fun DetailedPlaceSheetContent(
@@ -51,6 +53,8 @@ fun DetailedPlaceSheetContent(
5153
modifier: Modifier = Modifier,
5254
distanceStringToPlace: (Double?, Double?) -> String
5355
) {
56+
val context = LocalContext.current
57+
5458
Column(
5559
modifier = modifier
5660
.fillMaxWidth()
@@ -98,6 +102,9 @@ fun DetailedPlaceSheetContent(
98102
onFavoriteClick = {
99103
onFavoriteStarClick(ecosystemPlace.toPlace())
100104
},
105+
onDeepLinkClick = {
106+
context.openDeepLink("com.cornellappdev.android.eatery")
107+
},
101108
distanceString = distanceStringToPlace(
102109
ecosystemPlace.latitude,
103110
ecosystemPlace.longitude
@@ -122,6 +129,9 @@ fun DetailedPlaceSheetContent(
122129
onFavoriteClick = {
123130
onFavoriteStarClick(ecosystemPlace.toPlace())
124131
},
132+
onDeepLinkClick = {
133+
context.openDeepLink("com.cornellappdev.uplift")
134+
},
125135
distanceString = distanceStringToPlace(
126136
ecosystemPlace.latitude,
127137
ecosystemPlace.longitude

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.cornellappdev.transit.ui.components.home
22

3+
import android.content.ActivityNotFoundException
4+
import android.content.Intent
5+
import androidx.compose.foundation.clickable
36
import androidx.compose.foundation.layout.Column
47
import androidx.compose.foundation.layout.Row
58
import androidx.compose.foundation.layout.Spacer
@@ -10,9 +13,11 @@ import androidx.compose.foundation.layout.size
1013
import androidx.compose.material3.HorizontalDivider
1114
import androidx.compose.material3.Icon
1215
import androidx.compose.material3.Text
16+
import androidx.compose.material3.TextButton
1317
import androidx.compose.runtime.Composable
1418
import androidx.compose.ui.Alignment
1519
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.platform.LocalContext
1621
import androidx.compose.ui.res.painterResource
1722
import androidx.compose.ui.res.stringResource
1823
import androidx.compose.ui.unit.dp
@@ -28,12 +33,14 @@ import com.cornellappdev.transit.util.StringUtils.createDeepLink
2833
import com.cornellappdev.transit.util.TimeUtils.isOpenAnnotatedStringFromOperatingHours
2934
import com.cornellappdev.transit.util.TimeUtils.rotateOperatingHours
3035
import com.cornellappdev.transit.util.getAboutContent
36+
import androidx.core.net.toUri
3137

3238
@Composable
3339
fun EateryDetailsContent(
3440
eatery: Eatery,
3541
isFavorite: Boolean,
3642
onFavoriteClick: () -> Unit,
43+
onDeepLinkClick: () -> Unit,
3744
distanceString: String,
3845
) {
3946
Column(
@@ -88,12 +95,16 @@ fun EateryDetailsContent(
8895
val (annotatedString, inlineContent) =
8996
stringResource(R.string.view_menu).createDeepLink(R.drawable.eaterylink)
9097

91-
Text(
92-
text = annotatedString,
93-
inlineContent = inlineContent,
94-
style = Style.heading2,
95-
color = TransitBlue
96-
)
98+
TextButton(
99+
onClick = onDeepLinkClick
100+
) {
101+
Text(
102+
text = annotatedString,
103+
inlineContent = inlineContent,
104+
style = Style.heading2,
105+
color = TransitBlue,
106+
)
107+
}
97108

98109
Spacer(modifier = Modifier.height(24.dp))
99110

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.cornellappdev.transit.ui.components.home
22

3+
import android.content.ActivityNotFoundException
4+
import android.content.Intent
5+
import androidx.compose.foundation.clickable
36
import androidx.compose.foundation.layout.Column
47
import androidx.compose.foundation.layout.Row
58
import androidx.compose.foundation.layout.Spacer
@@ -10,12 +13,15 @@ import androidx.compose.foundation.layout.size
1013
import androidx.compose.material3.HorizontalDivider
1114
import androidx.compose.material3.Icon
1215
import androidx.compose.material3.Text
16+
import androidx.compose.material3.TextButton
1317
import androidx.compose.runtime.Composable
1418
import androidx.compose.ui.Alignment
1519
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.platform.LocalContext
1621
import androidx.compose.ui.res.painterResource
1722
import androidx.compose.ui.res.stringResource
1823
import androidx.compose.ui.unit.dp
24+
import androidx.core.net.toUri
1925
import com.cornellappdev.transit.R
2026
import com.cornellappdev.transit.models.ecosystem.UpliftGym
2127
import com.cornellappdev.transit.ui.theme.DividerGray
@@ -39,6 +45,7 @@ fun GymDetailsContent(
3945
gym: UpliftGym,
4046
isFavorite: Boolean,
4147
onFavoriteClick: () -> Unit,
48+
onDeepLinkClick: () -> Unit,
4249
distanceString: String
4350
) {
4451
val isOpen = getOpenStatus(gym.operatingHours()).isOpen
@@ -97,12 +104,16 @@ fun GymDetailsContent(
97104
val (annotatedString, inlineContent) =
98105
stringResource(R.string.view_gym).createDeepLink(R.drawable.upliftlink)
99106

100-
Text(
101-
text = annotatedString,
102-
inlineContent = inlineContent,
103-
style = Style.heading2,
104-
color = TransitBlue
105-
)
107+
TextButton(
108+
onClick = onDeepLinkClick
109+
) {
110+
Text(
111+
text = annotatedString,
112+
inlineContent = inlineContent,
113+
style = Style.heading2,
114+
color = TransitBlue,
115+
)
116+
}
106117

107118
Spacer(modifier = Modifier.height(24.dp))
108119

app/src/main/java/com/cornellappdev/transit/ui/viewmodels/HomeViewModel.kt

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,18 @@ class HomeViewModel @Inject constructor(
5656
) : ViewModel() {
5757

5858
val libraryCardsFlow: StateFlow<ApiResponse<List<LibraryCardUiState>>> =
59-
routeRepository.libraryFlow.map { response ->
59+
combine(
60+
routeRepository.libraryFlow,
61+
locationRepository.currentLocation
62+
) { response, location ->
63+
val usersLocation = location?.let { LatLng(it.latitude, it.longitude) }
6064
when (val filteredResponse = response.withExcludedLibrariesRemoved()) {
6165
is ApiResponse.Success -> {
66+
val sortedLibraries = filteredResponse.data.sortedBy {
67+
numericalDistanceToPlace(it.latitude, it.longitude, usersLocation)
68+
}
6269
ApiResponse.Success(
63-
filteredResponse.data
70+
sortedLibraries
6471
.map { it.toLibraryCardUiState() }
6572
)
6673
}
@@ -119,6 +126,9 @@ class HomeViewModel @Inject constructor(
119126
gymRepository.gymFlow,
120127
locationRepository.currentLocation
121128
) { printers, libraries, eateries, gyms, location ->
129+
val userLocation = location?.let { LatLng(it.latitude, it.longitude) }
130+
131+
122132
StaticPlaces(
123133
printers = sortApiResponse(
124134
response = if (printers is ApiResponse.Success) {
@@ -127,23 +137,27 @@ class HomeViewModel @Inject constructor(
127137
printers
128138
},
129139
getLatitude = { it.latitude },
130-
getLongitude = { it.longitude }
140+
getLongitude = { it.longitude },
141+
userLocation = userLocation
131142
),
132143
libraries = sortApiResponse(
133-
response = libraries,
144+
response = libraries.withExcludedLibrariesRemoved(),
134145
getLatitude = { it.latitude },
135-
getLongitude = { it.longitude }
136-
).withExcludedLibrariesRemoved(),
146+
getLongitude = { it.longitude },
147+
userLocation = userLocation
148+
),
137149
eateries = sortApiResponse(
138150
response = eateries,
139151
getLatitude = { it.latitude },
140152
getLongitude = { it.longitude },
153+
userLocation = userLocation,
141154
getIsOpen = { TimeUtils.getOpenStatus(it.operatingHours()).isOpen }
142155
),
143156
gyms = sortApiResponse(
144157
response = gyms,
145158
getLatitude = { it.latitude },
146159
getLongitude = { it.longitude },
160+
userLocation = userLocation,
147161
getIsOpen = { TimeUtils.getOpenStatus(it.operatingHours()).isOpen }
148162
)
149163
)
@@ -513,14 +527,14 @@ class HomeViewModel @Inject constructor(
513527
/**
514528
* Returns a numerical distance from a location to the current location if both exist, otherwise returns Double.MAX_VALUE
515529
*/
516-
fun numericalDistanceToPlace(latitude: Double?, longitude: Double?): Double {
517-
val currentLocationSnapshot = currentLocation.value
518-
return if (currentLocationSnapshot != null && latitude != null && longitude != null) {
530+
fun numericalDistanceToPlace(
531+
latitude: Double?,
532+
longitude: Double?,
533+
userLocation: LatLng?
534+
): Double {
535+
return if (userLocation != null && latitude != null && longitude != null) {
519536
calculateDistance(
520-
LatLng(
521-
currentLocationSnapshot.latitude,
522-
currentLocationSnapshot.longitude
523-
), LatLng(latitude, longitude)
537+
userLocation, LatLng(latitude, longitude)
524538
)
525539
} else {
526540
Double.MAX_VALUE
@@ -534,15 +548,16 @@ class HomeViewModel @Inject constructor(
534548
response: ApiResponse<List<T>>,
535549
getLatitude: (T) -> Double?,
536550
getLongitude: (T) -> Double?,
551+
userLocation: LatLng?,
537552
getIsOpen: ((T) -> Boolean)? = null
538553
): ApiResponse<List<T>> {
539554
if (response is ApiResponse.Success) {
540555
val sortedData = response.data.sortedWith(
541556
if (getIsOpen != null) {
542557
compareByDescending<T> { getIsOpen(it) }
543-
.thenBy { numericalDistanceToPlace(getLatitude(it),getLongitude(it)) }
558+
.thenBy { numericalDistanceToPlace(getLatitude(it),getLongitude(it), userLocation) }
544559
} else {
545-
compareBy { numericalDistanceToPlace(getLatitude(it),getLongitude(it)) }
560+
compareBy { numericalDistanceToPlace(getLatitude(it),getLongitude(it), userLocation) }
546561
}
547562
)
548563
return ApiResponse.Success(sortedData)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.cornellappdev.transit.util
2+
3+
import android.content.ActivityNotFoundException
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.util.Log
7+
import androidx.core.net.toUri
8+
9+
object IntentUtils {
10+
fun Context.openDeepLink(packageName: String) {
11+
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
12+
13+
if (launchIntent != null) {
14+
startActivity(launchIntent)
15+
} else {
16+
val playStoreIntent = Intent(Intent.ACTION_VIEW, "market://details?id=$packageName".toUri())
17+
.setPackage("com.android.vending")
18+
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
19+
try {
20+
startActivity(playStoreIntent)
21+
} catch (e: ActivityNotFoundException) {
22+
val webStoreIntent = Intent(Intent.ACTION_VIEW, "https://play.google.com/store/apps/details?id=$packageName".toUri())
23+
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
24+
try {
25+
startActivity(webStoreIntent)
26+
} catch (e2: ActivityNotFoundException) {
27+
Log.e("IntentUtils","no handler for play store web URL" ,e2)
28+
}
29+
}
30+
}
31+
}
32+
}

app/src/main/java/com/cornellappdev/transit/util/RouteOptionsDisplayProcessor.kt

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -258,13 +258,13 @@ private fun Route.effectiveArrivalInstantOrNull(
258258
return endInstant.plusSeconds(delaySeconds.toLong())
259259
}
260260

261-
/** True when route arrives on or before provided cutoff (including grace already applied by caller). */
261+
/** True when route arrives on or before provided cutoff (strict, no grace period). */
262262
private fun Route.arrivesBy(
263-
cutoffWithGrace: Instant,
263+
cutoff: Instant,
264264
diagnostics: RouteProcessingDiagnostics? = null,
265265
): Boolean {
266266
val arrivalInstant = effectiveArrivalInstantOrNull(diagnostics) ?: return false
267-
return !arrivalInstant.isAfter(cutoffWithGrace)
267+
return !arrivalInstant.isAfter(cutoff)
268268
}
269269

270270
/** Comparator for Arrive By ordering: latest departure first, then shorter distance. */
@@ -333,18 +333,16 @@ private fun compareByEffectiveLeaveTime(
333333

334334
/**
335335
* Section-level Arrive By filtering and ordering.
336-
* Keeps routes that arrive by cutoff (with grace), then sorts by latest departure first.
336+
* Keeps routes that arrive by cutoff, then sorts by latest departure first.
337337
*/
338338
private fun List<Route>?.filterAndSortRoutesForArriveBy(
339339
cutoff: Instant,
340340
diagnostics: RouteProcessingDiagnostics? = null,
341341
): List<Route>? {
342342
if (this == null) return null
343343

344-
val cutoffWithGrace = cutoff.plus(Duration.ofMinutes(ARRIVE_BY_CUTOFF_GRACE_MINUTES))
345-
346344
return this
347-
.filter { route -> route.arrivesBy(cutoffWithGrace, diagnostics) }
345+
.filter { route -> route.arrivesBy(cutoff, diagnostics) }
348346
.sortedWith(::compareArriveByRoutes)
349347
}
350348

app/src/main/java/com/cornellappdev/transit/util/TransitConstants.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ const val LEAVE_CUTOFF_HORIZON_MINUTES = 45L
2222

2323
const val LEAVE_CUTOFF_GRACE_MINUTES = 2L
2424

25-
const val ARRIVE_BY_CUTOFF_GRACE_MINUTES = 2L
2625

2726
// Hide transit options when walking arrives at the same time or sooner (+ tie buffer).
2827
const val WALKING_TRANSIT_TIE_MINUTES = 1L

0 commit comments

Comments
 (0)