Dodaj trasy ćwiczeń

Ten przewodnik jest zgodny z Health Connect w wersji 1.1.0-alpha12.

Trasy ćwiczeń umożliwiają użytkownikom śledzenie trasy GPS powiązanych aktywności fizycznych i udostępnianie map treningów innym aplikacjom.

Sprawdzanie dostępności Health Connect

Zanim spróbujesz użyć Health Connect, Twoja aplikacja powinna sprawdzić, czy Health Connect jest dostępna na urządzeniu użytkownika. Health Connect może nie być wstępnie zainstalowana na wszystkich urządzeniach lub może być wyłączona. Dostępność możesz sprawdzić za pomocą metody HealthConnectClient.getSdkStatus().

Jak sprawdzić dostępność Health Connect

fun checkHealthConnectAvailability(context: Context) {
    val providerPackageName = "com.google.android.apps.healthdata" // Or get from HealthConnectClient.DEFAULT_PROVIDER_PACKAGE_NAME
    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
}

W zależności od stanu zwróconego przez getSdkStatus() możesz poprosić użytkownika o zainstalowanie lub zaktualizowanie Health Connect ze Sklepu Google Play.

W tym przewodniku znajdziesz informacje o tym, jak poprosić użytkownika o uprawnienia, oraz o tym, jak aplikacje uzyskują uprawnienia do zapisywania danych o trasie w ramach sesji ćwiczeń.

Funkcje odczytu i zapisu tras ćwiczeń obejmują:

  1. Aplikacje tworzą nowe uprawnienia do zapisu tras ćwiczeń.
  2. Wstawianie odbywa się przez zapisanie sesji ćwiczeń z trasą jako polem.
  3. Odczyt:
    1. W przypadku właściciela sesji dostęp do danych uzyskuje się za pomocą odczytu sesji.
    2. Z aplikacji innej firmy za pomocą okna, które umożliwia użytkownikowi jednorazowe przyznanie uprawnień do odczytu trasy.

Jeśli użytkownik nie ma uprawnień do zapisu, a trasa nie jest ustawiona, trasa nie zostanie zaktualizowana.

Jeśli Twoja aplikacja ma uprawnienia do zapisu trasy i próbuje zaktualizować sesję, przekazując obiekt sesji bez trasy, istniejąca trasa zostanie usunięta.

Dostępność funkcji

Aby sprawdzić, czy urządzenie użytkownika obsługuje planowane ćwiczenia w Health Connect, sprawdź dostępność FEATURE_PLANNED_EXERCISE na kliencie:

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

  // Feature is available
} else {
  // Feature isn't available
}
Więcej informacji znajdziesz w artykule Sprawdzanie dostępności funkcji.

Wymagane uprawnienia

Dostęp do trasy ćwiczeń jest chroniony przez te uprawnienia:

  • android.permission.health.READ_EXERCISE_ROUTES
  • android.permission.health.WRITE_EXERCISE_ROUTE
Uwaga: w przypadku tego typu uprawnień READ_EXERCISE_ROUTES jest w liczbie mnogiej, a WRITE_EXERCISE_ROUTE w liczbie pojedynczej.

Aby dodać do aplikacji funkcję trasy ćwiczeń, zacznij od poproszenia o uprawnienia do typu danych ExerciseSession.

Oto uprawnienia, które musisz zadeklarować, aby móc zapisywać trasę ćwiczeń:

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

Aby odczytać trasę ćwiczeń, musisz poprosić o te uprawnienia:

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

Musisz też zadeklarować uprawnienia do ćwiczeń, ponieważ każda trasa jest powiązana z sesją ćwiczeń (1 sesja = 1 trening).

Aby poprosić o uprawnienia, użyj metody PermissionController.createRequestPermissionResultContract() podczas pierwszego łączenia aplikacji z Health Connect. Oto kilka uprawnień, o które możesz poprosić:

  • Odczyt danych o zdrowiu i aktywności fizycznej, w tym danych o trasie: HealthPermission.getReadPermission(ExerciseSessionRecord::class)
  • Zapisywanie danych o zdrowiu i aktywności fizycznej, w tym danych o trasie: HealthPermission.getWritePermission(ExerciseSessionRecord::class)
  • Zapisywanie danych o trasie ćwiczeń: HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE

Prośba o uprawnienia od użytkownika

Po utworzeniu instancji klienta aplikacja musi poprosić użytkownika o uprawnienia. Użytkownicy muszą mieć możliwość przyznawania i odmawiania uprawnień w dowolnym momencie.

