Registra un allenamento con ExerciseClient

Health Services fornisce un'assistenza di prima qualità per le app per l'allenamento tramite ExerciseClient. Con ExerciseClient, la tua app può controllare quando è in corso un allenamento, aggiungere obiettivi di allenamento e ricevere aggiornamenti sullo stato dell'allenamento, sugli eventi di allenamento o su altre metriche desiderate. Per ulteriori informazioni, consulta l'elenco completo dei tipi di esercizio fisico supportati dai servizi sanitari.

Vedi l'esempio di esercizio su GitHub.

Aggiungi dipendenze

Per aggiungere una dipendenza a Health Services, devi aggiungere il Repository Maven di Google al tuo progetto. Per ulteriori informazioni, consulta il Repository Maven di Google.

Quindi, nel file build.gradle a livello di modulo, aggiungi la dipendenza seguente:

Trendy

dependencies {
    implementation "androidx.health:health-services-client:1.1.0-alpha03"
}

Kotlin

dependencies {
    implementation("androidx.health:health-services-client:1.1.0-alpha03")
}

Struttura dell'app

Quando crei un'app per l'allenamento con Health Services, usa la seguente struttura:

Quando ti prepari per un esercizio e durante l'esercizio, la tua attività potrebbe essere interrotta per diversi motivi. L'utente può passare a un'altra app o tornare al quadrante. Il sistema potrebbe mostrare qualcosa sopra le tue attività oppure lo schermo potrebbe spegnersi dopo un periodo di inattività. Utilizza ForegroundService in esecuzione continua insieme a ExerciseClient per garantire il corretto funzionamento dell'intero esercizio.

L'utilizzo di un ForegroundService consente di utilizzare l'API OnGoing Activity per mostrare un indicatore sulle piattaforme di visualizzazione, consentendo all'utente di tornare rapidamente all'esercizio.

È essenziale richiedere i dati sulla posizione in modo appropriato nel servizio in primo piano. Nel file manifest, specifica foregroundServiceType="location" e specifica le autorizzazioni appropriate.

Usa AmbientLifecycleObserver per l'attività precedente all'allenamento, che contiene la chiamata prepareExercise() e per l'attività di allenamento. Tuttavia, non aggiornare il display durante l'esercizio in modalità Ambient: questo è dovuto al fatto che Health Services raggruppa i dati dell'allenamento quando lo schermo del dispositivo è in modalità Ambient per risparmiare energia, quindi le informazioni visualizzate potrebbero non essere recenti. Durante gli esercizi, mostra dati pertinenti all'utente, mostrando informazioni aggiornate o una schermata vuota.

Verifica le funzionalità

Ogni ExerciseType supporta determinati tipi di dati per le metriche e per gli obiettivi di allenamento. Verifica queste funzionalità all'avvio, perché possono variare in base al dispositivo. Un dispositivo potrebbe non supportare un determinato tipo di allenamento o non supportare una funzione specifica, ad esempio la pausa automatica. Inoltre, le funzionalità di un dispositivo possono cambiare nel tempo, ad esempio dopo un aggiornamento software.

All'avvio dell'app, esegui una query sulle funzionalità del dispositivo, quindi archivia ed elabora quanto segue:

  • Gli esercizi supportati dalla piattaforma.
  • Le funzionalità supportate per ogni allenamento.
  • I tipi di dati supportati per ogni allenamento.
  • Le autorizzazioni necessarie per ciascuno di questi tipi di dati.

Usa ExerciseCapabilities.getExerciseTypeCapabilities() con il tipo di allenamento desiderato per vedere quali metriche puoi richiedere, quali obiettivi di allenamento puoi configurare e quali altre funzionalità sono disponibili per quel tipo. come illustrato nell'esempio seguente:

val healthClient = HealthServices.getClient(this /*context*/)
val exerciseClient = healthClient.exerciseClient
lifecycleScope.launch {
    val capabilities = exerciseClient.getCapabilitiesAsync().await()
    if (ExerciseType.RUNNING in capabilities.supportedExerciseTypes) {
        runningCapabilities =
            capabilities.getExerciseTypeCapabilities(ExerciseType.RUNNING)
    }
}

