Добавьте маршруты тренировок

Это руководство совместимо с Health Connect версии 1.1.0-alpha12 .

Маршруты тренировок позволяют пользователям отслеживать GPS-маршрут для соответствующих занятий и делиться картами своих тренировок с другими приложениями.

В этом руководстве содержится информация о том, как запрашивать разрешения у пользователя, а также описывается, как приложения получают разрешение на запись данных маршрута в рамках сеанса тренировки.

Функциональность чтения и записи для маршрутов тренировок включает:

  1. Приложения создают новое разрешение на запись для маршрутов тренировок.
  2. Вставка происходит путем записи сеанса упражнений с маршрутом в качестве поля.
  3. Чтение:
    1. Для владельца сеанса доступ к данным осуществляется с помощью чтения сеанса.
    2. Из стороннего приложения через диалоговое окно, позволяющее пользователю предоставить однократное разрешение на чтение маршрута.

Если у пользователя нет прав на запись и маршрут не установлен, маршрут не обновляется.

Если ваше приложение имеет разрешение на запись маршрута и пытается обновить сеанс, передавая объект сеанса без маршрута, существующий маршрут удаляется.

Проверьте доступность Health Connect

Прежде чем использовать Health Connect, ваше приложение должно проверить наличие Health Connect на устройстве пользователя. Health Connect может быть предустановлен не на всех устройствах или отключен. Проверить наличие Health Connect можно с помощью метода HealthConnectClient.getSdkStatus() .

Как проверить доступность 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
}

В зависимости от статуса, возвращаемого getSdkStatus() , вы можете предложить пользователю установить или обновить Health Connect из Google Play Store, если это необходимо.

Доступность функций

Чтобы определить, поддерживает ли устройство пользователя запланированные упражнения в Health Connect, проверьте наличие FEATURE_PLANNED_EXERCISE на клиенте:

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

  // Feature is available
} else {
  // Feature isn't available
}
Более подробную информацию см. в разделе Проверка доступности функции .

Требуемые разрешения

Доступ к маршруту тренировки защищен следующими разрешениями:

  • android.permission.health.READ_EXERCISE_ROUTES
  • android.permission.health.WRITE_EXERCISE_ROUTE
Примечание: для этого типа разрешения READ_EXERCISE_ROUTES — множественное число, а WRITE_EXERCISE_ROUTE — единственное число.

Чтобы добавить в приложение возможность создания маршрутов тренировок, начните с запроса разрешений на запись для типа данных ExerciseSession .

Вот разрешение, которое вам необходимо заявить, чтобы иметь возможность записать маршрут тренировки:

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

Чтобы прочитать маршрут тренировки, вам необходимо запросить следующие разрешения:

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

Вам также необходимо заявить о разрешении на тренировку, поскольку каждый маршрут связан с сеансом тренировки (один сеанс = одна тренировка).

Чтобы запросить разрешения, используйте метод PermissionController.createRequestPermissionResultContract() при первом подключении приложения к Health Connect. Вот несколько разрешений, которые вам может понадобиться запросить:

  • Чтение данных о здоровье и физической форме, включая данные о маршруте: HealthPermission.getReadPermission(ExerciseSessionRecord::class)
  • Запись данных о здоровье и физической форме, включая данные о маршруте: HealthPermission.getWritePermission(ExerciseSessionRecord::class)
  • Запись данных маршрута тренировки: HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE

Запросить разрешения у пользователя

После создания клиентского экземпляра ваше приложение должно запрашивать разрешения у пользователя. Пользователи должны иметь возможность предоставлять или отклонять разрешения в любое время.

Для этого создайте набор разрешений для необходимых типов данных. Убедитесь, что разрешения в наборе предварительно объявлены в манифесте Android.

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

Используйте getGrantedPermissions , чтобы проверить, предоставлены ли приложению необходимые разрешения. Если нет, запросите их с помощью createRequestPermissionResultContract . Откроется экран разрешений 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)
  }
}

Поскольку пользователи могут предоставлять или отзывать разрешения в любое время, ваше приложение должно периодически проверять наличие предоставленных разрешений и обрабатывать ситуации, когда разрешение теряется.

Информация, включенная в запись сеанса тренировки

Каждая запись сеанса тренировки содержит следующую информацию:

  • Вид физической активности, например, езда на велосипеде.
  • Маршрут упражнения, содержащий такую ​​информацию, как широта, долгота и высота.

Поддерживаемые агрегации

Для ExerciseSessionRecord доступны следующие агрегированные значения:

Пример использования

В следующих фрагментах кода показано, как читать и записывать маршрут упражнений.

Прочитать маршрут упражнения

Ваше приложение не может считывать данные о маршрутах тренировок, созданные другими приложениями, когда оно работает в фоновом режиме.

Когда ваше приложение работает в фоновом режиме и пытается считать маршрут упражнений, созданный другим приложением, Health Connect возвращает ответ ExerciseRouteResult.ConsentRequired , даже если в вашем приложении всегда разрешен доступ к данным маршрута упражнений.

По этой причине мы настоятельно рекомендуем вам запрашивать маршруты при преднамеренном взаимодействии пользователя с вашим приложением, когда пользователь активно взаимодействует с пользовательским интерфейсом вашего приложения.

Дополнительную информацию о фоновом чтении см. в разделе Пример фонового чтения .

В следующем фрагменте кода показано, как прочитать сеанс в Health Connect и запросить маршрут из этого сеанса:

suspend fun readExerciseSessionAndRoute() {
    val endTime = Instant.now()
    val startTime = endTime.minus(Duration.ofHours(1))

    val grantedPermissions =
        healthConnectClient.permissionController.getGrantedPermissions()
    if (!grantedPermissions.contains(
          HealthPermission.getReadPermission(ExerciseSessionRecord::class))) {
        // The user doesn't allow the app to read exercise session data.
        return
    }

    val readResponse =
      healthConnectClient.readRecords(
        ReadRecordsRequest(
          ExerciseSessionRecord::class,
          TimeRangeFilter.between(startTime, endTime)
        )
      )
    val exerciseRecord = readResponse.records.first()
    val recordId = exerciseRecord.metadata.id

    // See https://developer.android.com/training/basics/intents/result#launch
    // for appropriately handling ActivityResultContract.
    val requestExerciseRouteLauncher = fragment.registerForActivityResul
    (ExerciseRouteRequestContract()) { exerciseRoute: ExerciseRoute? ->
            if (exerciseRoute != null) {
                displayExerciseRoute(exerciseRoute)
            } else {
                // Consent was denied
            }
        }

    val exerciseSessionRecord =
      healthConnectClient.readRecord(ExerciseSessionRecord::class, recordId).record

    when (val exerciseRouteResult = exerciseSessionRecord.exerciseRouteResult) {
        is ExerciseRouteResult.Data ->
            displayExerciseRoute(exerciseRouteResult.exerciseRoute)
        is ExerciseRouteResult.ConsentRequired ->
            requestExerciseRouteLauncher.launch(recordId)
        is ExerciseRouteResult.NoData -> Unit // No exercise route to show
        else -> Unit
    }
  }

  fun displayExerciseRoute(route: ExerciseRoute?) {
    val locations = route.route.orEmpty()
    for (location in locations) {
      // Handle location.
    }
  }

Напишите маршрут упражнений

Следующий код демонстрирует, как записать сеанс, включающий маршрут упражнений:

suspend fun InsertExerciseRoute(healthConnectClient: HealthConnectClient) {
    val grantedPermissions =
        healthConnectClient.permissionController.getGrantedPermissions()
    if (!grantedPermissions.contains(
          getWritePermission(ExerciseSessionRecord::class))) {
        // The user doesn't allow the app to write exercise session data.
        return
    }

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

    val exerciseRoute =
        if (grantedPermissions.contains(PERMISSION_WRITE_EXERCISE_ROUTE)) ExerciseRoute(
            listOf(
                ExerciseRoute.Location(
                    // Location times must be on or after the session start time
                    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(
                    // Location times must be before the session end time
                    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
        // The user doesn't allow the app to write exercise route data.
            null
    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.manualEntry(
            device = Device(type = Device.TYPE_PHONE)
        ),
    )
    val response = healthConnectClient.insertRecords(listOf(exerciseSessionRecord))
}

Сеансы упражнений

Тренировки могут включать в себя все, что угодно: от бега до бадминтона.

Запишите сеансы упражнений

Вот как создать запрос на вставку, включающий сеанс:

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"
            ),
            // ... other records
        )
    )
}

Прочитайте сеанс упражнений

Вот пример того, как читать сеанс упражнений:

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

Запись подтипа данных

Сеансы также могут состоять из необязательных подтипов данных, которые обогащают сеанс дополнительной информацией.

Например, сеансы упражнений могут включать классы ExerciseSegment , ExerciseLap и 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
)

Удалить сеанс тренировки

Удалить сеанс тренировки можно двумя способами:

  1. По временному диапазону.
  2. По UID.

Вот как можно удалить данные подтипа в соответствии с временным диапазоном:

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

Вы также можете удалить данные подтипа по уникальному идентификатору (UID). При этом удаляется только сеанс тренировки, а не связанные с ним данные:

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