Trainingsrouten hinzufügen

Diese Anleitung ist mit Health Connect-Version 1.1.0-alpha12 oder höher kompatibel.

Mit Trainingsrouten können Nutzer eine GPS-Route für zugehörige Trainingsaktivitäten aufzeichnen und Karten ihrer Trainings mit anderen Apps teilen.

Verfügbarkeit von Health Connect prüfen

Bevor du versuchst, Health Connect zu verwenden, sollte deine App prüfen, ob Health Connect auf dem Gerät des Nutzers verfügbar ist. Health Connect ist möglicherweise nicht auf allen Geräten vorinstalliert oder deaktiviert. Sie können die Verfügbarkeit mit der Methode HealthConnectClient.getSdkStatus() prüfen.

Verfügbarkeit von Health Connect prüfen

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
}

Je nach dem von getSdkStatus() zurückgegebenen Status kannst du den Nutzer bei Bedarf anleiten, Health Connect aus dem Google Play Store zu installieren oder zu aktualisieren.

In diesem Leitfaden wird beschrieben, wie du Berechtigungen vom Nutzer anforderst und wie Apps die Berechtigung erhalten, Routendaten im Rahmen einer Trainingseinheit zu schreiben.

Die Lese- und Schreibfunktionen für Trainingsrouten umfassen:

  1. Apps erstellen eine neue Schreibberechtigung für Trainingsrouten.
  2. Das Einfügen erfolgt durch Schreiben einer Trainingseinheit mit einer Route als Feld.
  3. Lesen:
    1. Für den Sitzungsinhaber wird über einen Sitzungslesevorgang auf Daten zugegriffen.
    2. Über eine Drittanbieter-App, über ein Dialogfeld, in dem der Nutzer einmalig das Lesen einer Route erlauben kann.

Wenn der Nutzer keine Schreibberechtigungen hat und die Route nicht festgelegt ist, wird die Route nicht aktualisiert.

Wenn Ihre App eine Schreibberechtigung für Routen hat und versucht, eine Trainingseinheit zu aktualisieren, indem sie ein Trainingseinheitsobjekt ohne Route übergibt, wird die vorhandene Route gelöscht.

Verfügbarkeit der Funktion

Um herauszufinden, ob das Gerät eines Nutzers geplante Trainings in Health Connect unterstützt, prüfen Sie, ob FEATURE_PLANNED_EXERCISE auf dem Client verfügbar ist:

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

  // Feature is available
} else {
  // Feature isn't available
}
Weitere Informationen finden Sie unter Verfügbarkeit von Funktionen prüfen.

Erforderliche Berechtigungen

Der Zugriff auf Trainingsrouten wird durch die folgenden Berechtigungen geschützt:

  • android.permission.health.READ_EXERCISE_ROUTES
  • android.permission.health.WRITE_EXERCISE_ROUTE
Hinweis: Bei diesem Berechtigungstyp ist READ_EXERCISE_ROUTES im Plural, während WRITE_EXERCISE_ROUTE im Singular ist.

Wenn du deiner App die Funktion „Trainingsroute“ hinzufügen möchtest, musst du zuerst Berechtigungen für den Datentyp ExerciseSession anfordern.

Hier ist die Berechtigung, die Sie deklarieren müssen, um „Trainingsroute” schreiben zu können:

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

Wenn du die Trainingsstrecke lesen möchtest, musst du die folgenden Berechtigungen anfordern:

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

Du musst auch eine Berechtigung für Training deklarieren, da jede Route mit einer Trainingseinheit verknüpft ist (eine Einheit = ein Training).

Wenn du Berechtigungen anfordern möchtest, verwende die Methode PermissionController.createRequestPermissionResultContract(), wenn du deine App zum ersten Mal mit Health Connect verbindest. Hier sind einige Berechtigungen, die Sie möglicherweise anfordern möchten:

  • Gesundheits- und Fitnessdaten, einschließlich Routendaten, lesen: HealthPermission.getReadPermission(ExerciseSessionRecord::class)
  • Gesundheits- und Fitnessdaten schreiben, einschließlich Routendaten: HealthPermission.getWritePermission(ExerciseSessionRecord::class)
  • Daten zu Trainingsrouten schreiben: HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE

Berechtigungen vom Nutzer anfordern

Nachdem Sie eine Clientinstanz erstellt haben, muss Ihre App Berechtigungen vom Nutzer anfordern. Nutzer müssen Berechtigungen jederzeit erteilen oder verweigern können. Erstellen Sie dazu eine Reihe von Berechtigungen für die erforderlichen Datentypen. Achte darauf, dass die Berechtigungen in der Gruppe zuerst in deinem Android-Manifest deklariert werden.

val permissions =
    setOf(
        HealthPermission.getReadPermission(ExerciseSessionRecord::class),
        HealthPermission.getWritePermission(ExerciseSessionRecord::class)
    )
Mit getGrantedPermissions kannst du prüfen, ob deiner App bereits die erforderlichen Berechtigungen gewährt wurden. Falls nicht, verwenden Sie createRequestPermissionResultContract, um diese Berechtigungen anzufordern. Der Bildschirm mit den Health Connect-Berechtigungen wird angezeigt.
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.") }
    }
}
Da Nutzer Berechtigungen jederzeit erteilen oder widerrufen können, muss deine App vor jeder Verwendung prüfen, ob die Berechtigungen vorhanden sind, und Szenarien verarbeiten, in denen die Berechtigung verloren geht.

In einem Trainingseinheitseintrag enthaltene Informationen

Jeder Datensatz einer Trainingseinheit enthält die folgenden Informationen:

  • Die Art des Trainings, z. B. Radfahren.
  • Die Trainingsroute, die Informationen wie Breiten- und Längengrad sowie Höhe enthält.

Unterstützte Aggregationen

Die folgenden aggregierten Werte sind für ExerciseSessionRecord verfügbar:

Nutzungsbeispiel

Die folgenden Code-Snippets zeigen, wie eine Trainingsroute gelesen und geschrieben wird.

Trainingsroute abrufen

Deine App kann keine von anderen Apps erstellten Trainingsstreckendaten lesen, wenn sie im Hintergrund ausgeführt wird.

Wenn deine App im Hintergrund ausgeführt wird und versucht, eine von einer anderen App erstellte Trainingsroute zu lesen, gibt Health Connect eine ExerciseRouteResult.ConsentRequired-Antwort zurück, auch wenn deine App die Berechtigung Immer zulassen für den Zugriff auf Trainingsroutendaten hat.

Daher empfehlen wir dringend, Routen nur dann anzufordern, wenn der Nutzer bewusst mit Ihrer App interagiert, also aktiv die Benutzeroberfläche Ihrer App verwendet.

Weitere Informationen zu Hintergrundlesevorgängen finden Sie unter Beispiel für Hintergrundlesevorgang.

Das folgende Code-Snippet zeigt, wie eine Trainingseinheit in Health Connect gelesen und eine Route aus dieser Trainingseinheit angefordert wird:

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

Trainingsroute schreiben

Der folgende Code zeigt, wie du eine Trainingseinheit mit einer Trainingsroute aufzeichnest:

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

Trainingseinheiten

Trainingseinheiten können alles von Laufen bis Badminton umfassen.

Trainingseinheiten schreiben

So erstellen Sie eine Einfügungsanfrage, die eine Sitzung enthält:

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

Trainingseinheit lesen

Hier ein Beispiel für die Interpretation einer Trainingseinheit:

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

Untertypdaten schreiben

Sitzungen können auch optionale Untertypdaten enthalten, die die Sitzung mit zusätzlichen Informationen anreichern.

Beispielsweise können Trainingseinheiten die Klassen ExerciseSegment, ExerciseLap und ExerciseRoute enthalten:

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

Trainingseinheit löschen

Es gibt zwei Möglichkeiten, eine Trainingseinheit zu löschen:

  1. Nach Zeitraum.
  2. Nach UID.

So löschen Sie Untertypdaten nach Zeitraum:

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

Sie können Untertypdaten auch nach UID löschen. Dadurch wird nur die Trainingseinheit gelöscht, nicht die zugehörigen Daten:

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