All'interno del valore ExerciseTypeCapabilities restituito, supportedDataTypes elenca i tipi di dati per i quali puoi richiedere dati. Questo varia a seconda del dispositivo, quindi assicurati di non richiedere un DataType non supportato, altrimenti la tua richiesta potrebbe non andare a buon fine.

Utilizza i campi supportedGoals e supportedMilestones per determinare se l'allenamento può supportare un obiettivo di allenamento che vuoi creare.

Se la tua app consente all'utente di usare la pausa automatica, devi verificare che questa funzionalità sia supportata dal dispositivo usando supportsAutoPauseAndResume. ExerciseClient rifiuta le richieste non supportate sul dispositivo.

L'esempio seguente verifica il supporto del tipo di dati HEART_RATE_BPM, della funzionalità degli obiettivi STEPS_TOTAL e della funzionalità di pausa automatica:

// Whether we can request heart rate metrics.
supportsHeartRate = DataType.HEART_RATE_BPM in runningCapabilities.supportedDataTypes

// Whether we can make a one-time goal for aggregate steps.
val stepGoals = runningCapabilities.supportedGoals[DataType.STEPS_TOTAL]
supportsStepGoals = 
    (stepGoals != null && ComparisonType.GREATER_THAN_OR_EQUAL in stepGoals)

// Whether auto-pause is supported.
val supportsAutoPause = runningCapabilities.supportsAutoPauseAndResume

Registrati per ricevere aggiornamenti sullo stato dell'allenamento

Gli aggiornamenti sull'allenamento vengono inviati a un ascoltatore. La tua app può registrare un solo listener alla volta. Imposta il tuo listener prima di iniziare l'esercizio, come mostrato nell'esempio seguente. Chi ascolta riceve aggiornamenti solo sugli allenamenti di proprietà della tua app.

val callback = object : ExerciseUpdateCallback {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
        val exerciseStateInfo = update.exerciseStateInfo
        val activeDuration = update.activeDurationCheckpoint
        val latestMetrics = update.latestMetrics
        val latestGoals = update.latestAchievedGoals
    }

    override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {
        // For ExerciseTypes that support laps, this is called when a lap is marked.
    }

    override fun onAvailabilityChanged(
        dataType: DataType<*, *>,
        availability: Availability
    ) {
        // Called when the availability of a particular DataType changes.
        when {
            availability is LocationAvailability -> // Relates to Location/GPS.
            availability is DataTypeAvailability -> // Relates to another DataType.
        }
    }
}
exerciseClient.setUpdateCallback(callback)

Gestisci la durata dell'allenamento

Health Services supporta al massimo un allenamento alla volta su tutte le app del dispositivo. Se viene monitorato un allenamento e un'altra app inizia a rilevare un nuovo allenamento, il primo allenamento termina.

Prima di iniziare l'allenamento, segui questi passaggi:

  • Controlla se un allenamento viene già monitorato e reagisci di conseguenza. Ad esempio, chiedi conferma all'utente prima di ignorare un allenamento precedente e di iniziarne uno nuovo.

L'esempio seguente mostra come verificare un allenamento esistente con getCurrentExerciseInfoAsync:

lifecycleScope.launch {
    val exerciseInfo = exerciseClient.getCurrentExerciseInfoAsync().await()
    when (exerciseInfo.exerciseTrackedStatus) {
        OTHER_APP_IN_PROGRESS -> // Warn user before continuing, will stop the existing workout.
        OWNED_EXERCISE_IN_PROGRESS -> // This app has an existing workout.
        NO_EXERCISE_IN_PROGRESS -> // Start a fresh workout.
    }
}

Autorizzazioni

Quando utilizzi ExerciseClient, assicurati che l'app richieda e mantenga le autorizzazioni necessarie. Se la tua app utilizza dati LOCATION, assicurati che l'app richieda e mantenga le autorizzazioni appropriate anche per questi.

