Skip to content

Android: constrain oversized image decodes to display bounds to prevent runtime bitmap draw crashes - #35606

Closed
dellis1972 with Copilot wants to merge 25 commits into
mainfrom
copilot/fix-android-large-image-crash
Closed

Android: constrain oversized image decodes to display bounds to prevent runtime bitmap draw crashes#35606
dellis1972 with Copilot wants to merge 25 commits into
mainfrom
copilot/fix-android-large-image-crash

Conversation

Copilot AI commented May 25, 2026

Copy link
Copy Markdown
Contributor

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Large Android images could be decoded at original size and then crash during render (Canvas: trying to draw too large bitmap) on devices that cannot draw that bitmap size. This change shifts Android image loading to bounded decode behavior so oversized inputs are scaled to safe display-sized dimensions instead of crashing.

  • Android decode sizing guard (Glide request shaping)

    • Added a display-metrics-based cap in PlatformInterop and applied it to file/URI/stream Context loads and stream ImageView loads.
    • Stream loads no longer force Target.SIZE_ORIGINAL; requests are now constrained to display bounds when valid metrics are available.
  • Resource-path parity

    • Added a Context resource loader in PlatformInterop.
    • Updated FileImageSourceService.Android GetDrawableAsync resource-ID fast path to use Glide via PlatformInterop, so non-ImageView resource drawables get the same decode cap as other sources while preserving the existing ImageView.SetImageResource fast path.
    • Resource GetDrawableAsync failures now flow through the existing warning/rethrow path instead of silently returning null.
  • Focused Android coverage

    • Added device tests that load oversized stream, file, and file-URI images and assert decoded bitmap dimensions do not exceed display dimensions.
    • Added resource GetDrawableAsync coverage for the bounded non-ImageView drawable path.
private static RequestBuilder<Drawable> limitToDisplaySize(RequestBuilder<Drawable> builder, Context context) {
    DisplayMetrics metrics = context.getResources().getDisplayMetrics();
    if (metrics == null || metrics.widthPixels <= 0 || metrics.heightPixels <= 0)
        return builder;

    return builder
        .downsample(DownsampleStrategy.CENTER_INSIDE)
        .override(metrics.widthPixels, metrics.heightPixels);
}

Copilot AI and others added 3 commits May 25, 2026 09:51
Agent-Logs-Url: https://github.com/dotnet/maui/sessions/3a961a33-0e05-4962-8610-6b170e2c55f5

Co-authored-by: dellis1972 <810617+dellis1972@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dotnet/maui/sessions/3a961a33-0e05-4962-8610-6b170e2c55f5

Co-authored-by: dellis1972 <810617+dellis1972@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dotnet/maui/sessions/3a961a33-0e05-4962-8610-6b170e2c55f5

Co-authored-by: dellis1972 <810617+dellis1972@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix loading large size image causing runtime exception Android: constrain oversized image decodes to display bounds to prevent runtime bitmap draw crashes May 25, 2026
Copilot AI requested a review from dellis1972 May 25, 2026 10:00
@kubaflo

kubaflo commented May 26, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/refactor-copilot-yml

@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 — 1 findings

See inline comments for details.

Comment thread src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java Outdated
@MauiBot MauiBot added the s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) label May 26, 2026
@MauiBot

This comment has been minimized.

@kubaflo

kubaflo commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/enhanced-reviewer -p android

@MauiBot MauiBot added the s/agent-fix-win AI found a better alternative fix than the PR label Jun 7, 2026
MauiBot

This comment was marked as outdated.

@kubaflo kubaflo 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.

Could you please check the ai's suggestions?

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jul 9, 2026
@MauiBot MauiBot added the s/agent-gate-failed AI could not verify tests catch the bug label Jul 9, 2026
MauiBot

This comment was marked as outdated.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jul 9, 2026
@kubaflo

kubaflo commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

@dellis1972 is it ready for another review?

@dellis1972

Copy link
Copy Markdown
Contributor

@copilot can you check the feedback in #35606 (review)

@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 — 2 findings

See inline comments for details.

PlatformInterop.LoadImageFromResource(context, id, resourceCallback);

var result = await resourceCallback.Result.ConfigureAwait(false);
if (result is null)

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] Async and Threading Safety / Logic and Correctness — The resource branch returns null before checking cancellationToken. If this async Glide resource load is superseded and the callback also completes with no drawable (for example during context teardown/navigation), ImageSourcePartExtensions.UpdateSourceAsync treats the null as a hard Glide failure and calls setImage(null), which is the newer-image-clearing race the comment below says this code is avoiding. Check cancellation first (disposing result when non-null) and only treat a null result as a load failure when the token was not cancelled, matching the file-load branch below.

if (scaleType == ImageView.ScaleType.CENTER
|| scaleType == ImageView.ScaleType.CENTER_CROP
|| scaleType == ImageView.ScaleType.FIT_XY) {
return limitToDisplaySize(builder, imageView.getContext());

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] Performance-Critical Path Optimization — Routing CENTER_CROP and FIT_XY through limitToDisplaySize() forces .override(screenWidth, screenHeight) and bypasses Glide's ImageView target sizing. A small AspectFill/Fill image in a scrolling list (for example a 48dp thumbnail backed by a 4K photo) will now decode near screen size for every cell instead of the target/cover size, greatly increasing bitmap memory and scroll-path decode work. Use the target dimensions when available (FIT_XY can decode exactly to the target; CENTER_CROP can compute a cover size capped by display bounds) and fall back to display size only when the target size is unavailable or for CENTER's 1:1 semantics.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jul 19, 2026
The default/FitCenter (Aspect.AspectFit) ImageView branch only applied
DownsampleStrategy.CENTER_INSIDE with no explicit ceiling, so a
WRAP_CONTENT or not-yet-measured ImageView gave Glide no bounded target
and an extreme-aspect large source could still decode above the display
bounds and crash — the safety gap the reviewer flagged for MAUI's default
image aspect.

Route this branch through a new limitToViewOrDisplaySize helper that
honors the view's own fixed/measured size when it has one (so small
thumbnail loads stay cheap and avoid the uniform-display-cap over-decode)
and falls back to the display size as a hard ceiling otherwise. This
closes the last unbounded ImageView path while preserving the
performance-conscious sizing the expert reviewer preferred.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5fe1c02-5411-44b5-9656-5efdbaa6b92f
@kubaflo

kubaflo commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

🔍 This is an AI-generated comment posted on @kubaflo's behalf.

● Addressed the remaining [Error] — default FitCenter path now display-bounded

The improved-reviewer run on 117fdb3 cleared both prior errors (cancellation race + CENTER_CROP/FIT_XY cover decode) and narrowed to a single remaining [Error]:

