pebble/src/fw/applib/health_service.c
2025-01-27 11:38:16 -08:00

1490 lines
57 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "health_service.h"
#include "health_service_private.h"
#include "applib/app.h"
#include "applib/applib_malloc.auto.h"
#include "applib/pbl_std/pbl_std.h"
#include "event_service_client.h"
#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "process_state/app_state/app_state.h"
#include "process_state/worker_state/worker_state.h"
#include "services/common/event_service.h"
#include "services/common/hrm/hrm_manager.h"
#include "services/normal/activity/activity.h"
#include "shell/prefs_syscalls.h"
#include "syscall/syscall.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
#include "util/size.h"
#include "util/stats.h"
// Fetching minute history can take a while, so we limit the amount of data we will ever access
// in one call to this
#define HS_MAX_MINUTE_DATA_SEC (2 * SECONDS_PER_HOUR)
// The limit to how old an HealthMetricHeartRateBPM sample can be and still return it within
// the peek function.
#define HS_MAX_AGE_HR_SAMPLE (15 * SECONDS_PER_MINUTE)
// ----------------------------------------------------------------------------------------------
static bool prv_is_heart_rate_metric(HealthMetric metric) {
return (metric == HealthMetricHeartRateBPM) || (metric == HealthMetricHeartRateRawBPM);
}
// ----------------------------------------------------------------------------------------------
// Checks whether the interval between start and end are specifying a time within the past minute.
static bool prv_interval_within_last_minute(time_t now_utc, time_t start, time_t end) {
const time_t last_minute = (now_utc - SECONDS_PER_MINUTE);
const bool within_last_minute = ((start <= end) &&
(start >= last_minute) &&
(end <= now_utc));
return within_last_minute;
}
static HealthAggregation prv_default_aggregation(HealthMetric metric) {
switch (metric) {
case HealthMetricStepCount:
case HealthMetricActiveSeconds:
case HealthMetricWalkedDistanceMeters:
case HealthMetricSleepSeconds:
case HealthMetricSleepRestfulSeconds:
case HealthMetricRestingKCalories:
case HealthMetricActiveKCalories:
return HealthAggregationSum;
case HealthMetricHeartRateBPM:
case HealthMetricHeartRateRawBPM:
return HealthAggregationAvg;
}
WTF;
return 0;
}
// ----------------------------------------------------------------------------------------------
static HealthServiceState* prv_get_state(bool ensure_cache_initialized) {
PebbleTask task = pebble_task_get_current();
HealthServiceState *result = NULL;
if (task == PebbleTask_App) {
result = app_state_get_health_service_state();
} else if (task == PebbleTask_Worker) {
result = worker_state_get_health_service_state();
} else {
WTF;
}
// clients can free the cache by calling health_service_events_unsubscribe()
if (result && ensure_cache_initialized && result->cache == NULL) {
result->cache = applib_type_zalloc(HealthServiceCache);
}
return result;
}
// ----------------------------------------------------------------------------------------------
static void prv_health_service_deinit_cache(HealthServiceState *state) {
if (state) {
applib_free(state->cache);
state->cache = NULL;
}
}
// ----------------------------------------------------------------------------------------------
// returns a time_t of a given time that represents midnight of the given local time.
static time_t prv_get_midnight_of_local_time(time_t now) {
struct tm *local_tm = pbl_override_gmtime(&now);
local_tm->tm_hour = 0;
local_tm->tm_min = 0;
local_tm->tm_sec = 0;
return pbl_override_mktime(local_tm);
}
// ----------------------------------------------------------------------------------------
// Return true if the passed in day is a weekend
static bool prv_is_weekend(DayInWeek day) {
return (day == Sunday) || (day == Saturday);
}
// ----------------------------------------------------------------------------------------------
// Return the activity metric that maps to the given health metric. We separate the two because
// in the future, the health APIs may need to go other services besides just the Activity service
// to get information.
static ActivityMetric prv_get_activity_metric(HealthMetric metric) {
switch (metric) {
case HealthMetricStepCount:
return ActivityMetricStepCount;
case HealthMetricActiveSeconds:
return ActivityMetricActiveSeconds;
case HealthMetricWalkedDistanceMeters:
return ActivityMetricDistanceMeters;
case HealthMetricSleepSeconds:
return ActivityMetricSleepTotalSeconds;
case HealthMetricSleepRestfulSeconds:
return ActivityMetricSleepRestfulSeconds;
case HealthMetricRestingKCalories:
return ActivityMetricRestingKCalories;
case HealthMetricActiveKCalories:
return ActivityMetricActiveKCalories;
case HealthMetricHeartRateBPM:
return ActivityMetricHeartRateFilteredBPM;
case HealthMetricHeartRateRawBPM:
return ActivityMetricHeartRateRawBPM;
}
WTF;
return 0;
}
// ----------------------------------------------------------------------------------------------
// Return true if this metric is implemented for the given aggregation type
static bool prv_metric_aggregation_implemented(HealthMetric metric, time_t time_start,
time_t time_end, HealthAggregation agg,
HealthServiceTimeScope scope) {
const time_t now_utc = sys_get_time();
switch (metric) {
case HealthMetricStepCount:
case HealthMetricActiveSeconds:
case HealthMetricWalkedDistanceMeters:
case HealthMetricSleepSeconds:
case HealthMetricSleepRestfulSeconds:
case HealthMetricRestingKCalories:
case HealthMetricActiveKCalories:
// We can only use HealthAggregationSum with accumulating metrics and scope doesn't matter
return (agg == HealthAggregationSum);
case HealthMetricHeartRateRawBPM: {
// Only support querying the current raw heart rate.
const bool query_cur_minute = prv_interval_within_last_minute(now_utc, time_start, time_end);
return ((agg == HealthAggregationAvg) && query_cur_minute);
}
case HealthMetricHeartRateBPM:
// For heart rate, we can only support avg, min, max with constraints on time
switch (agg) {
case HealthAggregationSum:
return false;
case HealthAggregationAvg:
{
// We used to unconditionally return true here which was a bug
// Fixing this bug broke some apps / watchfaces
Version legacy_version = {.major = 0x5, .minor = 0x54};
Version app_version = sys_get_current_app_sdk_version();
if (version_compare(app_version, legacy_version) < 0) {
return true;
}
// Fallthrough
}
case HealthAggregationMax:
case HealthAggregationMin: {
// Only supported using minute data (short time range, no scope) because
// we only store a few hours of HR minute data.
return (scope == HealthServiceTimeScopeOnce)
&& ((now_utc - time_start) <= HS_MAX_MINUTE_DATA_SEC);
}
}
break;
}
WTF;
}
// ----------------------------------------------------------------------------------------------
// Return the daily historical values for the given metric, retrieving from the cache if
// possible.
static bool prv_get_metric_daily_history(HealthServiceState *state, HealthMetric metric,
HealthServiceDailyHistory *daily) {
// Return cached data if we have it available.
if (state->cache && (metric == HealthMetricStepCount) && state->cache->step_daily_valid) {
memcpy(daily, &state->cache->steps_daily, sizeof(*daily));
// Get updated value for today. Getting only today's value is MUCH faster than getting
// the historical values
sys_activity_get_metric(prv_get_activity_metric(metric), 1, &daily->totals[0]);
return true;
}
// Read in the metric history
if (!sys_activity_get_metric(prv_get_activity_metric(metric),
ARRAY_LENGTH(daily->totals), daily->totals)) {
PBL_LOG(LOG_LEVEL_ERROR, "Error fetching metric data");
return false;
}
// Store in cache if we have space for it.
if (state->cache && (metric == HealthMetricStepCount)) {
memcpy(&state->cache->steps_daily, daily, sizeof(*daily));
state->cache->step_daily_valid = true;
}
return true;
}
// ----------------------------------------------------------------------------------------------
// Compute all stats (weekly, daily, weekend, weekday, etc.) for the given metric.
// @param[in] state our API state
// @param[in] metric which metric to compute stats for
// @param[out] stats the stats for this metric are returned here
// @param[in] weekly_day which day of the week to use when computing the weekly stats
static bool prv_get_metric_stats(HealthServiceState *state, HealthMetric metric,
HealthServiceMetricStats *stats, DayInWeek weekly_day) {
// Get the daily history for this metric
HealthServiceDailyHistory daily_totals;
if (!prv_get_metric_daily_history(state, metric, &daily_totals)) {
return false;
}
// What day of the week is it now?
const time_t now_utc = sys_get_time();
struct tm *local_tm = pbl_override_localtime(&now_utc);
// Compute weekly, weekday, and daily stats
*stats = (HealthServiceMetricStats) {};
const StatsBasicOp op = (StatsBasicOp_Sum | StatsBasicOp_Average | StatsBasicOp_Count
| StatsBasicOp_Min | StatsBasicOp_Max);
stats_calculate_basic(op, daily_totals.totals, ARRAY_LENGTH(daily_totals.totals),
health_service_private_weekday_filter, (void *)(uintptr_t)local_tm->tm_wday,
&stats->weekday.sum);
stats_calculate_basic(op, daily_totals.totals, ARRAY_LENGTH(daily_totals.totals),
health_service_private_weekend_filter, (void *)(uintptr_t)local_tm->tm_wday,
&stats->weekend.sum);
// We want to sum only the days that are this far from index 0 (which is local_tm.tm_wday)
int day_offset = local_tm->tm_wday - weekly_day;
if (day_offset < 0) {
day_offset += DAYS_PER_WEEK;
}
stats_calculate_basic(op, daily_totals.totals, ARRAY_LENGTH(daily_totals.totals),
health_service_private_weekly_filter, (void *)(uintptr_t)day_offset,
&stats->weekly.sum);
// If the average is 0 (this can happen if we don't have any history), set the averages based
// on today's total so far
time_t seconds_today = now_utc - sys_time_start_of_today();
HealthValue per_day_default = (daily_totals.totals[0] * SECONDS_PER_DAY)
/ MAX(1, seconds_today);
if (stats->weekday.sum == 0) {
stats->weekday = (HealthServiceStats) {
.sum = per_day_default,
.avg = per_day_default,
.min = per_day_default,
.max = per_day_default,
.count = 1,
};
}
if (stats->weekend.sum == 0) {
stats->weekend = (HealthServiceStats) {
.sum = per_day_default,
.avg = per_day_default,
.min = per_day_default,
.max = per_day_default,
.count = 1,
};
}
// Daily is just the sum of weekend and weekday
stats->daily.sum = stats->weekday.sum + stats->weekend.sum;
stats->daily.count = stats->weekday.count + stats->weekend.count;
stats->daily.avg = stats->daily.count ? stats->daily.sum / stats->daily.count : 0;
stats->daily.min = MIN(stats->weekday.min, stats->weekend.min);
stats->daily.max = MAX(stats->weekday.max, stats->weekend.max);
return true;
}
// ----------------------------------------------------------------------------------------------
// Return intra-day averages for the given metric
static bool prv_get_intraday_averages(HealthServiceState *state, HealthMetric metric,
ActivityMetricAverages *averages,
DayInWeek day_in_week) {
// If the cache is valid, return cached data.
if (state->cache && (metric == HealthMetricStepCount)
&& state->cache->step_averages_valid && (day_in_week == state->cache->step_averages_day)) {
memcpy(averages, &state->cache->step_averages, sizeof(*averages));
return true;
}
// Fetch the intraday averages, if available
if (state->cache && (metric == HealthMetricStepCount)) {
// Fill the cache if this is step count
sys_activity_get_step_averages(day_in_week, &state->cache->step_averages);
state->cache->step_averages_day = day_in_week;
state->cache->step_averages_valid = true;
memcpy(averages, &state->cache->step_averages, sizeof(*averages));
} else if (metric == HealthMetricStepCount) {
// For step count, we have intraday averages available
sys_activity_get_step_averages(day_in_week, averages);
} else {
// For other metrics, we don't.
memset(averages->average, ACTIVITY_METRIC_AVERAGES_UNKNOWN & 0xFF, sizeof(averages->average));
}
// If all metric averages are unknown, we will plug in a default
bool use_default = true;
if (metric == HealthMetricStepCount) {
for (int i = 0; i < ACTIVITY_NUM_METRIC_AVERAGES; i++) {
if (averages->average[i] != ACTIVITY_METRIC_AVERAGES_UNKNOWN) {
use_default = false;
break;
}
}
}
// Compute the default average value
uint16_t default_value = 0;
if (use_default) {
HealthServiceMetricStats stats;
if (!prv_get_metric_stats(state, metric, &stats, day_in_week)) {
return false;
}
HealthValue value_per_day = stats.weekly.avg;
default_value = value_per_day / ACTIVITY_NUM_METRIC_AVERAGES;
}
// Plug in the default value for any entries which are unknown.
for (int i = 0; i < ACTIVITY_NUM_METRIC_AVERAGES; i++) {
if (averages->average[i] == ACTIVITY_METRIC_AVERAGES_UNKNOWN) {
averages->average[i] = default_value;
}
// If this entry is cached, fix up the cache entry
if (state->cache && (metric == HealthMetricStepCount)) {
if (state->cache->step_averages.average[i] == ACTIVITY_METRIC_AVERAGES_UNKNOWN) {
state->cache->step_averages.average[i] = default_value;
}
}
}
return true;
}
// ----------------------------------------------------------------------------------------------
// Compute the sum of the chunks in the averages array that comprise the given time range from
// time_start to time_end. The averages array represents all the chunks for a day, and time_start
// to time_end is always <= 1 day.
static HealthValue prv_sum_intraday_averages(ActivityMetricAverages *averages, time_t time_start,
time_t time_end) {
PBL_ASSERTN((time_end - time_start) <= SECONDS_PER_DAY);
struct tm *local_tm = pbl_override_localtime(&time_start);
// Add up the metric averages for the passed in time range
time_t chunk_start_time = time_start;
const int k_seconds_per_step_avg = SECONDS_PER_DAY / ACTIVITY_NUM_METRIC_AVERAGES;
unsigned int second_idx = local_tm->tm_hour * SECONDS_PER_HOUR
+ local_tm->tm_min * SECONDS_PER_MINUTE + local_tm->tm_sec;
unsigned int chunk_idx = second_idx / k_seconds_per_step_avg;
HealthValue result = 0;
while (chunk_start_time < time_end) {
int seconds_left = time_end - chunk_start_time;
int seconds_in_chunk = k_seconds_per_step_avg - (second_idx % k_seconds_per_step_avg);
seconds_in_chunk = MIN(seconds_left, seconds_in_chunk);
if (averages->average[chunk_idx] != ACTIVITY_METRIC_AVERAGES_UNKNOWN) {
if (seconds_in_chunk == k_seconds_per_step_avg) {
result += averages->average[chunk_idx];
} else {
result += averages->average[chunk_idx] * seconds_in_chunk / k_seconds_per_step_avg;
}
}
// Increment indices and time to the next chunk
chunk_start_time += seconds_in_chunk;
second_idx += seconds_in_chunk;
second_idx %= SECONDS_PER_DAY;
chunk_idx++;
chunk_idx %= ACTIVITY_NUM_METRIC_AVERAGES;
}
return result;
}
// ----------------------------------------------------------------------------------------------
// Fills in the range structure based on time_start and time_end. This computes the following
// values:
// * How many whole days of data are needed to include time_start and time_end (range->num_days).
// This will alway be >= 1.
// * The index of the last day in the range relative to today (0 means today, 1 means yesterday,
// etc.) (range->last_day_idx).
// * How many seconds from the range we should count from the first day (range->seconds_first_day)
// * How many seconds from the range we should count from the last day (range->seconds_last_day)
// * How many seconds of data we have collected for the last day of the range
// (range->seconds_total_last_day)
T_STATIC bool prv_calculate_time_range(time_t time_start, time_t time_end,
HealthServiceTimeRange *range) {
// as the data set from activity_get_metric() uses day boundaries in local time we
// need to convert the arguments to local time
const time_t now = sys_time_utc_to_local(sys_get_time());
time_start = sys_time_utc_to_local(time_start);
time_end = sys_time_utc_to_local(time_end);
// we use this value as a reference to calculate the range of valid data entries
const time_t midnight_after_now = prv_get_midnight_of_local_time(now) + SECONDS_PER_DAY;
// never work with values in the future
time_end = MIN(time_end, now);
// never work with values older than the supported history of data
time_start = MAX(time_start,
midnight_after_now - (SECONDS_PER_DAY * ACTIVITY_HISTORY_DAYS));
if (time_end < time_start) {
return false;
}
if (range) {
const time_t midnight_before_start = prv_get_midnight_of_local_time(time_start);
const time_t midnight_before_end = prv_get_midnight_of_local_time(time_end);
// we treat time_end as exclusive, if one passes exactly midnight, we don't count that day
const time_t midnight_after_end = (midnight_before_end == time_end) ?
midnight_before_end :
(midnight_before_end + SECONDS_PER_DAY);
// no additional range changes (e.g. < 0 or >= ACTIVITY_HISTORY_DAYS needed due to checks above)
range->last_day_idx = (midnight_after_now - midnight_after_end) / SECONDS_PER_DAY;
// always positive and <= ACTIVITY_HISTORY_DAYS due to check above
range->num_days = (midnight_after_end - midnight_before_start) / SECONDS_PER_DAY;
// we calculate how many seconds are covered on the first/last day of the range to allow
// clients to do some interpolation.
// if there's only one day, we return the number of seconds in the total range for both values
const uint32_t seconds_first_day = SECONDS_PER_DAY - (time_start - midnight_before_start);
// compensate for cases where time_end is on a day boundary
const uint32_t seconds_last_day = (time_end == midnight_before_end) ?
SECONDS_PER_DAY : (time_end - midnight_before_end);
const uint32_t total_seconds = time_end - time_start;
range->seconds_first_day = (range->num_days == 1) ? total_seconds : seconds_first_day;
range->seconds_last_day = (range->num_days == 1) ? total_seconds : seconds_last_day;
range->seconds_total_last_day =
(range->last_day_idx == 0) ? (now - midnight_before_end) : SECONDS_PER_DAY;
}
return true;
}
// ----------------------------------------------------------------------------------------------
// Fill in the time_range and daily_history structures for this metric and time range.
// Returns HealthServiceAccessibilityMaskAvailable if this time span and metric are accessible.
static HealthServiceAccessibilityMask prv_get_range_and_daily_history(
HealthServiceState *state, HealthMetric metric, time_t time_start, time_t time_end,
HealthServiceTimeRange *time_range, HealthServiceDailyHistory *daily_history) {
PBL_ASSERTN((time_range != NULL) && (daily_history != NULL));
// TODO: PBL-31628 permission system to reply with HealthServiceAccessibilityMaskNoPermission
if (!prv_get_metric_daily_history(state, metric, daily_history)) {
return HealthServiceAccessibilityMaskNotAvailable;
}
if (!prv_calculate_time_range(time_start, time_end, time_range)) {
return HealthServiceAccessibilityMaskNotAvailable;
}
return HealthServiceAccessibilityMaskAvailable;
}
// ----------------------------------------------------------------------------------------------
// This adjusts the values in the values array that represent the first and last day of
// the given time range. If either of these are not totally included in the time range, we
// decrease their value proportionally to how many seconds in the range overlap them.
T_STATIC void prv_adjust_value_boundaries(HealthValue *values, size_t num_values,
const HealthServiceTimeRange *range) {
PBL_ASSERTN(values && range && range->seconds_total_last_day > 0);
if (((range->last_day_idx + range->num_days) > num_values) || (range->num_days < 1)) {
return;
}
// as all indices inside of values[] are relative to range.last_day_idx, we adjust the pointer
// once here to simplify the following lines
values += range->last_day_idx;
// last day might not be complete, yet (as it can be today)
values[0] =
(HealthValue)(((int64_t)values[0] * range->seconds_last_day) / range->seconds_total_last_day);
// only process first day if its in range and does not overlap with the last day
if ((range->num_days > 1) && (num_values >= range->num_days)) {
const uint32_t oldest_day_idx = range->num_days - 1;
values[oldest_day_idx] =
(HealthValue)(((int64_t)values[oldest_day_idx] * range->seconds_first_day) / SECONDS_PER_DAY);
}
}
// ----------------------------------------------------------------------------------------------
static HealthValue prv_compute_aggregate_using_daily_totals(
HealthServiceState *state, HealthMetric metric, time_t time_start, time_t time_end,
HealthAggregation aggregation) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return 0;
#else
HealthServiceTimeRange time_range = {};
HealthServiceDailyHistory daily_history = {};
const HealthServiceAccessibilityMask accessible =
prv_get_range_and_daily_history(state, metric, time_start, time_end, &time_range,
&daily_history);
if (accessible != HealthServiceAccessibilityMaskAvailable) {
return 0;
}
// If we are summing, scale the values for the first and last day of the time range. For
// min, max, and avg scaling does not apply.
if (aggregation == HealthAggregationSum) {
prv_adjust_value_boundaries(daily_history.totals, ARRAY_LENGTH(daily_history.totals),
&time_range);
}
HealthValue result = 0;
switch (aggregation) {
case HealthAggregationSum:
case HealthAggregationAvg:
for (uint32_t i = 0; i < time_range.num_days; i++) {
result += daily_history.totals[i + time_range.last_day_idx];
}
if (aggregation == HealthAggregationAvg) {
result = ROUND(result, time_range.num_days);
}
break;
case HealthAggregationMax:
result = INT32_MIN;
for (uint32_t i = 0; i < time_range.num_days; i++) {
result = MAX(result, daily_history.totals[i + time_range.last_day_idx]);
}
break;
case HealthAggregationMin:
result = INT32_MAX;
for (uint32_t i = 0; i < time_range.num_days; i++) {
result = MIN(result, daily_history.totals[i + time_range.last_day_idx]);
}
break;
}
return result;
#endif
}
// ----------------------------------------------------------------------------------------------
// Compute the value of the given metric using aggregation and averaging based on daily history
// values.
static HealthValue prv_compute_aggregate_averaged_using_daily_totals(
HealthServiceState *state, HealthMetric metric, time_t time_start, time_t time_end,
HealthAggregation aggregation, HealthServiceTimeScope scope) {
PBL_ASSERTN(scope != HealthServiceTimeScopeOnce);
// What day of the week is the scope for? For now, we will use the day of the week that
// time_start falls on. In the future, we could be better about blending weekday with weekend
// if the time range spans both
struct tm *local_tm = pbl_override_localtime(&time_start);
bool is_weekend = prv_is_weekend(local_tm->tm_wday);
// Compute all stats
HealthServiceMetricStats stats;
if (!prv_get_metric_stats(state, metric, &stats, local_tm->tm_wday)) {
return 0;
}
// Return the appropriate statistic given the scope and aggregation
HealthValue result = 0;
HealthServiceStats *which_stats = NULL;
if (scope == HealthServiceTimeScopeDaily) {
which_stats = &stats.daily;
} else if (scope == HealthServiceTimeScopeDailyWeekdayOrWeekend) {
if (is_weekend) {
which_stats = &stats.weekend;
} else {
which_stats = &stats.weekday;
}
} else if (scope == HealthServiceTimeScopeWeekly) {
which_stats = &stats.weekly;
} else {
APP_LOG(APP_LOG_LEVEL_ERROR, "Unsupported scope: %d", (int) scope);
result = 0;
}
// Get the result
switch (aggregation) {
case HealthAggregationSum:
// NOTE: the caller is asking for "sum" aggregation, but we only have one value stored per
// day, so we just need to compute the average amongst all the days.
result = which_stats->avg;
break;
case HealthAggregationAvg:
result = which_stats->avg;
break;
case HealthAggregationMin:
result = which_stats->min;
break;
case HealthAggregationMax:
result = which_stats->max;
break;
}
// Scale result by the actual amount of requested time if asked for a sum
if (aggregation == HealthAggregationSum) {
result = result * (time_end - time_start) / SECONDS_PER_DAY;
}
return result;
}
// ----------------------------------------------------------------------------------------------
// Compute the aggregated value of the given metric using values from minute history
static HealthValue prv_compute_aggregate_using_minute_history(
HealthServiceState *state, HealthMetric metric, time_t time_start, time_t time_end,
HealthAggregation aggregation) {
// Currently only implemented for heart rate BPM
PBL_ASSERTN(metric == HealthMetricHeartRateBPM);
// Can't execute this call if no cache
if (!state->cache) {
APP_LOG(APP_LOG_LEVEL_ERROR, "Not enough memory for health cache");
return 0;
}
HealthValue value = 0;
uint32_t num_samples = 0;
switch (aggregation) {
case HealthAggregationSum:
WTF; // Not supported
break;
case HealthAggregationAvg:
value = 0;
break;
case HealthAggregationMin:
value = INT32_MAX;
break;
case HealthAggregationMax:
value = INT32_MIN;
break;
}
// If the current value is within the time range, incorporate it into the stats
time_t now_utc = sys_get_time();
if (time_end > now_utc - SECONDS_PER_MINUTE) {
HealthValue current_value;
bool success = sys_activity_get_metric(ActivityMetricHeartRateRawBPM, 1, &current_value);
if (success && current_value != 0) {
num_samples++;
value = current_value;
}
}
HealthMinuteData *minute_data = state->cache->minute_data;
bool more_data = true;
while (more_data && (time_start < time_end)) {
uint32_t num_records = ARRAY_LENGTH(state->cache->minute_data);
PBL_LOG(LOG_LEVEL_DEBUG, "Fetching %"PRIu32" minute records for %d to %d...", num_records,
(int)time_start, (int)time_end);
bool success = sys_activity_get_minute_history(minute_data, &num_records, &time_start);
if (!success) {
APP_LOG(APP_LOG_LEVEL_WARNING, "Error fetching minute history");
break;
}
PBL_LOG(LOG_LEVEL_DEBUG, " Got %"PRIu32" minute records for %d", num_records,
(int)time_start);
if (num_records == 0) {
// No more data available
more_data = false;
break;
}
// Update the metric from this new batch of data
for (unsigned i = 0; (i < num_records) && (time_start < time_end);
i++, time_start += SECONDS_PER_MINUTE) {
if (minute_data[i].heart_rate_bpm == 0) {
// Ignore minutes that have no heart rate BPM
continue;
}
num_samples++;
switch (aggregation) {
case HealthAggregationAvg:
value += minute_data[i].heart_rate_bpm;
break;
case HealthAggregationMax:
value = MAX(value, minute_data[i].heart_rate_bpm);
break;
case HealthAggregationMin:
value = MIN(value, minute_data[i].heart_rate_bpm);
break;
case HealthAggregationSum:
WTF;
break;
}
}
}
// Post-process the metric if necessary
if (aggregation == HealthAggregationAvg) {
if (num_samples > 0) {
value = ROUND(value, num_samples);
}
}
if (num_samples == 0) {
// Error case: no samples
value = 0;
}
return value;
}
// ---------------------------------------------------------------------------------------------
// Init a metric alert info structure
static void prv_init_metric_alert(HealthServiceState *state, HealthMetric metric,
HealthValue threshold, HealthServiceMetricAlertInfo *info) {
int32_t value = 0;
sys_activity_get_metric(prv_get_activity_metric(metric), 1, &value);
info->prior_reading = value;
info->threshold = threshold;
}
// ---------------------------------------------------------------------------------------------
// Determine if we should generate a health metric alert event
static void prv_check_and_generate_metric_alert(HealthServiceState *state, HealthMetric metric,
HealthServiceMetricAlertInfo *info) {
if (info->threshold == 0) {
// No threshold set
return;
}
int32_t value;
bool success = sys_activity_get_metric(prv_get_activity_metric(metric), 1, &value);
if (!success) {
return;
}
bool went_above = ((value > info->threshold) && (info->prior_reading < info->threshold));
bool went_below = ((value < info->threshold) && (info->prior_reading > info->threshold));
if (went_above || went_below) {
state->event_handler(HealthEventMetricAlert, state->context);
info->prior_reading = value;
}
}
// ----------------------------------------------------------------------------------------------
T_STATIC void prv_health_event_handler(PebbleEvent *e, void *context) {
#if !defined(RECOVERY_FW)
HealthServiceState *state = prv_get_state(true);
PBL_ASSERTN(state && state->event_handler != NULL);
// If this is a significant update event, invalidate our cache
if (e->health_event.type == HealthEventSignificantUpdate) {
if (state->cache) {
state->cache->valid_flags = 0;
}
}
// If this is a step update, update the cached value for today
else if (e->health_event.type == HealthEventMovementUpdate) {
if (state->cache) {
state->cache->steps_daily.totals[0] = e->health_event.data.movement_update.steps;
}
}
state->event_handler(e->health_event.type, state->context);
// If we crossed an alert threshold, generate a metric alert event
if (state->cache) {
prv_check_and_generate_metric_alert(state, HealthMetricHeartRateBPM,
&state->cache->alert_threshold_heart_rate);
}
#endif // !defined(RECOVERY_FW)
}
// ----------------------------------------------------------------------------------------------
T_STATIC bool prv_activity_session_matches(const ActivitySession *session, HealthActivityMask mask,
time_t time_start, time_t time_end) {
PBL_ASSERTN(session);
const bool type_matches =
(session->type == ActivitySessionType_Sleep && ((mask & HealthActivitySleep) > 0))
|| (session->type == ActivitySessionType_Nap && ((mask & HealthActivitySleep) > 0))
|| (session->type == ActivitySessionType_RestfulSleep && ((mask & HealthActivityRestfulSleep) > 0))
|| (session->type == ActivitySessionType_RestfulNap && ((mask & HealthActivityRestfulSleep) > 0))
|| (session->type == ActivitySessionType_Walk && ((mask & HealthActivityWalk) > 0))
|| (session->type == ActivitySessionType_Run && ((mask & HealthActivityRun) > 0))
|| (session->type == ActivitySessionType_Open && ((mask & HealthActivityOpenWorkout) > 0));
if (!type_matches) {
return false;
}
unsigned int length_sec = session->length_min * SECONDS_PER_MINUTE;
const bool time_matches = session->start_utc < time_end &&
(time_t)(session->start_utc + length_sec) > time_start;
return time_matches;
}
// ----------------------------------------------------------------------------------------------
T_STATIC int64_t prv_session_compare(const ActivitySession *a, const ActivitySession *b,
HealthIterationDirection direction) {
PBL_ASSERTN(a && b);
switch (direction) {
case HealthIterationDirectionPast:
// sessions that end later come first
return (b->start_utc + (b->length_min * SECONDS_PER_MINUTE))
- (a->start_utc + (a->length_min * SECONDS_PER_MINUTE));
case HealthIterationDirectionFuture:
// sessions that start earlier come first
return a->start_utc - b->start_utc;
default:
WTF;
}
}
// ----------------------------------------------------------------------------------------------
static void prv_sessions_sort(ActivitySession *sessions, const uint32_t num_sessions,
HealthIterationDirection direction) {
// as the number of sessions is expected to be small (<=16), and we don't seem to have a generic
// sort implementation, we do a simple bubble sort here
for (uint32_t i = 0; i + 1 < num_sessions; i++) {
for (uint32_t j = i + 1; j < num_sessions; j++) {
if (prv_session_compare(&sessions[i], &sessions[j], direction) > 0) {
ActivitySession temp = sessions[i];
sessions[i] = sessions[j];
sessions[j] = temp;
}
}
}
}
// ----------------------------------------------------------------------------------------------
static MeasurementSystem prv_get_shell_prefs_metric_for_distance(void) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return MeasurementSystemUnknown;
#else
switch (sys_shell_prefs_get_units_distance()) {
case UnitsDistance_Miles:
return MeasurementSystemImperial;
case UnitsDistance_KM:
return MeasurementSystemMetric;
default:
return MeasurementSystemUnknown;
}
#endif
}
// ----------------------------------------------------------------------------------------------
// Filter callbacks used by stats_calculate_basic()
bool health_service_private_non_zero_filter(int index, int32_t value, void *context) {
return (index > 0 && value > 0);
}
bool health_service_private_weekday_filter(int index, int32_t value, void *tm_weekday_ref) {
const int tm_weekday = (int)(uintptr_t)tm_weekday_ref;
return (health_service_private_non_zero_filter(index, value, NULL) &&
IS_WEEKDAY(positive_modulo(tm_weekday - index, DAYS_PER_WEEK)));
}
bool health_service_private_weekend_filter(int index, int32_t value, void *tm_weekday_ref) {
const int tm_weekday = (int)(uintptr_t)tm_weekday_ref;
return (health_service_private_non_zero_filter(index, value, NULL) &&
IS_WEEKEND(positive_modulo(tm_weekday - index, DAYS_PER_WEEK)));
}
bool health_service_private_weekly_filter(int index, int32_t value, void *tm_weekday_ref) {
const int tm_weekday = (int)(uintptr_t)tm_weekday_ref;
return (health_service_private_non_zero_filter(index, value, NULL) &&
(positive_modulo(tm_weekday - index, DAYS_PER_WEEK) == 0));
}
// ----------------------------------------------------------------------------------------------
bool health_service_private_get_metric_history(HealthMetric metric, uint32_t history_len,
int32_t *history) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return false;
#else
// Look up which activity metric maps to the given health metric. We separate the two because
// in the future, the health APIs may need to go other services besides just the Activity service
// to get information.
ActivityMetric act_metric = prv_get_activity_metric(metric);
return sys_activity_get_metric(act_metric, history_len, history);
#endif
}
// ----------------------------------------------------------------------------------------------
HealthServiceAccessibilityMask health_service_metric_accessible(
HealthMetric metric, time_t time_start, time_t time_end) {
return health_service_metric_aggregate_averaged_accessible(metric, time_start, time_end,
prv_default_aggregation(metric),
HealthServiceTimeScopeOnce);
}
// ----------------------------------------------------------------------------------------------
HealthServiceAccessibilityMask health_service_metric_averaged_accessible(
HealthMetric metric, time_t time_start, time_t time_end, HealthServiceTimeScope scope) {
return health_service_metric_aggregate_averaged_accessible(metric, time_start, time_end,
prv_default_aggregation(metric),
scope);
}
// ----------------------------------------------------------------------------------------------
HealthServiceAccessibilityMask health_service_metric_aggregate_averaged_accessible(
HealthMetric metric, time_t time_start, time_t time_end, HealthAggregation aggregation,
HealthServiceTimeScope scope) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return HealthServiceAccessibilityMaskNotSupported;
#else
if (prv_is_heart_rate_metric(metric) && !sys_activity_prefs_heart_rate_is_enabled()) {
return HealthServiceAccessibilityMaskNoPermission;
}
if (!prv_metric_aggregation_implemented(metric, time_start, time_end, aggregation, scope)) {
return HealthServiceAccessibilityMaskNotSupported;
}
// Get our state
HealthServiceState *state = prv_get_state(false);
HealthServiceTimeRange time_range = {};
HealthServiceDailyHistory daily_history = {};
const HealthServiceAccessibilityMask accessible =
prv_get_range_and_daily_history(state, metric, time_start, time_end, &time_range,
&daily_history);
if (accessible != HealthServiceAccessibilityMaskAvailable) {
return accessible;
}
for (size_t i = 0; i < time_range.num_days; i++) {
if (daily_history.totals[time_range.last_day_idx + i] >= 0) {
return HealthServiceAccessibilityMaskAvailable;
}
}
return HealthServiceAccessibilityMaskNotAvailable;
#endif
}
// ----------------------------------------------------------------------------------------------
HealthValue health_service_sum_today(HealthMetric metric) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return 0;
#else
const time_t today_midnight = sys_time_start_of_today();
const time_t tomorrow_midnight = today_midnight + SECONDS_PER_DAY;
return health_service_sum(metric, today_midnight, tomorrow_midnight);
#endif
}
// ----------------------------------------------------------------------------------------------
HealthValue health_service_sum(HealthMetric metric, time_t time_start, time_t time_end) {
return health_service_aggregate_averaged(metric, time_start, time_end, HealthAggregationSum,
HealthServiceTimeScopeOnce);
}
// ----------------------------------------------------------------------------------------------
// Compute the sum of a metric, but averaged over multiple days.
HealthValue health_service_sum_averaged(HealthMetric metric, time_t time_start, time_t time_end,
HealthServiceTimeScope scope) {
return health_service_aggregate_averaged(metric, time_start, time_end, HealthAggregationSum,
scope);
}
// ----------------------------------------------------------------------------------------------
HealthValue health_service_peek_current_value(HealthMetric metric) {
time_t now_utc = sys_get_time();
return health_service_aggregate_averaged(metric, now_utc, now_utc, HealthAggregationAvg,
HealthServiceTimeScopeOnce);
}
static HealthValue prv_hr_aggregate_averaged(HealthServiceState *state, HealthMetric metric,
time_t time_start, time_t time_end,
HealthAggregation aggregation) {
PBL_ASSERTN(metric == HealthMetricHeartRateBPM || metric == HealthMetricHeartRateRawBPM);
time_t now_utc = sys_get_time();
const bool query_cur_minute = prv_interval_within_last_minute(now_utc, time_start, time_end);
const bool valid_hr_sample_num = ((now_utc - time_start) <= HS_MAX_MINUTE_DATA_SEC);
if (metric == HealthMetricHeartRateBPM) {
if (query_cur_minute) {
// If the client is querying the service for the most recent Stable/Median/Filtered value
// and it is within the last X minutes, return it. If it's older than X minutes, return 0.
// This is the behavior we shipped in FW 4.1, so we must keep it this way. We have added a new
// metric HealthMetricHeartRateRawBPM if the user wants the most recent reading.
HealthValue value;
sys_activity_get_metric(ActivityMetricHeartRateFilteredUpdatedTimeUTC, 1, &value);
const time_t hr_median_age = now_utc - value;
if (hr_median_age >= HS_MAX_AGE_HR_SAMPLE) {
return 0;
}
sys_activity_get_metric(ActivityMetricHeartRateFilteredBPM, 1, &value);
return value;
} else if (valid_hr_sample_num) {
// If this is scope-once, the metric is BPM, and the time range is less than
// HS_MAX_MINUTE_DATA_SEC, we can use minute history since the amount of data is manageable.
return prv_compute_aggregate_using_minute_history(state, metric, time_start, time_end,
aggregation);
}
} else if (metric == HealthMetricHeartRateRawBPM) {
// We don't allow the user to gather data from raw HR samples. Only return the current and
// return. If time_start and time_end were not for the current time, they are filtered out
// by the function above `prv_metric_aggregation_implemented`.
int32_t raw_bpm;
sys_activity_get_metric(ActivityMetricHeartRateRawBPM, 1, &raw_bpm);
return raw_bpm;
}
// Invalid
return 0;
}
// ----------------------------------------------------------------------------------------------
HealthValue health_service_aggregate_averaged(HealthMetric metric, time_t time_start,
time_t time_end, HealthAggregation aggregation,
HealthServiceTimeScope scope) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return 0;
#else
// Make sure this metric is supported by this type of aggregation
if (!prv_metric_aggregation_implemented(metric, time_start, time_end, aggregation, scope)) {
return 0;
}
// Get our state
HealthServiceState *state = prv_get_state(true);
if (scope == HealthServiceTimeScopeOnce && prv_is_heart_rate_metric(metric)) {
return prv_hr_aggregate_averaged(state, metric, time_start, time_end, aggregation);
}
// --------
// If asked for an averaged sum over less than a day, we can use the intraday averages
if ((scope != HealthServiceTimeScopeOnce) && (aggregation == HealthAggregationSum)
&& ((time_end - time_start) < SECONDS_PER_DAY)) {
// For now, we will use the day of the week that time_start falls on. In the future, we could
// be better about blending weekday with weekend if the time range spans both
struct tm *local_tm = pbl_override_localtime(&time_start);
bool is_weekend = prv_is_weekend(local_tm->tm_wday);
ActivityMetricAverages averages;
unsigned num_sums = 0;
HealthValue result = 0;
if (scope == HealthServiceTimeScopeWeekly) {
if (prv_get_intraday_averages(state, metric, &averages, local_tm->tm_wday)) {
result += prv_sum_intraday_averages(&averages, time_start, time_end);
num_sums++;
}
} else if ((scope == HealthServiceTimeScopeDaily)
|| (scope == HealthServiceTimeScopeDailyWeekdayOrWeekend)) {
for (DayInWeek day = Sunday; day <= Saturday; day++) {
if (scope == HealthServiceTimeScopeDailyWeekdayOrWeekend) {
if (is_weekend != prv_is_weekend(day)) {
continue;
}
}
if (prv_get_intraday_averages(state, metric, &averages, day)) {
result += prv_sum_intraday_averages(&averages, time_start, time_end);
num_sums++;
}
}
} else {
APP_LOG(APP_LOG_LEVEL_ERROR, "Unsupported scope: %d", (int) scope);
result = 0;
}
if (num_sums > 0) {
result = ROUND(result, num_sums);
}
return result;
}
// --------
// Default handling is to use daily totals
if (scope == HealthServiceTimeScopeOnce) {
return prv_compute_aggregate_using_daily_totals(state, metric, time_start, time_end,
aggregation);
} else {
return prv_compute_aggregate_averaged_using_daily_totals(state, metric, time_start, time_end,
aggregation, scope);
}
#endif
}
// ----------------------------------------------------------------------------------------------
bool health_service_events_subscribe(HealthEventHandler handler, void *context) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return false;
#else
HealthServiceState *state = prv_get_state(true);
if (!state) {
return false;
}
state->event_handler = handler;
state->context = context;
event_service_client_subscribe(&state->health_event_service_info);
// Post a "significant update" event
PebbleEvent event = {
.type = PEBBLE_HEALTH_SERVICE_EVENT,
.health_event = {
.type = HealthEventSignificantUpdate,
.data.significant_update = {
.day_id = 0,
},
},
};
sys_send_pebble_event_to_kernel(&event);
return true;
#endif
}
// ----------------------------------------------------------------------------------------------
bool health_service_events_unsubscribe(void) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return false;
#else
HealthServiceState *state = prv_get_state(false);
event_service_client_unsubscribe(&state->health_event_service_info);
state->event_handler = NULL;
prv_health_service_deinit_cache(state);
return true;
#endif
}
// ----------------------------------------------------------------------------------------------
HealthMetricAlert *health_service_register_metric_alert(HealthMetric metric,
HealthValue threshold) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return NULL;
#else
if (prv_is_heart_rate_metric(metric) && !sys_activity_prefs_heart_rate_is_enabled()) {
return NULL;
}
HealthServiceState *state = prv_get_state(true);
if (!state->cache) {
return NULL;
}
switch (metric) {
case HealthMetricHeartRateBPM:
// If already registered, it's an error since we only have room for one registration per
// metric right now
if (state->cache->alert_threshold_heart_rate.threshold != 0) {
APP_LOG(APP_LOG_LEVEL_INFO, "Only 1 alert allowed per metric");
return NULL;
}
prv_init_metric_alert(state, HealthMetricHeartRateBPM, threshold,
&state->cache->alert_threshold_heart_rate);
return (void *)HealthMetricHeartRateBPM;
default:
return NULL;
}
#endif
}
// ----------------------------------------------------------------------------------------------
bool health_service_cancel_metric_alert(HealthMetricAlert *alert) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return false;
#else
HealthServiceState *state = prv_get_state(true);
if (!state->cache) {
return NULL;
}
HealthMetric metric = (HealthMetric)alert;
if (prv_is_heart_rate_metric(metric) && !sys_activity_prefs_heart_rate_is_enabled()) {
return false;
}
switch (metric) {
case HealthMetricHeartRateBPM:
state->cache->alert_threshold_heart_rate = (HealthServiceMetricAlertInfo) {};
return true;
default:
return false;
}
#endif
}
// ----------------------------------------------------------------------------------------------
bool health_service_set_heart_rate_sample_period(uint16_t interval_sec) {
#if !CAPABILITY_HAS_BUILTIN_HRM
return false;
#else
if (!sys_activity_prefs_heart_rate_is_enabled()) {
return false;
}
// Get the app id
AppInstallId app_id = app_get_app_id();
if (app_id == INSTALL_ID_INVALID) {
return false;
}
// If interval is 0, the caller wants to unsubscribe
if (interval_sec == 0) {
HRMSessionRef hrm_session = sys_hrm_manager_get_app_subscription(app_id);
if (hrm_session != HRM_INVALID_SESSION_REF) {
sys_hrm_manager_unsubscribe(hrm_session);
}
return true;
}
// Subscribe now
HRMSessionRef hrm_session = sys_hrm_manager_app_subscribe(app_id, interval_sec, 0 /*expire_sec*/,
HRMFeature_BPM);
if (hrm_session == HRM_INVALID_SESSION_REF) {
PBL_LOG(LOG_LEVEL_ERROR, "Error subscribing");
return false;
}
return true;
#endif
}
// ----------------------------------------------------------------------------------------------
uint16_t health_service_get_heart_rate_sample_period_expiration_sec(void) {
#if !CAPABILITY_HAS_BUILTIN_HRM
return 0;
#else
if (!sys_activity_prefs_heart_rate_is_enabled()) {
return 0;
}
// Get the app id
AppInstallId app_id = app_get_app_id();
if (app_id == INSTALL_ID_INVALID) {
return 0;
}
// If not subscribed, return 0
HRMSessionRef hrm_session = sys_hrm_manager_get_app_subscription(app_id);
if (hrm_session == HRM_INVALID_SESSION_REF) {
return 0;
} else {
return HRM_MANAGER_APP_EXIT_EXPIRATION_SEC;
}
#endif
}
// ----------------------------------------------------------------------------------------------
uint32_t health_service_get_minute_history(HealthMinuteData *minute_data, uint32_t max_records,
time_t *time_start, time_t *time_end) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return false;
#else
if (!minute_data || max_records == 0 || !time_start) {
return 0;
}
if (time_end && *time_end < *time_start) {
return 0;
}
uint32_t num_records = max_records;
// only query for as many records as necessary for the given time span
if (time_end) {
const time_t lower_bounded_start = (*time_start / SECONDS_PER_MINUTE) * SECONDS_PER_MINUTE;
const time_t upper_bounded_end = *time_end + SECONDS_PER_MINUTE - 1;
const uint32_t needed_partial_minutes =
(upper_bounded_end - lower_bounded_start) / SECONDS_PER_MINUTE;
num_records = MIN(num_records, needed_partial_minutes);
}
const bool success = sys_activity_get_minute_history(minute_data, &num_records, time_start);
if (!success) {
return 0;
}
if (time_end) {
*time_end = *time_start + SECONDS_PER_MINUTE * num_records;
}
return num_records;
#endif
}
// ----------------------------------------------------------------------------------------------
HealthActivityMask health_service_peek_current_activities(void) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return HealthActivityNone;
#else
HealthValue sleep_state;
if (!sys_activity_get_metric(ActivityMetricSleepState, 1, &sleep_state)) {
return HealthActivityNone;
}
HealthActivityMask result = HealthActivityNone;
if (sleep_state == ActivitySleepStateLightSleep) {
result |= HealthActivitySleep;
}
// yes, when sleeping restful, there's also always an activity of HealthActivitySleep
// when calling health_service_activities_iterate()
if (sleep_state == ActivitySleepStateRestfulSleep) {
result |= (HealthActivitySleep | HealthActivityRestfulSleep);
}
if (sys_activity_sessions_is_session_type_ongoing(ActivitySessionType_Walk)) {
result |= HealthActivityWalk;
}
if (sys_activity_sessions_is_session_type_ongoing(ActivitySessionType_Run)) {
result |= HealthActivityRun;
}
if (sys_activity_sessions_is_session_type_ongoing(ActivitySessionType_Open)) {
result |= HealthActivityOpenWorkout;
}
return result;
#endif
}
// ----------------------------------------------------------------------------------------------
// The number of session we choose to store is arbitrary and taken from other examples
// today, we should store < 10 session, so the value is a trade-off between stack space and risk
// to miss sessions.
#define NUM_EVALUATED_SLEEP_SESSIONS 16
void health_service_activities_iterate(HealthActivityMask activity_mask,
time_t time_start, time_t time_end,
HealthIterationDirection direction,
HealthActivityIteratorCB callback, void *context) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return;
#else
HealthServiceState *state = prv_get_state(true);
if (!state->cache) {
return;
}
if (callback == NULL || activity_mask == HealthActivityNone) {
return;
}
uint32_t num_sessions = ARRAY_LENGTH(state->cache->sessions);
if (!sys_activity_get_sessions(&num_sessions, state->cache->sessions)) {
return;
}
const uint32_t actual_num_sessions = MIN(num_sessions, ARRAY_LENGTH(state->cache->sessions));
prv_sessions_sort(state->cache->sessions, actual_num_sessions, direction);
for (uint32_t idx = 0; idx < actual_num_sessions; idx++) {
const ActivitySession *const session = &state->cache->sessions[idx];
if (prv_activity_session_matches(session, activity_mask,
time_start, time_end)) {
HealthActivity session_activity = HealthActivityNone;
switch (session->type) {
case ActivitySessionType_Sleep:
case ActivitySessionType_Nap:
session_activity = HealthActivitySleep;
break;
case ActivitySessionType_RestfulSleep:
case ActivitySessionType_RestfulNap:
session_activity = HealthActivityRestfulSleep;
break;
case ActivitySessionType_Walk:
session_activity = HealthActivityWalk;
break;
case ActivitySessionType_Run:
session_activity = HealthActivityRun;
break;
case ActivitySessionType_Open:
session_activity = HealthActivityOpenWorkout;
break;
case ActivitySessionType_None:
case ActivitySessionTypeCount:
WTF;
break;
}
if (!callback(session_activity, session->start_utc,
session->start_utc + (session->length_min * SECONDS_PER_MINUTE),
context)) {
// clients can interrupt the iteration at any time
break;
}
}
}
#endif
}
// ----------------------------------------------------------------------------------------------
bool health_service_private_get_yesterdays_sleep_activity(HealthValue *enter_sec,
HealthValue *exit_sec) {
return
sys_activity_get_metric(ActivityMetricSleepEnterAtSeconds, 1, enter_sec) &
sys_activity_get_metric(ActivityMetricSleepExitAtSeconds, 1, exit_sec);
}
// ----------------------------------------------------------------------------------------------
HealthServiceAccessibilityMask health_service_any_activity_accessible(
HealthActivityMask activity_mask,
time_t start_time, time_t end_time) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return HealthServiceAccessibilityMaskNotSupported;
#else
// TODO: PBL-31628 permission system to reply with HealthServiceAccessibilityMaskNoPermission
if (activity_mask == HealthActivityNone) {
return HealthServiceAccessibilityMaskNotAvailable;
}
// TODO: PBL-31630 provide more accurate value for available time frame
// for now, we say that there's only 1 day worth of data for sleep sessions
HealthServiceTimeRange range;
if (!prv_calculate_time_range(start_time, end_time, &range)) {
return HealthServiceAccessibilityMaskNotAvailable;
}
if (range.last_day_idx > 2) {
return HealthServiceAccessibilityMaskNotAvailable;
}
return HealthServiceAccessibilityMaskAvailable;
#endif
}
// ----------------------------------------------------------------------------------------------
MeasurementSystem health_service_get_measurement_system_for_display(HealthMetric metric) {
#if !CAPABILITY_HAS_HEALTH_TRACKING
return MeasurementSystemUnknown;
#else
switch (metric) {
case HealthMetricWalkedDistanceMeters:
return prv_get_shell_prefs_metric_for_distance();
default:
return MeasurementSystemUnknown;
}
#endif
}
// ----------------------------------------------------------------------------------------------
void health_service_state_init(HealthServiceState *state) {
*state = (HealthServiceState) {
.health_event_service_info = {
.type = PEBBLE_HEALTH_SERVICE_EVENT,
.handler = &prv_health_event_handler,
},
};
}
// ----------------------------------------------------------------------------------------------
void health_service_state_deinit(HealthServiceState *state) {
prv_health_service_deinit_cache(state);
}