OpenTracksApp/OpenTracks
8
113
Fork
You've already forked OpenTracks
21

Feature: Heart rate zone alerts #2350

Open
zonk-labs wants to merge 13 commits from zonk-labs/OpenTracks:heartrate_zone_alerts into main
pull from: zonk-labs/OpenTracks:heartrate_zone_alerts
merge into: OpenTracksApp:main
OpenTracksApp:main
OpenTracksApp:bugExport#2285
OpenTracksApp:heartrate
OpenTracksApp:dashboard#2265
OpenTracksApp:fit
First-time contributor
Copy link

Heart rate zone alerts

Adds a feature to allow a user to non-visually monitor their heart rate zone, if there is a bluetooth heart rate sensor available.

Upon exiting a set heart rate zone, the app vibrates the device to notify the user, with a different pattern for exceeding and underperforming a zone while a track is recording. This can be useful for people using a phone armband for example.

Alert frequency can also be set.

Screenshot

Screenshot

License agreement
By opening this pull request, I am providing my contribution under the Apache License 2.0 (see LICENSE.md).

# Heart rate zone alerts Adds a feature to allow a user to non-visually monitor their heart rate zone, if there is a bluetooth heart rate sensor available. Upon exiting a set heart rate zone, the app vibrates the device to notify the user, with a different pattern for exceeding and underperforming a zone while a track is recording. This can be useful for people using a phone armband for example. Alert frequency can also be set. ## Screenshot ![Screenshot](/attachments/040113c3-ee2a-477d-ad17-b04d6eaa4ac2) **License agreement** By opening this pull request, I am providing my contribution under the _Apache License 2.0_ (see [LICENSE.md](LICENSE.md)).
If a heart rate sensor is present, a user can now set up vibration alerts for cardio zones.
Fix formatting and whitespace
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
e4c26dff21
zonk-labs force-pushed heartrate_zone_alerts from e4c26dff21
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
to 0bbfab3392
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
2026年06月17日 19:08:32 +02:00
Compare
zonk-labs force-pushed heartrate_zone_alerts from 0bbfab3392
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
to bbadf48854
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
2026年06月18日 10:34:36 +02:00
Compare
Fix small UI glitch
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
c2b0e92ddb
The visibility of the heart rate zone alert settings menu was "reset" to false every time the fragment is loaded, since isPreferenceVisible in the XML is set to false by default.
This results in onResume() changing Preference visibility even when the fragment is shown with a cardio sensor connected before loading.
The fix is to also check when the XML is loaded.
dennisguse left a comment
Copy link

Interesting first draft.
Comments included.