Per tutti i tipi di dati, prima di chiamare prepareExercise() o startExercise(), segui questi passaggi:

  • Specifica le autorizzazioni appropriate per i tipi di dati richiesti nel file AndroidManifest.xml.
  • Verifica che l'utente abbia concesso le autorizzazioni necessarie. Per maggiori informazioni, consulta Richiedere autorizzazioni app. Health Services rifiuta la richiesta se non sono già state concesse le autorizzazioni necessarie.

Per i dati sulla posizione, svolgi i seguenti passaggi aggiuntivi:

Preparati per un allenamento

Alcuni sensori, come il GPS o il battito cardiaco, potrebbero richiedere poco tempo per riscaldarsi oppure l'utente potrebbe voler visualizzare i dati prima di iniziare l'esercizio. Il metodo facoltativo prepareExerciseAsync() consente a questi sensori di riscaldarsi e di ricevere i dati senza avviare il timer per l'esercizio. activeDuration non è interessato da questo tempo di preparazione.

Prima di chiamare prepareExerciseAsync(), controlla quanto segue:

  • Controlla l'impostazione di geolocalizzazione a livello di piattaforma. L'utente controlla questa impostazione nel menu Impostazioni principale; è diversa dal controllo delle autorizzazioni a livello di app.

    Se l'impostazione è disattivata, informa l'utente di aver negato l'accesso alla posizione e chiedigli di abilitarla se la tua app richiede la posizione.

  • Conferma che l'app disponga delle autorizzazioni di runtime per i sensori del corpo, il riconoscimento dell'attività e la posizione precisa. Per le autorizzazioni mancanti, chiedi all'utente le autorizzazioni di runtime, fornendo un contesto adeguato. Se l'utente non concede un'autorizzazione specifica, rimuovi i tipi di dati associati all'autorizzazione dalla chiamata a prepareExerciseAsync(). Se non vengono concesse le autorizzazioni di accesso alla posizione o al sensore del corpo, non chiamare prepareExerciseAsync(), perché la chiamata di preparazione serve unicamente per ottenere un battito cardiaco stabile o una correzione GPS prima di iniziare un allenamento. L'app può comunque ottenere metriche basate sui passi, ritmo, velocità e altre metriche che non richiedono queste autorizzazioni.

Per assicurarti che la chiamata al numero prepareExerciseAsync() possa avere esito positivo:

  • Utilizza AmbientLifecycleObserver per l'attività pre-allenamento che contiene la chiamata di preparazione.
  • Chiama prepareExerciseAsync() dal servizio in primo piano. Se non è in un servizio ed è legato al ciclo di vita dell'attività, la preparazione del sensore potrebbe essere inutilmente terminata.
  • Chiama endExercise() per spegnere i sensori e ridurre il consumo di energia se l'utente esce dall'attività precedente all'allenamento.

Nell'esempio seguente viene illustrato come chiamare prepareExerciseAsync():

val warmUpConfig = WarmUpConfig(
    ExerciseType.RUNNING,
    setOf(
        DataType.HEART_RATE_BPM,
        DataType.LOCATION
    )
)
// Only necessary to call prepareExerciseAsync if body sensor or location
//permissions are given
exerciseClient.prepareExerciseAsync(warmUpConfig).await()

// Data and availability updates are delivered to the registered listener.

Una volta che l'app è nello stato PREPARING, gli aggiornamenti sulla disponibilità dei sensori vengono inviati nell'ExerciseUpdateCallback fino al giorno onAvailabilityChanged(). Queste informazioni possono poi essere presentate all'utente in modo che possa decidere se iniziare l'esercizio.

Inizia l'esercizio

Quando desideri avviare un allenamento, crea un ExerciseConfig per configurare il tipo di allenamento, i tipi di dati per i quali vuoi ricevere metriche ed eventuali obiettivi o traguardi di allenamento.

Gli obiettivi di allenamento sono costituiti da un DataType e una condizione. Gli obiettivi di allenamento sono obiettivi una tantum che vengono attivati quando una condizione è soddisfatta, ad esempio quando l'utente corre una determinata distanza. È anche possibile impostare un punto di riferimento per l'allenamento. I traguardi di allenamento possono essere attivati più volte, ad esempio ogni volta che l'utente corre un determinato punto oltre la distanza impostata.

