Adicionar trajetos de exercícios

Este guia é compatível com a versão 1.1.0-alpha12 ou mais recente do app Conexão Saúde.

Os trajetos de exercícios permitem que os usuários acompanhem por GPS o trajeto das atividades físicas associadas e compartilhem mapas dos treinos deles com outros apps.

Verificar a disponibilidade do app Conexão Saúde

Antes de tentar usar o Conexão Saúde, seu app precisa verificar se ele está disponível no dispositivo do usuário. A Conexão Saúde pode não estar pré-instalada em todos os dispositivos ou pode estar desativada. É possível verificar a disponibilidade usando o método HealthConnectClient.getSdkStatus().

Como verificar a disponibilidade do Conexão Saúde

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

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

Dependendo do status retornado por getSdkStatus(), você pode orientar o usuário a instalar ou atualizar o Conexão Saúde na Google Play Store, se necessário.

Este guia mostra como solicitar permissões ao usuário e descreve como os apps recebem a permissão para gravar dados de trajeto como parte de uma sessão de exercício.

A funcionalidade de leitura e gravação de trajetos de exercícios inclui:

  1. Os apps criam uma nova permissão de gravação para trajetos de exercícios.
  2. A inserção acontece com a criação de uma sessão de exercício com um trajeto como campo.
  3. Leitura:
    1. Para o proprietário da sessão, os dados são acessados usando uma leitura de sessão.
    2. Em um app de terceiros, em uma caixa de diálogo que permite ao usuário conceder uma leitura única de um trajeto.

Se o usuário não tiver permissões de gravação e o trajeto não estiver definido, ele não será atualizado.

Se o app tiver uma permissão de gravação de trajeto e tentar atualizar uma sessão transmitindo um objeto de sessão sem um trajeto, o trajeto atual será excluído.

Disponibilidade do recurso

Para determinar se o dispositivo de um usuário é compatível com exercícios planejados no app Conexão Saúde, verifique a disponibilidade de FEATURE_PLANNED_EXERCISE no cliente:

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

  // Feature is available
} else {
  // Feature isn't available
}
Consulte Verificar a disponibilidade de recursos para saber mais.

Permissões necessárias

O acesso ao trajeto do exercício é protegido pelas seguintes permissões:

  • android.permission.health.READ_EXERCISE_ROUTES
  • android.permission.health.WRITE_EXERCISE_ROUTE
Observação: para esse tipo de permissão, READ_EXERCISE_ROUTES é plural, enquanto WRITE_EXERCISE_ROUTE é singular.

Para adicionar o recurso de trajeto de exercício ao app, solicite permissões para o tipo de dados ExerciseSession.

Confira a permissão necessária para poder gravar trajetos de exercícios:

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

Para ler o trajeto do exercício, você precisa solicitar as seguintes permissões:

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

Você também precisa declarar uma permissão de exercício, já que cada trajeto está associado a uma sessão (o que é igual a um treino).

Para solicitar permissões, use o método PermissionController.createRequestPermissionResultContract() ao conectar seu app ao Conexão Saúde pela primeira vez. Várias permissões que podem ser solicitadas são:

  • Ler dados de saúde e condicionamento físico, incluindo dados de trajetos: HealthPermission.getReadPermission(ExerciseSessionRecord::class)
  • Gravar dados de saúde e condicionamento físico, incluindo dados de trajetos: HealthPermission.getWritePermission(ExerciseSessionRecord::class)
  • Gravar dados de trajeto de exercício: HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE

Solicitar permissões do usuário

Depois de criar uma instância de cliente, seu app precisa solicitar permissões aos usuários. Os usuários precisam concedê-las ou negá-las a qualquer momento. Para fazer isso, crie um conjunto de permissões para os tipos de dados necessários. Verifique se as permissões no conjunto foram declaradas no manifesto do Android primeiro.

