mirror of
https://github.com/google/pebble.git
synced 2025-05-04 17:01:40 -04:00
1490 lines
57 KiB
C
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, ¤t_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);
|
|
}
|