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

این راهنما با 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"
    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 پشتیبانی می‌کند یا نه، دردسترس بودن 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. از 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)
}

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

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