val permissions =
    setOf(
        HealthPermission.getReadPermission(ExerciseSessionRecord::class),
        HealthPermission.getWritePermission(ExerciseSessionRecord::class)
    )
Use getGrantedPermissions para verificar se o app já tem as permissões necessárias concedidas. Caso contrário, use createRequestPermissionResultContract para solicitá-las. Isso mostra a tela de permissões da Conexão Saúde.
val permissions = setOf(
        HealthPermission.getReadPermission(StepsRecord::class),
        HealthPermission.getWritePermission(StepsRecord::class),
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class)
    )

val requestPermissionsLauncher = rememberLauncherForActivityResult(
    contract = PermissionController.createRequestPermissionResultContract()
) { grantedPermissions ->
    if (grantedPermissions.containsAll(permissions)) {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions granted!") }
    } else {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions denied.") }
    }
}
Como os usuários podem conceder ou revogar permissões a qualquer momento, seu app precisa verificar as permissões sempre antes de usá-las e lidar com situações em que elas são perdidas.

Informações incluídas em um registro de sessão de exercício

Cada registro de sessão de exercício contém as seguintes informações:

  • O tipo de exercício, por exemplo, ciclismo.
  • O trajeto do exercício, que contém informações como latitude, longitude e altitude.

Agregações compatíveis

Os seguintes valores agregados estão disponíveis para ExerciseSessionRecord:

Exemplo de uso

Os snippets de código abaixo mostram como ler e gravar um trajeto de exercício.

Ler trajeto do exercício

O app não pode ler dados de percurso de exercícios criados por outros apps quando é executado em segundo plano.

Quando o app é executado em segundo plano e tenta ler um trajeto de exercício criado por outro app, o Conexão Saúde retorna uma resposta ExerciseRouteResult.ConsentRequired, mesmo que o app tenha acesso Sempre permitir aos dados de trajeto de exercício.

Por esse motivo, recomendamos que você solicite trajetos após uma interação deliberada do usuário com seu app, quando ele estiver interagindo ativamente com a interface do app.

Para saber mais sobre leituras em segundo plano, consulte Exemplo de leitura em segundo plano.

O snippet de código abaixo demonstra como ler uma sessão na Conexão Saúde e solicitar um trajeto dessa sessão:

private suspend fun readExerciseSessionAndRoute() {
    val client = healthConnectClient ?: return

    val endTime = Instant.now()
    val startTime = endTime.minus(Duration.ofHours(1))

    val grantedPermissions = client.permissionController.getGrantedPermissions()

    // 1. Verify basic Exercise Session permissions
    if (!grantedPermissions.contains(
            HealthPermission.getReadPermission(ExerciseSessionRecord::class)
        )
    ) {
        return
    }

    // 2. Read the sessions
    val readResponse = client.readRecords(
        ReadRecordsRequest(
            ExerciseSessionRecord::class,
            TimeRangeFilter.between(startTime, endTime)
        )
    )

    val exerciseRecord = readResponse.records.firstOrNull() ?: return
    val recordId = exerciseRecord.metadata.id

    // 3. Read the specific record to check for the route
    val sessionResponse = client.readRecord(ExerciseSessionRecord::class, recordId)

    // 4. Handle the Route Result directly from the response
    when (val routeResult = sessionResponse.record.exerciseRouteResult) {
        is ExerciseRouteResult.Data -> {
            displayExerciseRoute(routeResult.exerciseRoute)
        }
        is ExerciseRouteResult.ConsentRequired -> {
            // Since you are in a Service, you cannot launch ActivityResultLauncher.
            // Send a notification to the user to grant route-specific consent.
            handleConsentRequired(recordId)
        }
        is ExerciseRouteResult.NoData -> Unit
        else -> Unit
    }
}

private fun displayExerciseRoute(route: ExerciseRoute) {
    val locations = route.route.orEmpty()
    for (location in locations) {
        println(location)
    }
}

Gravar um trajeto de exercício

