Gravar um exercício com o ExerciseClient

Com o uso de ExerciseClient, os Recursos de saúde oferecem suporte de primeira classe a apps de treino. E graças ao ExerciseClient, seu app pode controlar quando um exercício está em andamento, adicionar metas de exercício e receber atualizações sobre o estado do exercício, eventos de exercício ou outras métricas desejadas. Para saber mais, consulte a lista completa de tipos de exercício com suporte aos Recursos de saúde.

Consulte a Exemplo de exercício no GitHub.

Adicionar dependências

Para adicionar uma dependência aos Recursos de saúde, é preciso adicionar o repositório Maven do Google ao seu projeto. Para mais informações, consulte a seção Repositório Maven do Google.

Em seguida, no arquivo build.gradle do módulo, adicione a dependência abaixo:

Groovy

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

Kotlin

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

Estrutura do app

Use a estrutura de apps abaixo ao criar um app de exercícios com os Recursos de saúde:

Ao se preparar para um treino e durante o treino, sua atividade pode ser interrompida por vários motivos. O usuário pode alternar para outro app ou voltar ao mostrador do relógio. O sistema pode mostrar algo por cima da atividade ou a tela pode ser desligada após um período de inatividade. Use um ForegroundService em execução contínua com ExerciseClient para garantir o funcionamento correto durante todo o treino.

O uso de um ForegroundService possibilita usar a API Ongoing Activity para mostrar um indicador na superfície do relógio, permitindo que o usuário retorne rapidamente ao treino.

É essencial solicitar os dados de local corretamente no serviço em primeiro plano. No seu arquivo de manifesto, especifique o serviço em primeiro plano necessário tipos e permissões:

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

Use AmbientLifecycleObserver para a atividade de pré-treino, que contém a chamada prepareExercise(), e para a atividade de treino. No entanto, não atualize a tela durante o treino no modo ambiente. Os Recursos de saúde agrupam dados de treino quando a tela do dispositivo está no modo ambiente para economizar energia. Por isso, as informações mostradas podem não ser recentes. Durante os treinos, mostre dados que façam sentido para o usuário, com informações atualizadas ou uma tela em branco.

Verificar os recursos

Cada ExerciseType oferece suporte a determinados tipos de dados para métricas e metas de exercício. Verifique esses recursos na inicialização porque eles podem variar dependendo do dispositivo. Um dispositivo pode não oferecer suporte a um determinado tipo de exercício ou não ter funções específicas, como a pausa automática. Além disso, os recursos de um dispositivo podem mudar com o tempo, por exemplo, após uma atualização de software.

Na inicialização do app, consulte os recursos do dispositivo para armazenar e processar estas informações:

  • Os exercícios com suporte na plataforma.
  • Os recursos com suporte em cada exercício.
  • Os tipos de dados com suporte em cada exercício.
  • As permissões necessárias para cada um desses tipos de dados.

Use ExerciseCapabilities.getExerciseTypeCapabilities() com seu tipo de exercício desejado para conferir quais tipos de métrica você pode solicitar, quais metas de exercícios podem ser configuradas e que outros recursos estão disponíveis para esse tipo. Isso é mostrado neste exemplo:

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

Dentro dos ExerciseTypeCapabilities retornados, os supportedDataTypes listam os tipos de dados para os quais você pode solicitar dados. Isso varia de acordo com o dispositivo. Se você solicitar um DataType sem suporte, sua solicitação poderá falhar.

Use os campos supportedGoals e supportedMilestones para determinar se o exercício oferece suporte à meta que você quer criar.

Se o app permitir que o usuário use a pausa automática, confira se essa funcionalidade tem suporte do dispositivo usando supportsAutoPauseAndResume. ExerciseClient rejeita solicitações que não tenham suporte do dispositivo.

O exemplo a seguir consulta o suporte ao tipo de dados HEART_RATE_BPM, o recurso de meta STEPS_TOTAL e a funcionalidade de pausa automática:

// 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

Registro para atualizações do estado do exercício

As atualizações de exercícios são enviadas ao listener. Seu app só pode registrar um único listener por vez. Configure o listener antes de iniciar o treino, conforme mostrado no exemplo a seguir. Seu listener só recebe atualizações sobre exercícios do 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)

Gerenciar o ciclo de vida do exercício

Os Recursos de saúde oferecem suporte a, no máximo, um exercício por vez em todos os apps no dispositivo. Caso um exercício esteja sendo monitorado e outro app comece a monitorar um novo exercício, o primeiro é encerrado.

