Skip to content

Commit b3d0463

Browse files
cagnuleinclaude
andauthored
Garmin Lap Workouts (#4619)
* Garmin Lap Workouts * Update trainprogram.cpp * Update dochartliveheart.js * lap button * fix max heart override * charts UI fix * margin settings * mail from Maksim 19/05/2026 * fixing mail 21/5/2026 * QZ advanced workout report from 24/5/2026 * HR zones tile and graph, disussion 24/5/2026 * mail 26/5/2026 first 3 points * power curve * ftp from garmin * Update garminconnect.cpp * 1/6/2026 mail * Update Home.qml * mail 5/6/2026 * Fix nested RepeatGroupDTO (e.g. 2x12 sprint) being silently dropped in Garmin workout XML When a RepeatGroupDTO contained another RepeatGroupDTO as an inner step (e.g. the outer 2× set wrapper around an inner 12× sprint block), appendGarminStep() was called on the inner group. Because that step has endCondition.conditionTypeKey="iterations" (not "time") and no targetType, it produced an empty <row/> with no attributes, which loadXML silently discarded — causing the entire sprint block to disappear from the plan. Introduce appendGarminSteps() which handles the mixed case: if all inner steps of a repeat group are leaf ExecutableStepDTOs, emit a <repeat times="N"> block as before; if any inner step is itself a RepeatGroupDTO, unroll the outer loop explicitly (since loadXML's single-level insideRepeat flag does not support nested <repeat> blocks). Add a regression test using the exact Sprint workout structure (warmup + 2×[12×(10s@497W+20s@93W)+5min rest] + cooldown) from the real debug log. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix redeclaration of QJsonObject main/outObj in templateinfosenderbuilder After merging master, the conflict resolution left duplicate declarations of main and outObj that were already declared earlier in the function. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix onInsetsChanged call to pass all 8 waterfall parameters Merge conflict resolution left the 4-param HEAD call but took master's 8-param native declaration, causing a Java compile error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * mail 19/06/2026 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 687dfcd commit b3d0463

28 files changed

Lines changed: 2130 additions & 299 deletions

docs/qz_faq_public.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ Sound alerts triggered by HR zone are not currently supported.
8888

8989
Follow this guide:
9090
https://github.com/cagnulein/qdomyos-zwift/wiki/How-do-i-get-the-debug-log-in-case-something-doesn't-work%3F
91-
9291
### 14. Calorie burn estimate looks way too high
9392
**Q: QZ shows an unrealistically high calorie burn (e.g. 30+ kcal/minute) - what's wrong?**
9493

@@ -116,4 +115,3 @@ After the reboot, QZ should advertise correctly and be visible to other apps/dev
116115

117116
Yes, QZ has a macOS build that runs on Apple Silicon. If you run into issues with your trainer not being detected or metrics not updating when both QZ and Zwift are on the same Mac, please share a debug log from a session that reproduces the problem so it can be diagnosed:
118117
https://github.com/cagnulein/qdomyos-zwift/wiki/How-do-i-get-the-debug-log-in-case-something-doesn't-work%3F
119-

src/android/src/CustomQtActivity.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
import android.view.WindowInsets;
1010
import android.view.WindowManager;
1111
import android.view.DisplayCutout;
12+
import android.graphics.Insets;
1213
import org.qtproject.qt5.android.bindings.QtActivity;
1314

1415
public class CustomQtActivity extends QtActivity {
1516
private static final String TAG = "CustomQtActivity";
1617

1718
// Declare the native method that will be implemented in C++
18-
private static native void onInsetsChanged(int top, int bottom, int left, int right);
19+
private static native void onInsetsChanged(int top, int bottom, int left, int right,
20+
int waterfallTop, int waterfallBottom,
21+
int waterfallLeft, int waterfallRight);
1922
private static native void nativeOnOAuthCallback(String callbackUrl);
2023

2124
private void dispatchOAuthCallback(Intent intent) {
@@ -63,6 +66,10 @@ public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
6366
int bottom = 0;
6467
int left = 0;
6568
int right = 0;
69+
int waterfallTop = 0;
70+
int waterfallBottom = 0;
71+
int waterfallLeft = 0;
72+
int waterfallRight = 0;
6673

6774
if (density > 0) {
6875
// Use system window insets as primary source
@@ -80,6 +87,15 @@ public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
8087
right = Math.max(right, Math.round(cutout.getSafeInsetRight() / density));
8188
top = Math.max(top, Math.round(cutout.getSafeInsetTop() / density));
8289
bottom = Math.max(bottom, Math.round(cutout.getSafeInsetBottom() / density));
90+
91+
// Android 11+ exposes curved waterfall display areas separately from cutouts.
92+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
93+
Insets waterfallInsets = cutout.getWaterfallInsets();
94+
waterfallLeft = Math.round(waterfallInsets.left / density);
95+
waterfallRight = Math.round(waterfallInsets.right / density);
96+
waterfallTop = Math.round(waterfallInsets.top / density);
97+
waterfallBottom = Math.round(waterfallInsets.bottom / density);
98+
}
8399
}
84100
}
85101
}
@@ -100,6 +116,13 @@ public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
100116
" Bottom:" + cutout.getSafeInsetBottom() +
101117
" Left:" + cutout.getSafeInsetLeft() +
102118
" Right:" + cutout.getSafeInsetRight());
119+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
120+
Insets waterfallInsets = cutout.getWaterfallInsets();
121+
Log.d(TAG, "Waterfall insets - Top:" + waterfallInsets.top +
122+
" Bottom:" + waterfallInsets.bottom +
123+
" Left:" + waterfallInsets.left +
124+
" Right:" + waterfallInsets.right);
125+
}
103126
}
104127
}
105128