O código a seguir demonstra como gravar uma sessão que inclui um trajeto de exercício:

private suspend fun insertExerciseRoute() {
    val client = healthConnectClient ?: return

    val grantedPermissions = client.permissionController.getGrantedPermissions()

    // 1. Verify Session Write Permission
    val hasWriteSession = grantedPermissions.contains(
        HealthPermission.getWritePermission(ExerciseSessionRecord::class)
    )
    if (!hasWriteSession) return

    val sessionStartTime = Instant.now()
    val sessionDuration = Duration.ofMinutes(20)
    val sessionEndTime = sessionStartTime.plus(sessionDuration)

    // 2. Build the route if route-specific write permission is granted
    val hasWriteRoute = grantedPermissions.contains(HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE)

    val exerciseRoute = if (hasWriteRoute) {
        ExerciseRoute(
            listOf(
                ExerciseRoute.Location(
                    time = sessionStartTime,
                    latitude = 6.5483,
                    longitude = 0.5488,
                    horizontalAccuracy = Length.meters(2.0),
                    verticalAccuracy = Length.meters(2.0),
                    altitude = Length.meters(9.0),
                ),
                ExerciseRoute.Location(
                    time = sessionEndTime.minusSeconds(1),
                    latitude = 6.4578,
                    longitude = 0.6577,
                    horizontalAccuracy = Length.meters(2.0),
                    verticalAccuracy = Length.meters(2.0),
                    altitude = Length.meters(9.2),
                )
            )
        )
    } else {
        null
    }

    // 3. Create the session record
    val exerciseSessionRecord = ExerciseSessionRecord(
        startTime = sessionStartTime,
        startZoneOffset = ZoneOffset.UTC,
        endTime = sessionEndTime,
        endZoneOffset = ZoneOffset.UTC,
        exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_BIKING,
        title = "Morning Bike Ride",
        exerciseRoute = exerciseRoute,
        metadata = Metadata.activelyRecorded(
            device = Device(type = Device.TYPE_PHONE)
        )
    )

    // 4. Insert into Health Connect
    client.insertRecords(listOf(exerciseSessionRecord))
}

Sessões de exercícios

As sessões de exercícios podem incluir várias atividades, de corrida a badminton.

Gravar sessões de exercícios

Confira como criar uma solicitação de inserção que inclui uma sessão:

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

Ler uma sessão de exercícios

Confira um exemplo de como ler uma sessão de exercícios:

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

Gravar dados de subtipo

As sessões também podem ser compostas por dados de subtipo opcionais, que fornecem mais informações para melhorar a sessão.

Por exemplo, as sessões de exercício podem incluir as classes ExerciseSegment, ExerciseLap e ExerciseRoute:

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

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

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

Excluir uma sessão de exercícios

Há duas maneiras de excluir uma sessão de exercícios:

  1. Por período.
  2. Por UID.

Confira como excluir dados de subtipo de acordo com o período:

suspend fun deleteExerciseSessionByTimeRange(
    healthConnectClient: HealthConnectClient,
    exerciseRecord: ExerciseSessionRecord,
) {
    val timeRangeFilter = TimeRangeFilter.between(exerciseRecord.startTime, exerciseRecord.endTime)
    healthConnectClient.deleteRecords(ExerciseSessionRecord::class, timeRangeFilter)
    // delete the associated distance record
    healthConnectClient.deleteRecords(DistanceRecord::class, timeRangeFilter)
}

Também é possível excluir dados de subtipo pelo UID. Isso exclui apenas a sessão de exercícios, não os dados associados:

suspend fun deleteExerciseSessionByUid(
    healthConnectClient: HealthConnectClient,
    exerciseRecord: ExerciseSessionRecord,
) {
    healthConnectClient.deleteRecords(
        ExerciseSessionRecord::class,
        recordIdsList = listOf(exerciseRecord.metadata.id),
        clientRecordIdsList = emptyList()
    )
}