תוכניות אימונים

המדריך הזה תואם לגרסה 1.1.0‎-alpha12 של Health Connect ואילך.

אפליקציית Health Connect מספקת סוג נתונים של אימון מתוכנן שמאפשר לאפליקציות אימון לכתוב תוכניות אימונים ולאפליקציות כושר לקרוא תוכניות אימונים. אפשר לקרוא את האימונים המתועדים כדי לנתח את הביצועים באופן מותאם אישית ולעזור למשתמשים להשיג את יעדי האימון שלהם.

איך בודקים אם אפליקציית Health Connect זמינה

לפני שמנסים להשתמש ב-Health Connect, האפליקציה צריכה לוודא שהשירות זמין במכשיר של המשתמש. יכול להיות שאפליקציית Health Connect לא מותקנת מראש בחלק מהמכשירים או שהיא מושבתת. אפשר לבדוק את הזמינות באמצעות ה-method‏ 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, אם יש צורך.

זמינות התכונה

כדי לדעת אם המכשיר של המשתמש מאפשר לתכנן אימונים ב-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_PLANNED_EXERCISE
  • android.permission.health.WRITE_PLANNED_EXERCISE

כדי להוסיף לאפליקציה אפשרות של אימונים מתוכננים, צריך קודם לבקש הרשאות לסוג הנתונים PlannedExerciseSession.

זו ההרשאה שצריך להצהיר עליה כדי שיהיה אפשר לכתוב אימון מתוכנן:

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

כדי לקרוא נתונים של אימונים מתוכננים, צריך לבקש את ההרשאות הבאות:

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

בקשת הרשאות מהמשתמש

אחרי שיוצרים מופע של לקוח, האפליקציה צריכה לבקש הרשאות מהמשתמש. המשתמשים חייבים להיות מסוגלים להעניק או לדחות הרשאות בכל שלב. כדי לעשות זאת, יוצרים קבוצת הרשאות לסוגי הנתונים הנדרשים. קודם צריך לוודא שההרשאות בקבוצה מוצהרות במניפסט של Android.

val permissions =
    setOf(
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class),
        HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class),
        HealthPermission.getWritePermission(PlannedExerciseSessionRecord::class),
        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.") }
    }
}
המשתמשים יכולים לתת או לבטל הרשאות בכל שלב, ולכן האפליקציה צריכה לבדוק אם ההרשאות ניתנו בכל פעם לפני שהיא משתמשת בהן, ולטפל במקרים שבהם הרשאה כלשהי בוטלה.

תוכניות האימונים מקושרות לסשנים של אימונים. לכן, כדי להשתמש בתכונה הזו של Health Connect, המשתמש חייב לתת הרשאה לשימוש בכל סוג רשומה שקשור לתוכנית אימונים.

לדוגמה, אם תוכנית אימונים מודדת את הדופק של המשתמש במהלך סדרה של ריצות, יכול להיות שהמפתח יצטרך להצהיר על ההרשאות הבאות והמשתמש יצטרך להעניק אותן כדי לכתוב את סשן האימון ולקרוא את התוצאות להערכה בשלב מאוחר יותר:

  • android.permission.health.READ_EXERCISE
  • android.permission.health.READ_EXERCISE_ROUTES
  • android.permission.health.READ_HEART_RATE
  • android.permission.health.WRITE_EXERCISE
  • android.permission.health.WRITE_EXERCISE_ROUTE
  • android.permission.health.WRITE_HEART_RATE

עם זאת, לעיתים קרובות האפליקציה שיוצרת תוכניות אימונים ומעריכה את הביצועים בהשוואה לתוכניות היא לא אותה אפליקציה שצורכת תוכניות אימונים וכותבת נתוני אימון בפועל. בהתאם לסוג האפליקציה, לא כל הרשאות הקריאה והכתיבה נדרשות. לדוגמה, יכול להיות שתצטרכו רק את ההרשאות האלה לכל סוג של אפליקציה:

אפליקציה של תוכנית אימונים אפליקציית אימונים
WRITE_PLANNED_EXERCISE READ_PLANNED_EXERCISE
READ_EXERCISE WRITE_EXERCISE
READ_EXERCISE_ROUTES WRITE_EXERCISE_ROUTE
READ_HEART_RATE WRITE_HEART_RATE

