Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/graphql/GameById.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ query GameById($id: String!) {
corScore
oppScore
}
ticketLink
}
}
11 changes: 7 additions & 4 deletions app/src/main/java/com/cornellappdev/score/model/Game.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ data class GameDetailsGame(
val time: String?,
val scoreBreakdown: List<List<String?>?>?,
val team: GameDetailsTeam?,
val boxScore: List<GameDetailsBoxScore>?
val boxScore: List<GameDetailsBoxScore>?,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It's a bit confusing that this is called boxScore but it's a list of box scores.

val ticketUrl: String?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use the URI type for this internally? I think the conversion logic should happen at the root so Game objects don't carry around possibly bad URLs.

)


Expand Down Expand Up @@ -126,7 +127,8 @@ data class DetailsCardData(
val daysUntilGame: Int?,
val hoursUntilGame: Int?,
val homeScore: Int,
val oppScore: Int
val oppScore: Int,
val ticketUrl: String?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here

)

// Scoring information by round of a game, used in the box score
Expand All @@ -146,7 +148,7 @@ data class TeamScore(
// Aggregated game data showing scores for both teams
data class GameData(
val teamScores: Pair<TeamScore, TeamScore>
){
) {
val maxPeriods: Int
get() =
maxOf(
Expand Down Expand Up @@ -298,7 +300,8 @@ fun GameDetailsGame.toGameCardData(): DetailsCardData {
homeScore = convertScores(scoreBreakdown?.getOrNull(0), sport).second
?: parsedScores?.first ?: 0,
oppScore = convertScores(scoreBreakdown?.getOrNull(1), sport).second
?: parsedScores?.second ?: 0
?: parsedScores?.second ?: 0,
ticketUrl = ticketUrl ?: ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve missing ticket URLs as absent.

DetailsCardData.ticketUrl is nullable, but this converts a missing URL into "", making downstream null checks ineffective. Keep it null or normalize blank values to null.

🐛 Proposed fix
-        ticketUrl = ticketUrl ?: ""
+        ticketUrl = ticketUrl?.takeIf { it.isNotBlank() }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ticketUrl = ticketUrl ?: ""
ticketUrl = ticketUrl?.takeIf { it.isNotBlank() }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/score/model/Game.kt` at line 304, The
current assignment ticketUrl = ticketUrl ?: "" forces missing URLs to an empty
string; update the mapping in Game.kt so DetailsCardData.ticketUrl remains
nullable by preserving nulls and converting blank strings to null instead (e.g.,
set ticketUrl = ticketUrl?.takeIf { it.isNotBlank() } or simply leave ticketUrl
as-is). Locate the assignment to ticketUrl in the Game -> DetailsCardData
mapping and replace the forced empty-string normalization with a
nullable-preserving normalization.

)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

What is the valid range for the alpha parameter of androidx.compose.ui.graphics.Color.copy in Jetpack Compose?

💡 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.kt

Repository: 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.kt

Repository: 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.kt

Repository: cuappdev/score-android

Length of output: 818


Fix alpha value in Color.copy() to use normalized [0.0, 1.0] range.

Color.copy(alpha = ...) expects a value in [0.0, 1.0]; 0.4f * 255 produces 102f, which is outside the valid range and causes incorrect color rendering. Use 0.4f directly.

Note: Similar issues exist in ScoreRepository.kt at multiple locations.

🎨 Proposed fix
-        color = parseColor(this.color).copy(alpha = 0.4f * 255),
+        color = parseColor(this.color).copy(alpha = 0.4f),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
color = parseColor(this.color).copy(alpha = 0.4f * 255),
color = parseColor(this.color).copy(alpha = 0.4f),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/score/model/GameByIdQueryMappers.kt` at
line 28, The color alpha is being set using 0.4f * 255 which yields an
out-of-range value for Color.copy(alpha = …) (it expects 0.0–1.0); update the
call in GameByIdQueryMappers (the expression with
parseColor(this.color).copy(...)) to use a normalized alpha value like 0.4f
instead of 0.4f * 255, and apply the same change to all similar usages in
ScoreRepository.kt where Color.copy(alpha) is passed a scaled value.

image = this.image,
name = this.name
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.cornellappdev.score.screen

import ScoringSummary
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.CalendarContract
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
Expand Down Expand Up @@ -31,6 +36,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
Expand All @@ -55,6 +61,10 @@ import com.cornellappdev.score.theme.Style.heading3
import com.cornellappdev.score.theme.White
import com.cornellappdev.score.viewmodel.GameDetailsViewModel
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale

@Composable
fun GameDetailsScreen(
Expand Down Expand Up @@ -220,16 +230,26 @@ fun GameDetailsContent(
}

Spacer(modifier = Modifier.weight(1f))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the spacing is slightly off here

Image

design
Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2026-04-22 at 6 05 37 PM I couldnt find the correct measurement, but this looks kind of similar to me


// ButtonPrimary(
// "Add to Calendar",
// painterResource(R.drawable.ic_calendar),
// onClick = {
// gameCard.toCalendarEvent()?.let { event ->
// addToCalendar(context = context, event)
// }
// }
// )
Row() {
ButtonPrimary(
"Buy Tickets",
painterResource(R.drawable.ticket),
onClick = {
gameCard.ticketUrl?.let { url ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is URL parsing logic being done on the view side, and this can throw. As with previous comments, moving to model side is probably better.

context.startActivity(intent)
}
}
Comment on lines +232 to +241

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Android ACTION_VIEW Intent startActivity ActivityNotFoundException resolveActivity documentation

💡 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 f

Repository: 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 3

Repository: cuappdev/score-android

Length of output: 48


🏁 Script executed:

# Search for gameCard definition and ticketUrl property
rg "ticketUrl" --type kotlin -B 2 -A 2

Repository: cuappdev/score-android

Length of output: 3435


🏁 Script executed:

# Check the gameCard data class definition
rg "data class.*GameCard|ticketUrl.*:" --type kotlin -A 5

Repository: 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.kt

Repository: 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 5

Repository: cuappdev/score-android

Length of output: 1459


🏁 Script executed:

# Check if packageManager is used elsewhere in Compose code
rg "packageManager|resolveActivity" --type kotlin

Repository: 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 -100

Repository: 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:

  1. Validate the URL is non-blank
  2. Parse and validate the URI scheme is http or https
  3. Use resolveActivity(context.packageManager) before calling startActivity() to prevent ActivityNotFoundException
  4. Only show the button if the URL is valid and an activity can handle it
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt` around
lines 232 - 241, In GameDetailsScreen where ButtonPrimary is rendered for "Buy
Tickets", validate gameCard.ticketUrl is non-blank, parse it into a Uri and
ensure the scheme is "http" or "https", then build the
Intent(Intent.ACTION_VIEW, uri) and call
intent.resolveActivity(context.packageManager) to verify an activity can handle
it; only render the ButtonPrimary when both the URL/URI scheme validation and
resolveActivity check pass, and when clicked call startActivity(intent) knowing
it is safe. Reference: GameDetailsScreen, ButtonPrimary, gameCard.ticketUrl,
Intent, Uri, context.packageManager, resolveActivity.

)
Spacer(Modifier.size(16.dp))
ButtonPrimary(
"Add to Calendar",
painterResource(R.drawable.ic_calendar),
onClick = {
addGameToCalendar(context, gameCard)
}
)
}
}

}
Expand Down Expand Up @@ -331,7 +351,8 @@ private fun GameDetailsPreview() {
daysUntilGame = 6,
hoursUntilGame = 144,
homeScore = 78,
oppScore = 75
oppScore = 75,
ticketUrl = ""
), navigateToGameScoreSummary = {}
)
}
Expand Down Expand Up @@ -382,7 +403,37 @@ private fun EmptyGameDetailsPreview() {
daysUntilGame = 0,
hoursUntilGame = 0,
homeScore = 0,
oppScore = 0
), navigateToGameScoreSummary = {}
oppScore = 0,
ticketUrl = ""
),
navigateToGameScoreSummary = {},
)
}

// helper
Comment thread
EmilJiang marked this conversation as resolved.
Outdated
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)
}
18 changes: 18 additions & 0 deletions app/src/main/res/drawable/ticket.xml
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>
Loading