Registra un allenamento con ExerciseClient

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

Consulta l'esempio di esercizio su GitHub.

Aggiungi dipendenze

Per aggiungere una dipendenza da Health Services, devi aggiungere il repository Maven di Google al tuo progetto. Per saperne di più, consulta il repository Maven di Google.

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

Groovy

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

Kotlin

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

Struttura dell'app

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

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

L'utilizzo di un ForegroundService ti consente di utilizzare l'API Ongoing Activity per mostrare un indicatore sulle superfici dell'orologio, consentendo all'utente di tornare rapidamente all'allenamento.

È essenziale richiedere i dati sulla posizione in modo appropriato nel servizio in primo piano. Nel file manifest, specifica i tipi di servizio in primo piano e le autorizzazioni necessari:

<manifest ...>
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <application ...>
    
      <!-- If your app is designed only for devices that run Wear OS 4
           or lower, use android:foregroundServiceType="location" instead. -->
      <service
          android:name=".MyExerciseSessionRecorder"
          android:foregroundServiceType="health|location">
      </service>
      
    </application>
</manifest>

Utilizza AmbientLifecycleObserver per l'attività pre-allenamento, che contiene la chiamata prepareExercise() e per l'attività di allenamento. Tuttavia, non aggiornare il display durante l'allenamento in modalità Ambient: questo perché i servizi per la salute raggruppano 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 allenamenti, mostra i dati che hanno senso per l'utente, visualizzando informazioni aggiornate o una schermata vuota.

Controllare le funzionalità

Ogni ExerciseType supporta determinati tipi di dati per le metriche e per 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 una funzione specifica, ad esempio la pausa automatica. Inoltre, le funzionalità di un dispositivo potrebbero cambiare nel tempo, ad esempio dopo un aggiornamento software.

All'avvio dell'app, esegui una query sulle funzionalità del dispositivo e memorizza 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 richieste per ciascuno di questi tipi di dati.

Usa ExerciseCapabilities.getExerciseTypeCapabilities() con il tipo di allenamento selezionato per vedere quali tipi di metriche puoi richiedere, quali obiettivi di allenamento puoi configurare e quali altre funzionalità sono disponibili per quel tipo. Ciò è mostrato nel seguente esempio:

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 di ExerciseTypeCapabilities restituito, supportedDataTypes sono elencati i tipi di dati per cui puoi richiedere i dati. Questo varia a seconda del dispositivo, quindi fai attenzione a non richiedere un DataType non supportato, altrimenti la 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 utilizzando supportsAutoPauseAndResume. ExerciseClient rifiuta le richieste non supportate sul dispositivo.

Il seguente esempio verifica il supporto per il tipo di dati HEART_RATE_BPM, la funzionalità di obiettivo 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

Registrarsi per ricevere aggiornamenti sullo stato dell'esercizio

Gli aggiornamenti dell'allenamento vengono inviati a un ascoltatore. La tua app può registrare un solo listener alla volta. Configura il listener prima di iniziare l'allenamento, come mostrato nell'esempio seguente. Il listener riceve aggiornamenti solo sugli esercizi 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)

Gestire la durata dell'esercizio

Servizi per la salute supporta al massimo un esercizio alla volta in tutte le app sul dispositivo. Se un allenamento viene monitorato e un'altra app inizia a monitorare un nuovo allenamento, il primo termina.

Prima di iniziare l'allenamento:

  • Controlla se un allenamento è già in fase di monitoraggio e reagisci di conseguenza. Ad esempio, chiedi all'utente di confermare prima di ignorare un allenamento precedente e iniziare a monitorarne uno nuovo.

L'esempio seguente mostra come verificare la presenza di un esercizio 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 dati LOCATION, assicurati che richieda e mantenga anche le autorizzazioni appropriate.

Per tutti i tipi di dati, prima di chiamare prepareExercise() o startExercise(), fai quanto segue:

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

Per i dati sulla posizione, segui questi passaggi aggiuntivi:

Prepararsi per un allenamento

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

Prima di effettuare la chiamata a prepareExerciseAsync(), controlla quanto segue:

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

    Se l'impostazione è disattivata, comunica all'utente che ha negato l'accesso alla posizione e chiedigli di attivarla se la tua app richiede la posizione.

  • Verifica che la tua app disponga delle autorizzazioni di runtime per i sensori del corpo (livello API 35 o inferiore) o per la frequenza cardiaca (livello API 36+), il riconoscimento dell'attività e la posizione esatta. Per le autorizzazioni mancanti, richiedi all'utente le autorizzazioni di runtime, fornendo un contesto adeguato. Se l'utente non concede un'autorizzazione specifica, rimuovi i tipi di dati associati a questa autorizzazione dalla chiamata a prepareExerciseAsync(). Se non vengono concessi né i permessi per i sensori del corpo (battito cardiaco a livello di API 36+) né quelli di localizzazione, non chiamare prepareExerciseAsync(), in quanto la chiamata di preparazione serve specificamente per acquisire un battito cardiaco stabile o una posizione GPS prima di iniziare un allenamento. L'app può comunque ottenere distanza, andatura, velocità e altre metriche basate sui passi che non richiedono queste autorizzazioni.

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

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