Il seguente esempio mostra come creare un obiettivo di ogni tipo:

const val CALORIES_THRESHOLD = 250.0
const val DISTANCE_THRESHOLD = 1_000.0 // meters

suspend fun startExercise() {
    // Types for which we want to receive metrics.
    val dataTypes = setOf(
        DataType.HEART_RATE_BPM,
        DataType.CALORIES_TOTAL,
        DataType.DISTANCE
    )

    // Create a one-time goal.
    val calorieGoal = ExerciseGoal.createOneTimeGoal(
        DataTypeCondition(
            dataType = DataType.CALORIES_TOTAL,
            threshold = CALORIES_THRESHOLD,
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        )
    )

    // Create a milestone goal. To make a milestone for every kilometer, set the initial
    // threshold to 1km and the period to 1km.
    val distanceGoal = ExerciseGoal.createMilestone(
        condition = DataTypeCondition(
            dataType = DataType.DISTANCE_TOTAL,
            threshold = DISTANCE_THRESHOLD,
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        ),
        period = DISTANCE_THRESHOLD
    )

    val config = ExerciseConfig(
        exerciseType = ExerciseType.RUNNING,
        dataTypes = dataTypes,
        isAutoPauseAndResumeEnabled = false,
        isGpsEnabled = true,
        exerciseGoals = mutableListOf<ExerciseGoal<Double>>(calorieGoal, distanceGoal)
    )
    exerciseClient.startExerciseAsync(config).await()
}

Puoi anche contrassegnare i giri per tutti gli allenamenti. Health Services fornisce un ExerciseLapSummary con metriche aggregate sul periodo giro.

L'esempio precedente mostra l'utilizzo di isGpsEnabled, che deve essere true quando si richiedono i dati sulla posizione. Tuttavia, l'uso del GPS può essere d'aiuto anche per altre metriche. Se ExerciseConfig specifica la distanza come DataType, per impostazione predefinita vengono utilizzati i passaggi per stimare la distanza. Abilitando facoltativamente il GPS, puoi usare le informazioni sulla posizione per stimare la distanza.

Mettere in pausa, riprendere e terminare un esercizio

Puoi mettere in pausa, riprendere e terminare gli esercizi utilizzando il metodo appropriato, come pauseExerciseAsync() o endExerciseAsync().

Utilizza lo stato da ExerciseUpdate come fonte attendibile. L'allenamento non viene considerato in pausa quando viene restituita la chiamata a pauseExerciseAsync(), ma quando questo stato si riflette nel messaggio ExerciseUpdate. Questo è particolarmente importante da considerare quando si tratta di stati dell'interfaccia utente. Se l'utente preme pausa, disattiva il pulsante Pausa e chiama pauseExerciseAsync() su Health Services. Attendi che Health Services raggiunga lo stato di pausa utilizzando ExerciseUpdate.exerciseStateInfo.state, quindi premi il pulsante per riprendere l'operazione. Questo perché gli aggiornamenti dello stato di Health Services possono richiedere più tempo rispetto alla pressione del pulsante. Pertanto, se colleghi tutte le modifiche dell'interfaccia alla pressione dei pulsanti, l'interfaccia utente potrebbe non essere sincronizzata con lo stato di Health Services.

Tienilo presente nelle seguenti situazioni:

  • Pausa automatica attivata: l'esercizio può essere messo in pausa o avviato senza interazione dell'utente.
  • Un'altra app avvia un esercizio: il tuo esercizio potrebbe essere interrotto senza interazione dell'utente.

Se l'esercizio della tua app viene terminato da un'altra app, l'app deve gestire correttamente la chiusura:

  • Salva lo stato dell'esercizio parziale in modo che i progressi di un utente non vengano cancellati.
  • Rimuovi l'icona Attività in corso e invia all'utente una notifica per informarlo che l'esercizio è stato terminato da un'altra app.

Inoltre, gestisci il caso in cui le autorizzazioni vengono revocate durante un esercizio in corso. Viene inviato utilizzando lo stato isEnded, con un valore di ExerciseEndReason di AUTO_END_PERMISSION_LOST. Gestisci questo caso in modo simile a quello di terminazione: salva lo stato parziale, rimuovi l'icona Attività in corso e invia una notifica su cosa è successo all'utente.

