Skip to content

[Android] Fix OnBackButtonPressed on root pages - #37709

Merged
kubaflo merged 13 commits into
inflight/currentfrom
jfversluis-fix-android-back-handling
Aug 21, 2026
Merged

kubaflo merged 13 commits into
inflight/currentfrom
jfversluis-fix-android-back-handling

Conversation

@jfversluis

@jfversluis jfversluis commented Aug 21, 2026 •

Copy link
Copy Markdown
Member

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 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
Root page before back without fix Android launcher after back without fix

With the fix, the same root page remains foregrounded and reports exactly one invocation:

Before back After back
Root page before back with fix Root page after back with one invocation

Ahamed-Ali and others added 12 commits August 13, 2026 23:08
### 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>
Copilot AI lite review requested due to automatic review settings August 21, 2026 09:03
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 37709

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 37709"

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.HasBackButtonPressedOverride to 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
Copilot AI review requested due to automatic review settings August 21, 2026 09:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Issues namespace 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

@kubaflo

This comment has been minimized.

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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 &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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.Method depends on reflection metadata being preserved for the target method. It is only consumed today from Window.Android.cs (Mono), but it is compiled into shared Microsoft.Maui.Controls code, so any future non-Android caller puts a MethodInfo lookup on a NativeAOT/full-trim path with no DynamicDependency/DynamicallyAccessedMembers anchoring 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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:

  1. FlyoutPage whose Flyout has the override — this is the case that is actually broken (see the comment on Window.cs:832). A test here would have caught it.
  2. Derived framework container without an override — e.g. class MyNav : NavigationPage {} with a single page must still return false via the stack-depth branch. Without this, a future change to HasBackButtonPressedOverride (e.g. switching from assembly comparison to GetType() != typeof(...)) would silently make every derived container claim consumption and no test would fail.
  3. Override that returns false — the check is declaration-based, so this must still return true. Pinning that in a test documents the deliberate trade-off called out on Window.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 MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Summary

@jfversluis — new AI review results are available based on commit 270c687.

Gate Passed Confidence Medium Platform Android


🗂️ 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.cs
  • src/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's OnBackPressedDispatcher callback 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: main at d48c3ba39b58c724e86aca99c3823458ca1df235; PR head 270c68781cde6a407da439b3f550c4f3c0a07e55. The local review commit is a7a19e760e.

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 existing Shell, NavigationPage, FlyoutPage, and TabbedPage state checks.
  • src/Controls/tests/Core.UnitTests/WindowsTests.cs: adds coverage for a derived page without an override and custom ContentPage/NavigationPage overrides.
  • 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:

  1. Primary Android regression test:
    pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "Issue37706"
  2. Targeted navigation regressions: the five relevant Window.CanConsumeBackNavigation facts in WindowsTests (ContentPage_ReturnsFalse, DerivedContentPageWithoutOverride_ReturnsFalse, ContentPageWithBackButtonOverride_ReturnsTrue, NavigationPageWithBackButtonOverride_ReturnsTrue, and NavigationPage_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

  1. Major — the same defect remains for an override on FlyoutPage.Flyout. FlyoutPage.OnBackButtonPressed() sends Back to the presented flyout before its detail, but Window.CanConsumeBackNavigation() only checks whether the flyout itself can close and then recurses into Detail. In split mode, a presented flyout page with an application override can therefore leave the Android callback disabled and never receive Back.
  2. Moderate — override detection performs type-level work per page instance. Each page allocates a delegate and resolves its MethodInfo on 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.
  3. 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 false causes 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.
  4. 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=no and 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/ and eng/, unrelated to PR #37709. The invocation bounds forbid editing, reverting, staging, deleting, or otherwise altering them; the skill separately forbids git checkout/restore/reset/clean/stash. The script's own two suggested remedies (commit them, or git checkout -- .) are both prohibited.
  • Because the script threw before writing state, .github/.baseline-state.json was never created. Skill Step 2 is explicit for this case: report Blocked before 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.

  1. pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter "Issue37706" — NOT RUN.
  2. 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 in src/).
  • The ~40 pre-existing harness-owned modifications under .github/ and eng/ 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/stash and 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.md
  • CustomAgentLogsTmp/PRState/37709/PRAgent/try-fix-1/content.md — this narrative
  • CustomAgentLogsTmp/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

  1. 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 requested dotnet build Microsoft.Maui.BuildTasks.slnf and exited 1. Full output: try-fix/attempt-2/test-output.log.

  2. 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.md was 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:

  1. CanConsumeBackNavigation is 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.
  2. The default-only probe bypasses application overrides on framework containers; NavigationPageWithBackButtonOverride_ReturnsTrue remains 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-false fact.
  • 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

⚠️ Deep UI tests — 117 passed, 0 failed, 1 skipped across 3 categories on platform-pool agent (replaces in-process counts above). 1 category reported 0 tests.

🧪 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.

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) and removed s/agent-review-in-progress AI review is currently running for this PR labels Aug 21, 2026
@kubaflo
kubaflo changed the base branch from main to inflight/current August 21, 2026 15:44
@kubaflo
kubaflo merged commit 9b869b0 into inflight/current Aug 21, 2026
28 of 37 checks passed
@kubaflo
kubaflo deleted the jfversluis-fix-android-back-handling branch August 21, 2026 15:44
@github-actions github-actions Bot added this to the .NET 10 SR11 milestone Aug 21, 2026
kubaflo pushed a commit that referenced this pull request Aug 22, 2026
Manual backport of #37709 for .NET MAUI 10.0.101.

Source head: 270c687

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1e7f77e3-4862-4b92-a662-d12b5918bb59
kubaflo pushed a commit that referenced this pull request Aug 27, 2026
> [!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">
|

---------
kubaflo added a commit that referenced this pull request Sep 1, 2026
…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`.
kubaflo pushed a commit that referenced this pull request Sep 2, 2026
> [!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">
|

---------
kubaflo pushed a commit that referenced this pull request Sep 3, 2026
> [!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">
|

---------
kubaflo pushed a commit that referenced this pull request Sep 6, 2026
> [!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">
|

---------
kubaflo pushed a commit that referenced this pull request Sep 7, 2026
> [!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">
|

---------
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-navigation NavigationPage platform/android s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

10 participants