[Android] Fix CollectionView ActivityIndicator not animating after header height change#35358
Conversation
Updated [Magick.NET-Q8-AnyCPU](https://github.com/dlemstra/Magick.NET) from 14.10.4 to 14.12.0. <details> <summary>Release notes</summary> _Sourced from [Magick.NET-Q8-AnyCPU's releases](https://github.com/dlemstra/Magick.NET/releases)._ ## 14.12.0 ### What's Changed - Added `FixByteOrder` to the `DcmReadDefines` (#1976) - Added `IconWriteDefines`. ### Related changes in ImageMagick since the last release of Magick.NET: - Correct bug in `Composite` when using `CopyAlpha` (#1985) - Fixed incorrect orientation of JPEG compressed TIFF images (#1991) - Heap-Buffer-Overflow write of single zero byte when parsing xml (GHSA-cr67-pvmx-2pp2) - Stack Overflow in DestroyXMLTree (GHSA-fwvm-ggf6-2p4x) - Out-of-Bounds read in sample operation (GHSA-pcvx-ph33-r5vv) - Stack Overflow via Recursive FX Expression Parsing (GHSA-f4qm-vj5j-9xpw) - Heap Buffer Overflow in ImageMagick MVG decoder (GHSA-x9h5-r9v2-vcww) - Heap overflow caused by integer overflow/wraparound in viff encoder on 32-bit builds (GHSA-v67w-737x-v2c9) - Stack-buffer-overflow in MNG encoder with oversized pallete (GHSA-98cp-rj9f-6v5g) - Integer overflow in despeckle operation causes heap buffer overflow on 32-bit builds (GHSA-26qp-ffjh-2x4v) - Off-by-One in MSL decoder could result in crash (GHSA-5xg3-585r-9jh5) - Heap buffer overflow when encoding JXL image with a 16-bit float (GHSA-jvgr-9ph5-m8v4) - Heap-use-after-free via XMP profile could result in a crash when printing the values (GHSA-r83h-crwp-3vm7) - Heap buffer overflow (WRITE) in the YAML and JSON encoders (GHSA-5592-p365-24xh) - Heap out-of-bounds write in JP2 encoder (GHSA-pwg5-6jfc-crvh) ### Library updates: - ImageMagick 7.1.2-19 (2026-04-12) - aom 3.13.3 (2026-04-02) - openexr 3.4.9 (2026-04-03) - freetype 2.14.3 (2026-03-22) - gdk-pixbuf 2.44.6 (2026-03-31) - harfbuzz 14.0.0 (2026-04-01) - liblzma 5.8.3 (2026-04-31) - libpng 1.6.56 (2026-03-25) **Full Changelog**: dlemstra/Magick.NET@14.11.1...14.12.0 ## 14.11.1 ### Related changes in ImageMagick since the last release of Magick.NET: - Stack-buffer-overflow WRITE in InterpretImageFilename due to overflow (GHSA-8793-7xv6-82cf) ### Library updates: - ImageMagick 7.1.2-18 (2026-03-23) - aom 3.13.2 (2026-03-19) - openexr 3.4.7 (2026-03-15) - harfbuzz 13.2.1 (2026-03-19) **Full Changelog**: dlemstra/Magick.NET@14.11.0...14.11.1 ## 14.11.0 ### What's Changed - Added `DcmReadDefines`. ### Related changes in ImageMagick since the last release of Magick.NET: - Access mode change for files created from 0666 to 0600 (ImageMagick/ImageMagick#8609) - Heap-buffer-overflow in NewXMLTree could result in crash (GHSA-gc62-2v5p-qpmp) ### Library updates: - ImageMagick 7.1.2-17 (2026-03-16) - openexr 3.4.6 (2026-03-01) - freetype 2.14.2 (2026-03-01) - harfbuzz 13.0.1 (2026-03-07) - libxml2 2.15.2 (2026-03-03) **Full Changelog**: dlemstra/Magick.NET@14.10.4...14.11.0 Commits viewable in [compare view](dlemstra/Magick.NET@14.10.4...14.12.0). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/dotnet/maui/network/alerts). </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…t#35333) Bump OpenTelemetry packages to latest stable versions in the maui-aspire-servicedefaults template: - OpenTelemetry.Exporter.OpenTelemetryProtocol: 1.9.0 to 1.15.3 - OpenTelemetry.Extensions.Hosting: 1.9.0 to 1.15.3 - OpenTelemetry.Instrumentation.Http: 1.9.0 to 1.15.1 - OpenTelemetry.Instrumentation.Runtime: 1.9.0 to 1.15.1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This pull request updates the following dependencies [marker]: <> (Begin:a71c12d9-5aa4-4b46-e2d6-08da0cf8cd95) ## From https://github.com/dotnet/xharness - **Subscription**: [a71c12d9-5aa4-4b46-e2d6-08da0cf8cd95](https://maestro.dot.net/subscriptions?search=a71c12d9-5aa4-4b46-e2d6-08da0cf8cd95) - **Build**: [20260430.4](https://dev.azure.com/dnceng/internal/_build/results?buildId=2964906) ([312724](https://maestro.dot.net/channel/2/github:dotnet:xharness/build/312724)) - **Date Produced**: May 1, 2026 7:05:11 AM UTC - **Commit**: [92962e5c46ac08a66ded4c5696209cc60f1a232f](dotnet/xharness@92962e5) - **Branch**: [main](https://github.com/dotnet/xharness/tree/main) [DependencyUpdate]: <> (Begin) - **Dependency Updates**: - From [11.0.0-prerelease.26229.1 to 11.0.0-prerelease.26230.4][1] - Microsoft.DotNet.XHarness.CLI - Microsoft.DotNet.XHarness.TestRunners.Common - Microsoft.DotNet.XHarness.TestRunners.Xunit [1]: dotnet/xharness@9d5a7e9...92962e5 [DependencyUpdate]: <> (End) [marker]: <> (End:a71c12d9-5aa4-4b46-e2d6-08da0cf8cd95) Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com>
> [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Description Replaces `review-rules.md` (flat 345-line checklist) with a dimensional expert review agent. Single source of truth for all review rules, organized into 30 dimensions for per-dimension sub-agent evaluation. Adds inline file:line PR comments alongside the existing wall-of-text summary. Extracted from 28k review comments across 5 maintainers via [extraction-pipeline](https://github.com/dotnet/fsharp/blob/main/.github/agents/extraction-pipeline.md). No functional code changes. Recreated from dotnet#35062 on a dotnet/maui branch (originally opened from a fork). ## What changed **Before:** `review-rules.md` had 345 lines of flat rules. `code-review` skill loaded them all into one context. Output was a single wall-of-text PR comment. **After:** Rules absorbed into `maui-expert-reviewer.md` as 30 dimensions with 200+ CHECK items. Each dimension runs as an independent sub-agent with focused context. Output is inline file:line PR comments via `inline-findings.json`. ## CI Flow ``` Review-PR.ps1 prompt: 1. code-review → maui-expert-reviewer agent → inline-findings.json 2. pr-review → Pre-Flight → Try-Fix → Report (sees findings, no duplication) Posting: post-inline-review.ps1 → .json → GitHub file:line comments (NEW) post-ai-summary-comment.ps1 → {phase}/content.md → wall-of-text (existing) CI: COMMENTS_VIA_FILE=true → agent writes .json, script posts Local: agent writes .json, code-review posts directly via gh api ``` ## Files | Action | File | What | |--------|------|------| | **Add** | `agents/maui-expert-reviewer.md` | 30 dimensions, 200+ CHECKs, routing table | | **Add** | `instructions/collectionview-{android,ios,windows}` | Platform-isolated CV rules | | **Add** | `instructions/{handler-patterns,layout-system,performance-hotpaths,public-api,threading-async}` | Domain-specific ambient guidance | | **Add** | `scripts/post-inline-review.ps1` | Posts .json as GitHub PR review | | **Del** | `skills/code-review/references/review-rules.md` | Absorbed into agent | | **Mod** | `skills/code-review/SKILL.md` | Delegates to agent | | **Mod** | `scripts/Review-PR.ps1` | Prompt + inline posting wiring | | **Mod** | `eng/pipelines/ci-copilot.yml` | `COMMENTS_VIA_FILE` env var | --------- Co-authored-by: kubaflo <kubaflo@users.noreply.github.com> Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Tomas Grosup <tomasgrosup@microsoft.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35358Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35358" |
…ory fails before tests
When a test category fails because the build or deploy crashed before any
test could run (e.g. CS0246 missing namespace, RS0016 PublicAPI errors),
the AI summary table previously showed '0/1 ✓' — the green-checkmark
'all passed' branch — because no per-test failures were parsed. That's
visually misleading: the row is FAILED but the cell looks healthy.
Two fixes:
1. Tests column distinguishes 'category failed AND no per-test failures
parsed' from 'all tests passed':
- 'build/deploy failed' (no tests at all)
- '0/1 — build/deploy failed before per-test results' (some discovered)
2. New optional 'build_tail' field captures the last 30 lines of stdout
when a category fails with zero per-test failures. The Failed test
details collapsible section then renders it in a code block so
reviewers see the actual compiler error / build crash inline,
instead of having to download the full CopilotLogs artifact.
This was discovered while running the regression-check pipeline against
PRs #35110 (142 RS0016 PublicAPI errors), #35281 (CS0246 NSAttributedString
missing for catalyst), and #35358 — all reported as '0/1 ✓' before the fix.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 3 findings
See inline comments for details.
Two related fixes that together restore meaningful per-test failure detail in the AI summary comment. ## 1. Parser input normalization (root cause of '1 failed test' bug) PowerShell's '& dotnet test 2>&1' wraps multi-line stderr blocks (and some console-logger output paths) as a single ErrorRecord/string with embedded newlines. The 'Get-DotNetTestResults' regex is anchored '^...$' (start/end of STRING, not line), so without splitting the parser would either skip those blocks entirely or — worse — collapse the entire multi-line block as one bogus 'test result' with all 100+ test names concatenated into a single name field. Observed on PR #35358 where 119 actual test failures were rendered as '1 failed test' with name='LightTheme_VerifyVisualState DarkTheme_…' (space-separated wall of 119 names) and duration='2 m 16 s 2 m 16 s …' (matching wall of 119 durations). Fix: at parser entry, split any captured element that contains '\n' or '\r' into individual lines before passing to the regex. Verified locally against the actual ViewBaseTests-output.log artifact: parser now returns 119 entries instead of 0/1. ## 2. Grouped failure rendering (avoids 65k comment overflow) Even after the parser fix, dumping 119 full error messages + first stack frames into a single comment would blow past GitHub's 65,536- character body limit. Most large failure clusters share a single root cause (e.g. all 119 ViewBaseTests fail with the same 'OneTimeSetUp: Timed out waiting for Go To Test button' because the host app didn't deploy properly). The new rendering: - Groups failed tests by the first 200 chars of error message - Shows full detail (sample names + 1500-char error + first stack frame) for the top 5 unique error groups - Adds a 'X tests failed with the same error' header when a group has multiple members, with the first 3 sample names inline - Always emits a fully-collapsed '<details>All N failed test names' block listing every individual failure so reviewers can re-run exactly the right set Wraps Group-Object output in @() because Group-Object returns a single GroupInfo (not an array) when all rows share the same key, and 'foreach' would then iterate the group's MEMBERS instead of the groups — re-introducing the same iteration bug we're trying to fix. Verified locally with a 119-failure simulation (117 with shared error, 2 unique): produces 3 groups, top group shows count=117 with sample names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some failure modes don't produce OneTimeSetUp-prefixed errors but are
still purely infrastructure (e.g. ADB0010 install failure causes 'Build
FAILED' before tests run, then NUnit reports every test in the
assembly as failed with regular test-name 'Failed X [2m 16s]' lines).
The previous heuristic only matched on '^OneTimeSetUp:' error prefix,
missing this very common ADB-broken-pipe pattern, leading to '119
failed tests' alarm on a single bad APK install.
New heuristic OR's two signals:
(a) every failure starts with 'OneTimeSetUp:' (driver couldn't reach
runner UI), OR
(b) build log contains 'Build FAILED' AND there were zero passes
(build/deploy died before any test could run, NUnit then 'fails'
the entire assembly).
When either is true AND any infra signal is found in the log, the
result is classified 🛠️ infra failure, which the renderer already
handles with a warning banner.
Verified locally on saved 119-failure ViewBaseTests log from #35358.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MauiBot
left a comment
There was a problem hiding this comment.
🤖 Automated review — alternative fix proposed
The expert-reviewer evaluation compared the PR fix against #1 automatically generated candidates and selected try-fix-1 as the strongest fix.
Why: try-fix-1 (conditional defer) preserves the PR's bug fix for the CollectionView recycler scenario but restores the synchronous fast path for the 99% case (single ActivityIndicator on a Page, programmatic show/hide on an idle view). It has the smallest behavioral blast radius of any candidate, no new infrastructure, and is the simplest code shape that addresses the reviewer's primary concern with the PR.
Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.
Candidate diff (`try-fix-1`)
diff --git a/src/Core/src/Handlers/ActivityIndicator/ActivityIndicatorHandler.Android.cs b/src/Core/src/Handlers/ActivityIndicator/ActivityIndicatorHandler.Android.cs
index ae9ace1cd561..b1c0c0c0c0c0 100644
--- a/src/Core/src/Handlers/ActivityIndicator/ActivityIndicatorHandler.Android.cs
+++ b/src/Core/src/Handlers/ActivityIndicator/ActivityIndicatorHandler.Android.cs
@@ -10,7 +10,30 @@ public partial class ActivityIndicatorHandler : ViewHandler<IActivityIndicator,
public static partial void MapIsRunning(IActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
- handler.PlatformView?.UpdateIsRunning(activityIndicator);
+ var progressBar = handler.PlatformView;
+ if (progressBar is null)
+ {
+ return;
+ }
+
+ // Fast path: when the view is attached and not currently mid-layout, apply synchronously
+ // to preserve the pre-existing ordering semantics for the common (non-recycler) case.
+ if (progressBar.IsAttachedToWindow && !progressBar.IsInLayout)
+ {
+ progressBar.UpdateIsRunning(activityIndicator);
+ return;
+ }
+
+ // Mid-layout / not yet attached (e.g. RecyclerView item being re-bound during a header
+ // height change): defer until after the layout traversal so that startAnimation()
+ // is not silently ignored on a view that is still being positioned.
+ progressBar.Post(() =>
+ {
+ if (progressBar.IsDisposed())
+ return;
+ if (handler.VirtualView is IActivityIndicator current)
+ progressBar.UpdateIsRunning(current);
+ });
}
public static partial void MapColor(IActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 7 findings
See inline comments for details.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 7 findings
See inline comments for details.
MauiBot
left a comment
There was a problem hiding this comment.
🤖 Automated review — alternative fix proposed
The expert-reviewer evaluation compared the PR fix against #1 automatically generated candidates and selected try-fix-1 as the strongest fix.
Why: try-fix-1 keeps the synchronous fast path that existing Android device tests depend on while still deferring via Post() in the actual mid-layout / unattached scenario described in #33780. It addresses the critical regression-risk and stale-PlatformView findings from the expert review with the smallest, simplest diff among the candidates.
Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.
Candidate diff (`try-fix-1`)
--- a/src/Core/src/Handlers/ActivityIndicator/ActivityIndicatorHandler.Android.cs
+++ b/src/Core/src/Handlers/ActivityIndicator/ActivityIndicatorHandler.Android.cs
@@ -10,7 +10,34 @@
public static partial void MapIsRunning(IActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
{
- handler.PlatformView?.UpdateIsRunning(activityIndicator);
+ var progressBar = handler.PlatformView;
+ if (progressBar is null)
+ {
+ return;
+ }
+
+ // Fast path: when the view is attached and not currently mid-layout, apply synchronously
+ // to preserve pre-existing ordering semantics for the common (non-recycler) case and to
+ // keep existing synchronous device tests working.
+ if (progressBar.IsAttachedToWindow && !progressBar.IsInLayout && !progressBar.IsLayoutRequested)
+ {
+ progressBar.UpdateIsRunning(activityIndicator);
+ return;
+ }
+
+ // Mid-layout / not yet attached (e.g. RecyclerView item being re-bound during a
+ // CollectionView header height change, see #33780): defer until after the layout
+ // traversal so that startAnimation() is not silently ignored on a view that is
+ // still being positioned.
+ progressBar.Post(() =>
+ {
+ if (progressBar.IsDisposed() || handler.PlatformView != progressBar)
+ {
+ return;
+ }
+
+ progressBar.UpdateIsRunning(activityIndicator);
+ });
}
public static partial void MapColor(IActivityIndicatorHandler handler, IActivityIndicator activityIndicator)
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
This PR targets an Android-specific issue where ActivityIndicator instances inside CollectionView items can stop animating after a header height change, by deferring the native ProgressBar.Visibility update to run after layout traversal.
Changes:
- Updates
ActivityIndicatorExtensions.UpdateIsRunningto sometimes defer the visibility update viaProgressBar.Post(...)whenIsInLayoutor not attached. - Adds a runnable-time
IsDisposed()guard before writingVisibility.
kubaflo
left a comment
There was a problem hiding this comment.
Could you please review the ai's summary?
🤖 AI Summary
📊 Review Session —
|
| Category | Tests | Snapshot diffs |
|---|---|---|
controls-ViewBaseTests |
118/119 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
🔍 Pre-Flight — Context & Validation
Issue: #33780 - CollectionView items fail to update ActivityIndicator state after header height change
PR: #35358 - [Android] Fix CollectionView ActivityIndicator not animating after header height change
Platforms Affected: Android
Files Changed: 1 implementation (src/Core/src/Platform/Android/ActivityIndicatorExtensions.cs, +22/-1), 0 tests
Key Findings
- Root cause (per PR): on Android, when a CollectionView header collapse triggers a RecyclerView layout traversal,
setVisibility(VISIBLE)invoked during that traversal can fail to start the indeterminate animation, because the views are not yet fully attached/positioned. Items near the bottom of the viewport are most often affected. - Current PR fix lives in
ActivityIndicatorExtensions.UpdateIsRunning(previously was in the handler). It pre-computestargetVisibilityand defers viaprogressBar.Post(...)whenprogressBar.IsInLayout || !progressBar.IsAttachedToWindow, else applies visibility synchronously. The lambda guards with!IsDisposed(). - The fix has been iterated significantly (multiple force-pushes): originally an unconditional
Post()inActivityIndicatorHandler.Android.cs, since refactored after MauiBot review to a conditional defer in the extension method, withtargetVisibilitysnapshot at call time. ⚠️ The!progressBar.IsAttachedToWindowbranch is suspect: existing device tests (IsRunningShouldRespectVisibilityAtInit,SettingIsRunningAfterInitShouldRespectHiddenVisibility,SettingIsRunningAfterInitShouldRespectCollapsedVisibility) create a handler and immediately readGetNativeIsRunning(handler)synchronously, without ever attaching the platform view to a window. With the new gate,IsAttachedToWindow == false, so the code goes down thePost()path — the runnable is queued viamRunQueueand only drains ononAttachedToWindow(), which never fires in those tests. The assertion runs before visibility is applied → tests will regress. This was flagged by the Copilot bot in the latest review pass and not addressed.- No regression test was added. The author argues UI screenshot tests are unreliable for indeterminate animation, WaitForElement only checks presence, and device tests pass with or without the fix because there is no API to assert "indeterminate Drawable is actively animating". Reasonable but a
ProgressBar.IsAnimating(viaIndeterminateDrawable is IAnimatable a && a.IsRunning) device test under a simulated layout traversal might still catch the visibility update — see code review notes. - The MauiBot comment recommending the fix live in the extension method has been addressed; the suggested gate of
IsInLayout || IsLayoutRequestedwas substituted withIsInLayout || !IsAttachedToWindow— the latter half is the part that creates the test regression risk.
Code Review Summary
Verdict: NEEDS_CHANGES
Confidence: high
Errors: 1 | Warnings: 2 | Suggestions: 2
Key code review findings:
- ❌
src/Core/src/Platform/Android/ActivityIndicatorExtensions.cs:16— Gate!progressBar.IsAttachedToWindowwill regress synchronous device tests that create a handler and immediately read native state without attaching to a window (ActivityIndicatorHandlerTests.IsRunningShouldRespectVisibilityAtInitand siblings). PreferprogressBar.IsInLayout || progressBar.IsLayoutRequested, as MauiBot originally suggested, OR keep the attach gate but apply synchronously when not attached (Post drains anyway on attach in the bug scenario, so the only thing the Post adds when not-attached is a deferred re-write — useless if not attached AND view will never attach, harmful in tests). ⚠️ src/Core/src/Platform/Android/ActivityIndicatorExtensions.cs:22— Lambda only checks!IsDisposed(); it does not checkIsAttachedToWindow. Per Android docs,Postrunnables for never-attached views queue indefinitely (viamRunQueue); when the view is later disposed, IsDisposed will return true and we skip — OK, but multiple stacked Post calls may all apply, and the most recent IsRunning toggle "wins" only if the call order matches the Post drain order (it does, per Handler.post FIFO). Risk is low.⚠️ src/Core/src/Platform/Android/ActivityIndicatorExtensions.cs:19— Per-call closure allocation (progressBar+targetVisibility) and a JavaRunnablewrapper across the JNI bridge whenever the gate is true. Mapper is a hot path; on the synchronous branch there is no allocation, but the device-test path will exercise this on every call.- 💡 Root cause may be addressable more robustly by restarting the indeterminate animation when
ProgressBar.OnAttachedToWindow()fires (or by togglingIsIndeterminateoff/on after a visibility change), instead of deferring the visibility write. - 💡 No regression test. A targeted device test that wraps the
CreateHandler/UpdateValuecalls insideprogressBar.IsLayoutRequested == true(forced viaprogressBar.RequestLayout()) could verify the defer path executes without a real RecyclerView.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #35358 | Conditional Post() in extension when IsInLayout || !IsAttachedToWindow |
ActivityIndicatorExtensions.cs |
Original PR; regression risk on device tests |
🔧 Fix — Analysis & Comparison
Try-Fix Candidates Summary (Phase 2 narrative for AI comment)
Four independent candidates were generated, each grounded in a different reviewer dimension. Tests were not executed in this environment (no Android device pool); verdicts are reasoning-based and grounded in the existing device-test contract.
Fix Candidates
| # | Source | Approach | Test Result (qualitative) | Files Changed | Notes |
|---|---|---|---|---|---|
| try-fix-1 | Regression Prevention | Gate defer on IsInLayout || IsLayoutRequested only — drop !IsAttachedToWindow |
✅ expected pass | 1 | Smallest, fixes the test-regression risk in the PR. Matches MauiBot's original recommendation. |
| try-fix-2 | Performance / Layout | Sync write + explicit IndeterminateDrawable.Start() poke |
✅ expected pass | 1 | Zero-deferral, zero JNI Runnable allocation; assumes drawable is IAnimatable. |
| try-fix-3 | Android Platform / Architectural | New MauiProgressBar that re-kicks animation in OnAttachedToWindow |
✅ expected pass | 2 (1 new) | Cleanest separation, but doesn't cover ActivityIndicatorHandler2 (MaterialActivityIndicator). |
| try-fix-4 | Threading / Idempotency | Defer via MainLooper Handler (not View.Post) + coalesce via ConditionalWeakTable |
✅ expected pass | 1 | Robust to "view never attaches"; more moving parts. |
| PR | PR #35358 | Defer via View.Post when IsInLayout || !IsAttachedToWindow |
1 | !IsAttachedToWindow branch causes existing ActivityIndicatorHandlerTests to receive a never-draining runnable. |
Cross-Pollination
Multi-model cross-pollination was performed by reasoning across all four dimensions (Regression Prevention, Performance, Architectural, Threading) before settling on the candidates above. Each candidate's "Cons" section explicitly considers concerns raised by the other dimensions:
- try-fix-1 vs others: simplest but doesn't optimize away allocation on slow path → try-fix-2/4 address that.
- try-fix-2 vs others: doesn't help cases where the drawable isn't
IAnimatable→ try-fix-1/3/4 handle generically. - try-fix-3 vs others: doesn't cover the "still attached, mid-layout" sub-case → try-fix-1/2/4 do.
- try-fix-4 vs others: more code → try-fix-1 achieves the same outcome with less.
No additional cross-pollination ideas surfaced — the four candidates collectively cover the meaningful design axes (sync vs defer; defer-queue choice; restart-vs-rewrite; handler-lifecycle vs extension).
Exhausted: Yes
Selected Fix: try-fix-1 — smallest, surgical, preserves the synchronous device-test contract while addressing the #33780 RecyclerView traversal scenario. The PR is one boolean predicate away from this — dropping the || !IsAttachedToWindow half of its gate is sufficient.
📋 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Identified critical regression risk in PR's !IsAttachedToWindow gate |
| Code Review (expert) | NEEDS_CHANGES (high) | 2 critical, 3 major, 3 moderate, 1 minor — see expert-pr-eval/content.md |
| Gate | No tests in PR — author has documented test-infra limitations | |
| Try-Fix | ✅ COMPLETE | 4 candidates generated across 4 dimensions; all qualitatively pass |
| Report | ✅ COMPLETE |
Comparative Candidate Analysis
All six candidates ranked. Tests were not run (no Android device pool available); rankings are based on qualitative regression-test compatibility (Android handler device tests) and #33780 repro coverage. Per the task rules, any candidate expected to fail existing regression tests is ranked below candidates expected to pass them.
| Rank | Candidate | Sync write preserved? | Existing device tests | #33780 fix | Recycling safe | Extra cost on hot path | Verdict |
|---|---|---|---|---|---|---|---|
| 🏆 1 | pr-plus-reviewer | ✅ always sync; Post only re-applies | ✅ pass | ✅ robust (explicit IAnimatable.Start()) |
✅ re-reads live state in lambda | none on common path | WINNER |
| 2 | try-fix-1 | ✅ on common path | ✅ pass | ✅ but relies on setVisibility(VISIBLE) restarting animation implicitly |
snapshot of targetVisibility only (low risk; coalesces by recency) |
none on common path | Close runner-up |
| 3 | try-fix-2 | ✅ always sync | ✅ pass | ✅ explicit IAnimatable.Start() |
✅ no defer at all | small Stop/Start JNI calls every Visible+Running call |
Strong; minor perf cost |
| 4 | try-fix-3 | ✅ unchanged extension | ✅ pass | ✅ on attach; partial if item rebinds without detach | n/a (lifecycle hook) | none in UpdateIsRunning |
More invasive; misses Material2 ActivityIndicator2 path |
| 5 | try-fix-4 | ✅ on common path | ✅ pass | ✅ defers correctly | ✅ coalesces via ConditionalWeakTable | one weak table lookup + handler post when slow | More moving parts than needed |
| ❌ 6 | pr (as submitted) | ❌ defers when !IsAttachedToWindow |
❌ regresses IsRunningShouldRespectVisibilityAtInit(false,*), …RespectHiddenVisibility, …RespectCollapsedVisibility |
closure+JNI Runnable on every detached call (including all device tests) | MUST CHANGE |
The PR as submitted is ranked last because two of the existing Android device tests are expected to fail under it (independently confirmed by the maui-expert-reviewer agent). The other four candidates and pr-plus-reviewer are all expected to pass the existing tests.
Winner Selection: pr-plus-reviewer
pr-plus-reviewer wins on three independent axes:
- Strictly preserves existing semantics. It applies the synchronous visibility write on every call (mirroring
maintoday). The deferred work is purely additive — a re-apply after layout — so no caller anywhere can regress. - Most robust CollectionView items fail to update ActivityIndicator state after header height change #33780 fix. It explicitly calls
IAnimatable.Start()on the indeterminate drawable after layout, which guarantees the animation resumes regardless of whethersetVisibility(VISIBLE)alone would have restarted it. - Recycling-safe. It re-reads
activityIndicatorstate inside the lambda, so a reboundProgressBaralways gets the current state — not a stale snapshot.
try-fix-1 is a very close runner-up and would be acceptable as a minimal patch — its only weakness is that it relies on setVisibility(VISIBLE) to implicitly restart the indeterminate animation, which the expert reviewer flagged as unproven.
Code Review Impact on Try-Fix
The Pre-Flight code review (and the parallel maui-expert-reviewer pass) both surfaced the same critical concern: the !IsAttachedToWindow half of the gate sends ActivityIndicatorHandlerTests.IsRunningShouldRespectVisibilityAtInit (and its Hidden/Collapsed siblings) into a deferred-Post path whose runnable never drains. This directly drove try-fix-1's design (drop that half of the gate) and informed try-fix-2's "skip the defer entirely" alternative. Try-fix-3 explored a lifecycle-driven recovery and try-fix-4 explored a different deferral queue — both useful as diversity baselines.
Summary
On Android, when a CollectionView header height change triggers a RecyclerView layout traversal, ActivityIndicators in items that are rebound during that traversal can stop animating because setVisibility(VISIBLE) runs before the item view is fully attached/positioned. The PR addresses this by deferring the visibility write via View.Post(...) whenever IsInLayout || !IsAttachedToWindow. The !IsAttachedToWindow half of that gate is incorrect: Android's View.post on a detached view queues into mRunQueue, which only drains via dispatchAttachedToWindow() — meaning unit/device-test code paths that never attach the view will never apply the visibility update. Three existing device tests assert exactly that.
Root Cause
ProgressBar.setVisibility(VISIBLE) issued during a RecyclerView layout traversal does not reliably (re)start the indeterminate Animatable drawable, because the host view is not yet positioned. The robust fix is twofold: (a) re-apply visibility after layout completes (View.Post from inside the traversal works for attached views), and (b) explicitly nudge IndeterminateDrawable as IAnimatable with Start().
Fix Quality
The PR's intent is correct (defer the work that has to wait for layout), but its gate is too broad and the deferred lambda doesn't actively restart animation — it just rewrites the same Visibility value. The pr-plus-reviewer patch — sync write + post-layout re-apply + explicit IAnimatable.Start() + live state re-read — is a strict superset of the PR's correctness while preserving all existing test semantics. The PR should adopt it (or, at minimum, drop the !IsAttachedToWindow half of the gate à la try-fix-1) and add a regression test for the #33780 repro.
@kubaflo AI summary suggested a “sync-first, then re-apply” approach — always applying setVisibility() synchronously first, then queuing a deferred Post() update with IAnimatable.Start() using IsInLayout || IsLayoutRequested as the condition. |
…ory fails before tests
When a test category fails because the build or deploy crashed before any
test could run (e.g. CS0246 missing namespace, RS0016 PublicAPI errors),
the AI summary table previously showed '0/1 ✓' — the green-checkmark
'all passed' branch — because no per-test failures were parsed. That's
visually misleading: the row is FAILED but the cell looks healthy.
Two fixes:
1. Tests column distinguishes 'category failed AND no per-test failures
parsed' from 'all tests passed':
- 'build/deploy failed' (no tests at all)
- '0/1 — build/deploy failed before per-test results' (some discovered)
2. New optional 'build_tail' field captures the last 30 lines of stdout
when a category fails with zero per-test failures. The Failed test
details collapsible section then renders it in a code block so
reviewers see the actual compiler error / build crash inline,
instead of having to download the full CopilotLogs artifact.
This was discovered while running the regression-check pipeline against
PRs #35110 (142 RS0016 PublicAPI errors), #35281 (CS0246 NSAttributedString
missing for catalyst), and #35358 — all reported as '0/1 ✓' before the fix.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two related fixes that together restore meaningful per-test failure detail in the AI summary comment. ## 1. Parser input normalization (root cause of '1 failed test' bug) PowerShell's '& dotnet test 2>&1' wraps multi-line stderr blocks (and some console-logger output paths) as a single ErrorRecord/string with embedded newlines. The 'Get-DotNetTestResults' regex is anchored '^...$' (start/end of STRING, not line), so without splitting the parser would either skip those blocks entirely or — worse — collapse the entire multi-line block as one bogus 'test result' with all 100+ test names concatenated into a single name field. Observed on PR #35358 where 119 actual test failures were rendered as '1 failed test' with name='LightTheme_VerifyVisualState DarkTheme_…' (space-separated wall of 119 names) and duration='2 m 16 s 2 m 16 s …' (matching wall of 119 durations). Fix: at parser entry, split any captured element that contains '\n' or '\r' into individual lines before passing to the regex. Verified locally against the actual ViewBaseTests-output.log artifact: parser now returns 119 entries instead of 0/1. ## 2. Grouped failure rendering (avoids 65k comment overflow) Even after the parser fix, dumping 119 full error messages + first stack frames into a single comment would blow past GitHub's 65,536- character body limit. Most large failure clusters share a single root cause (e.g. all 119 ViewBaseTests fail with the same 'OneTimeSetUp: Timed out waiting for Go To Test button' because the host app didn't deploy properly). The new rendering: - Groups failed tests by the first 200 chars of error message - Shows full detail (sample names + 1500-char error + first stack frame) for the top 5 unique error groups - Adds a 'X tests failed with the same error' header when a group has multiple members, with the first 3 sample names inline - Always emits a fully-collapsed '<details>All N failed test names' block listing every individual failure so reviewers can re-run exactly the right set Wraps Group-Object output in @() because Group-Object returns a single GroupInfo (not an array) when all rows share the same key, and 'foreach' would then iterate the group's MEMBERS instead of the groups — re-introducing the same iteration bug we're trying to fix. Verified locally with a 119-failure simulation (117 with shared error, 2 unique): produces 3 groups, top group shows count=117 with sample names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some failure modes don't produce OneTimeSetUp-prefixed errors but are
still purely infrastructure (e.g. ADB0010 install failure causes 'Build
FAILED' before tests run, then NUnit reports every test in the
assembly as failed with regular test-name 'Failed X [2m 16s]' lines).
The previous heuristic only matched on '^OneTimeSetUp:' error prefix,
missing this very common ADB-broken-pipe pattern, leading to '119
failed tests' alarm on a single bad APK install.
New heuristic OR's two signals:
(a) every failure starts with 'OneTimeSetUp:' (driver couldn't reach
runner UI), OR
(b) build log contains 'Build FAILED' AND there were zero passes
(build/deploy died before any test could run, NUnit then 'fails'
the entire assembly).
When either is true AND any infra signal is found in the log, the
result is classified 🛠️ infra failure, which the renderer already
handles with a warning banner.
Verified locally on saved 119-failure ViewBaseTests log from #35358.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ory fails before tests
When a test category fails because the build or deploy crashed before any
test could run (e.g. CS0246 missing namespace, RS0016 PublicAPI errors),
the AI summary table previously showed '0/1 ✓' — the green-checkmark
'all passed' branch — because no per-test failures were parsed. That's
visually misleading: the row is FAILED but the cell looks healthy.
Two fixes:
1. Tests column distinguishes 'category failed AND no per-test failures
parsed' from 'all tests passed':
- 'build/deploy failed' (no tests at all)
- '0/1 — build/deploy failed before per-test results' (some discovered)
2. New optional 'build_tail' field captures the last 30 lines of stdout
when a category fails with zero per-test failures. The Failed test
details collapsible section then renders it in a code block so
reviewers see the actual compiler error / build crash inline,
instead of having to download the full CopilotLogs artifact.
This was discovered while running the regression-check pipeline against
PRs #35110 (142 RS0016 PublicAPI errors), #35281 (CS0246 NSAttributedString
missing for catalyst), and #35358 — all reported as '0/1 ✓' before the fix.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two related fixes that together restore meaningful per-test failure detail in the AI summary comment. ## 1. Parser input normalization (root cause of '1 failed test' bug) PowerShell's '& dotnet test 2>&1' wraps multi-line stderr blocks (and some console-logger output paths) as a single ErrorRecord/string with embedded newlines. The 'Get-DotNetTestResults' regex is anchored '^...$' (start/end of STRING, not line), so without splitting the parser would either skip those blocks entirely or — worse — collapse the entire multi-line block as one bogus 'test result' with all 100+ test names concatenated into a single name field. Observed on PR #35358 where 119 actual test failures were rendered as '1 failed test' with name='LightTheme_VerifyVisualState DarkTheme_…' (space-separated wall of 119 names) and duration='2 m 16 s 2 m 16 s …' (matching wall of 119 durations). Fix: at parser entry, split any captured element that contains '\n' or '\r' into individual lines before passing to the regex. Verified locally against the actual ViewBaseTests-output.log artifact: parser now returns 119 entries instead of 0/1. ## 2. Grouped failure rendering (avoids 65k comment overflow) Even after the parser fix, dumping 119 full error messages + first stack frames into a single comment would blow past GitHub's 65,536- character body limit. Most large failure clusters share a single root cause (e.g. all 119 ViewBaseTests fail with the same 'OneTimeSetUp: Timed out waiting for Go To Test button' because the host app didn't deploy properly). The new rendering: - Groups failed tests by the first 200 chars of error message - Shows full detail (sample names + 1500-char error + first stack frame) for the top 5 unique error groups - Adds a 'X tests failed with the same error' header when a group has multiple members, with the first 3 sample names inline - Always emits a fully-collapsed '<details>All N failed test names' block listing every individual failure so reviewers can re-run exactly the right set Wraps Group-Object output in @() because Group-Object returns a single GroupInfo (not an array) when all rows share the same key, and 'foreach' would then iterate the group's MEMBERS instead of the groups — re-introducing the same iteration bug we're trying to fix. Verified locally with a 119-failure simulation (117 with shared error, 2 unique): produces 3 groups, top group shows count=117 with sample names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Some failure modes don't produce OneTimeSetUp-prefixed errors but are
still purely infrastructure (e.g. ADB0010 install failure causes 'Build
FAILED' before tests run, then NUnit reports every test in the
assembly as failed with regular test-name 'Failed X [2m 16s]' lines).
The previous heuristic only matched on '^OneTimeSetUp:' error prefix,
missing this very common ADB-broken-pipe pattern, leading to '119
failed tests' alarm on a single bad APK install.
New heuristic OR's two signals:
(a) every failure starts with 'OneTimeSetUp:' (driver couldn't reach
runner UI), OR
(b) build log contains 'Build FAILED' AND there were zero passes
(build/deploy died before any test could run, NUnit then 'fails'
the entire assembly).
When either is true AND any infra signal is found in the log, the
result is classified 🛠️ infra failure, which the renderer already
handles with a warning banner.
Verified locally on saved 119-failure ViewBaseTests log from #35358.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ader height change (#35358) > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Issue Details On Android, ActivityIndicator items inside a CollectionView stop animating after a header height change. When IsRunning is toggled back ON, the spinning animation does not appear for some items. ### Root Cause When the header collapses, RecyclerView triggers a layout traversal, measuring and repositioning all visible items. If setVisibility(VISIBLE) is invoked during this traversal, startAnimation() may not start the animation because the views are not yet fully attached and positioned. Items near the bottom of the list are laid out later in the traversal, which is why they are more likely to fail. ### Description of Change Post() is now used only when progressBar.IsInLayout || !progressBar.IsAttachedToWindow. In all other cases, visibility is updated synchronously. An early if (progressBar.IsDisposed()) { return; } check prevents accessing properties on a disposed view. An additional !IsDisposed() check inside the lambda skips the update if the view is disposed before the runnable executes. ### Why tests were not added Three test approaches were evaluated, and none were viable: - UI Test with VerifyScreenshot() — Not reliable. The ActivityIndicator spinner is an indeterminate animation, and its position varies across runs. This causes screenshot comparisons to fail even when the fix is working correctly, leading to a consistently flaky test. - UI Test with WaitForElement / WaitForNoElement — Not effective. These methods only verify element presence, not animation state. Since the ActivityIndicator exists in the visual tree regardless of whether it is spinning, this approach cannot distinguish between the broken and fixed behavior. - Device Test — Not effective. Device tests passed both with and without the fix. The current infrastructure does not provide a reliable way to verify whether an indeterminate ProgressBar animation is actively running or stuck. This issue is specific to the visual animation state—the element is present and correctly bound in all cases, but the spinning animation fails. Currently, MAUI’s test infrastructure does not provide a reliable way to validate indeterminate animation states on Android. ### Issues Fixed Fixes #33780 ### Screenshots | Before Issue Fix | After Issue Fix | |----------|----------| | <video width="300" height="600" src="https://github.com/user-attachments/assets/f7eef662-0f52-4fce-b6c0-ff652a800568"> | <video width="300" height="600" src="https://github.com/user-attachments/assets/f3137e92-6f5d-4b75-9413-b3b0510457f8"> ---------
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue Details
On Android, ActivityIndicator items inside a CollectionView stop animating after a header height change. When IsRunning is toggled back ON, the spinning animation does not appear for some items.
Root Cause
When the header collapses, RecyclerView triggers a layout traversal, measuring and repositioning all visible items. If setVisibility(VISIBLE) is invoked during this traversal, startAnimation() may not start the animation because the views are not yet fully attached and positioned. Items near the bottom of the list are laid out later in the traversal, which is why they are more likely to fail.
Description of Change
Post() is now used only when progressBar.IsInLayout || !progressBar.IsAttachedToWindow. In all other cases, visibility is updated synchronously.
An early if (progressBar.IsDisposed()) { return; } check prevents accessing properties on a disposed view. An additional !IsDisposed() check inside the lambda skips the update if the view is disposed before the runnable executes.
Why tests were not added
Three test approaches were evaluated, and none were viable:
UI Test with VerifyScreenshot() — Not reliable. The ActivityIndicator spinner is an indeterminate animation, and its position varies across runs. This causes screenshot comparisons to fail even when the fix is working correctly, leading to a consistently flaky test.
UI Test with WaitForElement / WaitForNoElement — Not effective. These methods only verify element presence, not animation state. Since the ActivityIndicator exists in the visual tree regardless of whether it is spinning, this approach cannot distinguish between the broken and fixed behavior.
Device Test — Not effective. Device tests passed both with and without the fix. The current infrastructure does not provide a reliable way to verify whether an indeterminate ProgressBar animation is actively running or stuck.
This issue is specific to the visual animation state—the element is present and correctly bound in all cases, but the spinning animation fails. Currently, MAUI’s test infrastructure does not provide a reliable way to validate indeterminate animation states on Android.
Issues Fixed
Fixes #33780
Screenshots
33780BeforeFix.mov
33780AfterFix.mov