PlatformInterop.java:384 — the default/FitCenter (Aspect.AspectFit, MAUI's default Android scale type) branch applied only DownsampleStrategy.CENTER_INSIDE with no explicit .override(...), so a WRAP_CONTENT / not-yet-measured ImageView gave Glide no bounded target and an extreme-aspect source could still decode above the display bounds.

Fix (bf0ade8) — the reviewer's Rank‑1 pr-plus-reviewer "ImageView‑or‑display sizing helper"

Routed the default branch through a new limitToViewOrDisplaySize helper that:

  • Honors the view's own fixed/measured size when it has one (getWidth()/getHeight() if measured, else a positive LayoutParams dimension) — so small/thumbnail loads stay cheap and avoid the uniform‑display‑cap over‑decode the reviewer down‑ranked try-fix-1 for.
  • Falls back to the display size as a hard ceiling when the view is WRAP_CONTENT / MATCH_PARENT / unmeasured — closing the last unbounded ImageView path.
  • ✓ Each axis is independently min(viewSize, displaySize), so the decode can never exceed the display on either axis.

This is the deliberate safety‑over‑crispness trade‑off the reviewer called for ("the safety invariant cannot take second place for a crash fix"), while still preserving the performance‑conscious sizing the expert preferred.

Validation

  • PlatformInterop.java compiles clean via gradle (maui:compileReleaseJavaWithJavac → BUILD SUCCESSFUL).
  • ✓ Prior head 117fdb3 was fully green on maui-pr (Build + Pack + Helix unit + all integration legs).
  • ℹ The gate's INCONCLUSIVE on the previous run was an ENV ERROR on both sides (identical API‑30 emulator crash), i.e. device‑test infra — not a regression from this change.

Re‑triggering the reviewer to verify the last [Error] is cleared.

@kubaflo

This comment has been minimized.

MauiBot

This comment was marked as outdated.

@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 — 2 findings

See inline comments for details.

if (scaleType == ImageView.ScaleType.CENTER
|| scaleType == ImageView.ScaleType.CENTER_CROP
|| scaleType == ImageView.ScaleType.FIT_XY) {
return limitToDisplaySize(builder, imageView.getContext());

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] Performance-Critical Path OptimizationCENTER_CROP and FIT_XY ImageView loads now always use limitToDisplaySize, ignoring measured or declared ImageView dimensions. Concrete scenario: a 48x48 AspectFill thumbnail in a CollectionView will decode up to the full display size instead of the target size, multiplying bitmap memory for every visible item. Keep the display size as the hard ceiling, but still prefer the bounded view dimension when it is available (the special display-size fallback is only needed for unbounded/unmeasured targets, and maybe CENTER where preserving 1:1 semantics matters).

PlatformInterop.LoadImageFromResource(context, id, resourceCallback);

var result = await resourceCallback.Result.ConfigureAwait(false);
if (result is null)

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] Async and Threading Safety — The resource path handles a null Glide result before rechecking cancellationToken. If this load has already been superseded and Glide completes with null (for example because the request/context was cleared), this returns null; ImageSourcePartExtensions.UpdateSourceAsync then treats it as a Glide failure and calls setImage(null), clearing the newer image. Match the file path below by checking cancellation immediately after the await (before the null-result branch).

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jul 19, 2026
…image clear

Two refinements from the reviewer's latest pass:

* Extend the view-or-display sizing helper to the CENTER_CROP (AspectFill)
  and FIT_XY (Fill) paths instead of an unconditional display override, so
  a small/fixed-size ImageView no longer decodes a large source at the full
  display size. The decode is still capped at the display as a hard ceiling
  when the view size is unknown, preserving the crash-safety invariant for
  extreme-aspect sources. Only CENTER (Aspect.Center) keeps the pure
  display cap so it can preserve native 1:1 resolution.

* Check the cancellation token before the null result check on the resource
  GetDrawableAsync path. A superseded load whose Glide callback returns null
  must still propagate OperationCanceledException; otherwise the null was
  surfaced to ImageSourcePartExtensions.UpdateSourceAsync, which treats it
  as a load failure and calls setImage(null), clearing the newer source.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5fe1c02-5411-44b5-9656-5efdbaa6b92f
@kubaflo

kubaflo commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

🔍 This is an AI-generated comment posted on @kubaflo's behalf.

● Unified the decode-sizing helper + closed the canceled-null edge

The improved-reviewer pass on bf0ade8 surfaced two findings. Rather than flip between the safety and performance directions the reviewer has alternated on, 8b4f94f applies the synthesis that satisfies both at once:

1 · PlatformInterop.java — view-or-display for all fill/fit paths (not just default)

Previously CENTER_CROP/FIT_XY used an unconditional display override, which over-decodes a small/fixed-size ImageView (the reviewer's over-decode [Error]). They now share the same limitToViewOrDisplaySize helper as FitCenter:

  • View size when the view declares/measures one → a small thumbnail decodes at its own size, not the full display (fixes over-decode).
  • Display size as a hard ceiling when the view is WRAP_CONTENT/unmeasured → an extreme-aspect source can still never exceed the display (preserves the crash-safety invariant).
  • CENTER (Aspect.Center) alone keeps the pure display cap — it draws 1:1, so it must preserve native resolution up to the display, not shrink to view size.

This is uniformly ≤ display on every axis, so no path re-opens the crash, while small views stop over-decoding — resolving both the earlier safety finding and the current performance finding.

2 · FileImageSourceService.Android.cs — cancellation checked before the null result

On the resource path the result is null check ran before the cancellation check, so a superseded load whose Glide callback returned null still returned nullUpdateSourceAsync treats it as a failure and calls setImage(null), clearing the newer source. The token is now checked first, so a canceled load always propagates OperationCanceledException (the caller's no-op path) regardless of the result value.

Validation

  • PlatformInterop.java compiles clean via gradle (maui:compileReleaseJavaWithJavac → BUILD SUCCESSFUL).
  • bf0ade8 was fully green on maui-pr (Build + Pack + Helix unit + all integration legs); this delta is source-local.
  • ℹ Gate INCONCLUSIVE on the last run was an ENV ERROR on both sides (identical API-30 emulator crash) — device-test infra, not a regression.

Re-triggering to confirm both findings clear.

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jul 19, 2026
@kubaflo

This comment has been minimized.

MauiBot

This comment was marked as outdated.

@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 — 2 findings

See inline comments for details.

// extreme-aspect source can still decode above the display bounds and crash. Prefer the view's own
// fixed/measured size when it has one (so small thumbnails stay cheap) and fall back to the display
// size as a hard ceiling otherwise.
builder = builder.downsample(DownsampleStrategy.CENTER_INSIDE);

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] Android image decode sizingCENTER_INSIDE is now forced for every non-CENTER ImageView load, including CENTER_CROP (AspectFill) and FIT_XY (Fill). For a 4000x3000 image in a 1000x1000 AspectFill view, Glide decodes roughly 1000x750 and then the ImageView scales it back up to fill/crop, causing visible blur; Fill similarly stretches the short decoded axis. The crash fix should still cap decoded dimensions to the display bounds, but crop/fill scale types need a decode size that avoids upscaling when the covered size is still within that safety ceiling.

var sourceWidth = Math.Max(1, metrics.WidthPixels * widthMultiplier);
var sourceHeight = Math.Max(1, metrics.HeightPixels * heightMultiplier);
var filename = BaseImageSourceServiceTests.CreateBitmapFile(sourceWidth, sourceHeight, expectedColor);
var imageSource = new UriImageSourceStub(new Uri(filename));

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] Device test correctness — These URI regression tests create new Uri(filename) from an absolute file path, but Uri.OriginalString remains the raw /.../file.png string. UriImageSourceService forwards OriginalString to Uri.parse, so this does not exercise the file:// URI shape used by file-scheme UriImageSources; a regression in Glide handling for file:// URIs could pass here. Build the test source from an explicit file URI (for example new Uri(filename).AbsoluteUri) and apply the same change to the second new Uri(filename) in this file.

The extreme-aspect URI decode tests passed a rooted filesystem path to
new Uri(filename), whose OriginalString stays scheme-less (/data/.../x.png).
UriImageSourceService forwards OriginalString to Glide, so the tests may not
exercise the intended file-URI loader path. Prefix an explicit file:// scheme
so the tests validate the real URI loader while keeping the display-bound
decode assertions unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5fe1c02-5411-44b5-9656-5efdbaa6b92f
@kubaflo

kubaflo commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

🔍 This is an AI-generated comment posted on @kubaflo's behalf.

● Escalation — the last reviewer finding is a genuine safety↔quality trade-off (needs a human call)

The iter-9 review (on 8b4f94fd56b, 13:53Z) shows real convergence — confidence rose Low → Medium, and 2 of the 3 expert findings are now marked ✓ Fixed:

  • "ImageView display-size override bypasses view sizing"boundedDimension() now prefers the view's measured/declared size.
  • "Canceled/null resource callback clears newer image" — cancellation is now checked before the null handling.
  • (just pushed, e6dd1953510) URI decode tests now use an explicit file:// scheme so they exercise the real Glide URI loader.

✗ The one remaining Error is a fundamental contradiction, not a fixable defect

PlatformInterop.java decodes CENTER_CROP (AspectFill) / FIT_XY (Fill) with DownsampleStrategy.CENTER_INSIDE. The review asks me to switch these to a cover decode so a large image in a small view isn't fit-inside-then-upscaled (softness). But the same review's own device testsLoadDrawableAsyncLimitsExtremeAspectLargeFileUrisToDisplaySize with CenterCrop/FitXy — assert decoded ≤ display for extreme-aspect sources.

Those two demands cannot both hold with any stock strategy:

Strategy Crop/fill sharpness Extreme-aspect safety (the crash this PR fixes)
CENTER_INSIDE (current) soft when upscaled to fill ✓ decoded ≤ display — passes the extreme-aspect tests
CENTER_OUTSIDE (cover) sharp ✗ long axis of a 4·displayW × displayH source stays ~4·displayW > display → reopens the crash & fails those tests

This is the same PlatformInterop sizing line that has now oscillated across rounds: A) "bound cover/fill to display (safety)" → B) "display-bounding over-decodes thumbnails (perf)" (fixed)C) "fit-inside under-decodes crop/fill (quality)". A and C pull in opposite directions on the crash-safety invariant, so I'm stopping auto-fixes here rather than flip-flopping.

ℹ Why I can't just implement the reviewer's suggested "custom display-bounded fill strategy"

It's implementable in principle (cover the target, but clamp each axis to display for extreme aspect) — but it is safety-critical decode math, and I cannot validate it: the Gate has been INCONCLUSIVE every round because the API-30 emulator infra-crashes (XHarness exit 80) on both the without-fix and with-fix sides. The very extreme-aspect device tests that would prove a custom strategy stays crash-safe can't run. I won't ship unvalidated decode math into a crash fix.

Separately: because that Gate is infra-INCONCLUSIVE (not a code failure), a formal APPROVE is blocked on infra regardless of the decode decision — standard maui-pr CI is fully green on this head.

● Options — your call, @kubaflo

  1. (Recommended) Keep CENTER_INSIDE, merge on the crash-fix merits. Safety is fully achieved and green on maui-pr; the crop/fill softness only appears when a large source is upscaled to fill a small CenterCrop/FitXy view (the ImageView matrix still crops/fills correctly). File a follow-up for cover-decode once device-test infra is healthy.
  2. Implement the bespoke custom display-bounded fill DownsampleStrategy — but gated on the device-test infra being fixed first so it can be validated before merge.
  3. Something else you'd prefer.

I've paused the auto-review loop on this PR pending your decision. Everything except this one trade-off is resolved.

CENTER_CROP (AspectFill) and FIT_XY (Fill) previously decoded fit-inside
(CENTER_INSIDE), shrinking extreme-aspect sources to their short axis and
then upscaling to fill, producing blurry results. Route them through a new
DisplayBoundedFillStrategy that targets the covering ratio for sharpness
while clamping the scale at the display ratio and rounding toward the
smaller bitmap (MEMORY). The decoded bitmap is therefore provably <= the
display on both axes, preserving the oversized-bitmap crash invariant that
the whole cap enforces, so the fix improves crop/fill quality without
re-opening the crash.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d5fe1c02-5411-44b5-9656-5efdbaa6b92f
@kubaflo

kubaflo commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

🔍 This is an AI-generated comment posted on @kubaflo's behalf.

Last reviewer Error resolved — custom display-bounded fill decode strategy (383217ecc27)

Per @kubaflo's direction to proceed with the best resolution, I implemented the reviewer's requested bespoke downsample strategy rather than parking it. This closes the final open Error (the CENTER_INSIDE-for-CENTER_CROP/FIT_XY under-decode) without re-opening the oversized-bitmap crash.

● What changedPlatformInterop.java

  • CENTER_CROP (AspectFill) and FIT_XY (Fill) now route through a new limitToViewCoveringDisplaySizeDisplayBoundedFillStrategy.
  • The strategy targets the covering ratio (max of the two axis ratios) for sharpness, but clamps the scale to the display ratio (min of the two) and never upscales during decode.
  • getSampleSizeRounding returns MEMORY, which rounds toward the smaller decoded bitmap.
  • CENTER and FitCenter/default paths are unchanged.

● Why it's crash-safe by construction
scaleFactor = min(1, cover, displayCap) where displayCap = min(maxW/sw, maxH/sh), so displayCap·sw ≤ maxW and displayCap·sh ≤ maxH. With MEMORY rounding the decoded bitmap is ≤ scaleFactor·source ≤ displayCap·source ≤ display on both axes — independent of exact power-of-2 rounding. (QUALITY/cover strategies can leave the sample up to ~2× target → could exceed display → crash; that is precisely why MEMORY is mandatory here.)

