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

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

בדיקת הזמינות של Health Connect

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

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

יכול להיות שתמצאו את קטע הקוד הבא ב-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(
        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(
        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()
)