-
Notifications
You must be signed in to change notification settings - Fork 0
Calendar+ticket #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Calendar+ticket #105
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,5 +28,6 @@ query GameById($id: String!) { | |
| corScore | ||
| oppScore | ||
| } | ||
| ticketLink | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -17,13 +17,15 @@ fun GameByIdQuery.Game.toGameDetails(): GameDetailsGame { | |||||
| time = this.time, | ||||||
| scoreBreakdown = this.scoreBreakdown, | ||||||
| team = this.team?.toGameDetailsTeam(), | ||||||
| boxScore = this.boxScore?.mapNotNull { it?.toGameDetailsBoxScore() } | ||||||
| boxScore = this.boxScore?.mapNotNull { it?.toGameDetailsBoxScore() }, | ||||||
| ticketUrl = ticketLink | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| fun GameByIdQuery.Team.toGameDetailsTeam(): GameDetailsTeam { | ||||||
| return GameDetailsTeam( | ||||||
| id = this.id, | ||||||
| color = parseColor(this.color).copy(alpha = 0.4f*255), | ||||||
| color = parseColor(this.color).copy(alpha = 0.4f * 255), | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: The valid range for the alpha parameter of androidx.compose.ui.graphics.Color.copy in Jetpack Compose is [0.0, 1.0] as a Float. This is confirmed by multiple official sources including the Android Developer documentation and Compose UI Graphics API references, which state that the alpha component is always in the range [0..1] for the Color class. The copy function takes alpha: Float = this.alpha, where this.alpha returns a value in [0..1]. In the Color constructor source code (for sRGB, the default), alpha is explicitly clamped using fastCoerceIn(0.0f, 1.0f) before encoding. While the copy function itself does not show explicit clamping in the signature, values outside [0,1] are handled consistently with the Color representation, which normalizes alpha to [0,1]. The internal encoding uses a 10-bit integer [0..1023] mapped from the float [0..1]. Citations:
🏁 Script executed: # First, find the file
fd -i GameByIdQueryMappers.ktRepository: cuappdev/score-android Length of output: 137 🏁 Script executed: # Examine the file around line 28
sed -n '20,35p' app/src/main/java/com/cornellappdev/score/model/GameByIdQueryMappers.ktRepository: cuappdev/score-android Length of output: 528 🏁 Script executed: # Search for similar patterns of alpha calculations with * 255
rg "copy\(alpha\s*=.*\*\s*255" app/src/main/java/Repository: cuappdev/score-android Length of output: 543 🏁 Script executed: # Get more context from ScoreRepository.kt to see these other instances
rg -B 2 -A 2 "copy\(alpha\s*=.*\*\s*255" app/src/main/java/com/cornellappdev/score/model/ScoreRepository.ktRepository: cuappdev/score-android Length of output: 818 Fix alpha value in Color.copy() to use normalized [0.0, 1.0] range.
Note: Similar issues exist in 🎨 Proposed fix- color = parseColor(this.color).copy(alpha = 0.4f * 255),
+ color = parseColor(this.color).copy(alpha = 0.4f),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| image = this.image, | ||||||
| name = this.name | ||||||
| ) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| package com.cornellappdev.score.screen | ||
|
|
||
| import ScoringSummary | ||
| import android.content.Intent | ||
| import android.net.Uri | ||
| import androidx.compose.foundation.Image | ||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.horizontalScroll | ||
|
|
@@ -31,6 +33,7 @@ import androidx.compose.ui.unit.sp | |
| import androidx.hilt.navigation.compose.hiltViewModel | ||
| import com.cornellappdev.score.R | ||
| import com.cornellappdev.score.components.BoxScore | ||
| import com.cornellappdev.score.components.ButtonPrimary | ||
| import com.cornellappdev.score.components.EmptyStateBox | ||
| import com.cornellappdev.score.components.ErrorState | ||
| import com.cornellappdev.score.components.GameDetailsLoadingScreen | ||
|
|
@@ -92,9 +95,16 @@ fun GameDetailsScreen( | |
| } | ||
|
|
||
| is ApiResponse.Success -> { | ||
| val context = LocalContext.current | ||
| GameDetailsContent( | ||
| gameCard = state.data, | ||
| navigateToGameScoreSummary = navigateToGameScoreSummary | ||
| navigateToGameScoreSummary = navigateToGameScoreSummary, | ||
| onAddToCalendar = { | ||
| gameDetailsViewModel.addGameToCalendar( | ||
| context, | ||
| state.data | ||
| ) | ||
| } | ||
| ) | ||
| } | ||
| } | ||
|
|
@@ -105,7 +115,8 @@ fun GameDetailsScreen( | |
| @Composable | ||
| fun GameDetailsContent( | ||
| gameCard: DetailsCardData, | ||
| navigateToGameScoreSummary: (List<ScoreEvent>) -> Unit | ||
| navigateToGameScoreSummary: (List<ScoreEvent>) -> Unit, | ||
| onAddToCalendar: () -> Unit = {} | ||
| ) { | ||
| val scrollState = rememberScrollState() | ||
| Column( | ||
|
|
@@ -211,25 +222,31 @@ fun GameDetailsContent( | |
| horizontalAlignment = Alignment.CenterHorizontally | ||
| ) { | ||
| Spacer(modifier = Modifier.height(40.dp)) | ||
|
|
||
| if (gameCard.daysUntilGame != null && gameCard.hoursUntilGame != null) { | ||
| TimeUntilStartCard( | ||
| gameCard.daysUntilGame, | ||
| gameCard.hoursUntilGame | ||
| ) | ||
| } | ||
|
|
||
| Spacer(modifier = Modifier.weight(1f)) | ||
|
|
||
| // ButtonPrimary( | ||
| // "Add to Calendar", | ||
| // painterResource(R.drawable.ic_calendar), | ||
| // onClick = { | ||
| // gameCard.toCalendarEvent()?.let { event -> | ||
| // addToCalendar(context = context, event) | ||
| // } | ||
| // } | ||
| // ) | ||
| Spacer(modifier = Modifier.height(100.dp)) | ||
| Row() { | ||
| ButtonPrimary( | ||
| "Buy Tickets", | ||
| painterResource(R.drawable.ticket), | ||
| onClick = { | ||
| gameCard.ticketUrl?.let { url -> | ||
| val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) | ||
| context.startActivity(intent) | ||
| } | ||
| } | ||
|
Comment on lines
+232
to
+241
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: When launching an Android ACTION_VIEW Intent via startActivity, an ActivityNotFoundException is thrown if no activity is available to handle the implicit intent. This commonly occurs with ACTION_VIEW for viewing URIs (e.g., http, geo, content) when no suitable app like a browser, gallery, or maps is installed or accessible. Official documentation recommends two primary strategies to handle this: 1. Check resolveActivity first: Query the PackageManager to verify if any activity can handle the intent before calling startActivity. If resolveActivity returns non-null, proceed safely. Example (Kotlin): val intent = Intent(Intent.ACTION_VIEW, uri) if (intent.resolveActivity(packageManager) != null) { startActivity(intent) } else { // Handle gracefully, e.g., show message or fallback } Example (Java): Intent intent = new Intent(Intent.ACTION_VIEW, uri); if (intent.resolveActivity(getPackageManager) != null) { startActivity(intent); } else { // Handle gracefully } 2. Wrap in try-catch: Catch ActivityNotFoundException as a fallback. Example (Kotlin): try { startActivity(intent) } catch (e: ActivityNotFoundException) { // Define what your app should do if no activity can handle the intent. } Example (Java): Similar structure. Best practice combines both: Use resolveActivity to proactively avoid the exception, and try-catch for defense-in-depth. For Android 11+, declare in AndroidManifest.xml for package visibility if resolveActivity returns unexpected nulls for common intents like https VIEW. This applies specifically to implicit intents like ACTION_VIEW, which rely on intent resolution via action, data (URI scheme/type), and categories against installed apps' manifest filters. Citations:
🏁 Script executed: find . -name "GameDetailsScreen.kt" -type fRepository: cuappdev/score-android Length of output: 137 🏁 Script executed: cat -n app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt | sed -n '220,250p'Repository: cuappdev/score-android Length of output: 1660 🏁 Script executed: # Check if there are any other URL validation patterns in the codebase for reference
rg "resolveActivity|ActivityNotFoundException" --type kotlin -A 3 -B 3Repository: cuappdev/score-android Length of output: 48 🏁 Script executed: # Search for gameCard definition and ticketUrl property
rg "ticketUrl" --type kotlin -B 2 -A 2Repository: cuappdev/score-android Length of output: 3435 🏁 Script executed: # Check the gameCard data class definition
rg "data class.*GameCard|ticketUrl.*:" --type kotlin -A 5Repository: cuappdev/score-android Length of output: 1890 🏁 Script executed: # Check imports in GameDetailsScreen to see if packageManager is accessible
head -50 app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.ktRepository: cuappdev/score-android Length of output: 2385 🏁 Script executed: # Check how context is obtained in GameDetailsScreen
rg "LocalContext" app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt -B 5 -A 5Repository: cuappdev/score-android Length of output: 1459 🏁 Script executed: # Check if packageManager is used elsewhere in Compose code
rg "packageManager|resolveActivity" --type kotlinRepository: cuappdev/score-android Length of output: 48 🏁 Script executed: # Check the full composable function signature to see context parameter
sed -n '150,250p' app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt | head -100Repository: cuappdev/score-android Length of output: 4594 Validate and safely launch ticket URLs. The current implementation can crash if no app handles the intent or if the URL is blank/malformed. Hide the button when the URL is invalid and check that an activity can handle the intent before launching. The fix should:
🤖 Prompt for AI Agents |
||
| ) | ||
| Spacer(Modifier.size(16.dp)) | ||
| ButtonPrimary( | ||
| "Add to Calendar", | ||
| painterResource(R.drawable.ic_calendar), | ||
| onClick = onAddToCalendar | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
@@ -331,7 +348,8 @@ private fun GameDetailsPreview() { | |
| daysUntilGame = 6, | ||
| hoursUntilGame = 144, | ||
| homeScore = 78, | ||
| oppScore = 75 | ||
| oppScore = 75, | ||
| ticketUrl = "" | ||
| ), navigateToGameScoreSummary = {} | ||
| ) | ||
| } | ||
|
|
@@ -382,7 +400,10 @@ private fun EmptyGameDetailsPreview() { | |
| daysUntilGame = 0, | ||
| hoursUntilGame = 0, | ||
| homeScore = 0, | ||
| oppScore = 0 | ||
| ), navigateToGameScoreSummary = {} | ||
| oppScore = 0, | ||
| ticketUrl = "" | ||
| ), | ||
| navigateToGameScoreSummary = {}, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,9 @@ | ||
| package com.cornellappdev.score.viewmodel | ||
|
|
||
| import android.content.Context | ||
| import android.content.Intent | ||
| import android.provider.CalendarContract | ||
| import android.util.Log | ||
| import androidx.lifecycle.SavedStateHandle | ||
| import androidx.navigation.toRoute | ||
| import com.cornellappdev.score.model.ApiResponse | ||
|
|
@@ -9,6 +13,10 @@ import com.cornellappdev.score.model.map | |
| import com.cornellappdev.score.model.toGameCardData | ||
| import com.cornellappdev.score.nav.root.ScoreScreens | ||
| import dagger.hilt.android.lifecycle.HiltViewModel | ||
| import java.time.LocalTime | ||
| import java.time.ZoneId | ||
| import java.time.format.DateTimeFormatter | ||
| import java.util.Locale | ||
| import javax.inject.Inject | ||
|
|
||
| data class GameDetailsUiState( | ||
|
|
@@ -43,4 +51,31 @@ class GameDetailsViewModel @Inject constructor( | |
| applyMutation { copy(loadedState = ApiResponse.Loading) } | ||
| scoreRepository.getGameById(gameId) | ||
| } | ||
|
|
||
| fun addGameToCalendar(context: Context, gameCard: DetailsCardData) { | ||
| val date = gameCard.date ?: return | ||
| val time = gameCard.time | ||
|
|
||
| val startDateTime = try { | ||
| val formatter = DateTimeFormatter.ofPattern("h:mm a", Locale.ENGLISH) | ||
| val localTime = LocalTime.parse(time.trim().uppercase().replace(".", ""), formatter) | ||
| date.atTime(localTime) | ||
| } catch (e: Exception) { | ||
| Log.e("Calendar", "Failed to parse time: '$time'", e) | ||
| date.atStartOfDay() | ||
| } | ||
|
|
||
| val zoneId = ZoneId.systemDefault() | ||
| val startMillis = startDateTime.atZone(zoneId).toInstant().toEpochMilli() | ||
| val endMillis = startDateTime.plusHours(2).atZone(zoneId).toInstant().toEpochMilli() | ||
|
|
||
| val intent = Intent(Intent.ACTION_INSERT, CalendarContract.Events.CONTENT_URI).apply { | ||
| putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis) | ||
| putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis) | ||
| putExtra(CalendarContract.Events.TITLE, gameCard.title) | ||
| putExtra(CalendarContract.Events.EVENT_LOCATION, gameCard.locationString) | ||
| putExtra(CalendarContract.Events.DESCRIPTION, "${gameCard.sport} - ${gameCard.gender}") | ||
| } | ||
| context.startActivity(intent) | ||
|
Comment on lines
+72
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: When calling Context.startActivity from a non-Activity context (like ApplicationContext or Service) with an implicit intent, you must add Intent.FLAG_ACTIVITY_NEW_TASK to the intent flags. Otherwise, Android throws AndroidRuntimeException: "Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?" This check occurs in ContextImpl.startActivity before intent resolution. This requirement is separate from ActivityNotFoundException, which is thrown later if no app can handle the implicit intent (no matching intent-filter with CATEGORY_DEFAULT). Official docs recommend checking intent.resolveActivity(getPackageManager) != null before startActivity to avoid ActivityNotFoundException. Use intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) for non-activity contexts. For implicit intents, always verify resolvability first. Example: Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com")); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (intent.resolveActivity(context.getPackageManager) != null) { context.startActivity(intent); } else { // Handle no app available } Citations:
🏁 Script executed: grep -A 25 "fun addGameToCalendar" app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.ktRepository: cuappdev/score-android Length of output: 1374 🏁 Script executed: rg "addGameToCalendar" app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt -B 3 -A 3Repository: cuappdev/score-android Length of output: 266 🏁 Script executed: rg "addGameToCalendar" app/src/main/java/com/cornellappdev/score/ -B 2 -A 2Repository: cuappdev/score-android Length of output: 1231 🏁 Script executed: rg -A 5 "onAddToCalendar = {" app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.ktRepository: cuappdev/score-android Length of output: 167 🏁 Script executed: rg "onAddToCalendar" app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt -A 5Repository: cuappdev/score-android Length of output: 614 Harden the calendar intent launch. This accepts any Proposed fix val intent = Intent(Intent.ACTION_INSERT, CalendarContract.Events.CONTENT_URI).apply {
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startMillis)
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, endMillis)
putExtra(CalendarContract.Events.TITLE, gameCard.title)
putExtra(CalendarContract.Events.EVENT_LOCATION, gameCard.locationString)
putExtra(CalendarContract.Events.DESCRIPTION, "${gameCard.sport} - ${gameCard.gender}")
}
+ if (context !is android.app.Activity) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+
+ try {
+ context.startActivity(intent)
+ } catch (e: android.content.ActivityNotFoundException) {
+ Log.w("Calendar", "No calendar app available to handle insert intent", e)
+ }
- context.startActivity(intent)🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| android:width="24dp" | ||
| android:height="24dp" | ||
| android:viewportWidth="24" | ||
| android:viewportHeight="24"> | ||
| <path | ||
| android:pathData="M14.551,0.866L0.862,14.677L3.307,17.122C6.362,16.266 7.218,17.977 6.362,20.299L9.051,22.866L22.74,9.055L20.296,6.611C17.167,7.491 16.792,4.777 16.996,3.311L14.551,0.866Z" | ||
| android:strokeWidth="1.22222" | ||
| android:fillColor="#00000000" | ||
| android:strokeColor="#ffffff" | ||
| android:strokeLineCap="round"/> | ||
| <path | ||
| android:pathData="M10.151,5.266L11.129,6.244M12.229,7.344L13.696,8.811M14.918,10.033L16.262,11.377M17.485,12.599L18.34,13.455" | ||
| android:strokeWidth="1.22222" | ||
| android:fillColor="#00000000" | ||
| android:strokeColor="#ffffff" | ||
| android:strokeLineCap="round"/> | ||
| </vector> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve missing ticket URLs as absent.
DetailsCardData.ticketUrlis nullable, but this converts a missing URL into"", making downstream null checks ineffective. Keep it null or normalize blank values to null.🐛 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents