Invalidate HRV summary cache when new data arrives

Add HrvCacheInvalidationReceiver to listen for ACTION_NEW_DATA broadcasts and invalidate the computed HRV summary cache for the current day.
This commit is contained in:
Gideon Zenz
2025-12-20 19:07:34 +01:00
committed by José Rebelo
parent 9ef4942102
commit 68f12ea92e
4 changed files with 136 additions and 1 deletions
@@ -205,7 +205,9 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
x.setValueFormatter(getHRVStatusChartDayValueFormatter(weeklyData));
HRVStatusDayData today = weeklyData.getCurrentDay();
mHRVStatusSevenDaysAvg.setText(today.weeklyAvg > 0 ? getString(R.string.hrv_status_unit, today.weeklyAvg) : "-");
// Show weekly average even if we don't have full 7 days - it's computed from available data
mHRVStatusSevenDaysAvg.setText(today.weeklyAvg > 0 ? getString(R.string.hrv_status_unit, today.weeklyAvg) :
(today.dayAvg > 0 ? getString(R.string.hrv_status_unit, today.dayAvg) : "-"));
mHRVStatusLastNight.setText(today.lastNight > 0 ? getString(R.string.hrv_status_unit, today.lastNight) : "-");
mHRVStatusLastNight5MinHighest.setText(today.lastNight5MinHigh > 0 ? getString(R.string.hrv_status_unit, today.lastNight5MinHigh) : "-");
mHRVStatusDayAvg.setText(today.dayAvg > 0 ? getString(R.string.hrv_status_unit, today.dayAvg) : "-");
@@ -447,6 +447,47 @@ public class ComputedHrvSummarySampleProvider implements TimeSampleProvider<HrvS
return generateSummaryForDay(cal.getTimeInMillis());
}
/**
* Invalidate cached summary for the current day for a specific device.
* This should be called when new HRV data is synced during the day to ensure
* the latest data is reflected in the computed summary.
*
* @param deviceAddress The MAC address of the device to invalidate cache for
*/
public static void invalidateCurrentDay(String deviceAddress) {
final Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
cal.set(Calendar.MILLISECOND, 999);
final long dayEndTimestamp = cal.getTimeInMillis();
final String cacheKey = deviceAddress + ":" + dayEndTimestamp;
synchronized (SUMMARY_CACHE) {
SUMMARY_CACHE.remove(cacheKey);
}
}
/**
* Clear cached summaries for a specific device. This should be called when new HRV data
* is synced to ensure cached values are recalculated with the latest data.
*
* @param deviceAddress The MAC address of the device to clear cache for
*/
public static void clearCache(String deviceAddress) {
synchronized (SUMMARY_CACHE) {
SUMMARY_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(deviceAddress + ":"));
}
}
/**
* Clear all cached summaries for all devices.
*/
public static void clearAllCache() {
SUMMARY_CACHE.clear();
}
private static class BaselineValues {
final int lowUpper; // Threshold between POOR and LOW
final int balancedLower; // Lower bound of balanced range (mean - stdDev)
@@ -0,0 +1,82 @@
/* Copyright (C) 2025 Gideon Zenz
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.ACTION_NEW_DATA;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.core.content.ContextCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.ComputedHrvSummarySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
/**
* Receiver that listens for new data events and invalidates the HRV summary cache
* for the current day. This ensures that computed HRV summaries are recalculated
* with the latest data when new HRV samples arrive during the day.
*/
public class HrvCacheInvalidationReceiver {
private static final Logger LOG = LoggerFactory.getLogger(HrvCacheInvalidationReceiver.class);
private Context context;
private boolean registered = false;
private HrvCacheInvalidationBroadcastReceiver hrvCacheInvalidationBroadcastReceiver;
public HrvCacheInvalidationReceiver() {
}
public void registerReceiver(Context context) {
this.context = context;
this.hrvCacheInvalidationBroadcastReceiver = new HrvCacheInvalidationBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter(ACTION_NEW_DATA);
ContextCompat.registerReceiver(this.context, this.hrvCacheInvalidationBroadcastReceiver, intentFilter, ContextCompat.RECEIVER_EXPORTED);
this.registered = true;
LOG.debug("HRV cache invalidation receiver registered");
}
public void unregisterReceiver() {
if (this.registered) {
try {
this.context.unregisterReceiver(this.hrvCacheInvalidationBroadcastReceiver);
this.registered = false;
LOG.debug("HRV cache invalidation receiver unregistered");
} catch (Exception e) {
LOG.error("Error unregistering HRV cache invalidation receiver", e);
}
}
}
private static class HrvCacheInvalidationBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_NEW_DATA.equals(intent.getAction())) {
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device != null) {
// Invalidate HRV summary cache for current day when new data arrives
ComputedHrvSummarySampleProvider.invalidateCurrentDay(device.getAddress());
}
}
}
}
}
@@ -77,6 +77,7 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.CMWeatherReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.CalendarReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.DeviceSettingsReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.GenericWeatherReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.HrvCacheInvalidationReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.IntentApiReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.KeyMissingReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.LineageOsWeatherReceiver;
@@ -269,6 +270,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private AutoConnectIntervalReceiver mAutoConnectInvervalReceiver = null;
private VolumeChangeReceiver mVolumeChangeReceiver = null;
private HrvCacheInvalidationReceiver mHrvCacheInvalidationReceiver = null;
private final List<CalendarReceiver> mCalendarReceiver = new ArrayList<>();
private CMWeatherReceiver mCMWeatherReceiver = null;
@@ -541,6 +543,9 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mKeyMissingReceiver = new KeyMissingReceiver();
ContextCompat.registerReceiver(this, mKeyMissingReceiver, new IntentFilter(KeyMissingReceiver.ACTION_KEY_MISSING), ContextCompat.RECEIVER_EXPORTED);
mHrvCacheInvalidationReceiver = new HrvCacheInvalidationReceiver();
mHrvCacheInvalidationReceiver.registerReceiver(this);
}
@Override
@@ -1598,6 +1603,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
unregisterReceiver(bluetoothCommandReceiver);
unregisterReceiver(deviceSettingsReceiver);
unregisterReceiver(intentApiReceiver);
if (mHrvCacheInvalidationReceiver != null) {
mHrvCacheInvalidationReceiver.unregisterReceiver();
mHrvCacheInvalidationReceiver = null;
}
}
@Override