افزودن مسیرهای تمرین

این راهنما با نسخه 1.1.0-alpha12 از Health Connect سازگار است.

مسیرهای تمرین به کاربران امکان می‌دهد مسیر 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() برمی‌گرداند، می‌توانید کاربر را راهنمایی کنید تا درصورت لزوم «اتصال به خدمات بهداشتی» را از «فروشگاه Google Play» نصب یا به‌روزرسانی کند.

این راهنما اطلاعاتی درباره نحوه درخواست اجازه‌ها از کاربر ارائه می‌دهد و همچنین نحوه دریافت اجازه برنامه‌ها برای نوشتن داده‌های مسیر به‌عنوان بخشی از جلسه تمرین را شرح می‌دهد.

عملکرد خواندن و نوشتن برای مسیرهای تمرین شامل موارد زیر می‌شود:

  1. برنامه‌ها اجازه نوشتن جدیدی برای مسیرهای تمرین ایجاد می‌کنند.
  2. افزودن با نوشتن جلسه تمرین با مسیر به‌عنوان فیلد آن انجام می‌شود.
  3. درحال خواندن:
    1. برای مالک جلسه، داده‌ها بااستفاده از خواندن جلسه قابل‌دسترسی است.
    2. از برنامه طرف سوم، ازطریق گفتگویی که به کاربر اجازه می‌دهد یک‌بار مسیر را بخواند.

اگر کاربر اجازه نوشتن نداشته باشد و مسیر تنظیم نشده باشد، مسیر به‌روز نمی‌شود.

اگر برنامه شما اجازه نوشتن مسیر داشته باشد و سعی کند جلسه‌ای را با گذراندن یک شیء جلسه بدون مسیر به‌روز کند، مسیر موجود حذف می‌شود.

دردسترس بودن ویژگی

برای تعیین اینکه دستگاه کاربر از تمرین برنامه‌ریزی‌شده در 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>

همچنین باید اجازه تمرین را اعلام کنید، زیرا هر مسیر با یک جلسه تمرین مرتبط است (یک جلسه = یک تمرین).

برای درخواست اجازه‌ها، هنگام اولین اتصال برنامه به Health Connect، از روش PermissionController.createRequestPermissionResultContract() استفاده کنید. چند اجازه که ممکن است بخواهید درخواست کنید عبارت‌اند از:

  • خواندن داده‌های سلامتی و تناسب اندام، ازجمله داده‌های مسیر: HealthPermission.getReadPermission(ExerciseSessionRecord::class)
  • نوشتن داده‌های سلامتی و تناسب اندام، ازجمله داده‌های مسیر: HealthPermission.getWritePermission(ExerciseSessionRecord::class)
  • نوشتن داده‌های مسیر تمرین: HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE

درخواست اجازه‌ها از کاربر

پس‌از ایجاد نمونه کارخواه، برنامه شما باید از کاربر اجازه درخواست کند. کاربران باید بتوانند در هر زمانی اجازه‌ها را اعطا یا رد کنند. برای انجام این کار، مجموعه‌ای از اجازه‌ها را برای انواع داده موردنیاز ایجاد کنید. ابتدا مطمئن شوید که اجازه‌های موجود در مجموعه در مانیفست Android شما اعلام شده باشد.

val permissions =
    setOf(
        HealthPermission.getReadPermission(ExerciseSessionRecord::class),
        HealthPermission.getWritePermission(ExerciseSessionRecord::class)
    )
از getGrantedPermissions استفاده کنید تا ببینید آیا برنامه شما ازقبل اجازه‌های لازم را دارد یا نه. درغیراین‌صورت، از createRequestPermissionResultContract برای درخواست این اجازه‌ها استفاده کنید. با این کار، صفحه اجازه‌های Health Connect نمایش داده می‌شود.
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.") }
    }
}
ازآنجایی‌که کاربران می‌توانند در هر زمانی اجازه‌ها را اعطا یا لغو کنند، برنامه شما باید هر بار قبل‌از استفاده از اجازه‌ها، آن‌ها را بررسی کند و سناریوهایی را که اجازه ازدست می‌رود مدیریت کند.

اطلاعات موجود در سابقه جلسه تمرین

هر سابقه جلسه تمرین حاوی اطلاعات زیر است:

  • نوع تمرین، برای مثال، دوچرخه‌سواری.
  • مسیر تمرین که حاوی اطلاعاتی ازجمله عرض جغرافیایی، طول جغرافیایی، و ارتفاع است.

تجمع‌های پشتیبانی‌شده

مقادیر تجمیعی زیر برای ExerciseSessionRecord دردسترس است:

نمونه استفاده

تکه‌کدهای زیر نحوه خواندن و نوشتن مسیر تمرین را نشان می‌دهد.

خواندن مسیر تمرین

وقتی برنامه شما در پس‌زمینه اجرا می‌شود نمی‌تواند داده‌های مسیر تمرین ایجادشده توسط برنامه‌های دیگر را بخواند.

وقتی برنامه شما در پس‌زمینه اجرا می‌شود و سعی می‌کند مسیر تمرینی را که برنامه دیگری ایجاد کرده است بخواند، Health Connect پاسخ ExerciseRouteResult.ConsentRequired برمی‌گرداند، حتی اگر برنامه شما دسترسی همیشه مجاز به داده‌های مسیر تمرین داشته باشد.

به همین دلیل، اکیداً توصیه می‌کنیم که مسیرها را پس‌از تعامل آگاهانه کاربر با برنامه‌تان درخواست کنید، یعنی زمانی که کاربر به‌طور فعال با رابط کاربری برنامه‌تان درگیر است.

برای کسب اطلاعات بیشتر درباره خواندن پس‌زمینه، مثال خواندن پس‌زمینه را ببینید.

تکه کد زیر نشان می‌دهد چگونه جلسه‌ای را در Health Connect بخوانید و مسیری را از آن جلسه درخواست کنید:

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

نوشتن مسیر تمرین

کد زیر نشان می‌دهد که چگونه می‌توان جلسه‌ای را که شامل مسیر تمرین است ضبط کرد:

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

جلسه‌های تمرین

جلسه‌های تمرین می‌تواند شامل هر چیزی از دویدن تا بدمینتون باشد.

نوشتن داده‌های جلسه تمرین

به این صورت می‌توانید درخواست درج را که شامل جلسه است بسازید:

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. براساس «شماره شناسایی یکتا».

در اینجا نحوه حذف داده‌های نوع فرعی براساس محدوده زمانی آمده است:

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

همچنین می‌توانید داده‌های نوع فرعی را براساس «شناسه کاربر» حذف کنید. با این کار فقط جلسه تمرین حذف می‌شود، نه داده‌های مرتبط:

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