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

‫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.

// Create a set of permissions for required data types
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.

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

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

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

קטע קוד כזה יכול להימצא ב-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.
    return
}

val plannedDuration = Duration.ofMinutes(90)
val plannedStartDate = LocalDate.now().plusDays(2)

val plannedExerciseSessionRecord = PlannedExerciseSessionRecord(
    startDate = plannedStartDate,
    duration = plannedDuration,
    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.manualEntry(
      device = Device(type = Device.Companion.TYPE_PHONE)
    )
)
val insertedPlannedExerciseSessions =
    healthConnectClient.insertRecords(listOf(plannedExerciseSessionRecord)).recordIdsList
val insertedPlannedExerciseSessionId = insertedPlannedExerciseSessions.first()

רישום נתוני פעילות ואימונים

יומיים לאחר מכן, המשתמש מתעד את אימון הכושר בפועל. לכתוב את זה ב-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.manualEntry(
      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.autoRecorded(
      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()
)