Antes de iniciar o exercício, faça o seguinte:

  • Confira se um exercício já está sendo monitorado e proceda da forma certa. Por exemplo, solicite a confirmação do usuário antes de substituir o exercício anterior e começar a monitorar um novo.

O exemplo abaixo mostra como verificar se há um exercício usando 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.
    }
}

Permissões

Ao usar o ExerciseClient, confira se o app solicita e mantém a as permissões necessárias. Caso seu app use dados de LOCATION, ele precisa solicitar e manter os as permissões apropriadas para isso também.

Para todos os tipos de dados, antes de chamar prepareExercise() ou startExercise(), faça o seguinte:

  • Especifique as permissões adequadas para os tipos de dados solicitados no arquivo AndroidManifest.xml.
  • Verifique se o usuário concedeu as permissões necessárias. Para mais informações, consulte Solicitar permissões do app. O serviço Recursos de saúde vai rejeitar a solicitação, se as permissões necessárias ainda não tiverem sido concedidas.

Para dados de local, siga estas outras etapas:

Preparar um treino

Alguns sensores, como GPS ou frequência cardíaca, podem demorar um pouco para começar a funcionar ou o usuário pode querer conferir dados antes de iniciar o treino. O método opcional prepareExerciseAsync() permite que esses sensores comecem a funcionar e que os dados sejam recebidos sem iniciar o cronômetro do treino. A activeDuration não é afetada por esse tempo de preparação.

Antes de fazer a chamada para prepareExerciseAsync(), confira o seguinte:

  • Confira a configuração de localização para toda a plataforma. O usuário controla essa configuração no menu principal do app. Ela é diferente da verificação de permissões do app.

    Se a configuração estiver desativada, notifique o usuário de que ele negou acesso à localização e solicite a ativação quando for necessário.

  • Verifique se o app tem permissões de execução para sensores corporais, reconhecimento de atividades e localização exata. Se não tiver, solicite ao usuário permissões de execução que forneçam o contexto adequado. Se o usuário não conceder uma permissão específica, remova os tipos de dados associados a essa permissão da chamada para prepareExerciseAsync(). Se as permissões de localização e o sensor corporal não forem fornecidos, não chame prepareExerciseAsync(), já que a chamada de preparo serve especificamente para conseguir uma frequência cardíaca estável ou o sinal do GPS antes de iniciar um exercício. O app ainda pode conseguir extrair a distância, o ritmo e a velocidade com base em passos e outras métricas que não exigem essas permissões.

Faça o seguinte para garantir que sua chamada para prepareExerciseAsync() seja bem-sucedida:

  • Use AmbientLifecycleObserver para a atividade de pré-treino que contém a chamada de preparo.
  • Chame prepareExerciseAsync() no seu serviço em primeiro plano. A preparação do sensor pode ser eliminada desnecessariamente se não estiver em um serviço e estiver vinculada ao ciclo de vida da atividade.
  • Chame endExercise() para desativar os sensores e reduzir o uso de energia, se o usuário sair da atividade antes do treino.

O exemplo a seguir mostra como chamar 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.

Quando o app está no estado PREPARING, as atualizações de disponibilidade do sensor são entregues no ExerciseUpdateCallback pelo onAvailabilityChanged(). Essas informações podem ser apresentadas ao usuário para que ele decida se quer iniciar o treino.

Iniciar o treino

Quando quiser iniciar um exercício, crie um objeto ExerciseConfig para configurar o tipo de exercício, os tipos de dados para os quais você quer receber métricas e quaisquer metas ou marcos de exercício.

Metas de exercício consistem em um DataType e uma condição. Elas são um objetivo único, que é acionado quando uma condição é atendida (por exemplo, quando o usuário corre determinada distância). Um marco de exercício também pode ser definido. Os marcos podem ser acionados várias vezes, como cada vez que o usuário atinge um determinado ponto após a distância definida.

O exemplo abaixo mostra como criar uma meta para cada 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()
}

Você também pode marcar voltas para todos os exercícios. Os Recursos de saúde fornecem um ExerciseLapSummary com métricas agregadas no período da volta.

O exemplo anterior mostra o uso de isGpsEnabled, que precisa ser definido como verdadeiro ao solicitar dados de local. No entanto, o uso de GPS também pode ajudar com outras métricas. Se a ExerciseConfig especificar a distância como um DataType, o padrão será usar etapas para estimar a distância. Se você ativar o GPS, as informações de localização poderão ser usadas para estimar a distância.

