Registra un allenamento con ExerciseClient

Health Services fornisce un'assistenza di primo livello per le app per gli allenamenti 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-alpha02"
}

Kotlin

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

Struttura dell'app

Utilizza la seguente struttura dell'app quando crei un'app per l'allenamento con Health Services:

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

L'utilizzo di un ForegroundService ti consente di usare l'API Onboarding 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 le autorizzazioni appropriate.

Usa AmbientLifecycleObserver per l'attività pre-allenamento, che contiene la chiamata prepareExercise() e per l'attività fisica. Tuttavia, non aggiornare il display durante l'esercizio in modalità Ambient: questo perché Health Services raggruppa i dati sull'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 significativi per l'utente, visualizzando informazioni aggiornate o una schermata vuota.

Verifica le funzionalità

Ogni ExerciseType supporta determinati tipi di dati per le metriche e gli obiettivi di allenamento. Controlla queste funzionalità all'avvio, perché possono variare a seconda del dispositivo. Un dispositivo potrebbe non supportare un determinato tipo di allenamento o non supportare una funzione specifica, come 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, 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. Questo è quanto mostrato 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. Questa opzione varia a seconda del dispositivo, quindi fai attenzione a 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 utilizzare la pausa automatica, devi verificare che questa funzionalità sia supportata dal dispositivo che utilizza supportsAutoPauseAndResume. ExerciseClient rifiuta le richieste non supportate sul dispositivo.

L'esempio seguente verifica il supporto per il tipo di dati HEART_RATE_BPM, la funzionalità per gli obiettivi STEPS_TOTAL e la 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 degli allenamenti vengono inviati a un ascoltatore. L'app può registrare un solo ascoltatore alla volta. Imposta il listener prima di iniziare l'esercizio, come mostrato nell'esempio seguente. Chi ascolta riceve solo aggiornamenti 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 durata allenamento

Health Services supporta al massimo un esercizio alla volta su tutte le app sul dispositivo. Se un allenamento viene monitorato 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 è già monitorato e reagisci di conseguenza. Ad esempio, chiedi conferma all'utente prima di sostituire un esercizio precedente e di iniziare a monitorarne uno nuovo.

L'esempio seguente mostra come verificare la presenza di 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 la tua app richieda e mantenga le autorizzazioni necessarie. Se la tua app utilizza i dati LOCATION, assicurati che l'app richieda e mantenga le autorizzazioni appropriate anche per questi dati.

Per tutti i tipi di dati, prima di chiamare prepareExercise() o startExercise(), procedi nel seguente modo:

  • 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 esercizio

Alcuni sensori, come il GPS o la frequenza cardiaca, potrebbero richiedere poco tempo per riscaldarsi oppure l'utente potrebbe voler visualizzare i propri 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(), verifica quanto segue:

  • Controlla l'impostazione della località 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 che ha negato l'accesso alla posizione e chiedigli di abilitarla se la tua app richiede la posizione.

  • Verifica che la tua app disponga delle autorizzazioni di runtime per i sensori del corpo, il riconoscimento dell'attività e la geolocalizzazione. 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 né il sensore del corpo né l'autorizzazione di accesso alla posizione vengono concesse, non chiamare prepareExerciseAsync(), perché la chiamata di preparazione è specifica per ottenere un battito cardiaco stabile o un rilevamento GPS prima di iniziare un allenamento. L'app può comunque ricevere informazioni basate su passi, velocità, velocità e altre metriche che non richiedono queste autorizzazioni.

Per assicurarti che la chiamata a prepareExerciseAsync() possa andare a buon fine:

  • Usa 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 è legata al ciclo di vita dell'attività, la preparazione del sensore potrebbe essere interrotta inutilmente.
  • Chiama endExercise() per spegnere i sensori e ridurre il consumo di energia se l'utente abbandona l'attività pre-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 nel ExerciseUpdateCallback fino al onAvailabilityChanged(). Queste informazioni possono quindi essere presentate all'utente in modo che possa decidere se iniziare l'esercizio.

Inizia l'esercizio

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

Gli obiettivi di allenamento consistono di 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. È possibile impostare anche un traguardo per l'allenamento. I traguardi dell'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 elemento ExerciseLapSummary con metriche aggregate sul periodo della giro.