@@ -109,7 +132,7 @@ public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
109132
// forces edge-to-edge, triggering this before QtActivity finishes
110133
// loading libqdomyos-zwift in its background thread).
111134
try {
112-
onInsetsChanged(top, bottom, left, right);
135+
onInsetsChanged(top, bottom, left, right, waterfallTop, waterfallBottom, waterfallLeft, waterfallRight);
113136
} catch (UnsatisfiedLinkError ignored) {
114137
// Qt not ready yet; insets will be re-applied once Qt initializes.
115138
}

src/androidstatusbar.cpp

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,27 +38,41 @@ int AndroidStatusBar::apiLevel() const
3838
#endif
3939
}
4040

41-
void AndroidStatusBar::onInsetsChanged(int top, int bottom, int left, int right)
41+
void AndroidStatusBar::onInsetsChanged(int top, int bottom, int left, int right, int waterfallTop,
42+
int waterfallBottom, int waterfallLeft, int waterfallRight)
4243
{
43-
if (m_top != top || m_bottom != bottom || m_left != left || m_right != right) {
44+
if (m_top != top || m_bottom != bottom || m_left != left || m_right != right ||
45+
m_waterfallTop != waterfallTop || m_waterfallBottom != waterfallBottom ||
46+
m_waterfallLeft != waterfallLeft || m_waterfallRight != waterfallRight) {
4447
m_top = top;
4548
m_bottom = bottom;
4649
m_left = left;
4750
m_right = right;
48-
qDebug() << "Insets changed - Top:" << m_top << "Bottom:" << m_bottom << "Left:" << m_left << "Right:" << m_right;
51+
m_waterfallTop = waterfallTop;
52+
m_waterfallBottom = waterfallBottom;
53+
m_waterfallLeft = waterfallLeft;
54+
m_waterfallRight = waterfallRight;
55+
qDebug() << "Insets changed - Top:" << m_top << "Bottom:" << m_bottom << "Left:" << m_left
56+
<< "Right:" << m_right << "WaterfallTop:" << m_waterfallTop
57+
<< "WaterfallBottom:" << m_waterfallBottom << "WaterfallLeft:" << m_waterfallLeft
58+
<< "WaterfallRight:" << m_waterfallRight;
4959
emit insetsChanged();
5060
}
5161
}
5262

5363
#ifdef Q_OS_ANDROID
5464
// JNI method with standard naming convention
5565
extern "C" JNIEXPORT void JNICALL
56-
Java_org_cagnulen_qdomyoszwift_CustomQtActivity_onInsetsChanged(JNIEnv *env, jobject thiz, jint top, jint bottom, jint left, jint right)
66+
Java_org_cagnulen_qdomyoszwift_CustomQtActivity_onInsetsChanged(JNIEnv *env, jobject thiz, jint top,
67+
jint bottom, jint left, jint right,
68+
jint waterfallTop, jint waterfallBottom,
69+
jint waterfallLeft, jint waterfallRight)
5770
{
5871
Q_UNUSED(env);
5972
Q_UNUSED(thiz);
6073
if (AndroidStatusBar::instance()) {
61-
AndroidStatusBar::instance()->onInsetsChanged(top, bottom, left, right);
74+
AndroidStatusBar::instance()->onInsetsChanged(top, bottom, left, right, waterfallTop, waterfallBottom,
75+
waterfallLeft, waterfallRight);
6276
}
6377
}
6478
#endif

src/androidstatusbar.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ class AndroidStatusBar : public QObject
1111
Q_PROPERTY(int navigationBarHeight READ navigationBarHeight NOTIFY insetsChanged)
1212
Q_PROPERTY(int leftInset READ leftInset NOTIFY insetsChanged)
1313
Q_PROPERTY(int rightInset READ rightInset NOTIFY insetsChanged)
14+
Q_PROPERTY(int waterfallTopInset READ waterfallTopInset NOTIFY insetsChanged)
15+
Q_PROPERTY(int waterfallBottomInset READ waterfallBottomInset NOTIFY insetsChanged)
16+
Q_PROPERTY(int waterfallLeftInset READ waterfallLeftInset NOTIFY insetsChanged)
17+
Q_PROPERTY(int waterfallRightInset READ waterfallRightInset NOTIFY insetsChanged)
18+
Q_PROPERTY(bool hasWaterfallDisplay READ hasWaterfallDisplay NOTIFY insetsChanged)
1419
Q_PROPERTY(int apiLevel READ apiLevel CONSTANT)
1520

1621
public:
@@ -23,10 +28,16 @@ class AndroidStatusBar : public QObject
2328
int navigationBarHeight() const { return m_bottom; }
2429
int leftInset() const { return m_left; }
2530
int rightInset() const { return m_right; }
31+
int waterfallTopInset() const { return m_waterfallTop; }
32+
int waterfallBottomInset() const { return m_waterfallBottom; }
33+
int waterfallLeftInset() const { return m_waterfallLeft; }
34+
int waterfallRightInset() const { return m_waterfallRight; }
35+
bool hasWaterfallDisplay() const { return m_waterfallTop > 0 || m_waterfallBottom > 0 || m_waterfallLeft > 0 || m_waterfallRight > 0; }
2636
int apiLevel() const;
2737

2838
public slots:
29-
void onInsetsChanged(int top, int bottom, int left, int right);
39+
void onInsetsChanged(int top, int bottom, int left, int right, int waterfallTop, int waterfallBottom,
40+
int waterfallLeft, int waterfallRight);
3041

3142
signals:
3243
void insetsChanged();
@@ -36,6 +47,10 @@ public slots:
3647
int m_bottom = 0;
3748
int m_left = 0;
3849
int m_right = 0;
50+
int m_waterfallTop = 0;
51+
int m_waterfallBottom = 0;
52+
int m_waterfallLeft = 0;
53+
int m_waterfallRight = 0;
3954

4055
static AndroidStatusBar* m_instance;
4156
};

0 commit comments

Comments
 (0)