إضافة مسارات التمارين الرياضية

يتوافق هذا الدليل مع الإصدار 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()، يمكنك توجيه المستخدم لتثبيت 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>

عليك أيضًا تضمين إذن تمرين رياضي، لأنّ كل مسار مرتبط بجلسة تمرين رياضي (جلسة واحدة = تمرين واحد).

لطلب الأذونات، استخدِم طريقة 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 وطلب مسار من تلك الجلسة:

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