Pausar, retomar e encerrar um treino

É possível pausar, retomar e encerrar os treinos usando o método adequado, como pauseExerciseAsync() ou endExerciseAsync().

Use o estado de ExerciseUpdate como a fonte da verdade. O treino não é considerado como pausado quando a chamada para pauseExerciseAsync() é retornada, mas quando esse estado é refletido na mensagem ExerciseUpdate. Quando se trata de estados da interface, é muito importante pensar nisso. Se o usuário pressionar "pausar", você precisará desativar o botão de pausa e chamar pauseExerciseAsync() nos Recursos de saúde. Aguarde até que os Recursos de saúde cheguem ao estado "pausado" usando ExerciseUpdate.exerciseStateInfo.state e toque no botão para "retomar". Isso ocorre porque as atualizações de estado dos Recursos de saúde podem levar mais tempo para serem entregues do que o pressionamento do botão. Se você vincular todas as mudanças de interface aos pressionamentos de botão, poderá haver uma dessincronização da interface com o estado dos Recursos de saúde.

Não esqueça:

  • A pausa automática está ativada: o treino pode ser pausado ou iniciado sem a interação do usuário.
  • Outro app inicia um treino: seu treino pode ser encerrado sem a interação do usuário.

Seu app precisa reagir corretamente quando um treino dele é encerrado por outro app.

  • Salvar o estado parcial de treino para que o progresso de um usuário não seja apagado.
  • Remover o ícone de atividade em andamento e enviar ao usuário uma notificação informando que o treino foi encerrado por outro app.

Quando permissões forem revogadas durante um exercício, seu app também precisa reagir. Essa informação é enviada pelo estado isEnded, com um ExerciseEndReason de AUTO_END_PERMISSION_LOST. Lide com essa situação de forma semelhante ao caso de encerramento: salve o estado parcial, remova o ícone da atividade em andamento e envie uma notificação sobre o que aconteceu com o usuário.

O exemplo a seguir mostra como verificar o encerramento corretamente:

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

Gerenciar a duração ativa

Durante um exercício, o app pode mostrar a duração ativa do treino. O app, os Recursos de saúde e a unidade de controle micro do dispositivo (MCU) — a baixa potência responsável pelo monitoramento de atividade física, todos precisam estar sincronizados, a mesma duração ativa atual. Para ajudar a gerenciar isso, o Recursos de Saúde envia um ActiveDurationCheckpoint, que fornece um ponto de fixação que o app pode usar para iniciar o timer.

Como a duração ativa é enviada pelo MCU e pode demorar um pouco para chegar ao app, ActiveDurationCheckpoint contém duas propriedades:

  • activeDuration: por quanto tempo o exercício está ativo
  • time: quando a duração ativa foi calculada

No app, a duração ativa de um exercício pode ser calculada com ActiveDurationCheckpoint usando a equação abaixo:

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

Esse cálculo considera o pequeno delta entre a duração ativa, que é calculada no MCU, e a chegada ao app e pode ser usado para propagar um cronômetro no app, além de garantir que o timer esteja perfeitamente alinhado com o horário dos Recursos de saúde e do MCU.

Se o exercício for pausado, o app vai esperar para reiniciar o timer na interface até que o tempo calculado tenha passado do tempo que a interface está mostrando no momento. Isso ocorre porque o sinal de pausa chega aos Recursos de saúde e ao MCU com um pequeno atraso. Por exemplo, se o app pausa em t=10 segundos, os Recursos de Saúde podem não fornecer a atualização PAUSED ao app até t=10,2 segundos.

Trabalhar com dados do ExerciseClient

As métricas dos tipos de dados que o app registrou são entregues em mensagens ExerciseUpdate.

O processador entrega mensagens somente quando ativado ou ao atingir um período máximo de geração de relatórios, por exemplo, a cada 150 segundos. Não dependa da frequência de ExerciseUpdate para avançar um cronômetro com a activeDuration. Consulte a Exemplo de exercício no GitHub para conferir um exemplo de como implementar um cronômetro independente.

Quando um usuário inicia um treino, mensagens ExerciseUpdate podem ser entregues com frequência, por exemplo, a cada segundo. Quando o usuário inicia o treino, a tela pode ser desligada. Os Recursos de saúde podem fornecer dados com uma frequência menor, mesmo que a amostragem continue sendo feita na mesma frequência, para evitar a ativação do processador principal. Quando o usuário confere a tela, todos os dados no processo de envio em lote são imediatamente enviados ao app.