L'esempio seguente mostra come verificare correttamente la risoluzione:

val callback = object : ExerciseUpdateCallback {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
        if (update.exerciseStateInfo.state.isEnded) {
            // Workout has either been ended by the user, or otherwise terminated
        }
        ...
    }
    ...
}

Gestisci durata attiva

Durante un allenamento, un'app può visualizzare la durata attiva dell'esercizio. L'app, Health Services e il dispositivo Micro Controller Unit (MCU), il processore a bassa potenza responsabile del monitoraggio dell'allenamento, devono essere tutti sincronizzati con la stessa durata attiva attuale. Per facilitare questa operazione, Health Services invia un ActiveDurationCheckpoint che fornisce un punto di ancoraggio da cui l'app può avviare il timer.

Poiché la durata attiva viene inviata dall'MCU e può richiedere un breve tempo per essere visualizzata nell'app, ActiveDurationCheckpoint contiene due proprietà:

  • activeDuration: da quanto tempo l'allenamento è stato attivo
  • time: quando è stata calcolata la durata attiva

Di conseguenza, nell'app la durata attiva di un allenamento può essere calcolata da ActiveDurationCheckpoint utilizzando la seguente equazione:

(now() - checkpoint.time) + checkpoint.activeDuration

Questo tiene conto del piccolo delta tra la durata attiva calcolata nell'MCU e l'arrivo nell'app. Questa metrica può essere utilizzata per avviare un cronometro nell'app e garantire che il timer dell'app sia perfettamente allineato con l'ora nei servizi sanitari e nell'MCU.

Se l'allenamento è in pausa, l'app attende il riavvio del timer nell'interfaccia utente fino a quando il tempo calcolato non è trascorso rispetto a quanto visualizzato attualmente nell'interfaccia utente. Questo perché il segnale di pausa raggiunge Health Services e l'MCU con un leggero ritardo. Ad esempio, se l'app viene messa in pausa a t=10 secondi, Health Services potrebbe non distribuire l'aggiornamento PAUSED all'app fino a t=10,2 secondi.

Lavorare con i dati di ExerciseClient

Le metriche per i tipi di dati per i quali la tua app è stata registrata vengono inviate in messaggi di ExerciseUpdate.

Il processore invia i messaggi solo quando è attivo o quando viene raggiunto un periodo massimo di generazione dei report, ad esempio ogni 150 secondi. Non fare affidamento sulla frequenza ExerciseUpdate per avanzare di un cronometro con il activeDuration. Consulta l'esempio di esercizio su GitHub per un esempio di come implementare un cronometro indipendente.

Quando un utente avvia un esercizio, i messaggi ExerciseUpdate possono essere recapitati di frequente, ad esempio ogni secondo. Quando l'utente avvia l'esercizio, lo schermo potrebbe disattivarsi. Health Services può quindi inviare dati meno spesso, ma comunque campionati alla stessa frequenza, per evitare di riattivare il processore principale. Quando l'utente guarda lo schermo, tutti i dati in fase di raggruppamento vengono inviati immediatamente alla tua app.

Controlla la frequenza di batch

In alcuni scenari, potresti voler controllare la frequenza con cui la tua app riceve determinati tipi di dati quando lo schermo è disattivato. Un oggetto BatchingMode consente alla tua app di ignorare il comportamento di raggruppamento predefinito per ricevere consegne di dati più spesso.

