feat: record the source app/device name from Health Connect

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qgqQKHSewRHVM8vSCLt9P
This commit is contained in:
Claude
2026-06-17 02:40:00 +00:00
parent 89ab47c0ec
commit d4c7577828
3 changed files with 33 additions and 2 deletions
@@ -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,
)
@@ -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 {
@@ -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 {