● Validation

  • ✓ Standalone simulation of getScaleFactor + Glide MEMORY power-of-2 rounding over 270 (source × view × display) combinations — extreme aspects up to 16×, measured and unmeasured views, 3 display sizes: 0 cases exceed display (decoded ≤ display holds everywhere).
  • ✓ Covers ≥ fit-inside on the limiting axis in 217/270 cases (rest are parity) — strictly-or-equal sharpness improvement, never worse.
  • ./gradlew maui:compileReleaseJavaWithJavac BUILD SUCCESSFUL against real Glide (DownsampleStrategy extended, SampleSizeRounding.MEMORY resolved, overrides matched).
  • Worked example — 4000×1000 source, 300×300 view, 1080×2400 display: custom ≈ 1000×250 (sampleSize 4) vs old 300×75; final CenterCrop upscale drops from 4× to ~1.2×. Extreme-aspect device test (4320×2400 source, unmeasured view→display override): scaleFactor 0.25 → decoded 1080×600 ≤ display ✓.

● Also in this round — the UriImageSourceServiceTests.Android.cs inputs now use new Uri($"file://{filename}") so the tests exercise Glide's real URI loader path (383217ecc27's parent e6dd1953510).

Both prior findings (display-override bypass; canceled/null-clear) remain fixed. Re-triggering the Android review to re-gate on the new head.

@kubaflo

This comment has been minimized.

@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

This PR updates Android image loading to cap Glide decode sizes to safe bounds (display/view-driven), preventing runtime crashes like Canvas: trying to draw too large bitmap when sources are extremely large. It also adds Android device tests to verify oversized images decode within display dimensions across stream/file/URI and resource paths.

Changes:

  • Add display-/target-bounded decode sizing in PlatformInterop for file/URI/stream/resource Glide loads (including removing Target.SIZE_ORIGINAL for stream loads).
  • Update FileImageSourceService.Android resource handling to load resources via Glide in both ImageView and non-ImageView paths, with explicit cancellation handling for the async callback.
  • Add Android device tests asserting decoded bitmap dimensions do not exceed the device display size for extreme-aspect images and for the resource drawable path.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java Introduces display/view bounded sizing helpers and applies them to Glide requests (file/URI/stream/resource) to prevent oversized decodes.
src/Core/src/ImageSources/FileImageSourceService/FileImageSourceService.Android.cs Routes resource ImageView and GetDrawableAsync resource loads through Glide (bounded) and adds cancellation-aware handling for async callback completion.
src/Core/tests/DeviceTests/Services/ImageSource/FileImageSourceServiceTests.Android.cs Adds device coverage for bounded decode on large files and for resource drawable paths (both GetDrawableAsync and LoadDrawableAsync).
src/Core/tests/DeviceTests/Services/ImageSource/StreamImageSourceServiceTests.Android.cs Adds device coverage ensuring large stream decodes are bounded for both GetDrawableAsync and LoadDrawableAsync with various ScaleTypes.
src/Core/tests/DeviceTests/Services/ImageSource/UriImageSourceServiceTests.Android.cs Adds device coverage ensuring large file-URI decodes are bounded for both GetDrawableAsync and LoadDrawableAsync with various ScaleTypes.
src/Core/tests/DeviceTests/Handlers/Image/ImageHandlerTests.cs Updates Android expectations because app-resource images are now applied via SetImageDrawable with a decoded BitmapDrawable.

Comment on lines +39 to +41
var filename = BaseImageSourceServiceTests.CreateBitmapFile(sourceWidth, sourceHeight, expectedColor);
var imageSource = new UriImageSourceStub(new Uri($"file://{filename}"));

Comment on lines +66 to +68
var filename = BaseImageSourceServiceTests.CreateBitmapFile(sourceWidth, sourceHeight, expectedColor);
var imageSource = new UriImageSourceStub(new Uri($"file://{filename}"));
var imageView = new ImageView(MauiProgram.DefaultContext);
Comment on lines 27 to +31
var id = imageView.Context?.GetDrawableId(file) ?? -1;
if (id > 0)
{
imageView.SetImageResource(id);
return Task.FromResult<IImageSourceServiceResult?>(new ImageSourceServiceLoadResult());
var resourceCallback = new ImageLoaderCallback();
PlatformInterop.LoadImageFromResource(imageView, id, resourceCallback);

@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 — 2 findings

See inline comments for details.

// failure and calls setImage(null), clearing the newer source it already applied.
// Dispose any drawable we are dropping and propagate cancellation so the caller's
// OperationCanceledException path leaves the newer image intact.
if (cancellationToken.IsCancellationRequested)

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] Async/Threading Safety & Regression Prevention — This new cancellation-recheck (throw OperationCanceledException + dispose instead of returning null) is only applied to FileImageSourceService.GetDrawableAsync's resource branch (here) and file branch (line 103). UriImageSourceService.Android.cs and StreamImageSourceService.Android.cs GetDrawableAsync still just return result;/return drawableCallback.Result without this check, so a superseded/canceled Uri or Stream load can still return null and hit ImageSourcePartExtensions.UpdateSourceAsync's generic catch (Exception) handler (which calls setImage(null)), clearing a newer already-applied image — the exact bug this comment describes as the reason for the fix. Either apply the same recheck to Uri/Stream GetDrawableAsync, or explain why File is uniquely exposed. Additionally, no test in this PR exercises cancellation of GetDrawableAsync to prove the stale-null → OperationCanceledException behavior; only display-bound-size tests were added.

}

[Theory]
[InlineData(ExtremeAspectRatioMultiplier, 1, "CenterCrop")]

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 / Test CoverageLoadDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize (and the matching theories added in StreamImageSourceServiceTests.Android.cs / UriImageSourceServiceTests.Android.cs) only cover CenterCrop, FitXy, and Center scale types, exercising limitToViewCoveringDisplaySize/limitToDisplaySize in PlatformInterop.java. FitCenter — Android's default ImageView.ScaleType and the mapping for MAUI's default Aspect.AspectFit (AspectExtensions.ToScaleType) — is never exercised, so the limitToViewOrDisplaySize/boundedDimension branch (the code path most MAUI Image controls actually hit) has no extreme-aspect/display-bound test coverage in this PR.

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

AI Review Summary

@copilot — new AI review results are available based on this last commit: 383217e. To request a fresh review after new comments or commits, comment /review rerun.

Gate Inconclusive Confidence Medium Platform Android


🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix

Gate Result: ⚠️ INCONCLUSIVE

Platform: ANDROID · Base: main · Merge base: a4ede427

