Repository navigation
[Android] Fix OnBackButtonPressed on root pages - #37709
Conversation
### Description of Change This pull request makes a minor update to the UI test pipeline configuration - Pipeline configuration simplification: * Removed the **duplicate** code that set the `$deviceType` variable and appended the `--skin` argument to the test command in `eng/pipelines/common/ui-tests-steps.yml`. The same code was already added at [line 133](https://github.com/dotnet/maui/blob/main/eng/pipelines/common/ui-tests-steps.yml#L133) as part of [PR #33409](#33409). The duplicate code was also added at [line 156](https://github.com/dotnet/maui/blob/main/eng/pipelines/common/ui-tests-steps.yml#L156) as part of [PR #31631](#31631).
<!-- Please let the below note in for people that find this PR --> > [!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 of Change This follow-up repairs the skill-evaluation failures exposed by #36799 and hardens the credentialed Vally evaluator so skill regressions, infrastructure failures, incomplete trials, parser differences, and reporting-side failures are classified accurately. ### Root Causes - `try-fix-capabilities` could exceed its numeric threshold while remaining red because Vally independently hard-fails execution errors and incomplete trials. Aggregate score never overrides those vetoes. - `pr-review-gh-auth` routed correctly, but its isolated fixture omitted the phase-specific unauthenticated public-GitHub fallback required by the grader. - Later failures exposed evaluator defects rather than skill regressions: PAT authentication did not prove model entitlement, model discovery did not initially match Vally 0.12's effective execution plan, matrix-only token offsets removed run-level rotation entropy, and parallel legs amplified shared PAT contention. - Run `31518503277` restored the synthetic target correctly, but one agent deleted Vally's pre-existing untracked `try-fix/` input to force an empty `git status`; Vally correctly treated that deletion as workspace drift. - Run `31524806025` completed every evaluation successfully, but a transient GitHub API fetch failure occurred before the comment step exported its already-computed verdict, making reporting red despite green suites. - Review found parser-semantic splits between Psych and Vally's pinned `yaml@2.9.0`: duplicate keys could be first-wins in AST policy checks but last-wins at runtime, while YAML merge keys/tags were interpreted differently and could hide executable evaluator fields from trusted validation. ### Changes - Replaced unreliable operational probes with bounded fixtures while preserving repeated trials, behavioral rubrics, strict thresholds, and hard-veto semantics. - Added the unauthenticated GitHub fallback fixture and deterministic skill-invocation/tool-call graders. - Upgraded to Vally 0.12 and made aggregation fail closed on execution errors, incomplete/skipped stimuli, malformed output, invocation failures, and missing mandatory coverage. - Added trusted fixture preparation plus path, symlink, Git, environment, executable, attachment, hook, settings, MCP, executor, and repository-control isolation. - Serialized matrix legs while retaining three trials per stimulus. - Made model discovery use only executed `eval*.vally.yaml` specs and Vally 0.12's effective executor and prompt-grader fields; implicit prompt-grader fallbacks are rejected. - Added model-aware PAT selection with run/rerun/matrix-distributed rotation and exact sentinel probes through the pinned evaluator runtime; reused it in the advisory soak. - Clarified that `try-fix` must preserve every pre-existing untracked path, including evaluator-loaded skill inputs. Trusted validation enforces both the skill contract and restoration fixture instruction while retaining `diff-empty` as the method-independent veto. - Exported the authoritative evaluation verdict before PR-comment API calls, added bounded API retries, and made comment publication advisory so transient GitHub failures cannot erase or invert the commit status. - Reduced soak workflow permissions to `contents: read` and made `/user` transport failures explicit without exposing PATs. - Made required-stimulus policy fail closed when a configured stimulus is renamed or removed, and bound each fixture marker/ref to the YAML stimulus that owns it so a correctly named decoy cannot satisfy the policy. - Parse each eval spec once and reject duplicate semantic mapping keys, textual YAML merge keys, and normalized explicit merge tags recursively before loading. This prevents Psych/Vally parser disagreement from bypassing prompt, ref, repository-control, or executable-environment validation. - Limited frozen-worktree test detection to full immutable commit IDs; ordinary explicit branch bases continue using authoritative PR file metadata. - Added deterministic regressions for model planning, PAT distribution and transport errors, deleted evaluator controls, host-only XAML invocation, evaluator-owned input preservation, required-stimulus rename/decoy/parser bypasses, branch-versus-immutable test detection, and reporting-side verdict preservation. ### Validation Current head: `fd8f3d543e1a1eb90e8432a0cbae07a18b52dbdd`. - Exact-head [Skill Validation run 31807806898](https://github.com/dotnet/maui/actions/runs/31807806898), [`maui-pr` build 1553690](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1553690), Build Analysis, CLA, static checks, Pester, and the hermeticity gate are green. - All evaluated suites passed. Key suites: `try-fix-restore-protocol` **1.00 / 1.00**, `try-fix-capabilities` **0.93 / 0.80**, `pr-review-gh-auth` **1.00 / 0.80**, and `verify-tests-fail-without-fix-capabilities` **0.94 / 0.87**. - Local trusted evaluator/reporting harness: **70 tests, 306 assertions, 0 failures/errors/skips**. - All **16** Vally specs pass strict lint with pinned `@microsoft/vally-cli@0.12.0`. - All **8** skill test directories pass trusted validation and resolve the effective plan to `gpt-5.6-sol` plus `claude-opus-5`; non-eval soak/hermeticity specs do not contaminate matrix selection. - Ruby syntax, Bash syntax, ShellCheck, workflow YAML parsing, Pester (**8 passed**), and `git diff --check` pass. - Final whole-diff review found no high-confidence correctness or security blockers; all review threads are resolved. - `pull_request_target` executes trusted workflow/helper code from default `main`; candidate selector/reporting helper behavior is therefore covered deterministically here and becomes the credentialed trusted path after merge. ### Issues Fixed Follow-up to #36799. --------- Co-authored-by: PureWeen <223556219+Copilot@users.noreply.github.com> Co-authored-by: Vally Fixture <vally-fixture@example.invalid> Copilot-Session: b9f7de2c-86d7-47cd-8ac8-aeff57d0b0f3 Copilot-Session: 806beb89-5041-4c7b-bcbf-998d6fbab8dc
<!-- Please let the below note in for people that find this PR --> > [!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 of Change Extend the existing `release-readiness` skill with deterministic, copy-ready Preview and servicing-release handoff generation from readiness JSON plus separately verified public evidence. - Keep missing or unverified release facts as explicit `TBD` values and enforce public-safe URLs, structured metadata, NuGet configuration, and Markdown output. - Correct Preview workload installation guidance with package-source mapping and the prior-major public compatibility feed while keeping workload-set packages isolated to `dotnet-workloads`. - Add focused regression coverage and run it in the release-readiness validation workflow. The renderer is a projection of the existing readiness engines rather than a second survey, so Preview and SR semantics remain distinct while sharing one public handoff format. ### Safety and Architecture - Public output requires an explicit JSON boolean `PublicEvidence: true`. - The renderer rebuilds NuGet XML from an allow-list and accepts only public `dnceng/public` Azure Artifacts feeds or the exact NuGet.org service index. - Public evidence URLs, structured build metadata, timestamps, Markdown, local paths, credential-shaped values, and owner/sign-off prose are validated or omitted. - Workload-set packages map only to `dotnet-workloads`; component and compatibility packages resolve through their appropriate public feeds. ### What Not to Do - Do not use the handoff renderer as a second readiness survey or let it select a newer build implicitly. - Do not copy private source-page content, credentials, owners, or sign-offs into public evidence. - Do not map unrelated component packages to `dotnet-workloads`; missing upstream packages can otherwise surface as misleading authorization failures. ### Validation - `pwsh -NoProfile -File .github/skills/release-readiness/tests/Test-ReleaseHandoff.ps1` - `pwsh -NoProfile -File .github/skills/release-readiness/tests/Test-ReleaseReadiness.ps1 -SkipE2E` ### Issues Fixed N/A --------- Co-authored-by: PureWeen <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3db5742d-7e40-4a23-a3ea-de3bc3f0848e
<!-- Please let the below note in for people that find this PR --> > [!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! ### Root Cause MAUI's release flow implemented NuGet.org authentication, retries, conflict handling, quota rotation, and publishing in `eng/scripts/push_nuget_org.ps1`. Gather and push failures could also continue without failing the release. The repository additionally carried a duplicate internal release YAML. ### Description of Change This makes `eng/pipelines/ci-official-release.yml` the canonical release pipeline and moves package publication to the approved 1ES release path: - Resolve the BAR build once and run one fail-fast `darc gather-drop`, limited to BAR NuGet package assets rather than unrelated symbol/blob assets. - Preserve package include/exclude behavior and validate non-empty workload-pack and workload-manifest sets. - Produce separate SBOM-backed `MauiPacksForNuGet` and `MauiManifestsForNuGet` artifacts. - Publish from production `releaseJob` jobs with `1ES.PublishNuget@1`, `useDotNetTask: false`, and the protected `nuget.org (dotnetframework)` service connection. - Require workload packs to publish and become resolvable before manifest approval is available. - Remove `maui-release-internal.yml`, `push_nuget_org.ps1`, raw API-key inputs, and the arbitrary feed URL. - Document the canonical release and recovery procedure. No public API changes are involved. ### Duplicate and Partial-Publish Recovery Internal build [3050066](https://dev.azure.com/dnceng/internal/_build/results?buildId=3050066) proved that the service connection authenticates, but also proved that the external-feed-backed task fails on HTTP 409 even with `allowPackageConflicts: true`. Task-level retries are unsafe after a partial batch because the retry can immediately encounter duplicates created by the first attempt. The final implementation therefore: - Generates an `expected-packages.json` identity manifest for each immutable artifact. - Rejects duplicate package identities, duplicate package file names, and unexpected `.nupkg` files before publication. - Verifies the copied helper's SHA-256 before execution. - Filters packages already visible through NuGet.org's flat-container API before publishing. - Uses separate recovery-only `nugetAlreadyAttemptedPackFilters` and `nugetAlreadyAttemptedManifestFilters` parameters for packages accepted but not yet visible during NuGet.org validation lag. - Runs `1ES.PublishNuget@1` only when packages remain and does not use task-level retries. - Verifies the complete expected package set with transient-query retries and a 30-minute deadline. ### Internal Validation - Internal branch: [`copilot/replace-custom-nuget-publishing`](https://dev.azure.com/dnceng/internal/_git/dotnet-maui?version=GBcopilot%2Freplace-custom-nuget-publishing) - Final internal commit: [`adf2ad774cd3a2092450df153e6b46a09a127552`](https://dev.azure.com/dnceng/internal/_git/dotnet-maui/commit/adf2ad774cd3a2092450df153e6b46a09a127552?refName=refs%2Fheads%2Fcopilot%2Freplace-custom-nuget-publishing) - Build 3050066 confirmed the registered `dotnet-maui-release` definition already executes the canonical YAML, so no pipeline-definition migration is needed. - Build [3050237](https://dev.azure.com/dnceng/internal/_build/results?buildId=3050237) showed that a fail-fast unfiltered gather stops on a missing Foldable symbol blob even though the shipping NuGet package downloads successfully. The gather now uses Darc's slash-free package-asset convention to exclude symbol/blob paths while remaining fail-fast for every publishable package. - Build [3050279](https://dev.azure.com/dnceng/internal/_build/results?buildId=3050279) confirmed the package-only gather succeeds and the `nuget.org (dotnetframework)` connection authenticates: it published `Microsoft.AspNetCore.Components.WebView.Maui.10.0.100`, then NuGet.org rejected `Microsoft.AspNetCore.Components.WebView.Maui.Msi.arm64.10.0.100` with HTTP 403. The first package lists `dotnetframework` as an owner; the MSI package lists only `Microsoft` and `Xamarin`. This is a NuGet.org package-ownership/service-connection blocker, not a pipeline task-wiring failure. - Pester 5.9.0: 1,668 tests passed, including 15 package-filtering/retry tests and 3 release-pipeline regression tests. - PowerShell parsing, YAML parsing, documentation spellcheck, and whitespace validation pass. Before production use, either the service-connection identity must be granted ownership of every MAUI package (including Xamarin-owned MSI packages), or the release pipeline must be given an approved Xamarin-scoped NuGet.org service connection. Once that external credential issue is resolved, rerun the same commit and BAR selection; the recovery logic will omit the package already published by build 3050279 and continue with the remaining set. ### What Not to Do - Do not rely on `allowPackageConflicts` for an external NuGet.org feed; build 3050066 proved it does not make 409 responses successful. - Do not add task-level retries around a multi-package publish; they are not safe after partial publication. - Do not restore broad `--continue-on-error` gather behavior; exclude non-release symbol/blob assets and keep publishable package gathering fail-fast. - Do not gather a different BAR drop while recovering a partial release; rerun the same commit and selection filters. - Do not exclude the Xamarin-owned MSI packages to make the build green; they are shipping assets and require a credential with matching NuGet.org ownership. ### Issues Fixed Fixes #36567 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com> Co-authored-by: Rui Marinho <me@ruimarinho.net> Copilot-Session: 150fe76a-707e-48ca-9e9d-38bfccead829
<!-- Please let the below note in for people that find this PR --> > [!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 of Change Enhance local net11 preview release-readiness runs with automatic health checks for the internal official `dotnet-maui` Azure DevOps pipeline (definition 1095, `dnceng/internal`). The local release-readiness skill/agent explicitly requests enriched output, then independently discovers builds for `refs/heads/net11.0` and the evaluated release branch when it exists. It requests a bounded, queue-time-ordered five-build window and prefers the newest exact-HEAD build, then scans in queue order while skipping candidates proven stale and buffering indeterminate evidence. Disagreeing possible outcomes remain UNKNOWN; a later proven-current failure remains RED only when every buffered candidate is also a completed failure/cancellation. A terminal window containing only same-branch failed/canceled indeterminate builds is `failed-or-stale` and BLOCKED because every possible build is either red or stale. The check reports build status, ID/number, source SHA, and URL. Current successful builds are green; failures/cancellations are red; partial success is a WATCH state requiring manual review; running builds are in progress; builds behind the newest trigger-eligible commit are stale; missing or malformed evidence is unknown. When branch HEAD advances, bounded local Git evaluates exact first-parent history and merge-result changes against `eng/pipelines/ci-official.yml`. Successful no-op or excluded-only advances remain current, while add/revert and merge-result-only source changes still require a newer official build. A conclusive Git non-ancestor result is stale; unavailable Git evidence remains unknown unless every possible same-branch result is independently blocking. ### Public/internal boundary Direct script output remains public-safe by default. GitHub Actions skips the internal query before invoking Azure CLI, while the local skill/agent explicitly passes `'-PublicSafe:$false'` for enriched artifacts that remain local. Public-safe output omits internal branch records, build IDs/numbers, SHAs, URLs, and the local-only table. Missing local Azure authentication fails open. Partial failures preserve successful branch evidence, so known red/stale results cannot disappear. Azure, GitHub, and local Git subprocesses have bounded execution; process exit and redirected-output draining share the same deadline. Local Git runs from the explicit repository root, performs a bounded fetch of only the evaluated branch when objects are absent, and maps unavailable currency evidence through the outcome lattice rather than assuming current or stale. Windows `.cmd`/`.bat` Azure CLI launchers run through `ComSpec` with structured arguments. Manual build overrides are accepted only when the build belongs to definition 1095, and Azure-controlled build numbers are constrained to the expected numeric format before entering agent-facing output. ### Tests - `pwsh -NoProfile -File .github/skills/release-readiness/tests/Test-ReleaseReadiness.ps1` — 2,276 passed, 0 failed - PowerShell parser validation for the helper, preview engine, and test harness - Live local smoke successfully queried both requested refs through the enriched local path - First-parent empty-commit, same-tree merge, merge-result source change, add/revert, non-ancestor, missing-object, exact/proven-current candidate selection, old-SHA retry, uncertain-failed/older-green UNKNOWN, uncertain-failed/older-failed RED, blank-source-SHA/canceled certainty, terminal failed-or-stale Details/NextAction, public-safe generic BLOCKED rendering, explicit-CWD, Windows launcher, trigger-exclusion, exact-path, timeout, auth-ordering, malformed-output, and public-redaction fixtures - GitHub Actions/public-safe smoke confirms the internal fetcher is not invoked and internal coordinates remain absent ### Design tradeoffs - Azure CLI remains the local adapter so public automation gains no credentials or SDK dependency. - A five-build, server-ordered window keeps the query bounded; selection skips conclusively stale candidates, buffers uncertainty, and reports only outcomes common to every possible currency result. - Certain terminal blocking uses a distinct `failed-or-stale` classification rather than falsely claiming the selected build is current; local guidance restores currency evidence before choosing failure repair versus a current-HEAD rerun, while public-safe output remains generic. - Local Git inspects exact first-parent commit and merge-result paths rather than an aggregate compare diff, preventing second-parent history and net-zero merges from producing false stale results while preserving add/revert evidence. - Missing local objects trigger one bounded fetch of the evaluated ref; unresolved evidence remains UNKNOWN unless every possible outcome blocks, while a conclusive non-ancestor result is stale. - Total authentication unavailability is skipped; partial access loss retains observed branch evidence. - `-InternalBuildId` remains a diagnostic override, but normal local skill/agent runs use branch-based discovery. ### Issues Fixed N/A --------- Co-authored-by: PureWeen <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e39d9d4-19e4-45f5-80f1-2bb3035c18c9
Removes the repository-owned VG20 import and synchronizes the relevant Arcade credential-removal changes on `main`. Tracked by https://dev.azure.com/dnceng/internal/_workitems/edit/12131 Copilot-Session: e890b71a-c1aa-416c-a15c-be8da9fdd9b4
### Description of Change Replace MAUI's long-lived `dn-bot-ceapex-package-r` dependency with a short-lived Entra token for OneLocBuild package restores. - Acquire the Azure DevOps token through the existing DevDiv `dnceng-onelocbuild-ceapex` workload-identity service connection. - Reuse Arcade's shared `get-federated-access-token.yml` helper. - Pass the resulting secret `CeapexEntraToken` to OneLocBuild for the ceapex LCL/LocTools feeds. - Keep the existing GitHub PR authentication unchanged. The DevDiv service connection is configured for workload identity federation and MAUI pipeline 13330 is authorized to use it. ### Validation - Parsed the updated pipeline as YAML. - Verified the shared federated-token helper exists on this branch. - Verified the legacy ceapex PAT is no longer referenced by MAUI's repo-owned localization pipeline. ### Issues Fixed Tracks [dnceng work item 10151](https://dev.azure.com/dnceng/internal/_workitems/edit/10151). Copilot-Session: 80f6a0c6-7947-4200-bb78-7f0fc01d671a
…atic event with WeakEventManager (#37645) <!-- Please let the below note in for people that find this PR --> > [!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! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details When a page or view model subscribes to BindingDiagnostics.BindingFailed (a helpful event that tells you when a data binding fails) and forgets to unsubscribe, that page never gets cleaned up from memory. Every page it holds — plus its images, view models, and data — stays alive until the app closes. Over time, the app uses more and more memory. ### Root Cause BindingFailed is a static event, which means it lives for the whole life of the app. Normal C# events keep a strong reference to whoever subscribed, so as long as the event is alive, its subscribers stay alive too. Since this event never goes away, any subscriber that doesn't call -= is stuck in memory forever. ### Description of Change Switched BindingFailed from a normal event to one backed by WeakEventManager, which keeps only a weak reference to subscribers — so the garbage collector can clean them up even if someone forgets to unsubscribe. Validated the behavior in the following platforms - [x] Android - [x] Windows - [x] iOS - [x] Mac ### Issues Fixed Fixes #37245 ### Fix reference [maui/src/Controls/src/Core/Application/Application.cs at bf62313 · …](https://github.com/dotnet/maui/blob/bf62313db2cecf3215f10c353720b341aba0a4ee/src/Controls/src/Core/Application/Application.cs#L263) [maui/src/Controls/src/Core/Command.cs at bf62313 · dotnet/maui](https://github.com/dotnet/maui/blob/bf62313db2cecf3215f10c353720b341aba0a4ee/src/Controls/src/Core/Command.cs#L128) [maui/src/Controls/src/Core/ImageSource.cs at bf62313 · dotnet/maui](https://github.com/dotnet/maui/blob/bf62313db2cecf3215f10c353720b341aba0a4ee/src/Controls/src/Core/ImageSource.cs#L175) ### Output ScreenShot |Before|After| |--|--| | <video src="https://github.com/user-attachments/assets/86c6d76d-a051-458b-beb3-6c254545f87a" >| <video src="https://github.com/user-attachments/assets/e3b02ce7-71a7-4e90-94e0-dd6587c8e3cf">|
<!-- Please let the below note in for people that find this PR --> > [!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! ## Summary - regenerate all 14 Copilot-based agentic workflows with gh-aw v0.86.2 - stage the installed Copilot CLI in a portable runner-local path instead of invoking `/usr/local/bin/copilot` - explicitly disable shell access in the automatic issue/PR labeler, as required when `min-integrity: none` - adopt gh-aw v0.86.2's intentional disabled-expiry sentinel for action-failure issues when no maintenance workflow exists ## Root cause The failing runs install Copilot CLI successfully, but gh-aw v0.85.4 then invokes `/usr/local/bin/copilot`. That path no longer exists on the hosted runner, so the engine exits with `ENOENT` before producing output or making a model request. gh-aw v0.86.2 includes the upstream path-portability fix and resolves the installed executable from `PATH`. The same generated path affected every Copilot workflow, including the labeler, whose failure reporting is intentionally disabled. Regeneration also incorporates [github/gh-aw#51425](github/gh-aw#51425). Because this repository does not generate an `agentics-maintenance.yml` consumer for the implicit action-failure expiry, all 14 lockfiles now set `GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS` to `"0"`. In gh-aw v0.86.2, explicit `0` means expiration is disabled; it prevents unenforceable expiry markers and does not make failure issues expire immediately. ## Validation - `gh aw compile --strict --validate --actionlint --no-emit` - verified all 14 lockfiles use gh-aw v0.86.2 and portable Copilot staging - verified no generated workflow references `/usr/local/bin/copilot` - verified all 14 generated locks use gh-aw's `"0"` disabled-expiry sentinel consistently No existing open MAUI PR addresses this gh-aw/Copilot path failure. Fixes #37430 Fixes #37431 Fixes #37432 Fixes #37435 Fixes #37442 --------- Co-authored-by: Copilot CI <copilot-ci@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 74cff858-a5a6-486e-a1d3-1d516fc3bdaa Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
<!-- Please let the below note in for people that find this PR --> > [!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 Adds .NET MAUI 10.0.100 and 11.0.0-preview.7 to both version selectors in the bug-report issue form. This lets users report issues against the released SR10 build and the active .NET 11 Preview 7 version. ---------
) <!-- Please let the below note in for people that find this PR --> > [!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! ### Problem The internal `dotnet-maui` official build currently fails to compile the pipeline YAML, before any stage runs: ``` /eng/pipelines/common/variables.yml (Line: 81, Col: 7): Expected at least one key-value pair in the mapping ``` ### Cause #37589 removed `- group: Publish-Build-Assets`, but that group was the only child of the conditional that enclosed it, leaving an empty block behind: ```yaml - group: AzureDevOps-Artifact-Feeds-Pats - ${{ if eq(variables['Build.DefinitionName'], 'dotnet-maui') }}: # <- nothing under it ``` Azure DevOps requires a conditional insertion block to contain at least one entry, so template expansion fails outright. ### Why PR validation stayed green The empty block is nested inside an outer guard: ```yaml - ${{ if or(eq(variables['Build.DefinitionName'], 'dotnet-maui'), eq(variables['Build.DefinitionName'], 'dotnet-maui-build')) }}: - ${{ if notin(variables['Build.Reason'], 'PullRequest') }}: ``` For the public `maui-pr` definition that outer condition is false, so Azure DevOps short-circuits and never expands the inner block. The breakage is therefore invisible to PR CI and only manifests on the internal official build. ### Fix Remove the empty conditional. No file in the repository references the `Publish-Build-Assets` variable group any more, so it is deleted outright rather than repopulated. ### Validation - `eng/pipelines/common/variables.yml` parses as valid YAML. - Swept all 39 YAML files under `eng/pipelines/` for conditional blocks with no body — this was the only one. - `grep` for `Publish-Build-Assets` across `eng/` and `.github/` returns no remaining references. This blocks the internal build entirely, so it is worth merging ahead of larger infra work such as #36257 (which carries the same one-line fix, but cannot be validated on the internal pipeline until this is on `main`). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Detect app and third-party OnBackButtonPressed overrides when deciding whether Android's predictive back callback can consume navigation. Add unit and Appium coverage for root page overrides. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 37709Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 37709" |
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Fixes an Android regression where root ContentPage instances that override Page.OnBackButtonPressed() were incorrectly treated as unable to consume back navigation, causing the OS back handling to bypass MAUI and exit the app (breaking expected override behavior while trying to preserve predictive back).
Changes:
- Add
Page.HasBackButtonPressedOverrideto detect effective overrides without invoking user code and cache the result per page. - Update
Window.CanConsumeBackNavigation(Page?)to treat pages with app/third-party back overrides as able to consume back (enabling MAUI’s back callback), while leaving framework container behavior unchanged. - Add unit tests for back-consumption detection and an Android Appium regression test for the root-page override scenario.
Show a summary per file
| File | Description |
|---|---|
| src/Controls/src/Core/Page/Page.cs | Adds cached override-detection for OnBackButtonPressed() based on the effective declaring type/assembly. |
| src/Controls/src/Core/Window/Window.cs | Uses HasBackButtonPressedOverride to decide whether back can be consumed (predictive back callback enablement). |
| src/Controls/tests/Core.UnitTests/WindowsTests.cs | Adds unit coverage for derived pages and override scenarios affecting CanConsumeBackNavigation. |
| src/Controls/tests/TestCases.HostApp/Issues/Issue37706.cs | Adds a HostApp issue page that overrides OnBackButtonPressed() and reports invocation count. |
| src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue37706.cs | Adds Android Appium test validating the override is invoked and the root page remains visible. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
Make the Issue37706 Appium test fail explicitly when the expected OnBackButtonPressed status does not appear. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4d61e91b-8ea9-4c8f-a6a1-6c03f6776499
There was a problem hiding this comment.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/Controls/tests/TestCases.HostApp/Issues/Issue37706.cs:6
- This issue page is missing the
Maui.Controls.Sample.Issuesnamespace declaration. The HostApp issue pages are discovered/organized under that namespace (most files in this folder use a file-scoped namespace), so leaving this type in the global namespace risks the page not being found by the issue gallery/test navigation.
[Issue(
IssueTracker.Github,
37706,
"Page.OnBackButtonPressed is not invoked on a root page",
PlatformAffected.Android)]
public class Issue37706 : ContentPage
src/Controls/tests/TestCases.HostApp/Issues/Issue37706.cs:36
- The status text uses singular "time" for all counts (e.g., "2 time"), which is grammatically incorrect and can make debugging failures harder. Pluralize the suffix so the label reads correctly for any count.
_statusLabel.Text = $"OnBackButtonPressed called {_backPressCount} time";
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Lite
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 4 findings
See inline comments for details.
| // Framework containers are handled below based on their current navigation state. | ||
| // App and third-party overrides must always receive the back press because only | ||
| // invoking the override can determine whether it consumes the navigation. | ||
| if (page.HasBackButtonPressedOverride) |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Logic and Correctness — The new early-return only inspects the page passed in, and the recursion below never reaches FlyoutPage.Flyout, so a FlyoutPage whose flyout page carries the override is still reported as non-consuming.
FlyoutPage.OnBackButtonPressed (FlyoutPage.cs:239) dispatches to Flyout.SendBackButtonPressed() first when IsPresented, and only then to Detail. But the case FlyoutPage arm below (line ~858) returns true only when IsPresented && CanChangeIsPresented, otherwise recurses into flyoutPage.Detail only — flyoutPage.Flyout is never passed through CanConsumeBackNavigation.
Concrete failing scenario: split-mode FlyoutPage on a tablet/landscape (CanChangeIsPresented == false, IsPresented == true), app-defined flyout page overrides OnBackButtonPressed and returns true, plain ContentPage detail with no override. → first branch is false, CanConsumeBackNavigation(Detail) is false → MauiOnBackPressedCallback.Enabled stays false → the system plays back-to-home and the flyout's override is never invoked. This is the same defect this PR fixes for root pages, unaddressed for the flyout branch, and it contradicts the comment added here ("App and third-party overrides must always receive the back press").
Suggested fix: add || CanConsumeBackNavigation(flyoutPage.Flyout) (guarded by IsPresented, to mirror the dispatch order in FlyoutPage.OnBackButtonPressed) to the FlyoutPage arm, plus a unit test.
|
|
||
| internal bool HasBackButtonPressedOverride => | ||
| _hasBackButtonPressedOverride ??= | ||
| new Func<bool>(OnBackButtonPressed).Method.DeclaringType is Type declaringType && |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[moderate] Trimming/AOT + Performance — new Func<bool>(OnBackButtonPressed).Method allocates a delegate and performs a runtime function-pointer→MethodInfo reverse lookup to derive a fact that is purely type-level, yet it is cached per Page instance (_hasBackButtonPressedOverride). Every Page in the app pays a delegate allocation + Delegate.GetMethodImpl on the first predictive-back state refresh, and every Page instance permanently grows by the extra bool? field.
I verified the virtual-binding semantics are correct (the C# ldvirtftn conversion makes Method.DeclaringType resolve to the most-derived override, including for grandchild types that do not re-override), so the result is right — the concern is cost and trim/AOT durability:
Delegate.Methoddepends on reflection metadata being preserved for the target method. It is only consumed today fromWindow.Android.cs(Mono), but it is compiled into sharedMicrosoft.Maui.Controlscode, so any future non-Android caller puts aMethodInfolookup on a NativeAOT/full-trim path with noDynamicDependency/DynamicallyAccessedMembersanchoring it.
Suggested fix: cache by Type in a static lookup (e.g. ConditionalWeakTable<Type, ...> or a Dictionary<Type, bool> behind a lock) so the reflection runs once per page type rather than once per instance, and drop the per-instance field.
| // Conservative default: return false for unknown page types. | ||
| // We cannot know whether a custom container's OnBackButtonPressed() returns true, | ||
| // so we avoid suppressing the back-to-home animation speculatively. | ||
| // Custom overrides were handled above, so the system can safely provide |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[moderate] Backward Compatibility / Regression Prevention — This rewritten comment understates the blast radius of the new early-return: the check keys off declaration of an override, not its return value, so any page that overrides OnBackButtonPressed and returns false now reports CanConsumeBackNavigation == true, enables MauiOnBackPressedCallback, and loses the Android predictive back-to-home animation (HandleBackNavigation falls through to base.OnBackPressed(), which exits without the system animation).
That is a defensible trade-off for the page that actually needs the callback, but the check is not scoped to the app's own pages — the assembly comparison matches any assembly other than Microsoft.Maui.Controls. Concrete scenario: an app whose pages all derive from a NuGet/MVVM base page class (Prism-style, or an in-house shared library) that overrides OnBackButtonPressed and mostly returns false — every page in that app now permanently loses the back-to-home animation, app-wide, with no opt-out.
Please state this explicitly in the comment (and ideally in the PR description) as a deliberate correctness-over-animation trade-off, since the current wording ("the system can safely provide the back-to-home animation for pages with framework behavior") reads as if the animation is preserved in the common case.
| } | ||
|
|
||
| [Fact] | ||
| public void NavigationPageWithBackButtonOverride_ReturnsTrue() |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[moderate] Regression Prevention and Test Coverage — The three added tests cover only the positive path (override present → true) and one plain negative (DerivedContentPage → false). Three discriminating cases are untested:
FlyoutPagewhoseFlyouthas the override — this is the case that is actually broken (see the comment onWindow.cs:832). A test here would have caught it.- Derived framework container without an override — e.g.
class MyNav : NavigationPage {}with a single page must still returnfalsevia the stack-depth branch. Without this, a future change toHasBackButtonPressedOverride(e.g. switching from assembly comparison toGetType() != typeof(...)) would silently make every derived container claim consumption and no test would fail. - Override that returns
false— the check is declaration-based, so this must still returntrue. Pinning that in a test documents the deliberate trade-off called out onWindow.cs:871.
Also consider a test for a grandchild type (class C : ContentPageWithBackButtonOverride {}) that does not re-override, to lock in the virtual-resolution behavior the implementation relies on.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@jfversluis — new AI review results are available based on commit
270c687.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ✅ PASSED
Platform: ANDROID · Base: main · Merge base: d48c3ba3
✅ Fix verified — 1 test(s) reproduce the bug (FAIL without the fix → PASS with it). 1 test(s) pass in both states and are not bug-reproducing; under the "at least one test reproduces the bug and none regress" rule they don't block the gate.
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🧪 WindowsTests WindowsTests |
❌ PASS — 134s | ✅ PASS — 137s |
🖥️ Issue37706 Issue37706 |
✅ FAIL — 1476s | ✅ PASS — 650s |
🔴 Without fix — 🧪 WindowsTests: PASS ❌ · 134s
(no coded error found; showing last 1200 chars)
teWindowKeepIsActivatedIsFalse [< 1 ms]
Passed WindowCanRetrieveDisplayDensity [4 ms]
Passed BindingIsActivatedProperty [9 ms]
Passed AddingTabSetsWindow [< 1 ms]
Passed ListViewWindowIsInheritedByLayoutsInViewCells [1 ms]
Passed NestedControlsAllHaveTheSameWindow [< 1 ms]
Passed ActivateWindowSendAppearingOnPage [1 ms]
Passed DetachingInTheMiddleUnsetsTheWindow [1 ms]
Passed MovingWindowDoNotTriggerSizeChanged [< 1 ms]
Passed ApplicationIsSetOnWindowBeforeAppearingIsCalledOnPage [< 1 ms]
Passed SetMainPage [< 1 ms]
Passed PageHasTheSameWindowWhenAddedLater [< 1 ms]
Passed WindowServiceScopeHandlesNullScope [1 ms]
Passed WindowServiceScopeWorksWithWindowCreationFlow [8 ms]
Passed SwappingPagesUpdatesTheWindow [< 1 ms]
Passed WindowIsStyleableWithImplicitStyle [< 1 ms]
Passed ActivateWindowWillSetIsActiveToTrue [< 1 ms]
[xUnit.net 00:00:01.79] Finished: Microsoft.Maui.Controls.Core.UnitTests
Passed DestroyedFiresDisappearingEvent [< 1 ms]
Passed TwoKeysSameWindow [32 ms]
Passed AddAndRemoveVisualDiagnosticAdorner [3 ms]
Passed RemovingTabUnSetsWindow [< 1 ms]
Test Run Successful.
Total tests: 58
Passed: 58
Total time: 2.3235 Seconds
🟢 With fix — 🧪 WindowsTests: PASS ✅ · 137s
(no coded error found; showing last 1200 chars)
teWindowKeepIsActivatedIsFalse [< 1 ms]
Passed WindowCanRetrieveDisplayDensity [10 ms]
Passed BindingIsActivatedProperty [13 ms]
Passed AddingTabSetsWindow [< 1 ms]
Passed ListViewWindowIsInheritedByLayoutsInViewCells [1 ms]
Passed NestedControlsAllHaveTheSameWindow [< 1 ms]
Passed ActivateWindowSendAppearingOnPage [1 ms]
Passed DetachingInTheMiddleUnsetsTheWindow [< 1 ms]
Passed MovingWindowDoNotTriggerSizeChanged [1 ms]
Passed ApplicationIsSetOnWindowBeforeAppearingIsCalledOnPage [< 1 ms]
Passed SetMainPage [< 1 ms]
Passed PageHasTheSameWindowWhenAddedLater [< 1 ms]
Passed WindowServiceScopeHandlesNullScope [1 ms]
Passed WindowServiceScopeWorksWithWindowCreationFlow [8 ms]
Passed SwappingPagesUpdatesTheWindow [< 1 ms]
Passed WindowIsStyleableWithImplicitStyle [< 1 ms]
Passed ActivateWindowWillSetIsActiveToTrue [< 1 ms]
[xUnit.net 00:00:03.01] Finished: Microsoft.Maui.Controls.Core.UnitTests
Passed DestroyedFiresDisappearingEvent [3 ms]
Passed TwoKeysSameWindow [45 ms]
Passed AddAndRemoveVisualDiagnosticAdorner [4 ms]
Passed RemovingTabUnSetsWindow [< 1 ms]
Test Run Successful.
Total tests: 58
Passed: 58
Total time: 3.7832 Seconds
🔴 Without fix — 🖥️ Issue37706: FAIL ✅ · 1476s
Error-relevant lines (filtered from the build log):
at Microsoft.Maui.TestCases.Tests.Issues.Issue37706.RootPageBackButtonOverrideIsInvokedOnce() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue37706.cs:line 24
🟢 With fix — 🖥️ Issue37706: PASS ✅ · 650s
(no coded error found; showing last 1200 chars)
.1 (x64)
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.12] Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.37] Discovered: Controls.TestCases.Android.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 08/21/2026 13:35:48 FixtureSetup for Issue37706(Android)
>>>>> 08/21/2026 13:35:51 RootPageBackButtonOverrideIsInvokedOnce Start
>>>>> 08/21/2026 13:35:54 RootPageBackButtonOverrideIsInvokedOnce Stop
Passed RootPageBackButtonOverrideIsInvokedOnce [3 s]
NUnit Adapter 4.5.0.0: Test execution complete
Results File: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue37706.trx
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 18.3040 Seconds
>>> TRX_RESULT_FILE: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue37706.trx
⚠️ Failure Details
- ❌ WindowsTests PASSED without fix (should fail) — tests don't catch the bug
📁 Fix files reverted (2 files)
src/Controls/src/Core/Page/Page.cssrc/Controls/src/Core/Window/Window.cs
📋 Pre-Flight — Context & Validation
PR #37709 Pre-Flight
Context
- PR:
[Android] Fix OnBackButtonPressed on root pages - Issue: #37706, a regression from Microsoft.Maui.Controls 10.0.90 to 10.0.100 on Android 15/16/17.
- Expected: Pressing Android Back invokes an application override of
Page.OnBackButtonPressed(). - Actual: On a root
ContentPage, MAUI'sOnBackPressedDispatchercallback is disabled, Android exits the app, and the override is never invoked. - Platform: Android.
- Gate: Already passed: the detected regression test fails without the PR fix and passes with it. Do not rerun gate verification and do not alter
gate/content.md. - Base/head:
mainatd48c3ba39b58c724e86aca99c3823458ca1df235; PR head270c68781cde6a407da439b3f550c4f3c0a07e55. The local review commit isa7a19e760e.
Existing PR Approach
The PR adds a cached Page.HasBackButtonPressedOverride property. It creates a typed delegate for the effective virtual OnBackButtonPressed implementation, inspects the delegate method's declaring assembly, and treats implementations outside the MAUI Controls assembly as application or third-party overrides. Window.CanConsumeBackNavigation returns true for such pages before applying its existing framework-container navigation-state checks. This preserves predictive back-to-home for framework-default pages while enabling MAUI dispatch when custom code may consume Back.
This is the approach both candidates must avoid repeating: cached delegate/reflection-based override classification on Page, followed by an early override check in Window.CanConsumeBackNavigation.
Diff Inspected
src/Controls/src/Core/Page/Page.cs: adds cached delegate-method override detection.src/Controls/src/Core/Window/Window.cs: consults that detection before the existingShell,NavigationPage,FlyoutPage, andTabbedPagestate checks.src/Controls/tests/Core.UnitTests/WindowsTests.cs: adds coverage for a derived page without an override and customContentPage/NavigationPageoverrides.src/Controls/tests/TestCases.HostApp/Issues/Issue37706.cs: root-page reproduction whose override increments visible state and consumes Back.src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue37706.cs: Android Appium regression test that presses Back, asserts exactly one invocation, and confirms the root page remains.
The PR currently changes 5 files (+122/-2). A prior review request to assert the Appium wait result has already been incorporated.
Bounded Validation Scope
Use only these targeted checks; do not run a full suite:
- Primary Android regression test:
pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "Issue37706" - Targeted navigation regressions: the five relevant
Window.CanConsumeBackNavigationfacts inWindowsTests(ContentPage_ReturnsFalse,DerivedContentPageWithoutOverride_ReturnsFalse,ContentPageWithBackButtonOverride_ReturnsTrue,NavigationPageWithBackButtonOverride_ReturnsTrue, andNavigationPage_SinglePage_ReturnsFalse) via the Controls Core unit-test project and an exact OR filter.
Each candidate gets one implementation/test pass and at most one focused correction/retest. A blocked device/environment phase must be recorded honestly rather than expanded into unrelated validation.
Workspace Safety
The checkout contains pre-existing dirty review-infrastructure files under .github/ and eng/. They are harness-owned and unrelated to this PR. Do not edit, restore, reset, stash, clean, or delete them. Follow the try-fix skill's script-only baseline/restore protocol and modify only paths allowed by .github/.baseline-state.json.
🔬 Code Review — Deep Analysis
Expert Evaluation — Submitted PR
Verdict: NEEDS_CHANGES
Confidence: medium
The submitted fix correctly restores Android back dispatch for root pages whose effective OnBackButtonPressed() implementation comes from application or third-party code. The typed delegate resolves the most-derived virtual override, the per-instance cached result is type-invariant, and the existing Android fallback still exits when an override returns false. The Gate result confirms the reported root-page regression fails without the fix and passes with it.
Actionable findings
- Major — the same defect remains for an override on
FlyoutPage.Flyout.FlyoutPage.OnBackButtonPressed()sends Back to the presented flyout before its detail, butWindow.CanConsumeBackNavigation()only checks whether the flyout itself can close and then recurses intoDetail. In split mode, a presented flyout page with an application override can therefore leave the Android callback disabled and never receive Back. - Moderate — override detection performs type-level work per page instance. Each page allocates a delegate and resolves its
MethodInfoon first inspection, then stores a nullable Boolean field. A type-keyed cache would avoid repeated work and provide a clearer place to document the reflection/trim contract. - Moderate — the animation trade-off is understated. Detection is based on where an override is declared, not what it returns. A shared or third-party base page that overrides and commonly returns
falsecauses all derived pages to route through MAUI and lose predictive back-to-home animation. Correct dispatch takes precedence, but the code should state that trade-off. - Moderate — tests omit the discriminating paths. Coverage should include the presented flyout-page override, a derived framework container without an override, an override returning
false, and inherited effective override behavior.
Risk assessment
The change is localized to Android back-registration behavior but affects every page inspected by CanConsumeBackNavigation(). There is no new subscription, handler-lifetime, or mutable cross-window state. The blocking issue is the untraversed flyout recipient; the other findings are performance, durability, documentation, and regression-coverage improvements suitable for one consolidated refinement.
🛠️ Try-Fix — Analysis & Comparison
PR #37709 — Try-Fix Candidate 1
Result: Blocked (environment/workspace precondition — baseline could not be established without violating workspace-safety rules)
1. Problem recap
Android 15/16/17 stopped invoking an application's overridden Page.OnBackButtonPressed() on a root page after MAUI 10.0.100 moved back handling to OnBackPressedDispatcher. MauiAppCompatActivity.ShouldRegisterPredictiveBackCallback() enables MauiOnBackPressedCallback only when IBackNavigationState.CanConsumeBackNavigation is true; Window.CanConsumeBackNavigation(Page) classifies a plain/derived root ContentPage as unable to consume navigation, so the callback stays disabled and Android exits the app without ever calling the override (issue #37706).
2. Candidate 1 approach — "fail-open back dispatch with system fallback"
Designed change set (not applied — see §5):
| File | Change |
|---|---|
src/Core/src/Platform/Android/MauiAppCompatActivity.cs |
Keep MauiOnBackPressedCallback.Enabled true whenever a MAUI Window is attached and at least one AndroidLifecycle.OnBackPressed delegate is registered — stop gating enablement on IBackNavigationState.CanConsumeBackNavigation. |
src/Core/src/Platform/Android/MauiAppCompatActivity.cs (HandleBackNavigation) |
When MAUI's dispatch chain (modal stack → Window.BackButtonPressed() → Page.SendBackButtonPressed() → Page.OnBackButtonPressed()) reports not handled, re-delegate to the platform default: temporarily set Enabled = false, re-invoke OnBackPressedDispatcher.OnBackPressed(), then restore Enabled. |
src/Controls/src/Core/Window/Window.cs |
CanConsumeBackNavigation is demoted from correctness gate to an animation hint only. |
3. Mechanism-level difference from the PR
PR mechanism (must not be repeated): cache a typed delegate for the effective Page.OnBackButtonPressed implementation, inspect delegate.Method.DeclaringType.Assembly, expose it as Page.HasBackButtonPressedOverride, and have Window.CanConsumeBackNavigation return true early for any page whose implementation lives outside the MAUI Controls assembly.
Shared failure mechanism of that family: it retains the predict-then-enable design and merely widens the static predicate. Consumption is still guessed from type metadata before the override runs, so any page whose decision is dynamic — conditional consumption, a custom container that delegates to a child, a page that consumes only in certain states — is still classified wrongly in one direction (missed dispatch) or the other (suppressed back-to-home).
Candidate 1's mechanism and its cause-to-effect chain: prediction is removed from the correctness path entirely. Because the callback is always enabled, OnBackButtonPressed() is always invoked; because it is always invoked, its actual return value — not a type-metadata classification — determines whether the app consumed Back; because a non-consuming result explicitly re-dispatches to the system's default handler, back-to-home still happens. No reflection, no delegate/method-declaring-type inspection, and no per-Page cached classification appears anywhere in that chain. It is a different root-cause hypothesis ("prediction is the wrong model") rather than the same detection expressed in different syntax or moved to a different call site.
Honest trade-off: Android reads OnBackPressedCallback.Enabled before the predictive-back drag preview commits. An always-enabled callback therefore loses the system back-to-home preview animation for framework-default pages (commit-time behaviour remains correct via the fallback). That preview fidelity is precisely what the PR's predicate protects, so this is a real design trade-off, not a strict improvement.
Structural constraint discovered (independent of the block): the mandated targeted unit facts DerivedContentPageWithoutOverride_ReturnsFalse, ContentPageWithBackButtonOverride_ReturnsTrue, and NavigationPageWithBackButtonOverride_ReturnsTrue hard-code the PR's prediction semantics for Window.CanConsumeBackNavigation. In C#, distinguishing "this subclass overrides the virtual" from "it does not" is only expressible through type metadata — i.e. exactly the reflection/delegate-inspection family this candidate is forbidden to repeat. Therefore every genuinely different mechanism must change the prediction model, and cannot satisfy those three read-only facts unchanged. This bounds the whole alternative-candidate space for this PR and should inform candidate 2 / the final report.
4. Candidate diff
None. git diff -- src/ is empty; fix.diff and reviewer-findings.diff are 0 bytes. No tracked file was modified at any point during this invocation.
5. Why it is Blocked — baseline could not be established
Step 2 of the try-fix skill requires:
pwsh .github/scripts/EstablishBrokenBaseline.ps1
It hard-failed:
ERROR: DIRTY WORKING DIRECTORY - Cannot establish baseline
M .github/scripts/BuildAndRunHostApp.ps1
M .github/scripts/EstablishBrokenBaseline.ps1
M .github/scripts/Review-PR.ps1
... (~40 files under .github/ and eng/) ...
EstablishBrokenBaseline.ps1 failed: Working directory is not clean.
Findings:
- The gate is
git status --porcelain --untracked-files=noand is unconditional — I grepped the script for-AllowDirty/-SkipClean/$env:overrides and there is no escape hatch. - Every dirty path is pre-existing, harness-owned review infrastructure under
.github/andeng/, unrelated to PR #37709. The invocation bounds forbid editing, reverting, staging, deleting, or otherwise altering them; the skill separately forbidsgit checkout/restore/reset/clean/stash. The script's own two suggested remedies (commit them, orgit checkout -- .) are both prohibited. - Because the script threw before writing state,
.github/.baseline-state.jsonwas never created. Skill Step 2 is explicit for this case: reportBlockedbefore editing any tracked file, since the restore script can only restore files it recorded. Editing source without a baseline would also have layered a candidate on top of the PR's fix rather than replacing it, making the comparison meaningless.
Accordingly no source edit was attempted, and neither mandated validation command was run.
6. Test commands and results
Both mandated commands: NOT RUN (no candidate diff existed to validate). No broader suite was run.
pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "Issue37706"— NOT RUN.dotnet test src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj --filter "<exact OR filter over ContentPage_ReturnsFalse, DerivedContentPageWithoutOverride_ReturnsFalse, ContentPageWithBackButtonOverride_ReturnsTrue, NavigationPageWithBackButtonOverride_ReturnsTrue, NavigationPage_SinglePage_ReturnsFalse>"— NOT RUN.
Per the skill's Pass/Fail table, a Pass requires the test command to execute and report success. "The approach is sound" is never a Pass, and an unexecuted test is Blocked, not Fail. Gate verification was not rerun and gate/content.md was not touched.
7. Inline expert self-review (performed inline, no sub-agent)
reviewer-findings.json = [] — 0 findings. There is no diff, so the Overarching Principles and the always-active dimensions (Logic and Correctness, Regression Prevention, Complexity Reduction) of .github/agents/maui-expert-reviewer.md have no changed lines to bind to. No findings were invented to fill the file.
8. Failure analysis
The block is an environment/workspace precondition, not a defect in the candidate design. EstablishBrokenBaseline.ps1 assumes a pristine tree, while this worktree intentionally ships harness drift the tool cannot distinguish from leaked try-fix state. Highest-value repo follow-up: give the script a path allow-list (ignore .github/** and eng/**, or accept an -IgnorePath / $env:TRYFIX_IGNORE_DIRTY parameter) when evaluating cleanliness; otherwise every try-fix invocation in a worktree prepared like this one will block at Step 2.
9. Restoration
pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore
→ Message: No baseline state found
→ Restored: False
This is the skill's explicitly accepted no-state outcome: baseline state was verified absent (.github/.baseline-state.json does not exist) and no attempt edits were made. Confirmed after restore:
git status --porcelain --untracked-files=no -- src/→ empty (no candidate code changes anywhere insrc/).- The ~40 pre-existing harness-owned modifications under
.github/andeng/remain exactly as found — untouched, unstaged, unreverted. - Only new files created are this attempt's artifacts under
CustomAgentLogsTmp/PRState/37709/PRAgent/. - No
git checkout/restore/reset/clean/stashand no filesystem deletion were used at any point.
10. Artifacts
CustomAgentLogsTmp/PRState/37709/PRAgent/try-fix/attempt-1/—baseline.log,approach.md,reviewer-findings.json([]),reviewer-findings.diff(empty),result.txt(Blocked),fix.diff(empty),test-output.log,analysis.mdCustomAgentLogsTmp/PRState/37709/PRAgent/try-fix-1/content.md— this narrativeCustomAgentLogsTmp/PRState/37709/PRAgent/try-fix/content.md— this narrative
PR #37709 — Try-Fix Candidate 2
Result: Blocked — Android validation could not build because repository build tasks were absent/out of date.
Approach
Changed only the default branch of Window.CanConsumeBackNavigation(Page?) to call page.SendBackButtonPressed() and use the leaf/custom page's actual Boolean result. Framework-container branches remained state-based to avoid knowingly mutating their navigation.
Difference from both prior mechanisms
The PR caches a typed delegate, inspects its declaring assembly, and classifies override identity. Candidate 2 uses no reflection, delegate/type metadata, cache, or override classification; it directly evaluates leaf behavior. Candidate 1 proposed an always-enabled Android callback and disable/re-dispatch fallback after an unconsumed commit. Candidate 2 does neither: it retains predictive registration and obtains the leaf answer pre-gesture by executing the virtual behavior. Thus it explores behavioral evidence rather than metadata prediction or commit-time fallback.
Full candidate diff
diff --git a/src/Controls/src/Core/Window/Window.cs b/src/Controls/src/Core/Window/Window.cs
index 55d90e5025..b7db9e2304 100644
--- a/src/Controls/src/Core/Window/Window.cs
+++ b/src/Controls/src/Core/Window/Window.cs
@@ -861,10 +861,9 @@ namespace Microsoft.Maui.Controls
return CanConsumeBackNavigation(multiPage.CurrentPage);
default:
- // Conservative default: return false for unknown page types.
- // We cannot know whether a custom container's OnBackButtonPressed() returns true,
- // so we avoid suppressing the back-to-home animation speculatively.
- return false;
+ // Ask leaf pages directly; framework containers above are evaluated from state
+ // because invoking their back behavior here could mutate navigation.
+ return page.SendBackButtonPressed();
}
}Exact tests/results
-
pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "Issue37706"Blocked before candidate compilation/test execution. The device was detected and restore succeeded, but all three script-internal build/deploy attempts failed at
src/Maui.InTree.targets(34,5): “required MSBuild tasks are not yet built or they are out of date”; it requesteddotnet build Microsoft.Maui.BuildTasks.slnfand exited 1. Full output:try-fix/attempt-2/test-output.log. -
dotnet test src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj --filter "FullyQualifiedName=Microsoft.Maui.Controls.Core.UnitTests.WindowsTests.ContentPage_ReturnsFalse|FullyQualifiedName=Microsoft.Maui.Controls.Core.UnitTests.WindowsTests.DerivedContentPageWithoutOverride_ReturnsFalse|FullyQualifiedName=Microsoft.Maui.Controls.Core.UnitTests.WindowsTests.ContentPageWithBackButtonOverride_ReturnsTrue|FullyQualifiedName=Microsoft.Maui.Controls.Core.UnitTests.WindowsTests.NavigationPageWithBackButtonOverride_ReturnsTrue|FullyQualifiedName=Microsoft.Maui.Controls.Core.UnitTests.WindowsTests.NavigationPage_SinglePage_ReturnsFalse"NOT RUN. The bounded-attempt rule requires immediate Blocked reporting/artifact completion/restoration when build or environment is blocked. No broader/substitute tests ran. Both groups did not execute/pass, so no Pass is claimed. Gate verification was not rerun;
gate/content.mdwas untouched.
Inline expert review
Reviewed the final diff inline against all Overarching Principles, always-active Logic/Correctness, Regression Prevention, Complexity Reduction, and navigation implications. reviewer-findings.json has 2 major findings:
CanConsumeBackNavigationis called during appearing/navigation refreshes, so the changed line executes app behavior before a Back gesture and potentially repeatedly. Issue37706 can increment before the press and again at commit.- The default-only probe bypasses application overrides on framework containers;
NavigationPageWithBackButtonOverride_ReturnsTrueremains unsatisfied.
No focused correction was viable without changing mechanisms: a safe pre-gesture signal that automatically tracks arbitrary overrides requires the forbidden override-metadata family; commit-time evaluation/fallback repeats Candidate 1. Invoking OnBackButtonPressed as a query is fundamentally unsafe because it is an action with side effects.
Failure analysis
The observed result is Blocked because the required command failed before compiling this hunk. Separately, review confirms the candidate is not viable. Candidate 1's structural finding holds: the mandated facts require distinguishing overridden from inherited behavior before intended invocation. Without metadata, explicit app opt-in (absent from the read-only fixtures), or always-on commit dispatch, there is no safe information source.
Restoration
Ran exactly pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore; output reported Restored: True for Page.cs and Window.cs. Verification found no target diff and no .github/.baseline-state.json. No checkout/restore/reset/clean/stash or filesystem deletion was used.
Artifacts
/home/vsts/work/1/s/CustomAgentLogsTmp/PRState/37709/PRAgent/try-fix/attempt-2/: baseline.log, approach.md, reviewer-findings.json, reviewer-findings.diff, result.txt, fix.diff, test-output.log, analysis.md.
🏁 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Winner: pr-plus-reviewer
The submitted fix has the right core mechanism and its Gate passed, but the expert review found a concrete branch of the same regression: a presented FlyoutPage.Flyout can receive Back before Detail, while the submitted CanConsumeBackNavigation() never inspects it. The pr-plus-reviewer candidate preserves the PR's tested root-page behavior, mirrors the actual flyout dispatch order, documents the predictive-animation trade-off, and adds discriminating coverage. Its focused unit validation passed 10/10 and the Android Issue37706 regression passed 1/1.
Candidate comparison
| Rank | Candidate | Evidence | Assessment |
|---|---|---|---|
| 1 | pr-plus-reviewer |
Gate inherited from PR; focused unit tests 10/10; Android regression 1/1 | Winner. Fixes the submitted root-page regression and the expert-identified split-mode flyout branch without invoking application behavior speculatively. |
| 2 | pr |
Trusted Gate passed | Sound root-page fix, but it leaves a presented flyout recipient untraversed. The per-instance cache and animation trade-off are non-blocking; the flyout omission is not. |
| 3 | try-fix-1 |
Blocked; no diff and no tests run | The always-enabled callback design would dispatch correctly but knowingly removes predictive back-to-home preview for ordinary pages. With no implementation or regression evidence, it cannot outrank validated candidates. |
| 4 | try-fix-2 |
Android validation blocked; unit tests not run | Invokes SendBackButtonPressed() while merely querying navigation state, causing user side effects before a gesture and potentially repeatedly. It also misses overrides on framework containers, so it is not viable independently of the environment block. |
Expert findings disposition
- Presented flyout override: fixed by checking the visible flyout before closeability and detail navigation; covered in split and hidden states.
- Behavioral trade-off: documented explicitly and covered by an override-returning-
falsefact. - Effective inherited override: covered by a grandchild-page fact.
- Per-instance cache: retained as a non-blocking, unmeasured optimization opportunity; the submitted implementation computes a type-invariant value once per page and introduces no correctness failure.
Because pr-plus-reviewer, rather than the raw submitted pr, is the winning candidate, the submitted PR needs the reviewer patch before approval.
📱 UI Tests — Navigation,Page,Window
Detected UI test categories: Navigation,Page,Window
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Navigation |
91/92 (1 skipped) ✓ | — |
Page |
26/26 ✓ | — |
Window |
0 tests | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
🧭 Next Steps — reviewer changes required
The reviewer-enhanced candidate identified changes that are not yet in the submitted PR.
Why: The reviewer-enhanced PR preserves the Gate-proven root-page fix while also following FlyoutPage's real dispatch order, closing the uncovered split-mode flyout override path. Its 10 focused unit tests and Android Issue37706 UI test all passed.
Address the actionable findings in this review before merging.
> [!NOTE] > @kamano1, thank you for reporting this regression. When the PR artifacts are available, please try them with your application and let us know whether they restore the expected behavior on your affected devices. ### Description Fixes #37706. .NET MAUI 10.0.100 moved Android back handling entirely to `OnBackPressedDispatcher`. To preserve Android's predictive back-to-home animation, MAUI enables its callback only when the current page can consume back navigation. A root `ContentPage` was always classified as unable to consume back, even when application code overrode `Page.OnBackButtonPressed()`, so Android bypassed MAUI and exited the app. This change detects the effective virtual `OnBackButtonPressed()` implementation without invoking user code. Overrides declared by applications or third-party controls now enable MAUI's callback, while ordinary pages and MAUI's framework container implementations retain the existing predictive back-to-home behavior. The result is cached per page and uses a typed delegate, avoiding string-based reflection and linker/AOT concerns. ### Tests - Added unit coverage for plain derived pages, custom `ContentPage` overrides, and custom `NavigationPage` overrides. - Added an Android Appium regression test that installs the issue page as `Window.Page`, presses the device back button, verifies the override runs exactly once, and verifies the root page remains visible. ### Runtime verification Verified on an Android 16 / API 36 emulator (`sdk_gphone64_arm64`) using a real Android back key event. Without the fix, the root page reports that the override has not run, then the app exits to the Android launcher: | Before back | After back | |---|---| | <img width="270" alt="Root page before back without fix" src="https://github.com/user-attachments/assets/030fc1aa-1680-4ba3-93b5-979aab7b8f8a"> | <img width="270" alt="Android launcher after back without fix" src="https://github.com/user-attachments/assets/37eec36d-6da1-40fb-b75c-f2e59ad7a3ef"> | With the fix, the same root page remains foregrounded and reports exactly one invocation: | Before back | After back | |---|---| | <img width="270" alt="Root page before back with fix" src="https://github.com/user-attachments/assets/b83c6682-a8a2-467b-a62a-f7385d141e13"> | <img width="270" alt="Root page after back with one invocation" src="https://github.com/user-attachments/assets/c6b8c53d-d99f-4138-9bd9-b409daaee0e6"> | ---------
…load (#37746) <!-- Please let the below note in for people that find this PR --> > [!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! <!-- release-readiness-agent: human-approval-required --> ## Summary Integrates the staged **.NET MAUI 10.0.101 (SR10.1)** payload from `inflight/10.0.101` into `release/10.0.1xx-sr10`, based on the shipped `10.0.100` release. The full release inventory and servicing decisions are tracked in #37723. The [authoritative regression query](https://github.com/dotnet/maui/issues?q=repo%3Adotnet%2Fmaui%20is%3Aissue%20is%3Aopen%20%28label%3A%22regressed-in-10.0.10%22%20OR%20label%3A%22regressed-in-10.0.20%22%20OR%20label%3A%22regressed-in-10.0.30%22%20OR%20label%3A%22regressed-in-10.0.40%22%20OR%20label%3A%22regressed-in-10.0.50%22%20OR%20label%3A%22regressed-in-10.0.60%22%20OR%20label%3A%22regressed-in-10.0.70%22%20OR%20label%3A%22regressed-in-10.0.80%22%20OR%20label%3A%22regressed-in-10.0.90%22%20OR%20label%3A%22regressed-in-10.0.100%22%29) remains the source for the servicing-regression scope. > [!IMPORTANT] > Checked items under **Staged in this PR** are merged into `inflight/10.0.101`, not yet into `release/10.0.1xx-sr10`. They reach the release branch only when this aggregate PR is merged. ## Status at a glance - [x] Stage the selected regression backports independently on `inflight/10.0.101`. - [x] Stage the SkiaSharp 4.150.1 payload and selective `SKPath.AddCircle` compatibility fix. - [x] Stage the isolated iOS 26 Liquid Glass TabBar payload from #37542. - [ ] Complete or explicitly defer the six unresolved servicing regressions listed below. - [ ] Complete the release gates and merge this PR into `release/10.0.1xx-sr10`. ## Staged in this PR ### Servicing regressions - [x] #35826 — Android MediaPicker completion from child activities. Source #35944; component backport #37724. - [x] #36298 — Windows dynamic `ContentPresenter` assignment. Source #36430; component backport #37725. - [x] #36749 — preserve custom iOS Button styling. Source #36769; component backport #37726. - [x] #36736 — align Android SwipeItem content. Source #36820; component backport #37727. - [x] #34563 — per-edge iOS safe-area handling. Source #37033; component backport #37728. - [x] #37706 — Android root-page back handling. Source #37709; component backport #37729. - [x] #37705 — Android Material 3 status-bar contrast. Source #37710; component backport #37730. - [x] #37423 — restore native iOS 26 Liquid Glass TabBar behavior through the isolated four-file payload from merged PR #37542. - [x] #37418 — Android off-screen `TranslationY` layout padding. Source fix #37772; focused backport commit `10799e67df`. - [x] #37361 — iOS RefreshView pull-to-refresh with an empty CollectionView. Source fix #37404; focused backport commit `d26faa8e93`. - [x] #37638 — Android `Screenshot.CaptureAsync` synchronous UI-thread deadlock. Source fix #37680; focused backport commit `778040a233`. ### Planned Skia payload - [x] Port the Skia/Svg.Skia update from #36255 through component backport #37731. - [x] Include only the required `SKPath.AddCircle` compatibility change from #36787; exclude its unrelated SourceGen changes. - [x] Confirm a matching SkiaSharp 4.150.1 native-assets/PDB build for `_SkiaSharpNativeAssetsVersion`, or record an explicit release-owner waiver. ### Integration follow-ups - [x] Apply the aggregate review fixes for safe-area cache invalidation, Activity-for-result cancellation ownership, and Android theme-test semantics. - [x] Keep the final branch payload as focused release commits without unrelated `inflight/current` history. - [x] Validate the #37418 and #37638 focused Android UI tests on the aggregate branch (2/2 passed). - [ ] Validate the #37361 focused iOS UI test on the aggregate branch. ## Already present on the SR10 base — no new merge needed - [x] #36852 — Android Shell hidden-fragment leak; fix #36903 is already in SR10. - [x] #36735 — Android ActivityIndicator visibility; fix #36748 is already in SR10. - [x] #36853 — Android Shell DI-singleton route blank page; fix #36903 is already in SR10. - [x] #36942 — Android UI event handling; fix #36988 is already in SR10. - [x] #37281 — Android scrolling inside shadowed containers; backport #37312 is already in SR10. ## Completed disposition — no backport required - [x] #37700 — closed as intended SR10 `SwipeView.Threshold` behavior from #36878; any SwipeItem sizing concern should be tracked separately. ## Not included yet — merge, defer, or explicitly disposition before payload freeze - [ ] #35301 — Windows CollectionView applies WinUI styling by default; no linked fix PR. - [ ] #34491 — Android CollectionView selection with `PointerGestureRecognizer`; replacement fix #37952 remains draft against `main`. - [ ] #35059 — iOS app becomes unresponsive when opening a ComboBox dropdown in landscape; no linked fix PR. - [ ] #37407 — iOS TimePicker default `t` format forces en-US; fix #37797 remains draft against `main`. - [ ] #36269 — Android `SafeAreaEdges` with Shell tab navigation and hidden TabBar; source fix #36474 remains open against `main` and has no SR10 backport. - [ ] #37657 — Android Shell/root-page `OnBackButtonPressed()` in Release builds; no linked fix PR. ## Release completion ### Before merging this PR - [ ] Refresh the authoritative query and reconcile #37723 with its current results. - [ ] Record an include/defer/close decision for every unresolved regression above. - [ ] Validate every included regression on its reported OS/version using the `10.0.101` candidate packages. - [ ] Complete current-head aggregate CI and resolve or explicitly waive every required build/device/UI-test failure. - [ ] Resolve or waive the Skia native-assets/PDB release gate. - [ ] Bump `eng/Versions.props` from patch `100` to `101` after the payload is frozen. - [ ] Obtain the required independent current-head maintainer approvals. - [ ] Merge #37746 into `release/10.0.1xx-sr10`. ### After merge - [ ] Confirm BAR promotion and the per-build validation feed used by the ship assessment. - [ ] Publish and tag `10.0.101`. - [ ] Complete the Visual Studio insertion/default-MAUI-version update. ## Notes - No automated `/backport` command was used for this release payload. - Supersedes #37745, which GitHub automatically closed when the integration branch was renamed from `vs/10.0.101` to `inflight/10.0.101`.
> [!NOTE] > @kamano1, thank you for reporting this regression. When the PR artifacts are available, please try them with your application and let us know whether they restore the expected behavior on your affected devices. ### Description Fixes #37706. .NET MAUI 10.0.100 moved Android back handling entirely to `OnBackPressedDispatcher`. To preserve Android's predictive back-to-home animation, MAUI enables its callback only when the current page can consume back navigation. A root `ContentPage` was always classified as unable to consume back, even when application code overrode `Page.OnBackButtonPressed()`, so Android bypassed MAUI and exited the app. This change detects the effective virtual `OnBackButtonPressed()` implementation without invoking user code. Overrides declared by applications or third-party controls now enable MAUI's callback, while ordinary pages and MAUI's framework container implementations retain the existing predictive back-to-home behavior. The result is cached per page and uses a typed delegate, avoiding string-based reflection and linker/AOT concerns. ### Tests - Added unit coverage for plain derived pages, custom `ContentPage` overrides, and custom `NavigationPage` overrides. - Added an Android Appium regression test that installs the issue page as `Window.Page`, presses the device back button, verifies the override runs exactly once, and verifies the root page remains visible. ### Runtime verification Verified on an Android 16 / API 36 emulator (`sdk_gphone64_arm64`) using a real Android back key event. Without the fix, the root page reports that the override has not run, then the app exits to the Android launcher: | Before back | After back | |---|---| | <img width="270" alt="Root page before back without fix" src="https://github.com/user-attachments/assets/030fc1aa-1680-4ba3-93b5-979aab7b8f8a"> | <img width="270" alt="Android launcher after back without fix" src="https://github.com/user-attachments/assets/37eec36d-6da1-40fb-b75c-f2e59ad7a3ef"> | With the fix, the same root page remains foregrounded and reports exactly one invocation: | Before back | After back | |---|---| | <img width="270" alt="Root page before back with fix" src="https://github.com/user-attachments/assets/b83c6682-a8a2-467b-a62a-f7385d141e13"> | <img width="270" alt="Root page after back with one invocation" src="https://github.com/user-attachments/assets/c6b8c53d-d99f-4138-9bd9-b409daaee0e6"> | ---------
> [!NOTE] > @kamano1, thank you for reporting this regression. When the PR artifacts are available, please try them with your application and let us know whether they restore the expected behavior on your affected devices. ### Description Fixes #37706. .NET MAUI 10.0.100 moved Android back handling entirely to `OnBackPressedDispatcher`. To preserve Android's predictive back-to-home animation, MAUI enables its callback only when the current page can consume back navigation. A root `ContentPage` was always classified as unable to consume back, even when application code overrode `Page.OnBackButtonPressed()`, so Android bypassed MAUI and exited the app. This change detects the effective virtual `OnBackButtonPressed()` implementation without invoking user code. Overrides declared by applications or third-party controls now enable MAUI's callback, while ordinary pages and MAUI's framework container implementations retain the existing predictive back-to-home behavior. The result is cached per page and uses a typed delegate, avoiding string-based reflection and linker/AOT concerns. ### Tests - Added unit coverage for plain derived pages, custom `ContentPage` overrides, and custom `NavigationPage` overrides. - Added an Android Appium regression test that installs the issue page as `Window.Page`, presses the device back button, verifies the override runs exactly once, and verifies the root page remains visible. ### Runtime verification Verified on an Android 16 / API 36 emulator (`sdk_gphone64_arm64`) using a real Android back key event. Without the fix, the root page reports that the override has not run, then the app exits to the Android launcher: | Before back | After back | |---|---| | <img width="270" alt="Root page before back without fix" src="https://github.com/user-attachments/assets/030fc1aa-1680-4ba3-93b5-979aab7b8f8a"> | <img width="270" alt="Android launcher after back without fix" src="https://github.com/user-attachments/assets/37eec36d-6da1-40fb-b75c-f2e59ad7a3ef"> | With the fix, the same root page remains foregrounded and reports exactly one invocation: | Before back | After back | |---|---| | <img width="270" alt="Root page before back with fix" src="https://github.com/user-attachments/assets/b83c6682-a8a2-467b-a62a-f7385d141e13"> | <img width="270" alt="Root page after back with one invocation" src="https://github.com/user-attachments/assets/c6b8c53d-d99f-4138-9bd9-b409daaee0e6"> | ---------
> [!NOTE] > @kamano1, thank you for reporting this regression. When the PR artifacts are available, please try them with your application and let us know whether they restore the expected behavior on your affected devices. ### Description Fixes #37706. .NET MAUI 10.0.100 moved Android back handling entirely to `OnBackPressedDispatcher`. To preserve Android's predictive back-to-home animation, MAUI enables its callback only when the current page can consume back navigation. A root `ContentPage` was always classified as unable to consume back, even when application code overrode `Page.OnBackButtonPressed()`, so Android bypassed MAUI and exited the app. This change detects the effective virtual `OnBackButtonPressed()` implementation without invoking user code. Overrides declared by applications or third-party controls now enable MAUI's callback, while ordinary pages and MAUI's framework container implementations retain the existing predictive back-to-home behavior. The result is cached per page and uses a typed delegate, avoiding string-based reflection and linker/AOT concerns. ### Tests - Added unit coverage for plain derived pages, custom `ContentPage` overrides, and custom `NavigationPage` overrides. - Added an Android Appium regression test that installs the issue page as `Window.Page`, presses the device back button, verifies the override runs exactly once, and verifies the root page remains visible. ### Runtime verification Verified on an Android 16 / API 36 emulator (`sdk_gphone64_arm64`) using a real Android back key event. Without the fix, the root page reports that the override has not run, then the app exits to the Android launcher: | Before back | After back | |---|---| | <img width="270" alt="Root page before back without fix" src="https://github.com/user-attachments/assets/030fc1aa-1680-4ba3-93b5-979aab7b8f8a"> | <img width="270" alt="Android launcher after back without fix" src="https://github.com/user-attachments/assets/37eec36d-6da1-40fb-b75c-f2e59ad7a3ef"> | With the fix, the same root page remains foregrounded and reports exactly one invocation: | Before back | After back | |---|---| | <img width="270" alt="Root page before back with fix" src="https://github.com/user-attachments/assets/b83c6682-a8a2-467b-a62a-f7385d141e13"> | <img width="270" alt="Root page after back with one invocation" src="https://github.com/user-attachments/assets/c6b8c53d-d99f-4138-9bd9-b409daaee0e6"> | ---------
> [!NOTE] > @kamano1, thank you for reporting this regression. When the PR artifacts are available, please try them with your application and let us know whether they restore the expected behavior on your affected devices. ### Description Fixes #37706. .NET MAUI 10.0.100 moved Android back handling entirely to `OnBackPressedDispatcher`. To preserve Android's predictive back-to-home animation, MAUI enables its callback only when the current page can consume back navigation. A root `ContentPage` was always classified as unable to consume back, even when application code overrode `Page.OnBackButtonPressed()`, so Android bypassed MAUI and exited the app. This change detects the effective virtual `OnBackButtonPressed()` implementation without invoking user code. Overrides declared by applications or third-party controls now enable MAUI's callback, while ordinary pages and MAUI's framework container implementations retain the existing predictive back-to-home behavior. The result is cached per page and uses a typed delegate, avoiding string-based reflection and linker/AOT concerns. ### Tests - Added unit coverage for plain derived pages, custom `ContentPage` overrides, and custom `NavigationPage` overrides. - Added an Android Appium regression test that installs the issue page as `Window.Page`, presses the device back button, verifies the override runs exactly once, and verifies the root page remains visible. ### Runtime verification Verified on an Android 16 / API 36 emulator (`sdk_gphone64_arm64`) using a real Android back key event. Without the fix, the root page reports that the override has not run, then the app exits to the Android launcher: | Before back | After back | |---|---| | <img width="270" alt="Root page before back without fix" src="https://github.com/user-attachments/assets/030fc1aa-1680-4ba3-93b5-979aab7b8f8a"> | <img width="270" alt="Android launcher after back without fix" src="https://github.com/user-attachments/assets/37eec36d-6da1-40fb-b75c-f2e59ad7a3ef"> | With the fix, the same root page remains foregrounded and reports exactly one invocation: | Before back | After back | |---|---| | <img width="270" alt="Root page before back with fix" src="https://github.com/user-attachments/assets/b83c6682-a8a2-467b-a62a-f7385d141e13"> | <img width="270" alt="Root page after back with one invocation" src="https://github.com/user-attachments/assets/c6b8c53d-d99f-4138-9bd9-b409daaee0e6"> | ---------
Note
@kamano1, thank you for reporting this regression. When the PR artifacts are available, please try them with your application and let us know whether they restore the expected behavior on your affected devices.
Description
Fixes #37706
Fixes #37657
Fixes #38030
.NET MAUI 10.0.100 moved Android back handling entirely to
OnBackPressedDispatcher. To preserve Android's predictive back-to-home animation, MAUI enables its callback only when the current page can consume back navigation. A rootContentPagewas always classified as unable to consume back, even when application code overrodePage.OnBackButtonPressed(), so Android bypassed MAUI and exited the app.This change detects the effective virtual
OnBackButtonPressed()implementation without invoking user code. Overrides declared by applications or third-party controls now enable MAUI's callback, while ordinary pages and MAUI's framework container implementations retain the existing predictive back-to-home behavior. The result is cached per page and uses a typed delegate, avoiding string-based reflection and linker/AOT concerns.Tests
ContentPageoverrides, and customNavigationPageoverrides.Window.Page, presses the device back button, verifies the override runs exactly once, and verifies the root page remains visible.Runtime verification
Verified on an Android 16 / API 36 emulator (
sdk_gphone64_arm64) using a real Android back key event.Without the fix, the root page reports that the override has not run, then the app exits to the Android launcher:
With the fix, the same root page remains foregrounded and reports exactly one invocation: