Piani di allenamento

Health Connect fornisce un tipo di dati allenamento pianificato per consentire alle app di allenamento di scrivere piani di allenamento e alle app di allenamento di leggere i piani di allenamento. Gli esercizi (allenamenti) registrati possono essere letti per un'analisi personalizzata delle prestazioni per aiutare gli utenti a raggiungere i loro obiettivi di allenamento.

Controlla la disponibilità di Health Connect

Prima di tentare di utilizzare Health Connect, l'app deve verificare che Health Connect sia disponibile sul dispositivo dell'utente. Health Connect potrebbe non essere preinstallato su tutti i dispositivi o potrebbe essere disattivato. Puoi verificare la disponibilità utilizzando il metodo HealthConnectClient.getSdkStatus().

Come verificare la disponibilità di Health Connect

fun checkHealthConnectAvailability(context: Context) {
    val providerPackageName = "com.google.android.apps.healthdata" // Or get from HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME
    val availabilityStatus = HealthConnectClient.getSdkStatus(context, providerPackageName)

    if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE) {
      // Health Connect is not available. Guide the user to install/enable it.
      // For example, show a dialog.
      return // early return as there is no viable integration
    }
    if (availabilityStatus == HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED) {
      // Health Connect is available but requires an update.
      // Optionally redirect to package installer to find a provider, for example:
      val uriString = "market://details?id=$providerPackageName&url=healthconnect%3A%2F%2Fonboarding"
      context.startActivity(
        Intent(Intent.ACTION_VIEW).apply {
          setPackage("com.android.vending")
          data = Uri.parse(uriString)
          putExtra("overlay", true)
          putExtra("callerId", context.packageName)
        }
      )
      return
    }
    // Health Connect is available, obtain a HealthConnectClient instance
    val healthConnectClient = HealthConnectClient.getOrCreate(context)
    // Issue operations with healthConnectClient
}

A seconda dello stato restituito da getSdkStatus(), puoi guidare l'utente all'installazione o all'aggiornamento di Connessione Salute dal Google Play Store, se necessario.

Disponibilità della funzionalità

Per determinare se il dispositivo di un utente supporta i piani di allenamento su Connessione Salute, controlla la disponibilità di FEATURE_PLANNED_EXERCISE sul client:

if (healthConnectClient
     .features
     .getFeatureStatus(
       HealthConnectFeatures.FEATURE_PLANNED_EXERCISE
     ) == HealthConnectFeatures.FEATURE_STATUS_AVAILABLE) {

  // Feature is available
} else {
  // Feature isn't available
}
Per saperne di più, consulta Verifica la disponibilità delle funzionalità.

Autorizzazioni obbligatorie

L'accesso all'allenamento pianificato è protetto dalle seguenti autorizzazioni:

  • android.permission.health.READ_PLANNED_EXERCISE
  • android.permission.health.WRITE_PLANNED_EXERCISE

Per aggiungere la funzionalità di allenamento pianificato alla tua app, inizia richiedendo le autorizzazioni per il tipo di dati PlannedExerciseSession.

Ecco l'autorizzazione che devi dichiarare per poter scrivere l'allenamento pianificato:

<application>
  <uses-permission
android:name="android.permission.health.WRITE_PLANNED_EXERCISE" />
...
</application>

Per leggere l'allenamento pianificato, devi richiedere le seguenti autorizzazioni:

<application>
  <uses-permission
android:name="android.permission.health.READ_PLANNED_EXERCISE" />
...
</application>

Richiedi le autorizzazioni all'utente

Dopo aver creato un'istanza client, l'app deve richiedere le autorizzazioni all'utente. Gli utenti devono poter concedere o negare le autorizzazioni in qualsiasi momento. A questo scopo, crea un set di autorizzazioni per i tipi di dati richiesti. Assicurati che le autorizzazioni nel set siano dichiarate prima nel manifest di Android.

val permissions =
    setOf(
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class),
        HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class),
        HealthPermission.getWritePermission(PlannedExerciseSessionRecord::class),
        HealthPermission.getReadPermission(ExerciseSessionRecord::class),
        HealthPermission.getWritePermission(ExerciseSessionRecord::class)
    )
Utilizza getGrantedPermissions per verificare se la tua app ha già le autorizzazioni richieste. In caso contrario, utilizza createRequestPermissionResultContract per richiedere queste autorizzazioni. Viene visualizzata la schermata delle autorizzazioni di Connessione Salute.
val permissions = setOf(
        HealthPermission.getReadPermission(StepsRecord::class),
        HealthPermission.getWritePermission(StepsRecord::class),
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class)
    )

val requestPermissionsLauncher = rememberLauncherForActivityResult(
    contract = PermissionController.createRequestPermissionResultContract()
) { grantedPermissions ->
    if (grantedPermissions.containsAll(permissions)) {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions granted!") }
    } else {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions denied.") }
    }
}
Poiché gli utenti possono concedere o revocare le autorizzazioni in qualsiasi momento, la tua app deve controllare le autorizzazioni ogni volta prima di utilizzarle e gestire gli scenari in cui l'autorizzazione viene revocata.

I piani di allenamento sono collegati alle sessioni di allenamento. Pertanto, l'utente deve concedere l'autorizzazione per utilizzare ogni tipo di record correlato a un piano di allenamento per utilizzare appieno questa funzionalità di Health Connect.

Ad esempio, se un piano di allenamento misura il battito cardiaco di un utente durante una serie di corse, lo sviluppatore potrebbe dover dichiarare e l'utente potrebbe dover concedere le seguenti autorizzazioni per scrivere la sessione di allenamento e leggere i risultati per una valutazione successiva:

  • android.permission.health.READ_EXERCISE
  • android.permission.health.READ_EXERCISE_ROUTES
  • android.permission.health.READ_HEART_RATE
  • android.permission.health.WRITE_EXERCISE
  • android.permission.health.WRITE_EXERCISE_ROUTE
  • android.permission.health.WRITE_HEART_RATE

Tuttavia, spesso l'app che crea piani di allenamento e valuta il rendimento rispetto ai piani non è la stessa dell'app che utilizza i piani di allenamento e scrive i dati di allenamento effettivi. A seconda del tipo di app, non saranno necessarie tutte le autorizzazioni di lettura e scrittura. Ad esempio, potresti aver bisogno solo di queste autorizzazioni per ogni tipo di app:

App per i piani di allenamento App di allenamento
WRITE_PLANNED_EXERCISE READ_PLANNED_EXERCISE
READ_EXERCISE WRITE_EXERCISE
READ_EXERCISE_ROUTES WRITE_EXERCISE_ROUTE
READ_HEART_RATE WRITE_HEART_RATE

Informazioni incluse in un record di sessione di allenamento pianificato

  • Titolo della sessione.
  • Un elenco di blocchi di allenamento pianificati.
  • Ora di inizio e di fine della sessione.
  • Tipo di esercizio.
  • Note per l'attività.
  • Metadati.
  • ID sessione di allenamento completata: viene scritto automaticamente al termine di una sessione di allenamento correlata a questa sessione di allenamento pianificata.

Informazioni incluse in un record di blocco di allenamento pianificato

Un blocco di allenamento pianificato contiene un elenco di passaggi di allenamento per supportare la ripetizione di diversi gruppi di passaggi (ad esempio, eseguire una sequenza di curl per i bicipiti, burpee e crunch cinque volte di seguito).

Informazioni incluse in un record di passaggio di allenamento pianificato

Aggregazioni supportate

Non sono presenti aggregazioni supportate per questo tipo di dati.

Esempio di utilizzo

Supponiamo che un utente pianifichi una corsa di 90 minuti tra due giorni. Questa corsa prevede tre giri intorno a un lago con un battito cardiaco target compreso tra 90 e 110 bpm.

  1. L'utente definisce una sessione di allenamento pianificata con le seguenti caratteristiche in un'app per i piani di allenamento:
    1. Inizio e fine pianificati della corsa
    2. Il tipo di esercizio (corsa)
    3. Numero di giri (ripetizioni)
    4. Obiettivo di rendimento per il battito cardiaco (tra 90 e 110 bpm)
  2. Queste informazioni vengono raggruppate in blocchi e passaggi di allenamento e scritte in Health Connect dall'app per i piani di allenamento come PlannedExerciseSessionRecord.
  3. L'utente esegue la sessione pianificata (corsa).
  4. I dati di allenamento relativi alla sessione vengono registrati:
    1. Da un dispositivo indossabile durante la sessione. Ad esempio, il battito cardiaco. Questi dati vengono scritti in Health Connect come tipo di record per l'attività. In questo caso, HeartRateRecord.
    2. Manualmente dall'utente dopo la sessione. Ad esempio, indicando l'inizio e la fine della corsa effettiva. Questi dati vengono scritti in Connessione Salute come ExerciseSessionRecord.
  5. In un secondo momento, l'app per i piani di allenamento legge i dati da Health Connect per valutare il rendimento effettivo rispetto agli obiettivi impostati dall'utente nella sessione di allenamento pianificata.

Pianifica gli esercizi e imposta gli obiettivi

Un utente può pianificare il proprio allenamento in futuro e impostare gli obiettivi. Scrivi questi dati in Health Connect come sessione di allenamento pianificata.

Nell'esempio descritto in Esempio di utilizzo, l'utente pianifica una corsa di 90 minuti tra due giorni. Questa corsa prevede tre giri intorno a un lago con un battito cardiaco target compreso tra 90 e 110 bpm.

Uno snippet come questo potrebbe essere presente nel gestore di moduli di un'app che registra le sessioni di allenamento pianificate in Health Connect. Potrebbe anche essere presente nel punto di acquisizione per le integrazioni, ad esempio con un servizio che offre allenamenti.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()

if (!grantedPermissions.contains(
        HealthPermission.getWritePermission(PlannedExerciseSessionRecord::class))) {
    // The user hasn't granted the app permission to write planned exercise session data.
    Log.w("HealthConnect", "Write permission for PlannedExerciseSessionRecord not granted.")
    return
}

val plannedExerciseSessionRecord = PlannedExerciseSessionRecord(
    startTime = startTime,
    endTime = endTime,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    blocks = listOf(
        PlannedExerciseBlock(
            repetitions = 1, steps = listOf(
                PlannedExerciseStep(
                    exerciseType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
                    exercisePhase = PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
                    completionGoal = ExerciseCompletionGoal.RepetitionsGoal(repetitions = 3),
                    performanceTargets = listOf(
                        ExercisePerformanceTarget.HeartRateTarget(
                            minHeartRate = 90.0, maxHeartRate = 110.0
                        )
                    )
                ),
            ), description = "Three laps around the lake"
        )
    ),
    title = "Run at lake",
    notes = null,
    metadata = Metadata(
        device = Device(type = Device.Companion.TYPE_PHONE),
    ),
    startZoneOffset = null,
    endZoneOffset = null,
)

try {
    // Attempt to insert the record
    val response = healthConnectClient.insertRecords(listOf(plannedExerciseSessionRecord))

    // If execution reaches here, the insert succeeded.
    // Safely extract the ID using firstOrNull()
    val insertedPlannedExerciseSessionId = response.recordIdsList.firstOrNull()

    if (insertedPlannedExerciseSessionId != null) {
        Log.d("HealthConnect", "Successfully inserted planned exercise session ID: $insertedPlannedExerciseSessionId")
    } else {
        Log.w("HealthConnect", "Insertion succeeded but no record IDs were returned.")
    }

} catch (e: Exception) {
    // Handle API failures, database errors, or system issues safely without crashing
    Log.e("HealthConnect", "Failed to insert planned exercise session record", e)
}

Registra i dati delle attività e dell'esercizio fisico

Due giorni dopo, l'utente registra la sessione di allenamento effettiva. Scrivi questi dati in Connessione Salute come sessione di allenamento.

In questo esempio, la durata della sessione dell'utente corrisponde esattamente alla durata pianificata.

Il seguente snippet potrebbe essere presente nel gestore di moduli di un'app che registra le sessioni di allenamento in Health Connect. Potrebbe anche essere presente nei gestori di acquisizione ed esportazione dei dati per un dispositivo indossabile in grado di rilevare e registrare le sessioni di allenamento.

insertedPlannedExerciseSessionId qui viene riutilizzato dall'esempio precedente. In un'app reale, l'ID verrebbe determinato dall'utente che seleziona una sessione di allenamento pianificata da un elenco di sessioni esistenti.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()
if (!grantedPermissions.contains(
        HealthPermission.getWritePermission(ExerciseSessionRecord::class))) {
    // The user doesn't granted the app permission to write exercise session data.
    return
}

val sessionDuration = Duration.ofMinutes(90)
val sessionEndTime = Instant.now()
val sessionStartTime = sessionEndTime.minus(sessionDuration)

val exerciseSessionRecord = ExerciseSessionRecord(
    startTime = sessionStartTime,
    startZoneOffset = ZoneOffset.UTC,
    endTime = sessionEndTime,
    endZoneOffset = ZoneOffset.UTC,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    segments = listOf(
        ExerciseSegment(
            startTime = sessionStartTime,
            endTime = sessionEndTime,
            repetitions = 3,
            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING
        )
    ),
    title = "Run at lake",
    plannedExerciseSessionId = insertedPlannedExerciseSessionId,
    metadata = Metadata(
        device = Device(type = Device.Companion.TYPE_PHONE)
    )
)
val insertedExerciseSessions =
    healthConnectClient.insertRecords(listOf(exerciseSessionRecord))

Un dispositivo indossabile registra anche il battito cardiaco durante la corsa. Il seguente snippet potrebbe essere utilizzato per generare record all'interno dell'intervallo target.

In un'app reale, le parti principali di questo snippet potrebbero essere presenti nel gestore di un messaggio di un dispositivo indossabile, che scriverebbe la misurazione in Health Connect al momento della raccolta.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()
if (!grantedPermissions.contains(
        HealthPermission.getWritePermission(HeartRateRecord::class))) {
    // The user doesn't granted the app permission to write heart rate record data.
    return
}

val samples = mutableListOf<HeartRateRecord.Sample>()
var currentTime = sessionStartTime
while (currentTime.isBefore(sessionEndTime)) {
    val bpm = Random.nextInt(21) + 90
    val heartRateRecord = HeartRateRecord.Sample(
        time = currentTime,
        beatsPerMinute = bpm.toLong(),
    )
    samples.add(heartRateRecord)
    currentTime = currentTime.plusSeconds(180)
}

val heartRateRecord = HeartRateRecord(
    startTime = sessionStartTime,
    startZoneOffset = ZoneOffset.UTC,
    endTime = sessionEndTime,
    endZoneOffset = ZoneOffset.UTC,
    samples = samples,
    metadata = Metadata(
        device = Device(type = Device.Companion.TYPE_WATCH)
    )
)
val insertedHeartRateRecords = healthConnectClient.insertRecords(listOf(heartRateRecord))

Valuta gli obiettivi di rendimento

Il giorno dopo l'allenamento dell'utente, puoi recuperare l'allenamento registrato, verificare la presenza di obiettivi di allenamento pianificati e valutare altri tipi di dati per determinare se gli obiettivi impostati sono stati raggiunti.

