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

Данное руководство совместимо с версией Health Connect 1.1.0-alpha12 .

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

Проверьте доступность 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, если это необходимо.

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

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

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

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

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

Проверьте доступность 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, если это необходимо.

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

Чтобы определить, поддерживает ли устройство пользователя запланированные тренировки в 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",
                metadata = Metadata.manualEntry()
            ),
            // ... 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,
  metadata = Metadata.manualEntry()
)

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

Есть два способа удалить тренировку:

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