From d4c75778281b92d0a044ff5b8b3ab1ea0346fe46 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 02:40:00 +0000 Subject: [PATCH] feat: record the source app/device name from Health Connect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of a generic "health_connect" source, capture the app/device that wrote the record (the Health Connect data origin) and use it as the workout source — so the card shows e.g. "SAMSUNG HEALTH" / "GOOGLE FIT" instead of "HEALTH CONNECT". resolveSourceName resolves the data-origin package to the installed app's label, falling back to a known-package map (Samsung Health, Google Fit, Fitbit, Garmin, Strava, Nike Run Club), then the raw package, then "Health Connect". DetectedWorkout carries this as `source` and it flows into the published kind 1301 source tag. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P --- .../workouts/health/DetectedWorkout.kt | 2 ++ .../workouts/health/HealthConnectManager.kt | 30 +++++++++++++++++++ .../suggestion/WorkoutSuggestionShared.kt | 3 +- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/DetectedWorkout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/DetectedWorkout.kt index 32ac42aaeb..bfb18e2e8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/DetectedWorkout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/DetectedWorkout.kt @@ -45,4 +45,6 @@ data class DetectedWorkout( val maxHeartRate: Int?, val steps: Int?, val elevationGainMeters: Double?, + /** Human-readable name of the app/device that wrote the record (e.g. "Samsung Health"). */ + val source: String, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/HealthConnectManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/HealthConnectManager.kt index be51a2d572..e313632d35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/HealthConnectManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/workouts/health/HealthConnectManager.kt @@ -60,6 +60,20 @@ class HealthConnectManager( /** How far back the New Workout carousel looks for workouts to offer. */ const val LOOKBACK_DAYS = 7L + private const val DEFAULT_SOURCE = "Health Connect" + + /** Friendly names for well-known writers when their app isn't installed to read a label from. */ + private val KNOWN_SOURCES = + mapOf( + "com.sec.android.app.shealth" to "Samsung Health", + "com.google.android.apps.fitness" to "Google Fit", + "com.google.android.apps.healthdata" to "Health Connect", + "com.fitbit.FitbitMobile" to "Fitbit", + "com.garmin.android.apps.connectmobile" to "Garmin Connect", + "com.strava" to "Strava", + "com.nike.plusgps" to "Nike Run Club", + ) + /** Read permissions needed to map a workout. */ val PERMISSIONS = setOf( @@ -168,9 +182,25 @@ class HealthConnectManager( maxHeartRate = totals?.get(HeartRateRecord.BPM_MAX)?.toInt(), steps = totals?.get(StepsRecord.COUNT_TOTAL)?.toInt(), elevationGainMeters = totals?.get(ElevationGainedRecord.ELEVATION_GAINED_TOTAL)?.inMeters, + source = resolveSourceName(session.metadata.dataOrigin.packageName), ) } + /** + * Friendly name of the app/device that wrote the record. Resolves the + * Health Connect data-origin package to the installed app's label + * ("Samsung Health", "Google Fit", …); falls back to a known-package map, + * then the raw package, then "Health Connect" when nothing is available. + */ + private fun resolveSourceName(packageName: String): String { + if (packageName.isBlank()) return DEFAULT_SOURCE + runCatching { + val pm = context.packageManager + return pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)).toString() + } + return KNOWN_SOURCES[packageName] ?: packageName + } + /** Aggregates the optional metrics over the session window. Null if aggregation fails. */ private suspend fun aggregate(session: ExerciseSessionRecord): AggregationResult? = try { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/WorkoutSuggestionShared.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/WorkoutSuggestionShared.kt index 4e6d0be653..9cb951f49e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/WorkoutSuggestionShared.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/workouts/suggestion/WorkoutSuggestionShared.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.workouts.suggestion import android.text.format.DateUtils import com.vitorpamplona.amethyst.service.workouts.health.DetectedWorkout import com.vitorpamplona.amethyst.ui.navigation.routes.Route -import com.vitorpamplona.quartz.experimental.fitness.workout.tags.SourceTag /** Builds the pre-filled composer route for a detected workout. Shared by the * feed suggestion banner and the New Workout carousel so they never drift. */ @@ -39,7 +38,7 @@ internal fun DetectedWorkout.toNewWorkoutRoute(title: String) = steps = steps ?: 0, elevationGainMeters = elevationGainMeters ?: 0.0, startTime = startTimeEpochSeconds, - source = SourceTag.HEALTH_CONNECT, + source = source, ) internal fun formatWorkoutDuration(totalSeconds: Long): String {