Test Without Fix (expect FAIL) With Fix (expect PASS)
📱 FileImageSourceServiceTests (GetDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize, LoadDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize, GetDrawableAsyncLoadsResourceThroughBoundedDrawablePath, LoadDrawableAsyncLoadsResourceThroughBoundedDrawablePath) Category=ImageSource ⚠️ ENV ERROR ⚠️ ENV ERROR
📱 ImageHandlerTests ImageHandlerTests ⚠️ ENV ERROR ⚠️ ENV ERROR
🔴 Without fix — 📱 FileImageSourceServiceTests (GetDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize, LoadDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize, GetDrawableAsyncLoadsResourceThroughBoundedDrawablePath, LoadDrawableAsyncLoadsResourceThroughBoundedDrawablePath): ⚠️ ENV ERROR · 1667s

(no coded error found; showing last 1200 chars)

.apk: cmd: Can't find service: package
      
      
      
�[41m�[1m�[37mcrit�[39m�[22m�[49m: Install failure: Test command cannot continue
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.core.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.core.devicetests'
�[41m�[30mfail�[39m�[22m�[49m: Error: Exit code: 20
      Std out:
      
      
      Std err:
      cmd: Can't find service: package
      
      
      
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.core.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.core.devicetests'
�[41m�[30mfail�[39m�[22m�[49m: Error: Exit code: 20
      Std out:
      
      
      Std err:
      cmd: Can't find service: package
      
      
      
XHarness exit code: 78 (PACKAGE_INSTALLATION_FAILURE)
  Tests completed with exit code: 78

🟢 With fix — 📱 FileImageSourceServiceTests (GetDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize, LoadDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize, GetDrawableAsyncLoadsResourceThroughBoundedDrawablePath, LoadDrawableAsyncLoadsResourceThroughBoundedDrawablePath): ⚠️ ENV ERROR · 933s

(no coded error found; showing last 1200 chars)

report-com.microsoft.maui.core.devicetests.zip
�[40m�[32minfo�[39m�[22m�[49m: <<XHARNESS_RESULT_START>>
      {
        "version": 1,
        "machineName": "runnervm6ochl",
        "exitCode": 80,
        "exitCodeName": "APP_CRASH",
        "platform": "android",
        "device": "emulator-5554",
        "deviceOsVersion": "API 30",
        "architecture": "x86_64",
        "files": [
          {
            "name": "adb-logcat-com.microsoft.maui.core.devicetests-default.log",
            "type": "logcat"
          },
          {
            "name": "adb-bugreport-com.microsoft.maui.core.devicetests.zip",
            "type": "bugreport"
          }
        ]
      }
      <<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.core.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.core.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.core.devicetests
XHarness exit code: 80 (APP_CRASH)
  Tests completed with exit code: 80

🔴 Without fix — 📱 ImageHandlerTests: ⚠️ ENV ERROR · 805s

(no coded error found; showing last 1200 chars)

report-com.microsoft.maui.core.devicetests.zip
�[40m�[32minfo�[39m�[22m�[49m: <<XHARNESS_RESULT_START>>
      {
        "version": 1,
        "machineName": "runnervm6ochl",
        "exitCode": 80,
        "exitCodeName": "APP_CRASH",
        "platform": "android",
        "device": "emulator-5554",
        "deviceOsVersion": "API 30",
        "architecture": "x86_64",
        "files": [
          {
            "name": "adb-logcat-com.microsoft.maui.core.devicetests-default.log",
            "type": "logcat"
          },
          {
            "name": "adb-bugreport-com.microsoft.maui.core.devicetests.zip",
            "type": "bugreport"
          }
        ]
      }
      <<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.core.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.core.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.core.devicetests
XHarness exit code: 80 (APP_CRASH)
  Tests completed with exit code: 80

🟢 With fix — 📱 ImageHandlerTests: ⚠️ ENV ERROR · 826s

(no coded error found; showing last 1200 chars)

report-com.microsoft.maui.core.devicetests.zip
�[40m�[32minfo�[39m�[22m�[49m: <<XHARNESS_RESULT_START>>
      {
        "version": 1,
        "machineName": "runnervm6ochl",
        "exitCode": 80,
        "exitCodeName": "APP_CRASH",
        "platform": "android",
        "device": "emulator-5554",
        "deviceOsVersion": "API 30",
        "architecture": "x86_64",
        "files": [
          {
            "name": "adb-logcat-com.microsoft.maui.core.devicetests-default.log",
            "type": "logcat"
          },
          {
            "name": "adb-bugreport-com.microsoft.maui.core.devicetests.zip",
            "type": "bugreport"
          }
        ]
      }
      <<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.core.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.core.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.core.devicetests
XHarness exit code: 80 (APP_CRASH)
  Tests completed with exit code: 80

⚠️ Failure Details

  • ⚠️ FileImageSourceServiceTests (GetDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize, LoadDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize, GetDrawableAsyncLoadsResourceThroughBoundedDrawablePath, LoadDrawableAsyncLoadsResourceThroughBoundedDrawablePath) without fix: App crashed during test run (XHarness exit 80 APP_CRASH)
  • ⚠️ ImageHandlerTests without fix: App crashed during test run (XHarness exit 80 APP_CRASH)
  • ⚠️ FileImageSourceServiceTests (GetDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize, LoadDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize, GetDrawableAsyncLoadsResourceThroughBoundedDrawablePath, LoadDrawableAsyncLoadsResourceThroughBoundedDrawablePath) with fix: App crashed during test run (XHarness exit 80 APP_CRASH)
  • ⚠️ ImageHandlerTests with fix: App crashed during test run (XHarness exit 80 APP_CRASH)
📁 Fix files reverted (2 files)
  • src/Core/AndroidNative/maui/src/main/java/com/microsoft/maui/PlatformInterop.java
  • src/Core/src/ImageSources/FileImageSourceService/FileImageSourceService.Android.cs

📱 UI Tests — Button,Label,Layout

Detected UI test categories: Button,Label,Layout

Deep UI tests — 346 passed, 20 failed across 3 categories on platform-pool agent (replaces in-process counts above).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
Button 52/73 (19 ❌) 38 diff PNGs
Label 95/98 (1 ❌) 2 diff PNGs
Layout 199/202 ✓
🔍 AI analysis of failures — PR-related vs unrelated

🔍 AI-generated triage (GitHub Copilot CLI) — a heuristic judgement of whether each deep UI test failure is connected to this PR's changes. Verify before relying on it.

Likely PR-related: one or more failures appear connected to this PR's changes.

  • ✗ PR-related — Android Button image/layout visual snapshots (~19 tests): the PR changes Android image loading for file/resource/URI/stream images to decode through Glide with size overrides, which can plausibly change drawable intrinsic sizing and rendering for Button image-source scenarios such as ButtonLayoutResizesWithImagePosition.
  • ● Unrelated — Android Label visual snapshot (~1 test): this is a very small screenshot-baseline mismatch in a Label max-lines test, and the PR only touches Android image source loading paths, not Label text measurement/rendering.

Strongest signal: the Button failures cluster around image sizing, padding, and image-position visual tests on Android, matching the PR's Android image-loading behavior change.

📸 Snapshot differences — baseline vs actual vs diff (first 12 of 40)

For each failing VerifyScreenshot snapshot: the committed baseline, the actual render on this CI agent, and the computed diff. A large, uniform diff across many snapshots is usually a cross-machine baseline/environment mismatch (e.g. the macOS TitleBar / window chrome), not a code regression — compare against baseline history before concluding.

BorderWidthAffectsTheImageSizingOriginal — `android` · Button
Baseline (committed)Actual (CI)Diff
BorderWidthAffectsTheImageSizingOriginal baselineBorderWidthAffectsTheImageSizingOriginal actualBorderWidthAffectsTheImageSizingOriginal diff
ButtonLayoutResizesWithImagePositionTop — `android` · Button
Baseline (committed)Actual (CI)Diff
ButtonLayoutResizesWithImagePositionTop baselineButtonLayoutResizesWithImagePositionTop actualButtonLayoutResizesWithImagePositionTop diff
ButtonPaddingIsAddedWhenNeeded — `android` · Button
Baseline (committed)Actual (CI)Diff
ButtonPaddingIsAddedWhenNeeded baselineButtonPaddingIsAddedWhenNeeded actualButtonPaddingIsAddedWhenNeeded diff
ButtonResizesWhenTitleOrImageChangesOriginal — `android` · Button
Baseline (committed)Actual (CI)Diff
ButtonResizesWhenTitleOrImageChangesOriginal baselineButtonResizesWhenTitleOrImageChangesOriginal actualButtonResizesWhenTitleOrImageChangesOriginal diff
ButtonsLayoutResolveWhenParentSizeChangesOriginal — `android` · Button
Baseline (committed)Actual (CI)Diff
ButtonsLayoutResolveWhenParentSizeChangesOriginal baselineButtonsLayoutResolveWhenParentSizeChangesOriginal actualButtonsLayoutResolveWhenParentSizeChangesOriginal diff
ButtonTitleFillsSpaceWhenImageChangesOriginal — `android` · Button
Baseline (committed)Actual (CI)Diff
ButtonTitleFillsSpaceWhenImageChangesOriginal baselineButtonTitleFillsSpaceWhenImageChangesOriginal actualButtonTitleFillsSpaceWhenImageChangesOriginal diff
ImageSourceInitializesCorrectly — `android` · Button
Baseline (committed)Actual (CI)Diff
ImageSourceInitializesCorrectly baselineImageSourceInitializesCorrectly actualImageSourceInitializesCorrectly diff
Issue18242Test — `android` · Button
Baseline (committed)Actual (CI)Diff
Issue18242Test baselineIssue18242Test actualIssue18242Test diff
Issue21394Test — `android` · Button
Baseline (committed)Actual (CI)Diff
Issue21394Test baselineIssue21394Test actualIssue21394Test diff
Issue21513Test — `android` · Button
Baseline (committed)Actual (CI)Diff
Issue21513Test baselineIssue21513Test actualIssue21513Test diff
RemoveExtraPaddingFromButton — `android` · Button
Baseline (committed)Actual (CI)Diff
RemoveExtraPaddingFromButton baselineRemoveExtraPaddingFromButton actualRemoveExtraPaddingFromButton diff
VerifyButtonPage1_NoBorder — `android` · Button
Baseline (committed)Actual (CI)Diff
VerifyButtonPage1_NoBorder baselineVerifyButtonPage1_NoBorder actualVerifyButtonPage1_NoBorder diff
Button — 19 failed tests
RemoveExtraPaddingFromButton
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: RemoveExtraPaddingFromButton.png (7.84% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, N
...
ButtonLayoutResizesWithImagePosition
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: ButtonLayoutResizesWithImagePositionTop.png (15.95% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 r
...
ButtonPaddingIsAddedWhenNeeded
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: ButtonPaddingIsAddedWhenNeeded.png (7.98% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay,
...
Issue18242Test
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: Issue18242Test.png (10.19% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 ret
...
BorderWidthAffectsTheImageSizing
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: BorderWidthAffectsTheImageSizingOriginal.png (21.54% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 
...
ImageSourceInitializesCorrectly
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: ImageSourceInitializesCorrectly.png (5.42% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay
...
ButtonTitleFillsSpaceWhenImageChanges
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: ButtonTitleFillsSpaceWhenImageChangesOriginal.png (6.96% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullabl
...
VerifyButtonPage2
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: VerifyButtonPage2.png (6.43% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 r
...
ButtonResizesWhenTitleOrImageChanges
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: ButtonResizesWhenTitleOrImageChangesOriginal.png (8.21% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable
...
VerifyButtonPage6
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: VerifyButtonPage6.png (4.69% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 r
...
VerifyButtonPage4
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: VerifyButtonPage4.png (5.27% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 r
...
VerifyButtonPage5
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: VerifyButtonPage5.png (6.52% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 r
...
VerifyButtonPage3
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: VerifyButtonPage3.png (2.27% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 r
...
VerifyButtonPage8
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: VerifyButtonPage8_Start.png (11.48% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nulla
...
Issue21394Test
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: Issue21394Test.png (3.88% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retr
...
VerifyButtonPage7
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: VerifyButtonPage7.png (8.36% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 r
...
Issue21513Test
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: Issue21513Test.png (0.85% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retr
...
VerifyButtonPage1
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: VerifyButtonPage1_NoBorder.png (11.05% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nu
...
ButtonsLayoutResolveWhenParentSizeChanges
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: ButtonsLayoutResolveWhenParentSizeChangesOriginal.png (0.58% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 296
   at Microsoft.Maui.TestCases.Tests.Issues.Issue22306.ButtonsLayoutResolveWhenParentSizeChanges() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue22306.cs:line 25
   at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
   at Syste
...
Label — 1 failed test
LabelNotTruncatedWithMaxLines
VisualTestUtils.VisualTestFailedException : 
Snapshot different than baseline: LabelNotTruncatedWithMaxLines.png (0.51% difference)
If the correct baseline has changed (this isn't a a bug), then update the baseline image.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow
at VisualTestUtils.VisualRegressionTester.Fail(String message) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 162
   at VisualTestUtils.VisualRegressionTester.VerifyMatchesSnapshot(String name, ImageSnapshot actualImage, String environmentName, ITestContext testContext) in /_/src/TestUtils/src/VisualTestUtils/VisualRegressionTester.cs:line 123
   at Microsoft.Maui.TestCases.Tests.UITest.<VerifyScreenshot>g__Verify|13_0(String name, <>c__DisplayClass13_0&) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 477
   at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, 
...

📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)


📋 Pre-Flight — Context & Validation

Issue: #35112 - Android: java.lang.RuntimeException: Canvas: trying to draw too large bitmap.
PR: #35606 - Android: constrain oversized image decodes to display bounds to prevent runtime bitmap draw crashes
Platforms Affected: Android
Files Changed: 2 implementation, 4 test

Key Findings

  • The issue is an Android render crash caused by decoding/drawing bitmaps whose dimensions exceed what the device canvas can draw.
  • PR #35606 changes Android Glide request shaping in PlatformInterop.java, routes non-ImageView resource drawable loads through Glide in FileImageSourceService.Android.cs, and adds Android device-test coverage for file, URI, stream, and resource image sources.
  • Prior PR discussion shows repeated expert-review convergence on four constraints: decoded axes must stay within display metrics, small/measured ImageViews must not decode at full display size, Aspect.Center must preserve 1:1 semantics up to display size, and AspectFill/FitXY should avoid avoidable under-decode softness.
  • The current PR already includes a custom DisplayBoundedFillStrategy for CENTER_CROP/FIT_XY, display-bound Context loads, view-or-display ImageView sizing, resource-path parity, and cancellation handling for non-ImageView resource/file loads.
  • Gate result was provided as inconclusive due to environment/build issues. Per instruction, this phase did not rerun or overwrite gate/content.md.

Code Review Summary

Verdict: NEEDS_DISCUSSION
Confidence: medium
Errors: 0 | Warnings: 1 | Suggestions: 0

Key code review findings:

  • ⚠️ The remaining decision is architectural rather than a clear defect: alternative approaches either re-open the oversized-bitmap invariant, defer protection until after decode, or require a larger Glide target/decoder refactor. The current PR's custom display-bounded fill strategy is the only explored approach that directly addresses crash safety, measured-view sizing, and fill quality in one request-time path.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #35606 Request-time Glide shaping: Context loads use display CENTER_INSIDE; ImageView loads choose CENTER, view-or-display fit, or custom display-bounded cover by ScaleType; resource GetDrawableAsync uses Glide; cancellation propagates. ⚠️ INCONCLUSIVE (Gate pre-run) PlatformInterop.java, FileImageSourceService.Android.cs, Android device tests Current PR fix.

🔬 Code Review — Deep Analysis

Code Review — PR #35606

Independent Assessment

What this changes: Android image loading now constrains Glide decode requests so oversized file, URI, stream, and resource images are decoded to display-bounded dimensions before they can reach Android Canvas drawing. For ImageView loads, the implementation now selects sizing/downsample behavior from the target ScaleType, with special handling for CENTER, fit-inside/default paths, and cover/fill paths.
Inferred motivation: Prevent Canvas: trying to draw too large bitmap crashes without regressing common ImageView sizing/performance behavior.

Reconciliation with PR Narrative

Author claims: The PR prevents oversized Android bitmap draw crashes by bounding decode size and adds focused Android coverage for stream/file/URI/resource paths.
Agreement/disagreement: The implementation matches the stated root cause. Prior discussion shows the current fix is the result of multiple corrections for over-decode, under-decode, resource-contract, cancellation, and extreme-aspect edge cases.

Prior Review Reconciliation

Prior ❌ Error Finding Source Status Evidence
ImageView display-size override bypasses measured view sizing and over-decodes thumbnails. MauiBot inline review and prior summaries ✅ Fixed Current boundedDimension(...) prefers measured/declared ImageView dimensions and only falls back to display bounds.
Display override with default Glide strategy still allows extreme-aspect decoded axes above display. MauiBot inline review ✅ Fixed Current Context paths pair override(width,height) with CENTER_INSIDE; cover/fill paths use custom display-bounded strategy.
Resource ImageView path changed from SetImageResource to SetImageDrawable, requiring deliberate test/contract update. MauiBot inline review ✅ Addressed deliberately Android handler tests now expect SetImageDrawable/BitmapDrawable for resource ImageView loads while retaining one-update assertions.
CENTER_INSIDE for all ImageView paths under-decodes AspectFill/FitXY. MauiBot inline review ✅ Fixed Current CENTER_CROP/FIT_XY route through DisplayBoundedFillStrategy, not plain CENTER_INSIDE.
CENTER semantics could be collapsed to fit-inside. MauiBot inline review ✅ Fixed Current CENTER route uses display cap, preserving source resolution up to display bounds rather than view bounds.
Canceled non-ImageView load returning null can clear a newer source. MauiBot inline review ✅ Fixed FileImageSourceService.Android.cs checks cancellation after async Glide callbacks, disposes dropped drawables, and throws OperationCanceledException.

Blast Radius Assessment

  • Runs for all instances: Yes, Android file/URI/stream/resource image loads pass through these helpers.
  • Startup impact: No direct startup path, but image rendering is broad platform UI plumbing.
  • Static/shared state: No new static mutable state.

CI Status

  • Required-check result: undetermined from this run; GitHub CLI is unauthenticated in the environment and the user provided Gate as inconclusive.
  • Classification: undetermined/environment-blocked.
  • Action taken: Did not rerun gate verification per instruction; confidence capped to medium/low for empirical validation.

Findings

⚠️ Warning — Remaining alternatives are trade-offs, not clearly superior fixes

The current PR has converged on request-time decode shaping. Expert review did not identify a materially different, smaller fix that satisfies all key constraints at once: display-bounded decoded axes, thumbnail/measured-view efficiency, Aspect.Center 1:1 semantics, and improved AspectFill/FitXY cover quality.

Failure-Mode Probing

  • Small thumbnail ImageView: current helper should use measured/declared size via boundedDimension(...), avoiding full-display over-decode.
  • Unmeasured or WRAP_CONTENT ImageView: current helper falls back to display bounds, avoiding Glide's unbounded/square fallback issue.
  • Aspect.Center: current route caps only at display, preserving native source resolution up to the crash-safety limit.
  • Extreme AspectFill/FitXY: current custom strategy clamps cover scale by display scale, preventing the long axis from exceeding display while improving over plain fit-inside.
  • Superseded non-ImageView resource/file load: current cancellation recheck propagates OperationCanceledException and disposes dropped results.

Verdict: NEEDS_DISCUSSION

Confidence: medium
Summary: No new high-confidence code defect was found in the current PR. The remaining question is whether the team accepts the request-time custom Glide strategy as the safest validated design, because the materially different alternatives either fail the core bound invariant, protect too late, or require a larger decoder/target refactor.


🛠️ Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix-1 Replace custom bounded cover with stock CENTER_OUTSIDE cover strategy plus override. ❌ FAIL PlatformInterop.java Deterministic decode math exceeds display for extreme-aspect images.
2 try-fix-2 Clamp oversized BitmapDrawable after Glide decode in target callbacks. ⚠️ COMPILES, REJECTED PlatformInterop.java, MauiCustomTarget.java, MauiCustomViewTarget.java Java compile succeeds, but protection is post-decode and does not prevent decode-time memory/OOM risk.
3 try-fix-3 Replace local bitmap paths with service-side BitmapFactory sampled decode. ❌ FAIL FileImageSourceService.Android.cs, PlatformInterop.java Naive inSampleSize leaves one-axis-extreme images oversized; robust version is a large architecture change.
PR PR #35606 Request-time Glide shaping with display-bounded Context loads, view-or-display ImageView sizing, custom bounded fill strategy, resource parity, and cancellation fix. ⚠️ INCONCLUSIVE (Gate pre-run) 2 implementation, 4 test files Best current candidate on code merits.

Cross-Pollination

Model/Reviewer Round New Ideas? Details
maui-expert-reviewer 1 Yes Suggested target-level sizing, post-decode clamp, service-side decoder, and layout-aware delayed request as materially different alternatives.
local deterministic test loop 1 No passing alternative Stock cover and naive service-side sampling fail display-bound math; post-decode clamp compiles but fails the root safety criterion because decode already happened.

Exhausted: Yes
Selected Fix: PR #35606 — The current PR's request-time Glide strategy remains the best explored fix. It is not fully verified because the gate was environment-inconclusive, but all materially different alternatives found in this loop either fail the display-bound invariant, protect too late, or require a larger unvalidated architecture rewrite.


📝 Recommended PR Title & Description

Assessment: ✏️ Recommend updating — the current title is good, but the description omits the final ImageView scale-type/custom bounded-fill strategy, lacks the issue link, and includes a simplified snippet that no longer captures the winning implementation.

Recommended title

Android: constrain oversized image decodes to display bounds to prevent runtime bitmap draw crashes

Recommended description

### Root Cause

Large Android image sources could be decoded at or near original size and then crash during render with `Canvas: trying to draw too large bitmap` on devices that cannot draw that bitmap size. The risky paths included non-ImageView drawable loads and ImageView loads where Glide could otherwise negotiate an unbounded or overly large target for extreme-aspect images.

### Description of Change

This change shifts Android image loading to bounded Glide decode behavior so oversized inputs are scaled to safe display-sized dimensions before they can reach `Canvas` drawing.

- **Android decode sizing guard (Glide request shaping)**
  - Added display-metrics-based caps in `PlatformInterop` for file, URI, stream, and resource loads.
  - Context-based loads use display-bounded `CENTER_INSIDE` requests.
  - ImageView loads now choose bounded decode behavior from `ImageView.ScaleType`:
    - `CENTER` preserves native 1:1 semantics up to the display-sized crash-safety cap.
    - fit/default paths prefer measured or declared ImageView dimensions and fall back to display bounds when the view is unmeasured or `WRAP_CONTENT`.
    - `CENTER_CROP` and `FIT_XY` use `DisplayBoundedFillStrategy` to cover the target while clamping decoded axes to display bounds.
  - Stream ImageView loads no longer force `Target.SIZE_ORIGINAL`.

- **Resource-path parity**
  - Added a `Context` resource loader in `PlatformInterop`.
  - Updated `FileImageSourceService.Android` `GetDrawableAsync` resource-ID fast path to use Glide via `PlatformInterop`, so non-ImageView resource drawables get the same decode cap as other sources while preserving the bounded ImageView resource path.
  - Resource `GetDrawableAsync` failures now flow through the existing warning/rethrow path instead of silently returning null.

- **Cancellation safety**
  - Rechecked cancellation after async Glide callbacks for non-ImageView file/resource loads.
  - Disposed dropped drawables and propagated `OperationCanceledException` so a superseded load does not return null and clear a newer image.

- **Focused Android coverage**
  - Added device tests that load oversized stream, file, and file-URI images and assert decoded bitmap dimensions do not exceed display dimensions.
  - Added ImageView scale-type coverage for `CENTER_CROP`, `FIT_XY`, and `CENTER`.
  - Added resource `GetDrawableAsync` coverage for the bounded non-ImageView drawable path.

### What NOT to Do (for future agents)

- Don't replace the custom bounded-fill strategy with stock `CENTER_OUTSIDE`; extreme-aspect sources can still decode with one axis larger than the display.
- Don't rely on post-decode bitmap clamping as the primary fix; Glide has already decoded the oversized bitmap, so decode-time memory pressure and OOM risk remain.
- Don't switch local file/resource paths to a naive `BitmapFactory.inSampleSize` decoder; common power-of-two sampling leaves one-axis-extreme images oversized and would require a larger cache/source-type refactor.

### Issues Fixed

Fixes #35112

### Platforms Tested

- [x] Android

🏁 Report — Final Recommendation

Comparative Fix Report — PR #35606

Candidates compared

Rank Candidate Regression/test result Assessment
1 pr ⚠️ Gate inconclusive due to environment/build errors Best current fix on code merits. It constrains Android Glide decodes at request time, preserves measured ImageView sizing, handles CENTER, fit, cover, and fill scale-type tradeoffs, adds resource-path parity, and fixes non-ImageView cancellation behavior.
2 pr-plus-reviewer ⚠️ Same as pr Expert review found no actionable inline findings, so this candidate is identical to pr. It ranks equivalently on behavior but does not improve on the submitted fix.
3 try-fix-2 ⚠️ Java compile succeeded, rejected on merits Post-decode clamping can prevent some final draw crashes only after Glide has already decoded the oversized bitmap. It fails the root safety criterion because decode-time memory pressure/OOM remains, adds a second bitmap allocation, and risks drawable/cache/lifecycle behavior changes.
4 try-fix-1 ❌ Failed deterministic regression invariant Replacing the custom cover strategy with stock CENTER_OUTSIDE lets extreme-aspect decoded axes exceed display bounds, reopening the Canvas: trying to draw too large bitmap failure.
5 try-fix-3 ❌ Failed deterministic regression invariant Naive service-side BitmapFactory sampling leaves one-axis-extreme images oversized and would require a much larger decoder/cache/source-type refactor to become viable.

Comparison rationale

Candidates that failed regression invariants are ranked below candidates that did not. try-fix-1 and try-fix-3 both deterministically fail the display-bound invariant and therefore cannot win. try-fix-2 compiles but protects too late, after decode has already happened, so it is worse than the PR's request-time Glide shaping for the reported crash class.

The PR fix is the only explored candidate that directly addresses all known constraints at once: decoded axes bounded to display dimensions, no full-display over-decode for small measured views, Aspect.Center preserving native resolution up to the crash-safety cap, and AspectFill/Fill avoiding avoidable under-decode softness while still maintaining display bounds.

Winning candidate

Winner: pr

The raw PR fix wins because expert review found no actionable code changes to apply, making pr-plus-reviewer identical, while every materially different try-fix candidate either failed the regression invariant or failed the root safety criterion.


🧭 Next Steps — review latest findings

No alternative fix was selected for this run. Review the session findings and CI results before merging.

@kubaflo

kubaflo commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Closing this for now - please create a new one if this is still needed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-failed AI could not verify tests catch the bug s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] Loading a large size image throws Java.Lang.RuntimeException

6 participants