L'esempio precedente mostra l'utilizzo di isGpsEnabled, che deve essere true quando vengono richiesti 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 usati i passi per stimare la distanza. Con l'attivazione del GPS, invece, 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 del giorno ExerciseUpdate come fonte attendibile. L'esercizio non viene considerato in pausa quando viene ripristinata 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 Servizi sanitari. Attendi che Health Services raggiunga lo stato di pausa utilizzando ExerciseUpdate.exerciseStateInfo.state, quindi premi il pulsante per riprendere. Questo perché gli aggiornamenti dello stato di Health Services possono richiedere più tempo rispetto alla pressione del pulsante, quindi se colleghi tutte le modifiche all'interfaccia utente 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 l'interazione dell'utente.
  • Un'altra app avvia un esercizio: l'esercizio potrebbe essere terminato senza interazione da parte 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 ExerciseEndReason di AUTO_END_PERMISSION_LOST. Gestisci questo caso in modo simile a quello relativo alla risoluzione: salva lo stato parziale, rimuovi l'icona Attività in corso e invia una notifica su ciò che è successo all'utente.

L'esempio seguente mostra come verificare correttamente la chiusura:

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 la durata attiva

Durante un allenamento, un'app può visualizzare la durata attiva dell'esercizio. L'app, i servizi sanitari e il dispositivo Micro Controller Unity (MCU), il processore a bassa potenza responsabile del monitoraggio degli allenamenti, devono essere tutti sincronizzati con la stessa durata attiva corrente. Per aiutarti a gestire ciò, 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ò impiegare un breve periodo di tempo per essere visualizzata nell'app, ActiveDurationCheckpoint contiene due proprietà:

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

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

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

Questo tiene conto del piccolo delta tra la durata attiva calcolata sull'MCU e l'arrivo all'app. Può essere utilizzato per avviare un cronometro nell'app e contribuire a 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 di riavviare il timer nell'interfaccia utente fino a quando il tempo calcolato non è passato a quello attualmente visualizzato 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 inviare 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 cui la tua app è registrata vengono recapitati nei 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 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 inizia l'esercizio, lo schermo potrebbe spegnersi. Health Services può quindi fornire dati meno spesso, ma comunque campionati con la 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 più frequenti dei dati.

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

  1. Controlla se la definizione specifica di BatchingMode è 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 BatchingMode in modo dinamico 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 di precisione associate a ciascun punto dati. Questo viene 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 ogni punto dati è abbastanza preciso per la tua applicazione.

Archivia e carica i dati

Utilizza Stanza per salvare in modo permanente i dati forniti da Health Services. Il caricamento dei dati avviene alla fine 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 corrente 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 l'esperienza utente eviti alcuni problemi comuni. Verifica quanto segue:

  • L'app controlla le funzionalità del tipo di allenamento e delle funzionalità 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 mantieni le autorizzazioni necessarie e le specifichi nel file manifest. Prima di chiamare prepareExerciseAsync(), la tua app conferma che le autorizzazioni di runtime sono concesse.
  • L'app utilizza getCurrentExerciseInfoAsync() per gestire i casi in cui:
    • Un allenamento è già in fase di monitoraggio e la tua app sostituisce l'allenamento precedente.
    • Un'altra app ha interrotto l'allenamento. Questo può accadere quando l'utente riapre l'app, viene visualizzato un messaggio che spiega che l'esercizio è stato interrotto perché un'altra app ha preso il controllo.
  • Se utilizzi i dati LOCATION:
    • La tua app mantiene un ForegroundService con il valore foregroundServiceType corrispondente per tutta la durata dell'allenamento (inclusa la chiamata di preparazione).
    • Verifica 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 impegnativi, in cui la ricezione di dati sulla posizione a bassa latenza è di grande importanza, valuta la possibilità di integrare il Fused Location Provider (FLP) e di utilizzare i relativi dati come correzione iniziale della posizione. Quando da Health Services sono disponibili informazioni sulla posizione più stabili, utilizzale al posto di FLP.
  • Se la tua app richiede il caricamento dei dati, tutte le chiamate di rete per il caricamento dei dati vengono differite fino al termine dell'allenamento. Altrimenti, per tutta la durata dell'esercizio, l'app effettua parsimonia le chiamate di rete necessarie.