L'esempio seguente mostra come chiamare prepareExerciseAsync():

val warmUpConfig = WarmUpConfig(
    ExerciseType.RUNNING,
    setOf(
        DataType.HEART_RATE_BPM,
        DataType.LOCATION
    )
)
// Only necessary to call prepareExerciseAsync if body sensor (API level 35
// or lower), heart rate (API level 36+), or location permissions are given.
exerciseClient.prepareExerciseAsync(warmUpConfig).await()

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

Una volta che l'app si trova nello stato PREPARING, gli aggiornamenti sulla disponibilità dei sensori vengono forniti in ExerciseUpdateCallback tramite onAvailabilityChanged(). Queste informazioni possono quindi essere presentate all'utente, che può decidere se iniziare l'allenamento.

Inizia l'allenamento

Quando vuoi iniziare un allenamento, crea un ExerciseConfig per configurare il tipo di allenamento, i tipi di dati per i quali vuoi ricevere le metriche e gli eventuali obiettivi o traguardi dell'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 viene soddisfatta una condizione, ad esempio quando l'utente corre una determinata distanza. È possibile impostare anche un traguardo di allenamento. Le tappe dell'allenamento possono essere attivate più volte, ad esempio ogni volta che l'utente supera 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 segnare i giri per tutti gli esercizi. Health Services fornisce un ExerciseLapSummary con metriche aggregate nel periodo del giro.

L'esempio precedente mostra l'utilizzo di isGpsEnabled, che deve essere true quando si richiedono i dati sulla posizione. Tuttavia, l'utilizzo del GPS può essere utile anche per altre metriche. Se ExerciseConfig specifica la distanza come DataType, per impostazione predefinita vengono utilizzati i passi per stimare la distanza. Se attivi il GPS, le informazioni sulla posizione possono essere utilizzate per stimare la distanza.

Mettere in pausa, riprendere e terminare un allenamento

Puoi mettere in pausa, riprendere e terminare gli allenamenti utilizzando il metodo appropriato, ad esempio pauseExerciseAsync() o endExerciseAsync().

Utilizza lo stato di ExerciseUpdate come fonte di riferimento. L'allenamento non viene considerato in pausa quando viene restituita la chiamata a pauseExerciseAsync(), ma quando questo stato viene riflesso nel messaggio ExerciseUpdate. Ciò è 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 sposta il pulsante per riprendere. Questo perché gli aggiornamenti dello stato di Servizi per la salute possono richiedere più tempo per essere inviati rispetto alla pressione del pulsante, quindi se colleghi tutte le modifiche dell'interfaccia utente alla pressione dei pulsanti, l'interfaccia utente può non essere sincronizzata con lo stato di Servizi per la salute.

Tieni presente questo aspetto nelle seguenti situazioni:

  • La pausa automatica è attivata:l'allenamento può essere messo in pausa o avviato senza l'interazione dell'utente.
  • Un'altra app avvia un allenamento: l'allenamento potrebbe essere interrotto senza l'interazione dell'utente.

Se l'allenamento della tua app viene interrotto da un'altra app, la tua app deve gestire l'interruzione in modo controllato:

  • Salva lo stato dell'allenamento 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 il suo allenamento è stato interrotto da un'altra app.

Gestisci anche il caso in cui le autorizzazioni vengono revocate durante un allenamento in corso. Questo viene inviato utilizzando lo stato isEnded, con un ExerciseEndReason di AUTO_END_PERMISSION_LOST. Gestisci questo caso in modo simile a quello di risoluzione: salva lo stato parziale, rimuovi l'icona Attività in corso e invia una notifica all'utente per informarlo di quanto accaduto.

L'esempio seguente mostra come verificare correttamente la terminazione:

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
        }
        ...
    }
    ...
}

Gestire la durata attiva

Durante un esercizio, un'app può visualizzare la durata attiva dell'allenamento. L'app, i Servizi per la salute e l'unità di microcontrollo (MCU) del dispositivo, ovvero il processore a basso consumo responsabile del monitoraggio dell'allenamento, devono essere sincronizzati e avere la stessa durata attiva corrente. Per facilitare la gestione, Servizi per la salute invia un ActiveDurationCheckpoint che fornisce un punto di ancoraggio da cui l'app può avviare il timer.