המידע שכלול ברשומה של אימון מתוכנן

  • שם הסשן.
  • רשימה של משבצות זמן לאימוני כושר מתוכננים.
  • שעת ההתחלה והסיום של הסשן.
  • סוג התרגיל.
  • הערות לגבי הפעילות.
  • מטא-נתונים.
  • מזהה של אימון שהושלם – נכתב באופן אוטומטי אחרי שאימון שקשור לאימון המתוכנן הזה הושלם.

המידע שכלול ברשומה של משבצת זמן לאימון מתוכנן

בלוק של אימון מתוכנן מכיל רשימה של שלבי אימון, כדי לתמוך בחזרה על קבוצות שונות של שלבים (לדוגמה, לבצע רצף של כפיפות זרוע, סמוך קום וכפיפות בטן חמש פעמים ברציפות).

המידע שכלול ברשומה של שלב באימון מתוכנן

צבירת נתונים נתמכת

אין צבירות נתונים נתמכות לסוג הנתונים הזה.

דוגמת שימוש

נניח שמשתמש מתכנן ריצה של 90 דקות בעוד יומיים. הריצה הזו תכלול שלוש הקפות סביב אגם עם דופק מטרה בין 90 ל-110 פעימות לדקה.

  1. אימון מתוכנן עם הפרטים הבאים מוגדר על ידי המשתמש באפליקציה של תוכנית אימונים:
    1. התחלה וסיום מתוכננים של ההרצה
    2. סוג הפעילות הגופנית (ריצה)
    3. מספר ההקפות (חזרות)
    4. יעד ביצועים לדופק (בין 90 ל-110 פעימות לדקה)
  2. המידע הזה מקובץ לתרגילים ולצעדים, ונכתב ב-Health Connect על ידי אפליקציית תוכנית האימונים בתור PlannedExerciseSessionRecord.
  3. המשתמש מבצע את האימון המתוכנן (ריצה).
  4. נתוני הפעילות הגופנית שקשורים לסשן מתועדים באחת מהדרכים הבאות:
    1. על ידי גאדג'ט לביש במהלך הפעילות. לדוגמה, דופק. הנתונים האלה נכתבים ב-Health Connect כסוג הרשומה של הפעילות. במקרה הזה, HeartRateRecord.
    2. באופן ידני על ידי המשתמש אחרי הסשן. לדוגמה, ציון ההתחלה והסיום של הריצה בפועל. הנתונים האלה נכתבים ב-Health Connect כExerciseSessionRecord.
  5. בשלב מאוחר יותר, האפליקציה של תוכנית האימונים קוראת נתונים מ-Health Connect כדי להעריך את הביצועים בפועל בהשוואה ליעדים שהמשתמש הגדיר באימון המתוכנן.

תכנון אימונים והגדרת יעדים

המשתמש יכול לתכנן את האימון שלו בעתיד ולהגדיר יעדים. כותבים את זה ב-Health Connect בתור אימון מתוכנן.

בדוגמה שמתוארת בקטע דוגמה לשימוש, המשתמש מתכנן ריצה של 90 דקות בעוד יומיים. הריצה הזו תכלול שלוש הקפות סביב אגם עם דופק מטרה של בין 90 ל-110 פעימות לדקה.

קטע קוד כזה יכול להופיע ב-form handler של אפליקציה שמתעדת אימונים מתוכננים ב-Health Connect. הוא יכול להיות גם בנקודת ההטמעה של שילובים, למשל עם שירות שמציע הדרכה.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()

if (!grantedPermissions.contains(
        HealthPermission.getWritePermission(PlannedExerciseSessionRecord::class))) {
    // The user hasn't granted the app permission to write planned exercise session data.
    Log.w("HealthConnect", "Write permission for PlannedExerciseSessionRecord not granted.")
    return
}