Aby to zrobić, utwórz zestaw uprawnień dla wymaganych typów danych. Upewnij się, że uprawnienia w zestawie są najpierw zadeklarowane w manifeście Androida.

// Create a set of permissions for required data types
val PERMISSIONS =
    setOf(
  HealthPermission.getReadPermission(ExerciseSessionRecord::class),
  HealthPermission.getWritePermission(ExerciseSessionRecord::class)
)

Użyj getGrantedPermissions, aby sprawdzić, czy Twoja aplikacja ma już wymagane uprawnienia. Jeśli nie, użyj createRequestPermissionResultContract, aby poprosić o te uprawnienia. Spowoduje to wyświetlenie ekranu uprawnień Health Connect.

// Create the permissions launcher
val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()

val requestPermissions = registerForActivityResult(requestPermissionActivityContract) { granted ->
  if (granted.containsAll(PERMISSIONS)) {
    // Permissions successfully granted
  } else {
    // Lack of required permissions
  }
}

suspend fun checkPermissionsAndRun(healthConnectClient: HealthConnectClient) {
  val granted = healthConnectClient.permissionController.getGrantedPermissions()
  if (granted.containsAll(PERMISSIONS)) {
    // Permissions already granted; proceed with inserting or reading data
  } else {
    requestPermissions.launch(PERMISSIONS)
  }
}

Ponieważ użytkownicy mogą przyznawać i cofać uprawnienia w dowolnym momencie, Twoja aplikacja musi sprawdzać uprawnienia za każdym razem przed ich użyciem i obsługiwać sytuacje, w których uprawnienia zostaną utracone.

Informacje zawarte w rekordzie sesji ćwiczeń

Każdy rekord sesji ćwiczeń zawiera te informacje:

  • Typ ćwiczenia, np. jazda na rowerze.
  • Trasa ćwiczeń, która zawiera informacje takie jak szerokość i długość geograficzna oraz wysokość.

Obsługiwane agregacje

W przypadku ExerciseSessionRecord dostępne są te wartości zagregowane:

Przykład użycia

Te fragmenty kodu pokazują, jak odczytywać i zapisywać trasę ćwiczeń.

Odczyt trasy ćwiczeń

Gdy aplikacja działa w tle, nie może odczytywać danych o trasie ćwiczeń utworzonych przez inne aplikacje.

Gdy aplikacja działa w tle i próbuje odczytać trasę ćwiczeń utworzoną przez inną aplikację, Health Connect zwraca odpowiedź ExerciseRouteResult.ConsentRequired, nawet jeśli Twoja aplikacja ma dostęp Zawsze zezwalaj do danych o trasie ćwiczeń.

Z tego powodu zdecydowanie zalecamy, aby prosić o trasy podczas celowej interakcji użytkownika z aplikacją, gdy użytkownik aktywnie korzysta z interfejsu aplikacji.

Więcej informacji o odczycie w tle znajdziesz w artykule Przykład odczytu w tle.

Ten fragment kodu pokazuje, jak odczytać sesję w Health Connect i poprosić o trasę z tej sesji:

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

Zapisywanie trasy ćwiczeń

Ten kod pokazuje, jak zarejestrować sesję, która obejmuje trasę ćwiczeń:

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(
            device = Device(type = Device.TYPE_PHONE)
        )
    )

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

Sesje ćwiczeń

Sesje ćwiczeń mogą obejmować wszystko, od biegania po badmintona.

Zapisywanie sesji ćwiczeń

Oto jak utworzyć żądanie wstawienia, które zawiera sesję:

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

Odczyt sesji ćwiczeń

Oto przykład odczytu sesji ćwiczeń:

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

Zapisywanie danych podtypu

Sesje mogą też zawierać opcjonalne dane podtypu, które wzbogacają sesję o dodatkowe informacje.

Na przykład sesje ćwiczeń mogą zawierać klasy ExerciseSegment, ExerciseLap i 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()
)

Usuwanie sesji ćwiczeń

Sesję ćwiczeń można usunąć na 2 sposoby:

  1. Według zakresu czasu.
  2. Według identyfikatora UID.

Oto jak usunąć dane podtypu według zakresu czasu:

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

Możesz też usunąć dane podtypu według identyfikatora UID. Spowoduje to usunięcie tylko sesji ćwiczeń, a nie powiązanych danych:

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