Skip to content
Open
Show file tree
Hide file tree
Changes from all 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>?,
val ticketUrl: String?
)


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?
)

// 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,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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)
}
)
}
}
Expand All @@ -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(
Expand Down Expand Up @@ -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
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 = onAddToCalendar
)
}
}

}
Expand Down Expand Up @@ -331,7 +348,8 @@ private fun GameDetailsPreview() {
daysUntilGame = 6,
hoursUntilGame = 144,
homeScore = 78,
oppScore = 75
oppScore = 75,
ticketUrl = ""
), navigateToGameScoreSummary = {}
)
}
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
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 Context.startActivity FLAG_ACTIVITY_NEW_TASK ActivityNotFoundException implicit intent documentation

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

Repository: cuappdev/score-android

Length of output: 1374


🏁 Script executed:

rg "addGameToCalendar" app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt -B 3 -A 3

Repository: cuappdev/score-android

Length of output: 266


🏁 Script executed:

rg "addGameToCalendar" app/src/main/java/com/cornellappdev/score/ -B 2 -A 2

Repository: cuappdev/score-android

Length of output: 1231


🏁 Script executed:

rg -A 5 "onAddToCalendar = {" app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt

Repository: cuappdev/score-android

Length of output: 167


🏁 Script executed:

rg "onAddToCalendar" app/src/main/java/com/cornellappdev/score/screen/GameDetailsScreen.kt -A 5

Repository: cuappdev/score-android

Length of output: 614


Harden the calendar intent launch.

This accepts any Context and launches an implicit intent directly. A non-Activity context requires FLAG_ACTIVITY_NEW_TASK and will crash at runtime. Similarly, a device without a calendar app will crash on ActivityNotFoundException.

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
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/cornellappdev/score/viewmodel/GameDetailsViewModel.kt`
around lines 72 - 79, The calendar Intent creation and launch (the Intent
assigned to variable intent and the call context.startActivity(intent)) must be
hardened: before calling startActivity, check whether
intent.resolveActivity(context.packageManager) != null to ensure a calendar app
exists and wrap the launch in a try/catch for ActivityNotFoundException; also if
the provided context is not an Activity (context !is Activity) add
Intent.FLAG_ACTIVITY_NEW_TASK to the intent so it can be started from
non-Activity contexts. Ensure these checks/wrapping are applied around the
existing intent and the context.startActivity(intent) call.

}
}
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