Huawei: ability to view heart rate zones and limits.

This commit is contained in:
Me7c7
2025-10-04 21:38:17 +03:00
parent eec87ab89c
commit 297507b7eb
29 changed files with 1903 additions and 516 deletions
+4
View File
@@ -247,6 +247,10 @@
android:name=".devices.huawei.ui.HuaweiStressCalibrationActivity"
android:label="@string/huawei_stress_calibrate"
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
<activity
android:name=".activities.heartratezones.HeartRateSettingsActivity"
android:label="@string/heart_rate_settings"
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
<activity
android:name=".activities.NotificationsAppIconUploadActivity"
android:label="@string/notifications_app_icon_upload"
@@ -0,0 +1,80 @@
/* Copyright (C) 2025 Me7c7
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.activities.heartratezones;
import android.os.Bundle;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class HeartRateSettingsActivity extends AbstractGBActivity {
private GBDevice device;
protected String fragmentTag() {
return HeartRateSettingsFragment.FRAGMENT_TAG;
}
protected Fragment newFragment() {
return HeartRateSettingsFragment.newInstance(device);
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
setContentView(R.layout.activity_settings);
if (device == null) {
GB.toast(getString(R.string.device_not_connected), Toast.LENGTH_SHORT, GB.INFO);
finish();
}
if (savedInstanceState == null) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(fragmentTag());
if (fragment == null) {
fragment = newFragment();
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings_container, fragment, fragmentTag())
.commit();
}
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == android.R.id.home) {
// Simulate a back press, so that we don't actually exit the activity when
// in a nested PreferenceScreen
this.getOnBackPressedDispatcher().onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@@ -0,0 +1,321 @@
/* Copyright (C) 2025 Me7c7
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.activities.heartratezones;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.MenuProvider;
import androidx.lifecycle.Lifecycle;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.tabs.TabLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.databinding.FragmentHeartRateSettingsBinding;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusicPlaylist;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesConfig;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class HeartRateSettingsFragment extends AbstractGBFragment implements MenuProvider {
private static final Logger LOG = LoggerFactory.getLogger(HeartRateSettingsFragment.class);
static final String FRAGMENT_TAG = "HUAWEI_HEART_RATE_SETTINGS_FRAGMENT";
private FragmentHeartRateSettingsBinding binding;
private GBDevice device;
private HeartRateZonesSpec spec;
private List<HeartRateZonesConfig> config;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final Bundle arguments = getArguments();
if (arguments == null) {
return null;
}
device = arguments.getParcelable(GBDevice.EXTRA_DEVICE);
if (device == null) {
return null;
}
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
binding = FragmentHeartRateSettingsBinding.inflate(inflater);
View view = binding.getRoot();
spec = coordinator.getHeartRateZonesSpec(device);
config = spec.getDeviceConfig();
if (config == null) {
return null;
}
for (HeartRateZonesConfig entry : config) {
binding.hrSettingsTabLayout.addTab(binding.hrSettingsTabLayout.newTab().setText(spec.getNameByType(requireContext(), entry.getType())));
}
if (config.size() == 1) {
binding.hrSettingsTabLayout.setVisibility(View.GONE);
}
binding.hrSettingsTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
updateData();
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
binding.hrSettingsZones.calculationMethodSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
updateZones();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
binding.hrSettingsLimits.highHrWarning.setOnCheckedChangeListener((compoundButton, b) -> saveCurrentLimitWarning(b));
binding.saveHrZonesSettings.setOnClickListener(view2 -> saveCurrentConfig());
updateData();
return view;
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
requireActivity().addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED);
}
@Override
public void onCreateMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
inflater.inflate(R.menu.menu_heart_rate_zones, menu);
}
@Override
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.hr_zones_reset_all) {
if (spec != null) {
new MaterialAlertDialogBuilder(this.requireContext())
.setTitle(R.string.hr_settings_zones_remove_all)
.setMessage(this.getString(R.string.hr_settings_zones_remove_all_description))
.setIcon(R.drawable.ic_warning)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
spec.clearConfig();
config = spec.getDeviceConfig();
updateData();
})
.setNegativeButton(android.R.string.no, null)
.show();
return true;
}
}
return false;
}
private static class CalculateMethodItem {
private final String name;
private final HeartRateZones.CalculationMethod method;
public CalculateMethodItem(@NonNull String name, HeartRateZones.CalculationMethod method) {
this.name = name;
this.method = method;
}
public HeartRateZones.CalculationMethod getMethod() {
return method;
}
@NonNull
@Override
public String toString() {
return name;
}
}
private HeartRateZonesConfig getSelectedHRConfig() {
int pos = binding.hrSettingsTabLayout.getSelectedTabPosition();
return config.get(pos);
}
private void saveCurrentConfig() {
final HeartRateZonesConfig hrConfig = getSelectedHRConfig();
spec.saveConfig(hrConfig);
}
private void saveCurrentLimitWarning(boolean checked) {
HeartRateZonesConfig hrConfig = getSelectedHRConfig();
hrConfig.setWarningEnable(checked);
}
private void updateData() {
final HeartRateZonesConfig hrConfig = getSelectedHRConfig();
if (hrConfig == null)
return;
List<CalculateMethodItem> types = new ArrayList<>();
List<HeartRateZones> methods = hrConfig.getConfigByMethods();
for (HeartRateZones zn : methods) {
types.add(new CalculateMethodItem(HeartRateZones.methodToString(requireContext(), zn.getMethod()), zn.getMethod()));
}
CalculateMethodItem[] myStringArray = types.toArray(new CalculateMethodItem[0]);
ArrayAdapter<CalculateMethodItem> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, myStringArray);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
binding.hrSettingsZones.calculationMethodSpinner.setAdapter(adapter);
int idx = 0;
for (int i = 0; i < myStringArray.length; i++) {
if (myStringArray[i].method == hrConfig.getCurrentCalculationMethod()) {
idx = i;
}
}
binding.hrSettingsZones.calculationMethodSpinner.setSelection(idx);
binding.hrSettingsLimits.limitHighHr.setText(String.valueOf(hrConfig.getWarningHRLimit()));
binding.hrSettingsLimits.highHrWarning.setChecked(hrConfig.getWarningEnable());
}
private void updateZones() {
HeartRateZonesConfig hrConfig = getSelectedHRConfig();
if (hrConfig == null)
return;
final CalculateMethodItem calcMethod = (CalculateMethodItem) binding.hrSettingsZones.calculationMethodSpinner.getSelectedItem();
HeartRateZones selected = null;
List<HeartRateZones> methods = hrConfig.getConfigByMethods();
for (HeartRateZones zn : methods) {
if (calcMethod.getMethod() == zn.getMethod()) {
selected = zn;
break;
}
}
if (selected == null)
return;
hrConfig.setCurrentCalculationMethod(calcMethod.getMethod());
if (calcMethod.getMethod() == HeartRateZones.CalculationMethod.LTHR) {
binding.hrSettingsZones.zonesMaxHrTitle.setText(getString(R.string.hr_settings_zones_lactate_threshold_hr));
} else {
binding.hrSettingsZones.zonesMaxHrTitle.setText(getString(R.string.hr_settings_zones_max_hr));
}
binding.hrSettingsZones.zonesMaxHr.setText(String.valueOf(selected.getHRThreshold()));
if (calcMethod.getMethod() == HeartRateZones.CalculationMethod.HRR) {
binding.hrSettingsZones.layoutZonesRestingHr.setVisibility(View.VISIBLE);
binding.hrSettingsZones.zonesRestingHr.setText(String.valueOf(selected.getHRResting()));
} else {
binding.hrSettingsZones.layoutZonesRestingHr.setVisibility(View.GONE);
}
if (calcMethod.getMethod() == HeartRateZones.CalculationMethod.LTHR) {
binding.hrSettingsZones.heartRateZoneMaxValue.setText(getString(R.string.n_a));
binding.hrSettingsZones.heartRateZoneMaxPercent.setText(getString(R.string.n_a));
} else {
binding.hrSettingsZones.heartRateZoneMaxValue.setText(String.valueOf(selected.getHRThreshold()));
binding.hrSettingsZones.heartRateZoneMaxPercent.setText(String.valueOf(100));
}
binding.hrSettingsZones.heartRateZone5Value.setText(String.valueOf(selected.getZone5()));
binding.hrSettingsZones.heartRateZone4Value.setText(String.valueOf(selected.getZone4()));
binding.hrSettingsZones.heartRateZone3Value.setText(String.valueOf(selected.getZone3()));
binding.hrSettingsZones.heartRateZone2Value.setText(String.valueOf(selected.getZone2()));
binding.hrSettingsZones.heartRateZone1Value.setText(String.valueOf(selected.getZone1()));
binding.hrSettingsZones.heartRateZone5Percent.setText(String.valueOf(selected.getPercentage(selected.getZone5())));
binding.hrSettingsZones.heartRateZone4Percent.setText(String.valueOf(selected.getPercentage(selected.getZone4())));
binding.hrSettingsZones.heartRateZone3Percent.setText(String.valueOf(selected.getPercentage(selected.getZone3())));
binding.hrSettingsZones.heartRateZone2Percent.setText(String.valueOf(selected.getPercentage(selected.getZone2())));
binding.hrSettingsZones.heartRateZone1Percent.setText(String.valueOf(selected.getPercentage(selected.getZone1())));
}
private void setDevice(final GBDevice device) {
final Bundle args = getArguments() != null ? getArguments() : new Bundle();
args.putParcelable("device", device);
setArguments(args);
}
@Override
public void onDestroyView() {
super.onDestroyView();
}
static HeartRateSettingsFragment newInstance(final GBDevice device) {
final HeartRateSettingsFragment fragment = new HeartRateSettingsFragment();
fragment.setDevice(device);
return fragment;
}
@Nullable
@Override
protected CharSequence getTitle() {
return getString(R.string.heart_rate_settings);
}
}
@@ -0,0 +1,48 @@
package nodomain.freeyourgadget.gadgetbridge.activities.heartratezones;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import androidx.annotation.NonNull;
public class HeartRateZonesDividerView extends View {
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final int linePx;
public HeartRateZonesDividerView(Context context) {
this(context, null);
}
public HeartRateZonesDividerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HeartRateZonesDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.linePx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.0f, getResources().getDisplayMetrics());
this.paint.setColor(Color.GRAY);
this.paint.setStrokeWidth(this.linePx);
this.paint.setStyle(Paint.Style.FILL_AND_STROKE);
}
@Override
public void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int horizontalCenter = width / 2;
int verticalCenter = height / 2;
canvas.save();
canvas.drawLine(horizontalCenter, this.linePx, width, this.linePx, this.paint);
canvas.drawLine(0.0f, verticalCenter, horizontalCenter, verticalCenter, this.paint);
canvas.drawLine(horizontalCenter, this.linePx, horizontalCenter, height - this.linePx, this.paint);
canvas.drawLine(horizontalCenter, height - this.linePx, width, height - this.linePx, this.paint);
canvas.restore();
}
}
@@ -96,6 +96,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample;
import nodomain.freeyourgadget.gadgetbridge.model.WeightSample;
import nodomain.freeyourgadget.gadgetbridge.model.WorkoutLoadSample;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -1105,4 +1106,9 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
public List<DeviceCardAction> getCustomActions() {
return Collections.emptyList();
}
@Override
public HeartRateZonesSpec getHeartRateZonesSpec(@NonNull GBDevice device) {
return null;
}
}
@@ -69,6 +69,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample;
import nodomain.freeyourgadget.gadgetbridge.model.WeightSample;
import nodomain.freeyourgadget.gadgetbridge.model.WorkoutLoadSample;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
@@ -914,4 +915,6 @@ public interface DeviceCoordinator {
List<DeviceCardAction> getCustomActions();
DeviceKind getDeviceKind(@NonNull GBDevice device);
HeartRateZonesSpec getHeartRateZonesSpec(@NonNull GBDevice device);
}
@@ -1,297 +0,0 @@
/* Copyright (C) 2024 Me7c7
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.devices.huawei;
// TODO: make this configurable
// TODO: retrieve from device and set real Rest HR
// NOTE: algorithms used in this class are generic. So this data can be used with other devices.
// We can move this class to global scope.
public class HeartRateZonesConfig {
public static final int TYPE_UPRIGHT = 1;
public static final int TYPE_SITTING = 2;
public static final int TYPE_SWIMMING = 3;
public static final int TYPE_OTHER = 4;
public static final int CALCULATE_METHOD_MHR = 0;
public static final int CALCULATE_METHOD_HRR = 1;
public static final int CALCULATE_METHOD_LTHR = 3;
private static final int DEFAULT_REST_HEART_RATE = 60;
public static final int MAXIMUM_HEART_RATE = 220;
private final int configType;
private int calculateMethod = CALCULATE_METHOD_MHR; // 0 - MHR, 1 - HRR, 3 - LTHR
private int maxHRThreshold;
private int restHeartRate = DEFAULT_REST_HEART_RATE;
private boolean warningEnable;
private int warningHRLimit;
//MHR percentage
private int MHRExtreme;
private int MHRAnaerobic;
private int MHRAerobic;
private int MHRFatBurning;
private int MHRWarmUp;
//HRR percentage
private int HRRAdvancedAnaerobic;
private int HRRBasicAnaerobic;
private int HRRLactate;
private int HRRAdvancedAerobic;
private int HRRBasicAerobic;
//LTHR percentage
private int LTHRAnaerobic;
private int LTHRLactate;
private int LTHRAdvancedAerobic;
private int LTHRBasicAerobic;
private int LTHRWarmUp;
private int LTHRThresholdHeartRate;
public HeartRateZonesConfig(int type, int age) {
this.configType = type;
this.warningEnable = true;
this.warningHRLimit = MAXIMUM_HEART_RATE - age;
resetHRZones(age);
}
public void resetHRZones(int age) {
this.maxHRThreshold = (MAXIMUM_HEART_RATE - age) - getHRCorrection();
resetHRRZonesConfig();
resetMHRZonesConfig();
//NOTE: LTHR only supported for TYPE_UPRIGHT
if (this.configType == TYPE_UPRIGHT) {
resetLTHRZonesConfig();
}
}
private void resetLTHRZonesConfig() {
this.LTHRThresholdHeartRate = Math.round(((float) this.restHeartRate) + (((float) ((this.maxHRThreshold - this.restHeartRate) * 85)) / 100.0f));
this.LTHRAnaerobic = Math.round(((float) (this.LTHRThresholdHeartRate * 102)) / 100.0f);
this.LTHRLactate = Math.round(((float) (this.LTHRThresholdHeartRate * 97)) / 100.0f);
this.LTHRAdvancedAerobic = Math.round(((float) (this.LTHRThresholdHeartRate * 89)) / 100.0f);
this.LTHRBasicAerobic = Math.round(((float) (this.LTHRThresholdHeartRate * 80)) / 100.0f);
this.LTHRWarmUp = Math.round(((float) (this.LTHRThresholdHeartRate * 67)) / 100.0f);
}
private void resetMHRZonesConfig() {
this.MHRExtreme = Math.round(((float) (this.maxHRThreshold * 90)) / 100.0f);
this.MHRAnaerobic = Math.round(((float) (this.maxHRThreshold * 80)) / 100.0f);
this.MHRAerobic = Math.round(((float) (this.maxHRThreshold * 70)) / 100.0f);
this.MHRFatBurning = Math.round(((float) (this.maxHRThreshold * 60)) / 100.0f);
this.MHRWarmUp = Math.round(((float) (this.maxHRThreshold * 50)) / 100.0f);
}
private void resetHRRZonesConfig() {
int calcHR = this.maxHRThreshold - this.restHeartRate;
this.HRRAdvancedAnaerobic = Math.round(((float) (calcHR * 95)) / 100.0f) + this.restHeartRate;
this.HRRBasicAnaerobic = Math.round(((float) (calcHR * 88)) / 100.0f) + this.restHeartRate;
this.HRRLactate = Math.round(((float) (calcHR * 84)) / 100.0f) + this.restHeartRate;
this.HRRAdvancedAerobic = Math.round(((float) (calcHR * 74)) / 100.0f) + this.restHeartRate;
this.HRRBasicAerobic = Math.round(((float) (calcHR * 59)) / 100.0f) + this.restHeartRate;
}
//TODO: I am not sure about this. But it looks correct.
private int getHRCorrection() {
return switch (this.configType) {
case TYPE_SITTING -> 6;
case TYPE_SWIMMING -> 10;
case TYPE_OTHER -> 5;
default -> 0;
};
}
public int getCalculateMethod() {
return this.calculateMethod;
}
public boolean getWarningEnable() {
return this.warningEnable;
}
public int getWarningHRLimit() {
return this.warningHRLimit;
}
public int getMaxHRThreshold() {
return this.maxHRThreshold;
}
public int getRestHeartRate() {
return this.restHeartRate;
}
public int getMHRWarmUp() {
return this.MHRWarmUp;
}
public int getMHRFatBurning() {
return this.MHRFatBurning;
}
public int getMHRAerobic() {
return this.MHRAerobic;
}
public int getMHRAnaerobic() {
return this.MHRAnaerobic;
}
public int getMHRExtreme() {
return this.MHRExtreme;
}
public int getHRRBasicAerobic() {
return this.HRRBasicAerobic;
}
public int getHRRAdvancedAerobic() {
return this.HRRAdvancedAerobic;
}
public int getHRRLactate() {
return this.HRRLactate;
}
public int getHRRBasicAnaerobic() {
return this.HRRBasicAnaerobic;
}
public int getHRRAdvancedAnaerobic() {
return this.HRRAdvancedAnaerobic;
}
public int getLTHRThresholdHeartRate() {
return this.LTHRThresholdHeartRate;
}
public int getLTHRAnaerobic() {
return this.LTHRAnaerobic;
}
public int getLTHRLactate() {
return this.LTHRLactate;
}
public int getLTHRAdvancedAerobic() {
return this.LTHRAdvancedAerobic;
}
public int getLTHRBasicAerobic() {
return this.LTHRBasicAerobic;
}
public int getLTHRWarmUp() {
return this.LTHRWarmUp;
}
private boolean checkValue(int val) {
return val >= 0 && val < MAXIMUM_HEART_RATE;
}
public boolean isValid() {
return checkValue(this.configType) &&
checkValue(this.calculateMethod) &&
checkValue(this.warningHRLimit) &&
checkValue(this.maxHRThreshold) &&
checkValue(this.restHeartRate) &&
checkValue(this.MHRWarmUp) &&
checkValue(this.MHRFatBurning) &&
checkValue(this.MHRAerobic) &&
checkValue(this.MHRAnaerobic) &&
checkValue(this.MHRExtreme) &&
checkValue(this.HRRBasicAerobic) &&
checkValue(this.HRRAdvancedAerobic) &&
checkValue(this.HRRLactate) &&
checkValue(this.HRRBasicAnaerobic) &&
checkValue(this.HRRAdvancedAnaerobic) &&
checkValue(this.LTHRThresholdHeartRate) &&
checkValue(this.LTHRAnaerobic) &&
checkValue(this.LTHRLactate) &&
checkValue(this.LTHRAdvancedAerobic) &&
checkValue(this.LTHRBasicAerobic) &&
checkValue(this.LTHRWarmUp) &&
this.warningHRLimit > 0;
}
public boolean hasValidMHRData() {
return MHRWarmUp > 0 && MHRFatBurning > 0 && MHRAerobic > 0 && MHRAnaerobic > 0 && MHRExtreme > 0 && maxHRThreshold > 0;
}
public boolean hasValidHRRData() {
return restHeartRate > 0 && HRRBasicAerobic > 0 && HRRAdvancedAerobic > 0 && HRRLactate > 0 && HRRBasicAnaerobic > 0 && HRRAdvancedAnaerobic > 0;
}
public boolean hasValidLTHRData() {
return LTHRThresholdHeartRate > 0 && LTHRAnaerobic > 0 && LTHRLactate > 0 && LTHRAdvancedAerobic > 0 && LTHRBasicAerobic > 0 && LTHRWarmUp > 0;
}
private int getZoneForHR(int heartRate, int zone5Threshold, int zone4Threshold, int zone3Threshold, int zone2Threshold, int zone1Threshold) {
if (heartRate >= MAXIMUM_HEART_RATE) {
return -1;
}
if (heartRate >= zone5Threshold) {
return 4;
}
if (heartRate >= zone4Threshold) {
return 3;
}
if (heartRate >= zone3Threshold) {
return 2;
}
if (heartRate >= zone2Threshold) {
return 1;
}
return heartRate >= zone1Threshold ? 0 : -1;
}
public int getMHRZone(int heartRate) {
return getZoneForHR(heartRate, MHRExtreme, MHRAnaerobic, MHRAerobic, MHRFatBurning, MHRWarmUp);
}
public int getHHRZone(int heartRate) {
return getZoneForHR(heartRate, HRRAdvancedAnaerobic, HRRBasicAnaerobic, HRRLactate, HRRAdvancedAerobic, HRRBasicAerobic);
}
public int getLTHRZone(int heartRate) {
return getZoneForHR(heartRate, LTHRAnaerobic, LTHRLactate, LTHRAdvancedAerobic, LTHRBasicAerobic, LTHRWarmUp);
}
public int getZoneByMethod(int heartRate, int method) {
if(method == CALCULATE_METHOD_LTHR) {
return getLTHRZone(heartRate);
} else if(method == CALCULATE_METHOD_MHR) {
return getMHRZone(heartRate);
}
return getHHRZone(heartRate);
}
public static boolean isCalculateMethodValidForPostureType(int type, int method) {
if(method == CALCULATE_METHOD_LTHR && type == TYPE_UPRIGHT) {
return true;
}
return (method == CALCULATE_METHOD_MHR) || (method == CALCULATE_METHOD_HRR);
}
}
@@ -44,6 +44,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiBRSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
@@ -315,4 +316,9 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
public boolean addBatteryPollingSettings() {
return true;
}
@Override
public HeartRateZonesSpec getHeartRateZonesSpec(@NonNull GBDevice device) {
return huaweiCoordinator.getHeartRateZonesSpec(device);
}
}
@@ -60,6 +60,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
@@ -344,6 +345,9 @@ public class HuaweiCoordinator {
if (supportsSendingGps())
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.WORKOUT, R.xml.devicesettings_workout_send_gps_to_band);
if(supportsTrack() || supportsHeartRateZones())
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.WORKOUT, R.xml.devicesettings_heartrate_settings);
// Other
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_find_phone);
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_disable_find_phone_with_dnd);
@@ -1140,4 +1144,7 @@ public class HuaweiCoordinator {
return supportsTruSleep() && supportsDictSleepSync();
}
public HeartRateZonesSpec getHeartRateZonesSpec(@NonNull GBDevice device) {
return new HuaweiHeartRateZonesSpec(device, this);
}
}
@@ -0,0 +1,70 @@
/* Copyright (C) 2025 Me7c7
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.devices.huawei;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
public class HuaweiHeartRateZones extends HeartRateZones {
public HuaweiHeartRateZones(CalculationMethod method, int HRThreshold) {
super(method, HRThreshold);
}
@Override
public void reset() {
switch(this.method) {
case MHR -> calculateMHRZonesConfig();
case HRR -> defaultHRRZonesConfig();
case LTHR -> defaultLTHRZonesConfig();
}
}
@Override
public long getPercentage(int zone) {
return switch(this.method) {
case MHR, LTHR -> Math.round((float) zone * 100.0 / (float) HRThreshold);
case HRR -> Math.round((float) (zone - HRResting) * 100.0 / (float) (HRThreshold - HRResting));
};
}
private void calculateMHRZonesConfig() {
zone5 = Math.round(((float) (HRThreshold * 90)) / 100.0f); // Extreme;
zone4 = Math.round(((float) (HRThreshold * 80)) / 100.0f); // Anaerobic;
zone3 = Math.round(((float) (HRThreshold * 70)) / 100.0f); // Aerobic;
zone2 = Math.round(((float) (HRThreshold * 60)) / 100.0f); // FatBurning;
zone1 = Math.round(((float) (HRThreshold * 50)) / 100.0f); // WarmUp;
}
private void defaultHRRZonesConfig() {
int calcHR = HRThreshold - HRResting;
zone5 = Math.round(((float) (calcHR * 95)) / 100.0f) + HRResting; // AdvancedAnaerobic
zone4 = Math.round(((float) (calcHR * 88)) / 100.0f) + HRResting; // BasicAnaerobic
zone3 = Math.round(((float) (calcHR * 84)) / 100.0f) + HRResting; // Lactate
zone2 = Math.round(((float) (calcHR * 74)) / 100.0f) + HRResting; // AdvancedAerobic
zone1 = Math.round(((float) (calcHR * 59)) / 100.0f) + HRResting; // BasicAerobic
}
private void defaultLTHRZonesConfig() {
HRThreshold = Math.round(((float) HRResting) + (((float) ((HRThreshold - HRResting) * 85)) / 100.0f)); // LTHRThresholdHeartRate
zone5 = Math.round(((float) (HRThreshold * 102)) / 100.0f); // Anaerobic
zone4 = Math.round(((float) (HRThreshold * 97)) / 100.0f); // Lactate
zone3 = Math.round(((float) (HRThreshold * 89)) / 100.0f); // AdvancedAerobic
zone2 = Math.round(((float) (HRThreshold * 80)) / 100.0f); // BasicAerobic
zone1 = Math.round(((float) (HRThreshold * 67)) / 100.0f); // WarmUp
}
}
@@ -0,0 +1,244 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
import android.content.Context;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesConfig;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesUtils;
public class HuaweiHeartRateZonesSpec extends HeartRateZonesSpec {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiHeartRateZonesSpec.class);
public static final int HUAWEI_TYPE_UPRIGHT = 1;
public static final int HUAWEI_TYPE_SITTING = 2;
public static final int HUAWEI_TYPE_SWIMMING = 3;
public static final int HUAWEI_TYPE_OTHER = 4;
public static final int HUAWEI_CALCULATE_METHOD_MHR = 0;
public static final int HUAWEI_CALCULATE_METHOD_HRR = 1;
public static final int HUAWEI_CALCULATE_METHOD_LTHR = 3;
private final HuaweiCoordinator coordinator;
public HuaweiHeartRateZonesSpec(GBDevice device, HuaweiCoordinator coordinator) {
super(device);
this.coordinator = coordinator;
}
@Override
public String getNameByType(Context context, HeartRateZonesSpec.PostureType type) {
return switch (type) {
case UPRIGHT -> context.getString(R.string.workout_posture_type_upright);
case SITTING -> context.getString(R.string.workout_posture_type_sitting);
case SWIMMING -> context.getString(R.string.activity_type_swimming);
case OTHER -> context.getString(R.string.activity_type_other);
};
}
private HeartRateZones loadOrCreateZones(HeartRateZonesSpec.PostureType type, HeartRateZones.CalculationMethod method, int maxHRThreshold) {
String prefs = getHrZoneConfig(type, method);
if (prefs != null) {
try {
Gson gson = new Gson();
return gson.fromJson(prefs, HuaweiHeartRateZones.class);
} catch (Exception e) {
LOG.error("Error load heart zones config from prefs ", e);
}
}
return new HuaweiHeartRateZones(method, maxHRThreshold);
}
@Override
public List<HeartRateZonesConfig> getDeviceConfig() {
List<HeartRateZonesConfig> res = new ArrayList<>();
int age = new ActivityUser().getAge();
if (!coordinator.supportsTrack() && coordinator.supportsHeartRateZones()) {
List<HeartRateZones> zones = new ArrayList<>();
int maxHRThreshold = (HeartRateZonesUtils.MAXIMUM_HEART_RATE - age) - getHRCorrection(HeartRateZonesSpec.PostureType.UPRIGHT);
zones.add(loadOrCreateZones(HeartRateZonesSpec.PostureType.UPRIGHT, HeartRateZones.CalculationMethod.MHR, maxHRThreshold));
if (coordinator.supportsExtendedHeartRateZones())
zones.add(loadOrCreateZones(HeartRateZonesSpec.PostureType.UPRIGHT, HeartRateZones.CalculationMethod.HRR, maxHRThreshold));
res.add(loadOrCreateHeartRateZonesConfig(HeartRateZonesSpec.PostureType.UPRIGHT,
true,
HeartRateZonesUtils.MAXIMUM_HEART_RATE - age,
HeartRateZones.CalculationMethod.MHR,
zones
));
return res;
}
if (!coordinator.supportsTrack())
return null;
res.add(loadOrCreateHeartRateZonesConfig(HeartRateZonesSpec.PostureType.UPRIGHT,
true,
HeartRateZonesUtils.MAXIMUM_HEART_RATE - age,
HeartRateZones.CalculationMethod.MHR,
getUprightZone(age)
));
res.add(loadOrCreateHeartRateZonesConfig(HeartRateZonesSpec.PostureType.SITTING,
true,
HeartRateZonesUtils.MAXIMUM_HEART_RATE - age,
HeartRateZones.CalculationMethod.MHR,
getSittingZone(age)
));
res.add(loadOrCreateHeartRateZonesConfig(HeartRateZonesSpec.PostureType.SWIMMING,
true,
HeartRateZonesUtils.MAXIMUM_HEART_RATE - age,
HeartRateZones.CalculationMethod.MHR,
getSwimmingZone(age)
));
res.add(loadOrCreateHeartRateZonesConfig(HeartRateZonesSpec.PostureType.OTHER,
true,
HeartRateZonesUtils.MAXIMUM_HEART_RATE - age,
HeartRateZones.CalculationMethod.MHR,
getOtherZone(age)
));
return res;
}
private List<HeartRateZones> getUprightZone(int age) {
List<HeartRateZones> res = new ArrayList<>();
int maxHRThreshold = (HeartRateZonesUtils.MAXIMUM_HEART_RATE - age) - getHRCorrection(PostureType.UPRIGHT);
res.add(loadOrCreateZones(PostureType.UPRIGHT, HeartRateZones.CalculationMethod.MHR, maxHRThreshold));
res.add(loadOrCreateZones(PostureType.UPRIGHT, HeartRateZones.CalculationMethod.HRR, maxHRThreshold));
res.add(loadOrCreateZones(PostureType.UPRIGHT, HeartRateZones.CalculationMethod.LTHR, maxHRThreshold));
return res;
}
private List<HeartRateZones> getSittingZone(int age) {
List<HeartRateZones> res = new ArrayList<>();
int maxHRThreshold = (HeartRateZonesUtils.MAXIMUM_HEART_RATE - age) - getHRCorrection(PostureType.SITTING);
res.add(loadOrCreateZones(PostureType.SITTING, HeartRateZones.CalculationMethod.MHR, maxHRThreshold));
res.add(loadOrCreateZones(PostureType.SITTING, HeartRateZones.CalculationMethod.HRR, maxHRThreshold));
return res;
}
private List<HeartRateZones> getSwimmingZone(int age) {
List<HeartRateZones> res = new ArrayList<>();
int maxHRThreshold = (HeartRateZonesUtils.MAXIMUM_HEART_RATE - age) - getHRCorrection(PostureType.SWIMMING);
res.add(loadOrCreateZones(PostureType.SWIMMING, HeartRateZones.CalculationMethod.MHR, maxHRThreshold));
res.add(loadOrCreateZones(PostureType.SWIMMING, HeartRateZones.CalculationMethod.HRR, maxHRThreshold));
return res;
}
private List<HeartRateZones> getOtherZone(int age) {
List<HeartRateZones> res = new ArrayList<>();
int maxHRThreshold = (HeartRateZonesUtils.MAXIMUM_HEART_RATE - age) - getHRCorrection(HeartRateZonesSpec.PostureType.OTHER);
res.add(loadOrCreateZones(PostureType.OTHER, HeartRateZones.CalculationMethod.MHR, maxHRThreshold));
res.add(loadOrCreateZones(PostureType.OTHER, HeartRateZones.CalculationMethod.HRR, maxHRThreshold));
return res;
}
//TODO: I am not sure about this. But it looks correct.
private int getHRCorrection(HeartRateZonesSpec.PostureType type) {
return switch (type) {
case SITTING -> 6;
case SWIMMING -> 10;
case OTHER -> 5;
default -> 0;
};
}
public static HeartRateZonesConfig getByPosture(List<HeartRateZonesConfig> zones, HeartRateZonesSpec.PostureType type) {
for (HeartRateZonesConfig cfg : zones) {
if (cfg.getType() == type) {
return cfg;
}
}
return null;
}
public static HeartRateZones getByMethod(HeartRateZonesConfig cfg, HeartRateZones.CalculationMethod method) {
for (HeartRateZones zn : cfg.getConfigByMethods()) {
if (zn.getMethod() == method) {
return zn;
}
}
return null;
}
public static HeartRateZones getHRZonesConfigByPostureAndCalculationMethod(List<HeartRateZonesConfig> zones, HeartRateZonesSpec.PostureType type, HeartRateZones.CalculationMethod method) {
HeartRateZonesConfig cfg = getByPosture(zones, type);
if (cfg == null) {
cfg = getByPosture(zones, PostureType.UPRIGHT); // Use as default
}
if (cfg == null)
return null;
return getByMethod(cfg, method);
}
public static HeartRateZonesSpec.PostureType fromHuaweiPostureType(int type) {
return switch (type) {
case HUAWEI_TYPE_SITTING -> PostureType.SITTING;
case HUAWEI_TYPE_SWIMMING -> PostureType.SWIMMING;
case HUAWEI_TYPE_OTHER -> PostureType.OTHER;
default -> PostureType.UPRIGHT;
};
}
public static int toHuaweiPostureType(HeartRateZonesSpec.PostureType type) {
return switch (type) {
case UPRIGHT -> HUAWEI_TYPE_UPRIGHT;
case SITTING -> HUAWEI_TYPE_SITTING;
case SWIMMING -> HUAWEI_TYPE_SWIMMING;
case OTHER -> HUAWEI_TYPE_OTHER;
};
}
public static HeartRateZones.CalculationMethod fromHuaweiCalculationMethod(int type) {
return switch (type) {
case HUAWEI_CALCULATE_METHOD_MHR -> HeartRateZones.CalculationMethod.MHR;
case HUAWEI_CALCULATE_METHOD_LTHR -> HeartRateZones.CalculationMethod.LTHR;
default -> HeartRateZones.CalculationMethod.HRR;
};
}
public static int toHuaweiCalculationMethod(HeartRateZones.CalculationMethod type) {
return switch (type) {
case MHR -> HUAWEI_CALCULATE_METHOD_MHR;
case HRR -> HUAWEI_CALCULATE_METHOD_HRR;
case LTHR -> HUAWEI_CALCULATE_METHOD_LTHR;
};
}
public static int getZoneForHR(int heartRate, HeartRateZones zones) {
return getZoneForHR(heartRate, zones.getZone5(), zones.getZone4(), zones.getZone3(), zones.getZone2(), zones.getZone1());
}
private static int getZoneForHR(int heartRate, int zone5Threshold, int zone4Threshold, int zone3Threshold, int zone2Threshold, int zone1Threshold) {
if (heartRate >= HeartRateZonesUtils.MAXIMUM_HEART_RATE) {
return -1;
}
if (heartRate >= zone5Threshold) {
return 4;
}
if (heartRate >= zone4Threshold) {
return 3;
}
if (heartRate >= zone3Threshold) {
return 2;
}
if (heartRate >= zone2Threshold) {
return 1;
}
return heartRate >= zone1Threshold ? 0 : -1;
}
}
@@ -45,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiLESupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
@@ -323,4 +324,8 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
public boolean addBatteryPollingSettings() {
return true;
}
public HeartRateZonesSpec getHeartRateZonesSpec(@NonNull GBDevice device) {
return huaweiCoordinator.getHeartRateZonesSpec(device);
}
}
@@ -32,6 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
import nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui.HuaweiStressCalibrationActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
@@ -172,6 +173,16 @@ public class HuaweiSettingsCustomizer implements DeviceSpecificSettingsCustomize
});
}
final Preference hrSettings = handler.findPreference("pref_perform_heart_rate_settings");
if (hrSettings != null) {
hrSettings.setOnPreferenceClickListener(preference -> {
final Intent intent = new Intent(handler.getContext(), HeartRateSettingsActivity.class);
intent.putExtra(GBDevice.EXTRA_DEVICE, handler.getDevice());
handler.getContext().startActivity(intent);
return true;
});
}
// Huawei devices do not support lookahead > 7 days
final Preference calendarLookahead = handler.findPreference(DeviceSettingsPreferenceConst.PREF_CALENDAR_LOOKAHEAD_DAYS);
if (calendarLookahead != null) {
@@ -1,147 +0,0 @@
/* Copyright (C) 2024 Me7c7
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.devices.huawei;
public class HuaweiSportHRZones {
private final HeartRateZonesConfig otherHRZonesConfig;
private final HeartRateZonesConfig sittingHRZonesConfig;
private final HeartRateZonesConfig uprightHRZonesConfig;
private final HeartRateZonesConfig swimmingHRZonesConfig;
public HuaweiSportHRZones(int age) {
this.uprightHRZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_UPRIGHT, age);
this.sittingHRZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_SITTING, age);
this.swimmingHRZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_SWIMMING, age);
this.otherHRZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_OTHER, age);
}
public HeartRateZonesConfig getHRZonesConfigByType(int type) {
if (type == HeartRateZonesConfig.TYPE_SITTING) {
return this.sittingHRZonesConfig;
} else if (type == HeartRateZonesConfig.TYPE_SWIMMING) {
return this.swimmingHRZonesConfig;
} else if (type == HeartRateZonesConfig.TYPE_OTHER) {
return this.otherHRZonesConfig;
}
return this.uprightHRZonesConfig;
}
public byte[] getHRZonesData() {
HeartRateZonesConfig uprightConfig = getHRZonesConfigByType(HeartRateZonesConfig.TYPE_UPRIGHT);
HeartRateZonesConfig sittingConfig = getHRZonesConfigByType(HeartRateZonesConfig.TYPE_SITTING);
HeartRateZonesConfig swimmingConfig = getHRZonesConfigByType(HeartRateZonesConfig.TYPE_SWIMMING);
HeartRateZonesConfig otherConfig = getHRZonesConfigByType(HeartRateZonesConfig.TYPE_OTHER);
if (!uprightConfig.isValid() || !sittingConfig.isValid() || !swimmingConfig.isValid() || !otherConfig.isValid()) {
return null;
}
HuaweiTLV tlv = new HuaweiTLV();
if (uprightConfig.hasValidMHRData()) {
tlv.put(0x2, (byte) uprightConfig.getMHRWarmUp())
.put(0x3, (byte) uprightConfig.getMHRFatBurning())
.put(0x4, (byte) uprightConfig.getMHRAerobic())
.put(0x5, (byte) uprightConfig.getMHRAnaerobic())
.put(0x6, (byte) uprightConfig.getMHRExtreme())
.put(0x7, (byte) uprightConfig.getMaxHRThreshold())
.put(0x8, (byte) (uprightConfig.getWarningEnable() ? 1 : 0))
.put(0x9, (byte) uprightConfig.getWarningHRLimit())
.put(0xa, (byte) uprightConfig.getCalculateMethod())
.put(0xb, (byte) uprightConfig.getMaxHRThreshold());
}
tlv.put(0xc, (byte) uprightConfig.getRestHeartRate());
if (uprightConfig.hasValidHRRData()) {
tlv.put(0xd, (byte) uprightConfig.getHRRBasicAerobic())
.put(0xe, (byte) uprightConfig.getHRRAdvancedAerobic())
.put(0xf, (byte) uprightConfig.getHRRLactate())
.put(0x10, (byte) uprightConfig.getHRRBasicAnaerobic())
.put(0x11, (byte) uprightConfig.getHRRAdvancedAnaerobic());
}
if (uprightConfig.hasValidLTHRData()) {
tlv.put(0x3f, (byte) uprightConfig.getLTHRThresholdHeartRate())
.put(0x40, (byte) uprightConfig.getLTHRAnaerobic())
.put(0x41, (byte) uprightConfig.getLTHRLactate())
.put(0x42, (byte) uprightConfig.getLTHRAdvancedAerobic())
.put(0x43, (byte) uprightConfig.getLTHRBasicAerobic())
.put(0x44, (byte) uprightConfig.getLTHRWarmUp());
}
if (sittingConfig.hasValidMHRData()) {
tlv.put(0x12, (byte) (sittingConfig.getWarningEnable() ? 1 : 0))
.put(0x13, (byte) sittingConfig.getCalculateMethod())
.put(0x14, (byte) sittingConfig.getWarningHRLimit())
.put(0x15, (byte) sittingConfig.getMHRWarmUp())
.put(0x16, (byte) sittingConfig.getMHRFatBurning())
.put(0x17, (byte) sittingConfig.getMHRAerobic())
.put(0x18, (byte) sittingConfig.getMHRAnaerobic())
.put(0x19, (byte) sittingConfig.getMHRExtreme())
.put(0x1a, (byte) sittingConfig.getMaxHRThreshold());
}
if (sittingConfig.hasValidHRRData()) {
tlv.put(0x1b, (byte) sittingConfig.getRestHeartRate())
.put(0x1c, (byte) sittingConfig.getHRRBasicAerobic())
.put(0x1d, (byte) sittingConfig.getHRRAdvancedAerobic())
.put(0x1e, (byte) sittingConfig.getHRRLactate())
.put(0x1f, (byte) sittingConfig.getHRRBasicAnaerobic())
.put(0x20, (byte) sittingConfig.getHRRAdvancedAnaerobic());
}
if (swimmingConfig.hasValidMHRData()) {
tlv.put(0x21, (byte) (swimmingConfig.getWarningEnable() ? 1 : 0))
.put(0x22, (byte) swimmingConfig.getCalculateMethod())
.put(0x23, (byte) swimmingConfig.getWarningHRLimit())
.put(0x24, (byte) swimmingConfig.getMHRWarmUp())
.put(0x25, (byte) swimmingConfig.getMHRFatBurning())
.put(0x26, (byte) swimmingConfig.getMHRAerobic())
.put(0x27, (byte) swimmingConfig.getMHRAnaerobic())
.put(0x28, (byte) swimmingConfig.getMHRExtreme())
.put(0x29, (byte) swimmingConfig.getMaxHRThreshold());
}
if (swimmingConfig.hasValidHRRData()) {
tlv.put(0x2a, (byte) swimmingConfig.getRestHeartRate())
.put(0x2b, (byte) swimmingConfig.getHRRBasicAerobic())
.put(0x2c, (byte) swimmingConfig.getHRRAdvancedAerobic())
.put(0x2d, (byte) swimmingConfig.getHRRLactate())
.put(0x2e, (byte) swimmingConfig.getHRRBasicAnaerobic())
.put(0x2f, (byte) swimmingConfig.getHRRAdvancedAnaerobic());
}
if (otherConfig.hasValidMHRData()) {
tlv.put(0x30, (byte) (otherConfig.getWarningEnable() ? 1 : 0))
.put(0x31, (byte) otherConfig.getCalculateMethod())
.put(0x32, (byte) otherConfig.getWarningHRLimit())
.put(0x33, (byte) otherConfig.getMHRWarmUp())
.put(0x34, (byte) otherConfig.getMHRFatBurning())
.put(0x35, (byte) otherConfig.getMHRAerobic())
.put(0x36, (byte) otherConfig.getMHRAnaerobic())
.put(0x37, (byte) otherConfig.getMHRExtreme())
.put(0x38, (byte) otherConfig.getMaxHRThreshold());
}
if (otherConfig.hasValidHRRData()) {
tlv.put(0x39, (byte) otherConfig.getRestHeartRate())
.put(0x3a, (byte) otherConfig.getHRRBasicAerobic())
.put(0x3b, (byte) otherConfig.getHRRAdvancedAerobic())
.put(0x3c, (byte) otherConfig.getHRRLactate())
.put(0x3d, (byte) otherConfig.getHRRBasicAnaerobic())
.put(0x3e, (byte) otherConfig.getHRRAdvancedAnaerobic());
}
return tlv.serialize();
}
}
@@ -20,11 +20,13 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiReportThreshold;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiRunPaceConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesConfig;
public class FitnessData {
@@ -618,35 +620,32 @@ public class FitnessData {
HuaweiTLV subTlv = new HuaweiTLV().
put(0x08, heartRateZonesConfig.getWarningEnable());
if (
heartRateZonesConfig.hasValidMHRData() &&
heartRateZonesConfig.getWarningHRLimit() > 0 &&
heartRateZonesConfig.getMaxHRThreshold() > 0
) {
HeartRateZones mhr = HuaweiHeartRateZonesSpec.getByMethod(heartRateZonesConfig, HeartRateZones.CalculationMethod.MHR);
if (mhr != null && mhr.hasValidData() && heartRateZonesConfig.getWarningHRLimit() > 0 && mhr.getHRThreshold() > 0) {
subTlv
.put(0x09, (byte) heartRateZonesConfig.getWarningHRLimit())
.put(0x02, (byte) heartRateZonesConfig.getMHRWarmUp())
.put(0x03, (byte) heartRateZonesConfig.getMHRFatBurning())
.put(0x04, (byte) heartRateZonesConfig.getMHRAerobic())
.put(0x05, (byte) heartRateZonesConfig.getMHRAnaerobic())
.put(0x06, (byte) heartRateZonesConfig.getMHRExtreme())
.put(0x07, (byte) heartRateZonesConfig.getMaxHRThreshold())
.put(0x0b, (byte) heartRateZonesConfig.getMaxHRThreshold());
.put(0x02, (byte) mhr.getZone1())
.put(0x03, (byte) mhr.getZone2())
.put(0x04, (byte) mhr.getZone3())
.put(0x05, (byte) mhr.getZone4())
.put(0x06, (byte) mhr.getZone5())
.put(0x07, (byte) mhr.getHRThreshold())
.put(0x0b, (byte) mhr.getHRThreshold());
}
if (id == id_extended && heartRateZonesConfig.hasValidHRRData()) {
subTlv
.put(0x0d, (byte) heartRateZonesConfig.getHRRBasicAerobic())
.put(0x0e, (byte) heartRateZonesConfig.getHRRAdvancedAerobic())
.put(0x0f, (byte) heartRateZonesConfig.getHRRLactate())
.put(0x10, (byte) heartRateZonesConfig.getHRRBasicAnaerobic())
.put(0x11, (byte) heartRateZonesConfig.getHRRAdvancedAnaerobic());
}
if (id == id_extended && heartRateZonesConfig.getRestHeartRate() > 0) {
subTlv
.put(0x0a, (byte) heartRateZonesConfig.getCalculateMethod())
.put(0x0c, (byte) heartRateZonesConfig.getRestHeartRate());
if (id == id_extended) {
HeartRateZones hrr = HuaweiHeartRateZonesSpec.getByMethod(heartRateZonesConfig, HeartRateZones.CalculationMethod.HRR);
if (hrr != null && hrr.hasValidData()) {
subTlv
.put(0x0d, (byte) hrr.getZone1())
.put(0x0e, (byte) hrr.getZone2())
.put(0x0f, (byte) hrr.getZone3())
.put(0x10, (byte) hrr.getZone4())
.put(0x11, (byte) hrr.getZone5())
.put(0x0a, (byte) HuaweiHeartRateZonesSpec.toHuaweiCalculationMethod(heartRateZonesConfig.getCurrentCalculationMethod()))
.put(0x0c, (byte) hrr.getHRResting());
}
}
this.tlv = new HuaweiTLV().put(0x81, subTlv);
@@ -732,8 +731,8 @@ public class FitnessData {
this.tlv = new HuaweiTLV()
.put(0x01, enabled);
if(enabled)
this.tlv.put(0x02, highHeartRateAlert);
if (enabled)
this.tlv.put(0x02, highHeartRateAlert);
this.isEncrypted = true;
this.complete = true;
@@ -753,7 +752,7 @@ public class FitnessData {
this.tlv = new HuaweiTLV()
.put(0x01, enabled);
if(enabled)
if (enabled)
this.tlv.put(0x02, lowHeartRateAlert);
this.isEncrypted = true;
@@ -811,7 +810,7 @@ public class FitnessData {
this.tlv = new HuaweiTLV()
.put(0x01, enabled);
if(enabled)
if (enabled)
this.tlv.put(0x02, lowHeartRateAlert);
this.isEncrypted = true;
@@ -0,0 +1,145 @@
/* Copyright (C) 2025 Me7c7
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.model.heartratezones;
import android.content.Context;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
public abstract class HeartRateZones {
public enum CalculationMethod {
MHR,
HRR,
LTHR,
}
public static String methodToString(Context context, CalculationMethod method) {
return switch (method) {
case MHR -> context.getString(R.string.hr_settings_zones_calculation_method_mhr);
case HRR -> context.getString(R.string.hr_settings_zones_calculation_method_hrr);
case LTHR -> context.getString(R.string.hr_settings_zones_calculation_method_lthr);
};
}
protected final CalculationMethod method;
protected int HRThreshold;
protected int HRResting;
protected int zone5;
protected int zone4;
protected int zone3;
protected int zone2;
protected int zone1;
public HeartRateZones(CalculationMethod method, int HRThreshold) {
this.method = method;
this.HRThreshold = HRThreshold;
this.HRResting = HeartRateZonesUtils.DEFAULT_REST_HEART_RATE;
reset();
}
public abstract void reset();
public abstract long getPercentage(int zone);
public CalculationMethod getMethod() {
return method;
}
public int getHRThreshold() {
return HRThreshold;
}
public void setHRThreshold(int HRThreshold) {
this.HRThreshold = HRThreshold;
}
public int getHRResting() {
return HRResting;
}
public void setHRResting(int HRResting) {
this.HRResting = HRResting;
}
public int getZone5() {
return zone5;
}
public void setZone5(int zone5) {
this.zone5 = zone5;
}
public int getZone4() {
return zone4;
}
public void setZone4(int zone4) {
this.zone4 = zone4;
}
public int getZone3() {
return zone3;
}
public void setZone3(int zone3) {
this.zone3 = zone3;
}
public int getZone2() {
return zone2;
}
public void setZone2(int zone2) {
this.zone2 = zone2;
}
public int getZone1() {
return zone1;
}
public void setZone1(int zone1) {
this.zone1 = zone1;
}
public boolean isValid() {
return HeartRateZonesUtils.checkValue(this.HRThreshold) &&
HeartRateZonesUtils.checkValue(this.HRResting) &&
HeartRateZonesUtils.checkValue(this.zone1) &&
HeartRateZonesUtils.checkValue(this.zone2) &&
HeartRateZonesUtils.checkValue(this.zone3) &&
HeartRateZonesUtils.checkValue(this.zone4) &&
HeartRateZonesUtils.checkValue(this.zone5) &&
this.zone1 <= this.zone2 &&
this.zone2 <= this.zone3 &&
this.zone3 <= this.zone4 &&
this.zone4 <= this.zone5;
}
public boolean hasValidData() {
boolean valid = this.zone1 > 0 && this.zone2 > 0 && this.zone3 > 0 && this.zone4 > 0 && this.zone5 > 0 && this.HRThreshold > 0;
if (method == CalculationMethod.HRR) {
valid &= this.HRResting >0;
}
return valid;
}
}
@@ -0,0 +1,87 @@
/* Copyright (C) 2025 Me7c7
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.model.heartratezones;
import java.util.List;
public class HeartRateZonesConfig {
private final HeartRateZonesSpec.PostureType type;
// warnings
private boolean warningEnable;
private int warningHRLimit;
private HeartRateZones.CalculationMethod currentCalculationMethod;
private final List<HeartRateZones> configByMethods;
public HeartRateZonesConfig(HeartRateZonesSpec.PostureType type, boolean warningEnable, int warningHRLimit, HeartRateZones.CalculationMethod currentCalculationMethod, List<HeartRateZones> configByMethods) {
this.type = type;
this.warningEnable = warningEnable;
this.warningHRLimit = warningHRLimit;
this.currentCalculationMethod = currentCalculationMethod;
this.configByMethods = configByMethods;
}
public HeartRateZonesSpec.PostureType getType() {
return type;
}
public boolean getWarningEnable() {
return warningEnable;
}
public void setWarningEnable(boolean warningEnable) {
this.warningEnable = warningEnable;
}
public int getWarningHRLimit() {
return warningHRLimit;
}
public void setWarningHRLimit(int warningHRLimit) {
this.warningHRLimit = warningHRLimit;
}
public HeartRateZones.CalculationMethod getCurrentCalculationMethod() {
return currentCalculationMethod;
}
public void setCurrentCalculationMethod(HeartRateZones.CalculationMethod currentCalculationMethod) {
this.currentCalculationMethod = currentCalculationMethod;
}
public List<HeartRateZones> getConfigByMethods() {
return configByMethods;
}
public void reset() {
for(HeartRateZones zn: configByMethods) {
zn.reset();
}
}
public boolean isValid() {
boolean valid = HeartRateZonesUtils.checkValue(this.warningHRLimit) && this.warningHRLimit > 0 && !configByMethods.isEmpty();
for (HeartRateZones cfg : configByMethods) {
valid &= cfg.isValid();
}
return valid;
}
}
@@ -0,0 +1,116 @@
/* Copyright (C) 2025 Me7c7
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.model.heartratezones;
import android.content.Context;
import android.content.SharedPreferences;
import com.google.gson.Gson;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
public abstract class HeartRateZonesSpec {
public enum PostureType {
UPRIGHT,
SITTING,
SWIMMING,
OTHER
}
public static final String HR_ZONES_PREF_KEY_PREFIX = "heart_rate_zones_";
public static final String HR_ZONES_PREF_KEY = HR_ZONES_PREF_KEY_PREFIX + "posture_%s_method_%s";
public static final String HR_ZONES_PREF_WARNING_ENABLED_KEY = HR_ZONES_PREF_KEY_PREFIX + "posture_%s_warning_enabled";
public static final String HR_ZONES_PREF_HEART_RATE_LIMIT_KEY = HR_ZONES_PREF_KEY_PREFIX + "posture_%s_heart_rate_limit";
public static final String HR_ZONES_PREF_CALCULATE_METHOD_KEY = HR_ZONES_PREF_KEY_PREFIX + "posture_%s_calculate_method";
protected final GBDevice device;
public HeartRateZonesSpec(GBDevice device) {
this.device = device;
}
public abstract String getNameByType(Context context, PostureType type);
public abstract List<HeartRateZonesConfig> getDeviceConfig();
private String getPrefKey(HeartRateZonesSpec.PostureType type, HeartRateZones.CalculationMethod method) {
return String.format(Locale.ROOT, HR_ZONES_PREF_KEY, type.toString(), method.toString());
}
protected String getHrZoneConfig(PostureType type, HeartRateZones.CalculationMethod method) {
DevicePrefs devicePreferences = GBApplication.getDevicePrefs(device);
String key = getPrefKey(type, method);
return devicePreferences.getString(key, null);
}
protected HeartRateZonesConfig loadOrCreateHeartRateZonesConfig(PostureType type, boolean warningEnable, int warningHRLimit, HeartRateZones.CalculationMethod currentCalculationMethod, List<HeartRateZones> configByMethods) {
DevicePrefs devicePreferences = GBApplication.getDevicePrefs(device);
warningEnable = devicePreferences.getBoolean(String.format(Locale.ROOT, HR_ZONES_PREF_WARNING_ENABLED_KEY, type.toString()), warningEnable);
warningHRLimit = devicePreferences.getInt(String.format(Locale.ROOT, HR_ZONES_PREF_HEART_RATE_LIMIT_KEY, type.toString()), warningHRLimit);
HeartRateZones.CalculationMethod[] values = HeartRateZones.CalculationMethod.values();
int val = devicePreferences.getInt(String.format(Locale.ROOT, HR_ZONES_PREF_CALCULATE_METHOD_KEY, type.toString()), currentCalculationMethod.ordinal());
if (val >= 0 && val < values.length) {
currentCalculationMethod = values[val];
}
return new HeartRateZonesConfig(type,
warningEnable,
warningHRLimit,
currentCalculationMethod,
configByMethods
);
}
public void saveConfig(HeartRateZonesConfig config) {
final DevicePrefs devicePreferences = GBApplication.getDevicePrefs(device);
final SharedPreferences.Editor editor = devicePreferences.getPreferences().edit();
editor.putBoolean(String.format(Locale.ROOT, HR_ZONES_PREF_WARNING_ENABLED_KEY, config.getType().toString()), config.getWarningEnable());
editor.putInt(String.format(Locale.ROOT, HR_ZONES_PREF_HEART_RATE_LIMIT_KEY, config.getType().toString()), config.getWarningHRLimit());
editor.putInt(String.format(Locale.ROOT, HR_ZONES_PREF_CALCULATE_METHOD_KEY, config.getType().toString()), config.getCurrentCalculationMethod().ordinal());
Gson gson = new Gson();
for (HeartRateZones zn : config.getConfigByMethods()) {
String key = getPrefKey(config.getType(), zn.getMethod());
editor.putString(key, gson.toJson(zn));
}
editor.apply();
}
public void clearConfig() {
final DevicePrefs devicePreferences = GBApplication.getDevicePrefs(device);
SharedPreferences sharedPrefs = devicePreferences.getPreferences();
SharedPreferences.Editor editor = sharedPrefs.edit();
Map<String, ?> allEntries = sharedPrefs.getAll();
for (Map.Entry<String, ?> entry : allEntries.entrySet()) {
if(entry.getKey().startsWith(HR_ZONES_PREF_KEY_PREFIX)) {
editor.remove(entry.getKey());
}
}
editor.apply();
}
}
@@ -0,0 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.model.heartratezones;
public class HeartRateZonesUtils {
public static final int MAXIMUM_HEART_RATE = 220;
public static final int DEFAULT_REST_HEART_RATE = 60;
public static boolean checkValue(int val) {
return val >= 0 && val < MAXIMUM_HEART_RATE;
}
}
@@ -52,8 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.Activity
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSportHRZones;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@@ -75,8 +74,9 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.workout.WorkoutChart;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
@@ -798,7 +798,7 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
//NOTE: The method of retrieving HR zones from the Huawei watch is not discovered. It may not return zones.
// So they are calculated based on config.
HeartRateZonesConfig HRZonesCfg = null;
HeartRateZones HRZonesCfg = null;
Integer zonePostureType = parseAndValidatePostureType(additionalValues.get("postureType"));
LOG.info("Workout HR Zone Workout Posture Type: {}", zonePostureType);
@@ -809,10 +809,9 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
int zoneCalculateMethod = summary.getHrZoneType();
LOG.info("Workout HR Zone Calculate Type: {}", zoneCalculateMethod);
if (zonePostureType != null && HeartRateZonesConfig.isCalculateMethodValidForPostureType(zonePostureType, zoneCalculateMethod)) {
ActivityUser activityUser = new ActivityUser();
HuaweiSportHRZones hrSportZones = new HuaweiSportHRZones(activityUser.getAge());
HRZonesCfg = hrSportZones.getHRZonesConfigByType(zonePostureType);
if (zonePostureType != null) {
HeartRateZonesSpec spec = gbDevice.getDeviceCoordinator().getHeartRateZonesSpec(gbDevice);
HRZonesCfg = HuaweiHeartRateZonesSpec.getHRZonesConfigByPostureAndCalculationMethod(spec.getDeviceConfig(), HuaweiHeartRateZonesSpec.fromHuaweiPostureType(zonePostureType), HuaweiHeartRateZonesSpec.fromHuaweiCalculationMethod(zoneCalculateMethod));
}
int dataDelta = 5;
@@ -830,7 +829,7 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
if (HRZonesCfg != null) {
int zoneIdx = HRZonesCfg.getZoneByMethod(dataSample.getHeartRate() & 0xFF, zoneCalculateMethod);
int zoneIdx = HuaweiHeartRateZonesSpec.getZoneForHR(dataSample.getHeartRate() & 0xFF, HRZonesCfg);
if (zoneIdx != -1 && dataIdx < (dataSamples.size() - 1)) {
HRZones[zoneIdx] += dataDelta;
}
@@ -20,7 +20,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class HuaweiWorkoutUtils {
@@ -29,27 +29,27 @@ public class HuaweiWorkoutUtils {
//TODO: discover and add more activity types. Should be same as in the watch.
private static Map<ActivityKind, Integer> createActivityHRZoneType() {
final Map<ActivityKind, Integer> result = new HashMap<>();
result.put(ActivityKind.RUNNING, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.WALKING, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.CYCLING, HeartRateZonesConfig.TYPE_SITTING);
result.put(ActivityKind.MOUNTAIN_HIKE, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.INDOOR_RUNNING, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.POOL_SWIM, HeartRateZonesConfig.TYPE_SWIMMING);
result.put(ActivityKind.INDOOR_CYCLING, HeartRateZonesConfig.TYPE_SITTING);
result.put(ActivityKind.SWIMMING_OPENWATER, HeartRateZonesConfig.TYPE_SWIMMING);
result.put(ActivityKind.INDOOR_WALKING, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.HIKING, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.JUMP_ROPING, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.PINGPONG, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.BADMINTON, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.TENNIS, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.SOCCER, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.BASKETBALL, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.VOLLEYBALL, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.ELLIPTICAL_TRAINER, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.ROWING_MACHINE, HeartRateZonesConfig.TYPE_SITTING);
result.put(ActivityKind.STEPPER, HeartRateZonesConfig.TYPE_UPRIGHT);
result.put(ActivityKind.YOGA, HeartRateZonesConfig.TYPE_OTHER);
result.put(ActivityKind.RUNNING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.WALKING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.CYCLING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_SITTING);
result.put(ActivityKind.MOUNTAIN_HIKE, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.INDOOR_RUNNING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.POOL_SWIM, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_SWIMMING);
result.put(ActivityKind.INDOOR_CYCLING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_SITTING);
result.put(ActivityKind.SWIMMING_OPENWATER, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_SWIMMING);
result.put(ActivityKind.INDOOR_WALKING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.HIKING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.JUMP_ROPING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.PINGPONG, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.BADMINTON, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.TENNIS, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.SOCCER, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.BASKETBALL, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.VOLLEYBALL, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.ELLIPTICAL_TRAINER, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.ROWING_MACHINE, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_SITTING);
result.put(ActivityKind.STEPPER, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
result.put(ActivityKind.YOGA, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_OTHER);
return Collections.unmodifiableMap(result);
}
@@ -21,10 +21,15 @@ import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSportHRZones;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesConfig;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiP2PManager;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class HuaweiP2PTrackService extends HuaweiBaseP2PService {
private final Logger LOG = LoggerFactory.getLogger(HuaweiP2PTrackService.class);
@@ -70,13 +75,151 @@ public class HuaweiP2PTrackService extends HuaweiBaseP2PService {
return "SystemApp";
}
private boolean isNotValid(HeartRateZonesConfig cfg) {
if(cfg == null)
return true;
return !cfg.isValid();
}
private boolean isNotValidZone(HeartRateZones cfg) {
if(cfg == null)
return true;
return !cfg.isValid();
}
public byte[] getHRZonesData() {
HuaweiHeartRateZonesSpec spec = new HuaweiHeartRateZonesSpec(manager.getSupportProvider().getDevice(), manager.getSupportProvider().getHuaweiCoordinator());
List<HeartRateZonesConfig> zones = spec.getDeviceConfig();
HeartRateZonesConfig uprightConfig = HuaweiHeartRateZonesSpec.getByPosture(zones, HeartRateZonesSpec.PostureType.UPRIGHT);
HeartRateZonesConfig sittingConfig = HuaweiHeartRateZonesSpec.getByPosture(zones, HeartRateZonesSpec.PostureType.SITTING);
HeartRateZonesConfig swimmingConfig = HuaweiHeartRateZonesSpec.getByPosture(zones, HeartRateZonesSpec.PostureType.SWIMMING);
HeartRateZonesConfig otherConfig = HuaweiHeartRateZonesSpec.getByPosture(zones, HeartRateZonesSpec.PostureType.OTHER);
if (isNotValid(uprightConfig) || isNotValid(sittingConfig) || isNotValid(swimmingConfig) || isNotValid(otherConfig)) {
return null;
}
HeartRateZones uprightMhr = HuaweiHeartRateZonesSpec.getByMethod(uprightConfig, HeartRateZones.CalculationMethod.MHR);
HeartRateZones uprightHrr = HuaweiHeartRateZonesSpec.getByMethod(uprightConfig, HeartRateZones.CalculationMethod.HRR);
HeartRateZones uprightLthr = HuaweiHeartRateZonesSpec.getByMethod(uprightConfig, HeartRateZones.CalculationMethod.LTHR);
if(isNotValidZone(uprightMhr) || isNotValidZone(uprightHrr) || isNotValidZone(uprightLthr)) {
return null;
}
HeartRateZones sittingMhr = HuaweiHeartRateZonesSpec.getByMethod(sittingConfig, HeartRateZones.CalculationMethod.MHR);
HeartRateZones sittingHrr = HuaweiHeartRateZonesSpec.getByMethod(sittingConfig, HeartRateZones.CalculationMethod.HRR);
if(isNotValidZone(sittingMhr) || isNotValidZone(sittingHrr)) {
return null;
}
HeartRateZones swimmingMhr = HuaweiHeartRateZonesSpec.getByMethod(swimmingConfig, HeartRateZones.CalculationMethod.MHR);
HeartRateZones swimmingHrr = HuaweiHeartRateZonesSpec.getByMethod(swimmingConfig, HeartRateZones.CalculationMethod.HRR);
if(isNotValidZone(swimmingMhr) || isNotValidZone(swimmingHrr)) {
return null;
}
HeartRateZones otherMhr = HuaweiHeartRateZonesSpec.getByMethod(otherConfig, HeartRateZones.CalculationMethod.MHR);
HeartRateZones otherHrr = HuaweiHeartRateZonesSpec.getByMethod(otherConfig, HeartRateZones.CalculationMethod.HRR);
if(isNotValidZone(otherMhr) || isNotValidZone(otherHrr)) {
return null;
}
HuaweiTLV tlv = new HuaweiTLV();
if (uprightMhr.hasValidData()) {
tlv.put(0x2, (byte) uprightMhr.getZone1())
.put(0x3, (byte) uprightMhr.getZone2())
.put(0x4, (byte) uprightMhr.getZone3())
.put(0x5, (byte) uprightMhr.getZone4())
.put(0x6, (byte) uprightMhr.getZone5())
.put(0x7, (byte) uprightMhr.getHRThreshold())
.put(0x8, (byte) (uprightConfig.getWarningEnable() ? 1 : 0))
.put(0x9, (byte) uprightConfig.getWarningHRLimit())
.put(0xa, (byte) HuaweiHeartRateZonesSpec.toHuaweiCalculationMethod(uprightConfig.getCurrentCalculationMethod()))
.put(0xb, (byte) uprightMhr.getHRThreshold());
}
tlv.put(0xc, (byte) uprightHrr.getHRResting());
if (uprightHrr.hasValidData()) {
tlv.put(0xd, (byte) uprightHrr.getZone1())
.put(0xe, (byte) uprightHrr.getZone2())
.put(0xf, (byte) uprightHrr.getZone3())
.put(0x10, (byte) uprightHrr.getZone4())
.put(0x11, (byte) uprightHrr.getZone5());
}
if (uprightLthr.hasValidData()) {
tlv.put(0x3f, (byte) uprightLthr.getHRThreshold())
.put(0x40, (byte) uprightLthr.getZone5())
.put(0x41, (byte) uprightLthr.getZone4())
.put(0x42, (byte) uprightLthr.getZone3())
.put(0x43, (byte) uprightLthr.getZone2())
.put(0x44, (byte) uprightLthr.getZone1());
}
if (sittingMhr.hasValidData()) {
tlv.put(0x12, (byte) (sittingConfig.getWarningEnable() ? 1 : 0))
.put(0x13, (byte) HuaweiHeartRateZonesSpec.toHuaweiCalculationMethod(sittingConfig.getCurrentCalculationMethod()))
.put(0x14, (byte) sittingConfig.getWarningHRLimit())
.put(0x15, (byte) sittingMhr.getZone1())
.put(0x16, (byte) sittingMhr.getZone2())
.put(0x17, (byte) sittingMhr.getZone3())
.put(0x18, (byte) sittingMhr.getZone4())
.put(0x19, (byte) sittingMhr.getZone5())
.put(0x1a, (byte) sittingMhr.getHRThreshold());
}
if (sittingHrr.hasValidData()) {
tlv.put(0x1b, (byte) sittingHrr.getHRResting())
.put(0x1c, (byte) sittingHrr.getZone1())
.put(0x1d, (byte) sittingHrr.getZone2())
.put(0x1e, (byte) sittingHrr.getZone3())
.put(0x1f, (byte) sittingHrr.getZone4())
.put(0x20, (byte) sittingHrr.getZone5());
}
if (swimmingMhr.hasValidData()) {
tlv.put(0x21, (byte) (swimmingConfig.getWarningEnable() ? 1 : 0))
.put(0x22, (byte) HuaweiHeartRateZonesSpec.toHuaweiCalculationMethod(swimmingConfig.getCurrentCalculationMethod()))
.put(0x23, (byte) swimmingConfig.getWarningHRLimit())
.put(0x24, (byte) swimmingMhr.getZone1())
.put(0x25, (byte) swimmingMhr.getZone2())
.put(0x26, (byte) swimmingMhr.getZone3())
.put(0x27, (byte) swimmingMhr.getZone4())
.put(0x28, (byte) swimmingMhr.getZone5())
.put(0x29, (byte) swimmingMhr.getHRThreshold());
}
if (swimmingHrr.hasValidData()) {
tlv.put(0x2a, (byte) swimmingHrr.getHRResting())
.put(0x2b, (byte) swimmingHrr.getZone1())
.put(0x2c, (byte) swimmingHrr.getZone2())
.put(0x2d, (byte) swimmingHrr.getZone3())
.put(0x2e, (byte) swimmingHrr.getZone4())
.put(0x2f, (byte) swimmingHrr.getZone5());
}
if (otherMhr.hasValidData()) {
tlv.put(0x30, (byte) (otherConfig.getWarningEnable() ? 1 : 0))
.put(0x31, (byte) HuaweiHeartRateZonesSpec.toHuaweiCalculationMethod(otherConfig.getCurrentCalculationMethod()))
.put(0x32, (byte) otherConfig.getWarningHRLimit())
.put(0x33, (byte) otherMhr.getZone1())
.put(0x34, (byte) otherMhr.getZone2())
.put(0x35, (byte) otherMhr.getZone3())
.put(0x36, (byte) otherMhr.getZone4())
.put(0x37, (byte) otherMhr.getZone5())
.put(0x38, (byte) otherMhr.getHRThreshold());
}
if (otherHrr.hasValidData()) {
tlv.put(0x39, (byte) otherHrr.getHRResting())
.put(0x3a, (byte) otherHrr.getZone1())
.put(0x3b, (byte) otherHrr.getZone2())
.put(0x3c, (byte) otherHrr.getZone3())
.put(0x3d, (byte) otherHrr.getZone4())
.put(0x3e, (byte) otherHrr.getZone5());
}
return tlv.serialize();
}
public void sendHeartZoneConfig() {
ActivityUser activityUser = new ActivityUser();
HuaweiSportHRZones hrZones = new HuaweiSportHRZones(activityUser.getAge());
byte[] data = hrZones.getHRZonesData();
byte[] data = getHRZonesData();
if (data == null) {
LOG.error("Incorrect Heart Rate config");
return;
@@ -18,10 +18,11 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesConfig;
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendHeartRateZonesConfig extends Request {
@@ -44,7 +45,9 @@ public class SendHeartRateZonesConfig extends Request {
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
HeartRateZonesConfig heartRateZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_UPRIGHT, new ActivityUser().getAge());
HuaweiHeartRateZonesSpec spec = new HuaweiHeartRateZonesSpec(supportProvider.getDevice(), supportProvider.getHuaweiCoordinator());
List<HeartRateZonesConfig> zones = spec.getDeviceConfig();
HeartRateZonesConfig heartRateZonesConfig = HuaweiHeartRateZonesSpec.getByPosture(zones, HeartRateZonesSpec.PostureType.UPRIGHT);
if (supportProvider.getHuaweiCoordinator().supportsExtendedHeartRateZones()) {
return FitnessData.HeartRateZoneConfigPacket.Request.requestExtended(paramsProvider, heartRateZonesConfig).serialize();
} else {
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/hr_settings_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/hr_settings_limits"
layout="@layout/fragment_heart_rate_settings_limit" />
<include
android:id="@+id/hr_settings_zones"
layout="@layout/fragment_heart_rate_settings_zones" />
<Button
android:id="@+id/save_hr_zones_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/save_configuration" />
</LinearLayout>
</ScrollView>
</LinearLayout>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/pref_heart_rate_high_alert" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/high_hr_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/hr_settings_hr_limit" />
<TextView
android:id="@+id/limit_high_hr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
@@ -0,0 +1,402 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/hr_settings_zones_calculation_method" />
<Spinner
android:id="@+id/calculation_method_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/zones_max_hr_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/hr_settings_zones_max_hr" />
<TextView
android:id="@+id/zones_max_hr"
android:gravity="end"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_zones_resting_hr"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/hr_settings_zones_resting_hr" />
<TextView
android:id="@+id/zones_resting_hr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="end" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/hr_zone_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:gravity="center"
android:minHeight="10dp"
android:text="Zone"
android:textColor="#777777"
android:textSize="15sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/hr_value_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="5sp"
android:gravity="center"
android:minWidth="100dp"
android:minHeight="15dp"
android:text="@string/heart_rate"
android:textColor="#777777"
android:textSize="15sp"
app:layout_constraintEnd_toStartOf="@+id/hr_percentage_title"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/hr_percentage_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:minWidth="100dp"
android:minHeight="15dp"
android:text="%"
android:textColor="#777777"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RelativeLayout
android:id="@+id/layout_hr_zone_5"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/heart_rate_zone_5_divider"
app:layout_constraintBottom_toTopOf="@+id/layout_hr_zone_4"
app:layout_constraintEnd_toStartOf="@+id/heart_rate_zone_5_divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/layout_hr_zone_values">
<TextView
android:id="@+id/tv_heart_rate_zone_5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Zone 5" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_hr_zone_4"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/heart_rate_zone_4_divider"
app:layout_constraintBottom_toTopOf="@+id/layout_hr_zone_3"
app:layout_constraintEnd_toStartOf="@+id/heart_rate_zone_4_divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layout_hr_zone_5">
<TextView
android:id="@+id/tv_heart_rate_zone_4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Zone 4" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_hr_zone_3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/heart_rate_zone_3_divider"
app:layout_constraintBottom_toTopOf="@+id/layout_hr_zone_2"
app:layout_constraintEnd_toStartOf="@+id/heart_rate_zone_3_divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layout_hr_zone_4">
<TextView
android:id="@+id/tv_heart_rate_zone_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Zone 3" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_hr_zone_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/heart_rate_zone_2_divider"
app:layout_constraintBottom_toTopOf="@+id/layout_hr_zone_1"
app:layout_constraintEnd_toStartOf="@+id/heart_rate_zone_2_divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layout_hr_zone_3">
<TextView
android:id="@+id/tv_heart_rate_zone_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Zone 2" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_hr_zone_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_toStartOf="@+id/heart_rate_zone_1_divider"
app:layout_constraintBottom_toBottomOf="@+id/layout_hr_zone_values"
app:layout_constraintEnd_toStartOf="@+id/heart_rate_zone_1_divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layout_hr_zone_2">
<TextView
android:id="@+id/tv_heart_rate_zone_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Zone 1" />
</RelativeLayout>
<nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateZonesDividerView
android:id="@+id/heart_rate_zone_5_divider"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="6dp"
android:layout_marginTop="25dp"
android:layout_marginBottom="5dp"
app:layout_constraintBottom_toTopOf="@+id/heart_rate_zone_4_divider"
app:layout_constraintEnd_toStartOf="@+id/layout_hr_zone_values"
app:layout_constraintStart_toEndOf="@+id/layout_hr_zone_5"
app:layout_constraintTop_toTopOf="@+id/layout_hr_zone_values" />
<nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateZonesDividerView
android:id="@+id/heart_rate_zone_4_divider"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="6dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
app:layout_constraintBottom_toTopOf="@+id/heart_rate_zone_3_divider"
app:layout_constraintEnd_toStartOf="@+id/layout_hr_zone_values"
app:layout_constraintStart_toEndOf="@+id/layout_hr_zone_4"
app:layout_constraintTop_toBottomOf="@+id/heart_rate_zone_5_divider" />
<nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateZonesDividerView
android:id="@+id/heart_rate_zone_3_divider"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="6dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
app:layout_constraintBottom_toTopOf="@+id/heart_rate_zone_2_divider"
app:layout_constraintEnd_toStartOf="@+id/layout_hr_zone_values"
app:layout_constraintStart_toEndOf="@+id/layout_hr_zone_3"
app:layout_constraintTop_toBottomOf="@+id/heart_rate_zone_4_divider" />
<nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateZonesDividerView
android:id="@+id/heart_rate_zone_2_divider"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="6dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
app:layout_constraintBottom_toTopOf="@+id/heart_rate_zone_1_divider"
app:layout_constraintEnd_toStartOf="@+id/layout_hr_zone_values"
app:layout_constraintStart_toEndOf="@+id/layout_hr_zone_2"
app:layout_constraintTop_toBottomOf="@+id/heart_rate_zone_3_divider" />
<nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateZonesDividerView
android:id="@+id/heart_rate_zone_1_divider"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="6dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="25dp"
app:layout_constraintBottom_toBottomOf="@+id/layout_hr_zone_values"
app:layout_constraintEnd_toStartOf="@+id/layout_hr_zone_values"
app:layout_constraintStart_toEndOf="@+id/layout_hr_zone_1"
app:layout_constraintTop_toBottomOf="@+id/heart_rate_zone_2_divider" />
<LinearLayout
android:id="@+id/layout_hr_zone_values"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginEnd="6dp"
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@+id/hr_percentage_title"
app:layout_constraintStart_toStartOf="@+id/hr_value_title"
app:layout_constraintTop_toBottomOf="@+id/hr_percentage_title">
<TextView
android:id="@+id/heart_rate_zone_max_value"
android:layout_width="match_parent"
android:layout_height="35dp"
android:gravity="center"
android:text="@string/n_a" />
<TextView
android:id="@+id/heart_rate_zone_5_value"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/n_a" />
<TextView
android:id="@+id/heart_rate_zone_4_value"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/n_a" />
<TextView
android:id="@+id/heart_rate_zone_3_value"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/n_a" />
<TextView
android:id="@+id/heart_rate_zone_2_value"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/n_a" />
<TextView
android:id="@+id/heart_rate_zone_1_value"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/n_a" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_pace_range_percentage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5sp"
android:layout_marginEnd="6sp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="@+id/hr_percentage_title"
app:layout_constraintStart_toStartOf="@+id/hr_percentage_title"
app:layout_constraintTop_toBottomOf="@+id/hr_percentage_title">
<TextView
android:id="@+id/heart_rate_zone_max_percent"
android:layout_width="match_parent"
android:layout_height="35dp"
android:gravity="center"
android:text="@string/n_a" />
<TextView
android:id="@+id/heart_rate_zone_5_percent"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/n_a" />
<TextView
android:id="@+id/heart_rate_zone_4_percent"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/n_a" />
<TextView
android:id="@+id/heart_rate_zone_3_percent"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/n_a" />
<TextView
android:id="@+id/heart_rate_zone_2_percent"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/n_a" />
<TextView
android:id="@+id/heart_rate_zone_1_percent"
android:layout_width="match_parent"
android:layout_height="35dp"
android:layout_marginTop="28dp"
android:gravity="center"
android:text="@string/n_a" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activities.heartratezones.HeartRateSettingsFragment">
<item
android:id="@+id/hr_zones_reset_all"
android:icon="@drawable/ic_delete_forever"
android:title="@string/hr_settings_zones_remove_all"
app:iconTint="?attr/actionmenu_icon_color"
app:showAsAction="never" />
</menu>
+14
View File
@@ -4356,4 +4356,18 @@
<string name="huawei_send_gps_and_time_summary">Send GPS and Time to device for weather and search stars optimization. Location should be set in the settings</string>
<string name="garmin_mlr_protocol">Multi-Link Reliable Protocol</string>
<string name="feature_experimental_unstable">Experimental, might be unstable</string>
<string name="heart_rate_settings">Heart rate settings</string>
<string name="heart_rate_settings_summary">View current workout heart rate settings and zones</string>
<string name="workout_posture_type_upright">Upright</string>
<string name="workout_posture_type_sitting">Sitting</string>
<string name="hr_settings_hr_limit">Heart rate limit</string>
<string name="hr_settings_zones_calculation_method">Calculation method</string>
<string name="hr_settings_zones_calculation_method_mhr">MHR</string>
<string name="hr_settings_zones_calculation_method_hrr">HRR</string>
<string name="hr_settings_zones_calculation_method_lthr">LTHR</string>
<string name="hr_settings_zones_resting_hr">Resting heart rate</string>
<string name="hr_settings_zones_max_hr">Maximum heart rate</string>
<string name="hr_settings_zones_lactate_threshold_hr">Lactate threshold heart rate</string>
<string name="hr_settings_zones_remove_all">Reset all</string>
<string name="hr_settings_zones_remove_all_description">Are you sure you want to delete the heart rate zones and limits for all posture configurations and reset them to default?</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:key="pref_perform_heart_rate_settings"
android:icon="@drawable/ic_heartrate"
android:title="@string/heart_rate_settings"
android:summary="@string/heart_rate_settings_summary">
</Preference>
</androidx.preference.PreferenceScreen>