Controlar a taxa de lotes

Em alguns casos, convém controlar a frequência com que seu app recebe determinados tipos de dados enquanto a tela está desligada. Um objeto BatchingMode permite que seu app substitua o comportamento de lote padrão para receber entregas de dados com mais frequência.

Para configurar a taxa de lotes, siga estas etapas:

  1. Confira se a definição BatchingMode específica tem suporte no 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. Especifique que o objeto ExerciseConfig precisa usar um BatchingMode específico, conforme mostrado no snippet de código abaixo.

    val config = ExerciseConfig(
        exerciseType = ExerciseType.WORKOUT,
        dataTypes = setOf(
            DataType.HEART_RATE_BPM,
            DataType.TOTAL_CALORIES
        ),
        // ...
        batchingModeOverrides = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    )
    
  3. Também é possível configurar BatchingMode dinamicamente no treino em vez de ter um comportamento de lote específico durante todo o treino:

    val desiredModes = setOf(BatchingMode.HEART_RATE_5_SECONDS)
    exerciseClient.overrideBatchingModesForActiveExercise(desiredModes)
    
  4. Para limpar o BatchingMode personalizado e retornar ao comportamento padrão, transmita um conjunto vazio para exerciseClient.overrideBatchingModesForActiveExercise().

Carimbos de data/hora

O ponto no tempo de cada ponto de dados representa a duração desde a inicialização do dispositivo. Para converter isso em um carimbo de data/hora, faça o seguinte:

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

Esse valor pode ser usado com getStartInstant() ou getEndInstant() para cada ponto de dados.

Precisão de dados

Alguns tipos de dados podem ter informações de precisão associadas a cada ponto de dados, representadas na propriedade accuracy.

As classes HrAccuracy e LocationAccuracy podem ser preenchidas para os tipos de dados HEART_RATE_BPM e LOCATION, respectivamente. Quando presente, use a propriedade accuracy para determinar se cada ponto de dados é suficiente para o aplicativo.

Armazenar e fazer upload de dados

Use o Room para persistir dados enviados do Recursos de saúde. O upload de dados acontece no fim do exercício usando um mecanismo como o Work Manager. Isso garante que as chamadas de rede para o upload de dados sejam adiadas até o fim do exercício, o que minimiza o consumo de energia durante o exercício e simplifica o trabalho realizado.

Lista de verificação de integração

Antes de publicar seu app que usa os Recursos de saúde ExerciseClient, consulte a lista de verificação a seguir para garantir que a experiência do usuário evite alguns problemas comuns. Confirme o seguinte:

  • Seu app verifica os recursos do tipo de exercício e dos recursos do dispositivo cada vez que o aplicativo é executado. Dessa forma, você pode detectar quando um dispositivo ou exercício específico não é compatível com um dos tipos de dados de que seu app precisa.
  • Você solicita e mantém as permissões necessárias e as especifica em no arquivo de manifesto. Antes de chamar prepareExerciseAsync(), seu app confirma se as permissões de execução foram concedidas.
  • Seu app usa getCurrentExerciseInfoAsync() para processar os casos em que:
    • Um exercício já está sendo monitorado e seu aplicativo substitui o anterior exercício.
    • Outro app encerrou seu exercício. Isso pode acontecer quando o usuário abre o app novamente, ele recebe uma mensagem explicando que o exercício parou porque outro app assumiu o controle.
  • Se você estiver usando dados de LOCATION:
    • O app mantém um ForegroundService com as foregroundServiceType durante todo o exercício (incluindo da chamada de preparação).
    • Verifique se o GPS está ativado no dispositivo usando isProviderEnabled(LocationManager.GPS_PROVIDER) e solicita que o usuário abra as configurações de localização, se necessário.
    • Para casos de uso exigentes, em que o recebimento de dados de local com é muito importante, considere integrar a API Provedor de localização (FLP) e usa os dados dele como uma correção inicial de local. Quando estiver mais estável informações de local disponíveis nos Recursos de saúde, use esses dados do FLP.
  • Se o app exigir o upload de dados, todas as chamadas de rede para esse processo serão ser adiado até o fim do exercício. Caso contrário, ao longo do exercício, faz as chamadas de rede necessárias com moderação.