val plannedExerciseSessionRecord = PlannedExerciseSessionRecord(
    startTime = startTime,
    endTime = endTime,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    blocks = listOf(
        PlannedExerciseBlock(
            repetitions = 1, steps = listOf(
                PlannedExerciseStep(
                    exerciseType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
                    exercisePhase = PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
                    completionGoal = ExerciseCompletionGoal.RepetitionsGoal(repetitions = 3),
                    performanceTargets = listOf(
                        ExercisePerformanceTarget.HeartRateTarget(
                            minHeartRate = 90.0, maxHeartRate = 110.0
                        )
                    )
                ),
            ), description = "Three laps around the lake"
        )
    ),
    title = "Run at lake",
    notes = null,
    metadata = Metadata.activelyRecorded(
        device = Device(type = Device.Companion.TYPE_PHONE),
    ),
    startZoneOffset = null,
    endZoneOffset = null,
)

try {
    // Attempt to insert the record
    val response = healthConnectClient.insertRecords(listOf(plannedExerciseSessionRecord))

    // If execution reaches here, the insert succeeded.
    // Safely extract the ID using firstOrNull()
    val insertedPlannedExerciseSessionId = response.recordIdsList.firstOrNull()

    if (insertedPlannedExerciseSessionId != null) {
        Log.d("HealthConnect", "Successfully inserted planned exercise session ID: $insertedPlannedExerciseSessionId")
    } else {
        Log.w("HealthConnect", "Insertion succeeded but no record IDs were returned.")
    }

} catch (e: Exception) {
    // Handle API failures, database errors, or system issues safely without crashing
    Log.e("HealthConnect", "Failed to insert planned exercise session record", e)
}

תיעוד נתוני אימונים ופעילות

יומיים לאחר מכן, המשתמש מתעד את סשן האימון בפועל. כותבים את הנתונים האלה ב-Health Connect בתור סשן אימון.

בדוגמה הזו, משך הסשן של המשתמש תאם בדיוק למשך הזמן המתוכנן.

קטע הקוד הבא יכול להופיע ב-form handler של אפליקציה שמתעדת אימונים ב-Health Connect. הוא יכול להופיע גם ב-handlers של הטמעת נתונים ושל ייצוא נתונים עבור מכשיר לביש שמסוגל לזהות ולתעד אימונים.

הערך insertedPlannedExerciseSessionId כאן הוא שימוש חוזר מהדוגמה הקודמת. באפליקציה אמיתית, המזהה ייקבע על ידי המשתמש שיבחר אימון מתוכנן מתוך רשימה של אימונים קיימים.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()
if (!grantedPermissions.contains(
        HealthPermission.getWritePermission(ExerciseSessionRecord::class))) {
    // The user doesn't granted the app permission to write exercise session data.
    return
}

val sessionDuration = Duration.ofMinutes(90)
val sessionEndTime = Instant.now()
val sessionStartTime = sessionEndTime.minus(sessionDuration)

val exerciseSessionRecord = ExerciseSessionRecord(
    startTime = sessionStartTime,
    startZoneOffset = ZoneOffset.UTC,
    endTime = sessionEndTime,
    endZoneOffset = ZoneOffset.UTC,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    segments = listOf(
        ExerciseSegment(
            startTime = sessionStartTime,
            endTime = sessionEndTime,
            repetitions = 3,
            segmentType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING
        )
    ),
    title = "Run at lake",
    plannedExerciseSessionId = insertedPlannedExerciseSessionId,
    metadata = Metadata.activelyRecorded(
        device = Device(type = Device.Companion.TYPE_PHONE)
    )
)
val insertedExerciseSessions =
    healthConnectClient.insertRecords(listOf(exerciseSessionRecord))

מכשיר לביש מתעד גם את הדופק שלהם במהלך הריצה. אפשר להשתמש בקטע הקוד הבא כדי ליצור רשומות בטווח היעד.

באפליקציה אמיתית, החלקים העיקריים של קטע הקוד הזה עשויים להימצא ב-handler של הודעה ממכשיר לביש, שיכתוב את המדידה ל-Health Connect לאחר האיסוף.

// Verify the user has granted all necessary permissions for this task
val grantedPermissions =
    healthConnectClient.permissionController.getGrantedPermissions()
if (!grantedPermissions.contains(
        HealthPermission.getWritePermission(HeartRateRecord::class))) {
    // The user doesn't granted the app permission to write heart rate record data.
    return
}

val samples = mutableListOf<HeartRateRecord.Sample>()
var currentTime = sessionStartTime
while (currentTime.isBefore(sessionEndTime)) {
    val bpm = Random.nextInt(21) + 90
    val heartRateRecord = HeartRateRecord.Sample(
        time = currentTime,
        beatsPerMinute = bpm.toLong(),
    )
    samples.add(heartRateRecord)
    currentTime = currentTime.plusSeconds(180)
}

val heartRateRecord = HeartRateRecord(
    startTime = sessionStartTime,
    startZoneOffset = ZoneOffset.UTC,
    endTime = sessionEndTime,
    endZoneOffset = ZoneOffset.UTC,
    samples = samples,
    metadata = Metadata.activelyRecorded(
        device = Device(type = Device.Companion.TYPE_WATCH)
    )
)
val insertedHeartRateRecords = healthConnectClient.insertRecords(listOf(heartRateRecord))

הערכת יעדי הביצועים

ביום שאחרי האימון של המשתמש, אתם יכולים לאחזר את האימון שתועד, לבדוק אם יש יעדי אימון מתוכננים ולהעריך סוגי נתונים נוספים כדי לקבוע אם היעדים שהוגדרו הושגו.

קטע קוד כזה יכול להופיע במשימה תקופתית להערכת יעדי ביצועים, או כשמציגים רשימה של תרגילים והתראה על יעדי ביצועים באפליקציה.

    // Verify the user has granted all necessary permissions for this task
    val grantedPermissions =
        healthConnectClient.permissionController.getGrantedPermissions()
    if (!grantedPermissions.containsAll(
            listOf(
                HealthPermission.getReadPermission(ExerciseSessionRecord::class),
                HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class),
                HealthPermission.getReadPermission(HeartRateRecord::class)
            )
        )
    ) {
        // The user doesn't granted the app permission to read exercise session record data.
        return
    }

    val searchDuration = Duration.ofDays(1)
    val searchEndTime = Instant.now()
    val searchStartTime = searchEndTime.minus(searchDuration)

    val response = healthConnectClient.readRecords(
        ReadRecordsRequest<ExerciseSessionRecord>(
            timeRangeFilter = TimeRangeFilter.between(searchStartTime, searchEndTime)
        )
    )
    for (exerciseRecord in response.records) {
        val plannedExerciseRecordId = exerciseRecord.plannedExerciseSessionId
        val plannedExerciseRecord =
            if (plannedExerciseRecordId == null) null else healthConnectClient.readRecord(
                PlannedExerciseSessionRecord::class, plannedExerciseRecordId
            ).record
        if (plannedExerciseRecord != null) {
            val aggregateRequest = AggregateRequest(
                metrics = setOf(HeartRateRecord.BPM_AVG),
                timeRangeFilter = TimeRangeFilter.between(
                    exerciseRecord.startTime, exerciseRecord.endTime
                ),
            )
            val aggregationResult = healthConnectClient.aggregate(aggregateRequest)

            val maxBpm = aggregationResult[HeartRateRecord.BPM_MAX]
            val minBpm = aggregationResult[HeartRateRecord.BPM_MIN]
            if (maxBpm != null && minBpm != null) {
                plannedExerciseRecord.blocks.forEach { block ->
                    block.steps.forEach { step ->
                        step.performanceTargets.forEach { target ->
                            when (target) {
                                is ExercisePerformanceTarget.HeartRateTarget -> {
                                    val minTarget = target.minHeartRate
                                    val maxTarget = target.maxHeartRate
                                    if(
                                        minBpm >= minTarget && maxBpm <= maxTarget
                                    ) {
                                        // Success!
                                    }
                                }
                                // Handle more target types
                            }
                        }
                    }
                }
            }
        }
    }
}

סשנים של פעילות גופנית

אימונים יכולים לכלול כל דבר, החל מריצה ועד בדמינטון.

כתיבה של נתוני אימונים

כך יוצרים בקשת הוספה שכוללת סשן:

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