Uno snippet come questo si troverebbe probabilmente in un job periodico per valutare gli obiettivi di rendimento o durante il caricamento di un elenco di esercizi e la visualizzazione di una notifica sugli obiettivi di rendimento in un'app.

    // Verify the user has granted all necessary permissions for this task
    val grantedPermissions =
        healthConnectClient.permissionController.getGrantedPermissions()
    if (!grantedPermissions.containsAll(
            listOf(
                HealthPermission.getReadPermission(ExerciseSessionRecord::class),
                HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class),
                HealthPermission.getReadPermission(HeartRateRecord::class)
            )
        )
    ) {
        // The user doesn't granted the app permission to read exercise session record data.
        return
    }

    val searchDuration = Duration.ofDays(1)
    val searchEndTime = Instant.now()
    val searchStartTime = searchEndTime.minus(searchDuration)

    val response = healthConnectClient.readRecords(
        ReadRecordsRequest<ExerciseSessionRecord>(
            timeRangeFilter = TimeRangeFilter.between(searchStartTime, searchEndTime)
        )
    )
    for (exerciseRecord in response.records) {
        val plannedExerciseRecordId = exerciseRecord.plannedExerciseSessionId
        val plannedExerciseRecord =
            if (plannedExerciseRecordId == null) null else healthConnectClient.readRecord(
                PlannedExerciseSessionRecord::class, plannedExerciseRecordId
            ).record
        if (plannedExerciseRecord != null) {
            val aggregateRequest = AggregateRequest(
                metrics = setOf(HeartRateRecord.BPM_AVG),
                timeRangeFilter = TimeRangeFilter.between(
                    exerciseRecord.startTime, exerciseRecord.endTime
                ),
            )
            val aggregationResult = healthConnectClient.aggregate(aggregateRequest)

            val maxBpm = aggregationResult[HeartRateRecord.BPM_MAX]
            val minBpm = aggregationResult[HeartRateRecord.BPM_MIN]
            if (maxBpm != null && minBpm != null) {
                plannedExerciseRecord.blocks.forEach { block ->
                    block.steps.forEach { step ->
                        step.performanceTargets.forEach { target ->
                            when (target) {
                                is ExercisePerformanceTarget.HeartRateTarget -> {
                                    val minTarget = target.minHeartRate
                                    val maxTarget = target.maxHeartRate
                                    if(
                                        minBpm >= minTarget && maxBpm <= maxTarget
                                    ) {
                                        // Success!
                                    }
                                }
                                // Handle more target types
                            }
                        }
                    }
                }
            }
        }
    }
}

Sessioni di allenamento

Le sessioni di allenamento possono includere qualsiasi attività, dalla corsa al badminton.

Scrivi le sessioni di allenamento

Ecco come creare una richiesta di inserimento che includa una sessione:

suspend fun writeExerciseSession(healthConnectClient: HealthConnectClient) {
    healthConnectClient.insertRecords(
        listOf(
            ExerciseSessionRecord(
                startTime = START_TIME,
                startZoneOffset = START_ZONE_OFFSET,
                endTime = END_TIME,
                endZoneOffset = END_ZONE_OFFSET,
                exerciseType = ExerciseSessionRecord.ExerciseType.RUNNING,
                title = "My Run",
                metadata = Metadata.manualEntry()
            ),
            // ... other records
        )
    )
}

Leggi una sessione di allenamento

Di seguito è riportato un esempio di come leggere una sessione di allenamento:

suspend fun readExerciseSessions(
    healthConnectClient: HealthConnectClient,
    startTime: Instant,
    endTime: Instant
) {
    val response =
        healthConnectClient.readRecords(
            ReadRecordsRequest(
                ExerciseSessionRecord::class,
                timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
            )
        )
    for (exerciseRecord in response.records) {
        // Process each exercise record
        // Optionally pull in with other data sources of the same time range.
        val distanceRecord =
            healthConnectClient
                .readRecords(
                    ReadRecordsRequest(
                        DistanceRecord::class,
                        timeRangeFilter =
                            TimeRangeFilter.between(
                                exerciseRecord.startTime,
                                exerciseRecord.endTime
                            )
                    )
                )
                .records
    }
}

Scrivi i dati del sottotipo

Le sessioni possono anche essere composte da dati di sottotipo facoltativi, che arricchiscono la sessione con informazioni aggiuntive.

Ad esempio, le sessioni di allenamento possono includere le classi ExerciseSegment, ExerciseLap e ExerciseRoute:

val segments = listOf(
  ExerciseSegment(
    startTime = Instant.parse("2022-01-02T10:10:10Z"),
    endTime = Instant.parse("2022-01-02T10:10:13Z"),
    segmentType = ActivitySegmentType.BENCH_PRESS,
    repetitions = 373
  )
)

val laps = listOf(
  ExerciseLap(
    startTime = Instant.parse("2022-01-02T10:10:10Z"),
    endTime = Instant.parse("2022-01-02T10:10:13Z"),
    length = 0.meters
  )
)

ExerciseSessionRecord(
  exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_CALISTHENICS,
    startTime = Instant.parse("2022-01-02T10:10:10Z"),
    endTime = Instant.parse("2022-01-02T10:10:13Z"),
  startZoneOffset = ZoneOffset.UTC,
  endZoneOffset = ZoneOffset.UTC,
  segments = segments,
  laps = laps,
  route = route,
  metadata = Metadata.manualEntry()
)