Interesting first draft. Comments included.
@ -12,27 +12,43 @@ public class HeartRateZones {
privatefinalHeartRatemax;
finalpublicstaticintZONE_5=5;

Why?
This may be an enum.

Why? This may be an enum.
zonk-labs marked this conversation as resolved
@ -0,0 +21,4 @@
publicclass HeartRateZoneAlertManagerimplementsSharedPreferences.OnSharedPreferenceChangeListener{
finalstaticprivateDurationTOTALTIME_OFF=Duration.ofMillis(Long.MAX_VALUE);

private fields after public fields

private fields after public fields
zonk-labs marked this conversation as resolved
@ -0,0 +28,4 @@
privateDurationnextTotalTime=TOTALTIME_OFF;
privateStatisticstrackStatistics;
publicfinalstaticintDECREASE_INTENSITY=1;

These constants are not a good idea.
Looks like an enum.

Also not used outside of this class.

These constants are not a good idea. Looks like an enum. Also not used outside of this class.
zonk-labs marked this conversation as resolved
@ -0,0 +76,4 @@
return;
}
}catch(NullPointerExceptionignore){
return;

No.

No.
Author
First-time contributor
Copy link

sensorDataSet() or heartRate() may return null if the sensor is disconnected. If ignoring the exception isn't an option, any suggestions?

sensorDataSet() or heartRate() may return null if the sensor is disconnected. If ignoring the exception isn't an option, any suggestions?

This is a normal case and not an exception.
Please handle it using if-clauses.

This is a normal case and not an exception. Please handle it using if-clauses.
zonk-labs marked this conversation as resolved
@ -0,0 +95,4 @@
}
}
privateVibratorgetVibrator(){

There should be helper method for this.

There should be helper method for this.
Author
First-time contributor
Copy link

There isn't. ActivityUtils currently doesn't implement different vibration patterns.

There isn't. ActivityUtils currently doesn't implement different vibration patterns.

I understand that the desired functionality is not yet there.
My point was: please do not implement similar functionalities in all over the place.
If we now need more fancier vibrations, I would recommend to create VibrateUtils and move all these things there.

I understand that the desired functionality is not yet there. My point was: please do not implement similar functionalities in all over the place. If we now need more fancier vibrations, I would recommend to create VibrateUtils and move all these things there.
zonk-labs marked this conversation as resolved
@ -0,0 +104,4 @@
return(Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
}
privatesynchronizedvoidvibrateAlertIncrease(){

See ActivityUtils

See ActivityUtils
zonk-labs marked this conversation as resolved
@ -72,10 +72,8 @@ public class DefaultsSettingsFragment extends PreferenceFragmentCompat implement
intentriesId=switch(unitSystem){
caseMETRIC->R.array.stats_rate_metric_options;
caseIMPERIAL_FEET,IMPERIAL_METER->

Please remove formatting changes.

Please remove formatting changes.
zonk-labs marked this conversation as resolved
@ -0,0 +10,4 @@
importde.dennisguse.opentracks.R;
importde.dennisguse.opentracks.services.announcement.HeartRateZoneAlertManager;
publicclass HeartRateZoneAlertSettingsFragmentextendsPreferenceFragmentCompatimplementsPreference.OnPreferenceClickListener{

Should be part of the sensor fragment (for now).

Should be part of the sensor fragment (for now).
zonk-labs marked this conversation as resolved
@ -587,0 +616,4 @@
for(inti=0;i<entryValues.length;i++){
intvalue=Integer.parseInt(entryValues[i]);
entries[i]=resources.getString(R.string.settings_heart_rate_zone_alert_zone)+""+value;

This is not nice for localization as order is often language dependent.
Should be format string.

Same for the values: brackets may not be suitable and should be localized.

This is not nice for localization as order is often language dependent. Should be format string. Same for the values: brackets may not be suitable and should be localized.
zonk-labs marked this conversation as resolved
@ -587,0 +617,4 @@
for(inti=0;i<entryValues.length;i++){
intvalue=Integer.parseInt(entryValues[i]);
entries[i]=resources.getString(R.string.settings_heart_rate_zone_alert_zone)+""+value;
switch(value){

Technically: this is the HeartRateZone enum.

Technically: this is the HeartRateZone enum.
zonk-labs marked this conversation as resolved
@ -392,0 +397,4 @@
<string name="heart_rate_zone_alert_selected_zone_default" translatable="false">2</string>
<string name="heart_rate_zone_alert_demo_exceeding" translatable="false">heart_rate_zone_alert_demo_exceeding</string>
<string name="heart_rate_zone_alert_demo_missing" translatable="false">heart_rate_zone_alert_demo_missing</string>
<string-array name="heart_rate_zone_alert_values" translatable="false">

Why?

Why?
Author
First-time contributor
Copy link

The user needs some kind of way to know what the alert is. It would be difficult to determine which pattern is the threshold for too fast/slow while running/biking/rowing/etc.

The user needs some kind of way to know what the alert is. It would be difficult to determine which pattern is the threshold for too fast/slow while running/biking/rowing/etc.
zonk-labs marked this conversation as resolved
zonk-labs changed title from (削除) Feature: Heart rate zone alerts (削除ここまで) to WIP: Feature: Heart rate zone alerts 2026年06月20日 13:02:26 +02:00
File was unnecessarily
 included in autoformatting action.
Use enums and format strings appropriate for RTL i18n
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
41837bd87e
HeartRateZones now has a member enum HeartRateZone which also removes the need for unnecessary constants in HeartRateZoneAlertManager.
Use ActivityUtils for vibration (where possible).
Separate alert action and hardware access into AlertUtils in the ui package.
Remove alert demo/example buttons. Need to figure out some other way to make the vibration patterns not be confusing.
UI for heart rate zone alerts moved to SensorsSettingsFragment
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
928e1b56fa
If there is no heart rate sensor connected, the settings for configuring the
alert will not be displayed.
Move HeartZoneAlertManager into its own alerts package
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
ae6717122b
Semantically this makes more sense.
zonk-labs changed title from (削除) WIP: Feature: Heart rate zone alerts (削除ここまで) to Feature: Heart rate zone alerts 2026年06月21日 17:55:24 +02:00
Clean up string and settings xml files
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
24844a5b52
Harmonize string constant names to fit with existing naming scheme.
Remove unused key 'settings_heart_rate_zone_alert_key' from settings.xml.
Remove changes caused by autoformat.
dennisguse left a comment
Copy link

See comments.

For HeartRateZoneAlertManager: one or two tests would be great.

See comments. For HeartRateZoneAlertManager: one or two tests would be great.
@ -12,27 +12,78 @@ public class HeartRateZones {
privatefinalHeartRatemax;
publicenumHeartRateZone{

I am not a huge fan that HeartRateZone is in HeartRateZones.
Suggestion:

  • move HeartRateZone into a separate file.

  • move HeartRateZones to alerts/ and rename

  • inline code either into HeartRateZoneAlertManager or HeartRateZone

I am not a huge fan that HeartRateZone is in HeartRateZones. Suggestion: * move HeartRateZone into a separate file. * move HeartRateZones to alerts/ and rename * inline code either into HeartRateZoneAlertManager or HeartRateZone
zonk-labs marked this conversation as resolved
@ -15,0 +20,4 @@
ZONE_4(4,80,90),
ZONE_5(5,90,100);
privatefinalintZONE;

fields in lower case.

fields in lower case.
zonk-labs marked this conversation as resolved
@ -15,0 +25,4 @@
privatefinalintMAX_CUTOFF_PERCENT;
HeartRateZone(intzone,intminCutoffPercent,intmaxCutoffPercent){
this.ZONE=zone;

zoneId.

And why is it needed?
Can you not just use the order in the enum?
Technically, HeartRateZone.values() gives you an exact order.

PS/ it is not enforced that zoneId is unique.

zoneId. And why is it needed? Can you not just use the order in the enum? Technically, HeartRateZone.values() gives you an exact order. PS/ it is not enforced that zoneId is unique.
zonk-labs marked this conversation as resolved
@ -15,0 +30,4 @@
this.MAX_CUTOFF_PERCENT=maxCutoffPercent;
}
publicinttoInt(){

Make fields public (are final) and remove getters.

Make fields public (are final) and remove getters.
zonk-labs marked this conversation as resolved
@ -15,0 +42,4 @@
returnthis.MIN_CUTOFF_PERCENT;
}
publicdoublegetMinCutoffPercentAsDouble(){

Is this necessary?

Is this necessary?
zonk-labs marked this conversation as resolved
@ -0,0 +20,4 @@
publicclass HeartRateZoneAlertManagerimplementsSharedPreferences.OnSharedPreferenceChangeListener{
finalstaticprivateDurationTOTALTIME_OFF=Duration.ofMillis(Long.MAX_VALUE);

private static final

private static final
zonk-labs marked this conversation as resolved
@ -0,0 +48,4 @@
}
privateHeartRateextractHeartRateFromRecordingData(@NonNullRecordingDatadata){
SensorDataSetdataSet=data.sensorDataSet();

if (data.sensorDataSet() != null && data.sensorDataSet().hearRate() != null) return data.sensorDataSet().hearRate() .data;

Can be inlined into heartRateZoneAlertIfNeeded

if (data.sensorDataSet() != null && data.sensorDataSet().hearRate() != null) return data.sensorDataSet().hearRate() .data; Can be inlined into heartRateZoneAlertIfNeeded
zonk-labs marked this conversation as resolved
@ -0,0 +73,4 @@
intcomparison=heartRateZones.getZoneFromRate(currentHeartRate)
.compareTo(PreferencesUtils.getSelectedHeartRateZone());
if(comparison>0){
AlertUtils.alertAboveThreshold(context);

if (comparison == 0) {
return;
}
AltertUtils.alertOutsideThreshold(context, comparison > 0)

if (comparison == 0) { return; } AltertUtils.alertOutsideThreshold(context, comparison > 0)
zonk-labs marked this conversation as resolved
@ -0,0 +79,4 @@
}
}
publicvoidstart(StatisticstrackStatistics){

First: all public methods
Then: all private methods

And list public methods by usage (start goes first)

First: all public methods Then: all private methods And list public methods by usage (start goes first)
zonk-labs marked this conversation as resolved
@ -585,2 +585,4 @@
}
publicstaticbooleanisHeartRateSensorConnected(){
return!getKey(R.string.sensor_type_value_none)

sensor_type_value_none is duplicated here.

sensor_type_value_none is duplicated here.
zonk-labs marked this conversation as resolved
@ -108,1 +119,4 @@
}
privatevoidtoggleHeartRateZoneAlertSettingsFromSharedPreferences(){
PreferenceheartRateZoneAlertPreference=

No line break in this statement.

No line break in this statement.
zonk-labs marked this conversation as resolved
@ -109,0 +129,4 @@
if(preference.getKey().equals(getString(R.string.settings_sensor_bluetooth_heart_rate_key))){
PreferenceheartRateZoneAlertPreferenceGroup=
findPreference(getString(R.string.settings_heart_rate_zone_alert_preference_group_key));
if(((String)newValue).equals(getString(R.string.sensor_type_value_none))){

heartRateZoneAlertPreferenceGroup.setVisible(!getString(R.string.sensor_type_value_none).equals(newValue))

heartRateZoneAlertPreferenceGroup.setVisible(!getString(R.string.sensor_type_value_none).equals(newValue))
zonk-labs marked this conversation as resolved
@ -0,0 +14,4 @@
*<p>
*Forexample,ausermayhavesetaheartratesetpointaboveorbelowwhichanalertmaybetriggered.
*/
publicclass AlertUtils{

VibrateUtils.
Please move the ActivityUtils.vibrate() here as well.

VibrateUtils. Please move the ActivityUtils.vibrate() here as well.
zonk-labs marked this conversation as resolved
Move into de.dennisguise.opentracks.services.alerts
Remove superfluous fields and use Java standard notation.
HeartRateZones is restored to aadb79bf, add public getMax() to access private
max HeartRate member.
HeartRateZoneAlertManager implements an Alert interface to monitor updates sent
from the TrackRecordingService via an AlertManager.
Remove AlertUtils and move vibration logic into VibrateUtils.
Add notifications to be displayed when an alert triggers using the
HeartRateZoneAlertNotificationManager class.
Clean up PreferencesUtils and SensorsSettingsFragment
Some checks failed
Android Test / Android Lint (pull_request) Has been cancelled
Android Test / android-check (pull_request) Has been cancelled
8da392377c
Remove duplicate String lookup in isHeartRateSensorConnected() in
PreferencesUtils.
Simplify toggle logic in SensorsSettingsFragment onPreferenceChange().
Author
First-time contributor
Copy link

Tests for HeartRateZoneAlertManager and possibly HeartRateZoneAlertNotificationManager are currently WIP.

Tests for HeartRateZoneAlertManager and possibly HeartRateZoneAlertNotificationManager are currently WIP.
HeartRateZone
Some checks are pending
Android Test / Android Lint (pull_request) Blocked by required conditions
Android Test / android-check (pull_request) Blocked by required conditions
ad6c64152b
Fix potential problem with divide by 0.
Fix SensorsSettingsFragment UI toggle logic.
Some checks are pending
Android Test / Android Lint (pull_request) Blocked by required conditions
Android Test / android-check (pull_request) Blocked by required conditions
This pull request can be merged automatically.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u heartrate_zone_alerts:zonk-labs-heartrate_zone_alerts
git switch zonk-labs-heartrate_zone_alerts

Merge

Merge the changes and update on Forgejo.
git switch main
git merge --no-ff zonk-labs-heartrate_zone_alerts
git switch zonk-labs-heartrate_zone_alerts
git rebase main
git switch main
git merge --ff-only zonk-labs-heartrate_zone_alerts
git switch zonk-labs-heartrate_zone_alerts
git rebase main
git switch main
git merge --no-ff zonk-labs-heartrate_zone_alerts
git switch main
git merge --squash zonk-labs-heartrate_zone_alerts
git switch main
git merge --ff-only zonk-labs-heartrate_zone_alerts
git switch main
git merge zonk-labs-heartrate_zone_alerts
git push origin main
Sign in to join this conversation.
No reviewers
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
OpenTracksApp/OpenTracks!2350
Reference in a new issue
OpenTracksApp/OpenTracks
No description provided.
Delete branch "zonk-labs/OpenTracks:heartrate_zone_alerts"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?