Per configurare la frequenza di raggruppamento, completa i seguenti passaggi:

  1. Controlla se la definizione di BatchingMode specifica è supportata dal dispositivo:

    // Confirm BatchingMode support to control heart rate stream to phone.
    suspend fun supportsHrWorkoutCompanionMode(): Boolean {
        val capabilities = exerciseClient.getCapabilities()
        return BatchingMode.HEART_RATE_5_SECONDS in
                capabilities.supportedBatchingModeOverrides
    }
    
  2. Specifica che l'oggetto ExerciseConfig deve utilizzare un determinato BatchingMode, come mostrato nel seguente snippet di codice.

    val config = ExerciseConfig(
        exerciseType = ExerciseType.WORKOUT,
        dataTypes = setOf(
            DataType.HEART_RATE_BPM,
            DataType.TOTAL_CALORIES
        ),
        // ...
        batchingModeOverrides = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    )
    
  3. Facoltativamente, puoi configurare dinamicamente BatchingMode durante l'esercizio, anziché mantenere un comportamento di raggruppamento specifico per tutta la durata dell'esercizio:

    val desiredModes = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    exerciseClient.overrideBatchingModesForActiveExercise(desiredModes)
    
  4. Per cancellare il valore BatchingMode personalizzato e tornare al comportamento predefinito, passa un set vuoto in exerciseClient.overrideBatchingModesForActiveExercise().

Timestamp

Il point-in-time di ogni punto dati rappresenta la durata dall'avvio del dispositivo. Per convertirlo in un timestamp:

val bootInstant =
    Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())

Questo valore può quindi essere utilizzato con getStartInstant() o getEndInstant() per ogni punto dati.

Accuratezza dei dati

Alcuni tipi di dati possono avere informazioni sulla precisione associate a ciascun punto dati. Questo è rappresentato nella proprietà accuracy.

Le classi HrAccuracy e LocationAccuracy possono essere compilate rispettivamente per i tipi di dati HEART_RATE_BPM e LOCATION. Se presente, utilizza la proprietà accuracy per determinare se ciascun punto dati è di precisione sufficiente per l'applicazione.

Archivia e carica dati

Usa Stanza per salvare in modo permanente i dati forniti da Health Services. Il caricamento dei dati avviene al termine dell'esercizio utilizzando un meccanismo come Work Manager. Ciò garantisce che le chiamate di rete per il caricamento dei dati vengano differite fino al termine dell'esercizio, riducendo al minimo il consumo di energia durante l'esercizio e semplificando il lavoro.

Elenco di controllo dell'integrazione

Prima di pubblicare l'app che utilizza ExerciseClient di Health Services, consulta il seguente elenco di controllo per assicurarti che la tua esperienza utente eviti alcuni problemi comuni. Verifica quanto segue:

  • La tua app verifica le funzionalità del tipo di allenamento e le capacità del dispositivo ogni volta che viene eseguita. In questo modo, puoi rilevare quando un determinato dispositivo o allenamento non supporta uno dei tipi di dati richiesti dalla tua app.
  • Richiedi e gestisci le autorizzazioni necessarie e le specifichi nel file manifest. Prima di chiamare prepareExerciseAsync(), la tua app conferma che sono state concesse le autorizzazioni di runtime.
  • L'app utilizza getCurrentExerciseInfoAsync() per gestire i casi in cui:
    • Un allenamento è già stato monitorato e la tua app sostituisce l'allenamento precedente.
    • Un'altra app ha interrotto l'allenamento. Questo può accadere quando l'utente riapre l'app e riceve un messaggio che spiega che l'allenamento è stato interrotto perché un'altra app ha preso il comando.
  • Se utilizzi i dati LOCATION:
    • La tua app conserva un ForegroundService con il valore foregroundServiceType corrispondente per tutta la durata dell'esercizio (inclusa la chiamata di preparazione).
    • Controlla che il GPS sia attivato sul dispositivo utilizzando isProviderEnabled(LocationManager.GPS_PROVIDER) e richiedi all'utente di aprire le impostazioni di geolocalizzazione, se necessario.
    • Per i casi d'uso più complessi, in cui la ricezione di dati sulla posizione a bassa latenza è molto importante, valuta la possibilità di integrare il Fused Location Provider (FLP) e di utilizzarne i dati come correzione iniziale della posizione. Quando da Health Services disponibili informazioni sulla posizione più stabili, usale al posto di FLP.
  • Se la tua app richiede il caricamento di dati, qualsiasi chiamata di rete per caricare i dati viene differita fino al termine dell'allenamento. Altrimenti, durante l'allenamento, la tua app effettua le chiamate di rete necessarie con parsimonia.