Poiché la durata attiva viene inviata dalla MCU e può richiedere un po' di tempo per arrivare nell'app, ActiveDurationCheckpoint contiene due proprietà:

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

Pertanto, 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 sulla MCU e l'arrivo nell'app. Questo valore può essere utilizzato per inizializzare un cronometro nell'app e contribuire a garantire che il timer dell'app sia perfettamente allineato con l'ora in Health Services e nella MCU.

Se l'allenamento è in pausa, l'app attende di riavviare il timer nell'interfaccia utente finché il tempo calcolato non supera quello visualizzato nell'interfaccia utente. Questo perché il segnale di pausa raggiunge i servizi sanitari e la MCU con un leggero ritardo. Ad esempio, se l'app viene messa in pausa a t=10 secondi, Servizi per la salute potrebbe non inviare l'aggiornamento PAUSED all'app fino a t=10,2 secondi.

Utilizzare i dati di ExerciseClient

Le metriche per i tipi di dati per cui la tua app è registrata vengono fornite nei messaggi ExerciseUpdate.

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

Quando un utente inizia un allenamento, i messaggi ExerciseUpdate possono essere inviati di frequente, ad esempio ogni secondo. Quando l'utente inizia l'allenamento, lo schermo potrebbe spegnersi. I servizi sanitari possono 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 batch vengono immediatamente inviati alla tua app.

Controllare 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 è spento. Un oggetto BatchingMode consente alla tua app di ignorare il comportamento di batch predefinito per ricevere i dati più frequentemente.

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'allenamento, invece di avere un comportamento di batching specifico per tutta la durata dell'allenamento:

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

Timestamp

Il punto nel tempo di ogni punto dati rappresenta la durata dall'avvio del dispositivo. Per convertire questo valore in un timestamp:

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

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

Accuratezza dei dati

Alcuni tipi di dati possono avere informazioni sull'accuratezza associate a ogni punto dati. Questo valore è rappresentato nella proprietà accuracy.

Le classi HrAccuracy e LocationAccuracy possono essere compilate per i tipi di dati HEART_RATE_BPM e LOCATION, rispettivamente. Se presente, utilizza la proprietà accuracy per determinare se ogni punto dati è sufficientemente accurato per la tua applicazione.

Archivia e carica i dati

Utilizza Room per rendere persistenti i dati forniti da Health Services. Il caricamento dei dati avviene al termine dell'esercizio utilizzando un meccanismo come Work Manager. In questo modo è possibile verificare che le chiamate di rete per caricare i dati vengano posticipate fino al termine dell'allenamento, riducendo al minimo il consumo energetico durante l'allenamento e semplificando il lavoro.

Elenco di controllo dell'integrazione

Prima di pubblicare l'app che utilizza ExerciseClient di Servizi per la salute, consulta il seguente elenco di controllo per verificare che l'esperienza utente eviti alcuni problemi comuni. Verifica che:

  • La tua app controlla le funzionalità del tipo di allenamento e del dispositivo ogni volta che viene eseguita. In questo modo, puoi rilevare quando un particolare dispositivo o allenamento non supporta uno dei tipi di dati di cui la tua app ha bisogno.
  • Richiedi e mantieni le autorizzazioni necessarie e specificale nel file manifest. Prima di chiamare prepareExerciseAsync(), la tua app conferma che le autorizzazioni di runtime siano concesse.
  • La tua 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. Ciò potrebbe accadere quando l'utente riapre l'app e visualizza un messaggio che spiega che l'allenamento è stato interrotto perché un'altra app ha preso il controllo.
  • Se utilizzi i dati di LOCATION:
    • La tua app mantiene un ForegroundService con il foregroundServiceType corrispondente per tutta la durata dell'esercizio (inclusa la chiamata di preparazione).
    • Verifica che il GPS sia attivato sul dispositivo utilizzando isProviderEnabled(LocationManager.GPS_PROVIDER) e chiede all'utente di aprire le impostazioni di geolocalizzazione, se necessario.
    • Per i casi d'uso più impegnativi, in cui la ricezione di dati sulla posizione con 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 sono disponibili informazioni sulla posizione più stabili da Servizi per la salute, utilizzale al posto di FLP.
  • Se la tua app richiede il caricamento dei dati, tutte le chiamate di rete per caricare i dati vengono posticipate fino al termine dell'allenamento. In caso contrario, durante l'esercizio, l'app effettua le chiamate di rete necessarie con parsimonia.