Android: constrain oversized image decodes to display bounds to prevent runtime bitmap draw crashes - #35606
Android: constrain oversized image decodes to display bounds to prevent runtime bitmap draw crashes#35606dellis1972 with Copilot wants to merge 25 commits into
Conversation
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>
|
/review -b feature/refactor-copilot-yml |
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 1 findings
See inline comments for details.
This comment has been minimized.
This comment has been minimized.
|
/review -b feature/enhanced-reviewer -p android |
kubaflo
left a comment
There was a problem hiding this comment.
Could you please check the ai's suggestions?
This comment has been minimized.
This comment has been minimized.
|
@dellis1972 is it ready for another review? |
|
@copilot can you check the feedback in #35606 (review) |
MauiBot
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
🔍 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()); |
There was a problem hiding this comment.
🔍 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.
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
● Addressed the remaining
|
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Performance-Critical Path Optimization — CENTER_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) |
There was a problem hiding this comment.
🔍 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).
…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
● Unified the decode-sizing helper + closed the canceled-null edgeThe 1 ·
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Android image decode sizing — CENTER_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)); |
There was a problem hiding this comment.
🔍 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
● Escalation — the last reviewer finding is a genuine safety↔quality trade-off (needs a human call)The iter-9 review (on
✗ The one remaining Error is a fundamental contradiction, not a fixable defect
Those two demands cannot both hold with any stock strategy:
This is the same ℹ 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 ● Options — your call, @kubaflo
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
Last reviewer Error resolved — custom display-bounded fill decode strategy ( 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 ● What changed —
● Why it's crash-safe by construction ● Validation
● Also in this round — the Both prior findings (display-override bypass; canceled/null-clear) remain fixed. Re-triggering the Android review to re-gate on the new head. |
This comment has been minimized.
This comment has been minimized.
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
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
PlatformInteropfor file/URI/stream/resource Glide loads (including removingTarget.SIZE_ORIGINALfor stream loads). - Update
FileImageSourceService.Androidresource handling to load resources via Glide in bothImageViewand non-ImageViewpaths, 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. |
| var filename = BaseImageSourceServiceTests.CreateBitmapFile(sourceWidth, sourceHeight, expectedColor); | ||
| var imageSource = new UriImageSourceStub(new Uri($"file://{filename}")); | ||
|
|
| var filename = BaseImageSourceServiceTests.CreateBitmapFile(sourceWidth, sourceHeight, expectedColor); | ||
| var imageSource = new UriImageSourceStub(new Uri($"file://{filename}")); | ||
| var imageView = new ImageView(MauiProgram.DefaultContext); |
| 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
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
🔍 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")] |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[moderate] Regression Prevention / Test Coverage — LoadDrawableAsyncLimitsExtremeAspectLargeFilesToDisplaySize (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.
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
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.
🗂️ 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 |
||
📱 ImageHandlerTests ImageHandlerTests |
🔴 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.javasrc/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
VerifyScreenshotsnapshot: 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.
ButtonResizesWhenTitleOrImageChangesOriginal — `android` · Button
| Baseline (committed) | Actual (CI) | Diff |
|---|---|---|
![]() | ![]() | ![]() |
ButtonsLayoutResolveWhenParentSizeChangesOriginal — `android` · Button
| Baseline (committed) | Actual (CI) | 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 inFileImageSourceService.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.Centermust preserve 1:1 semantics up to display size, andAspectFill/FitXYshould avoid avoidable under-decode softness. - The current PR already includes a custom
DisplayBoundedFillStrategyforCENTER_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. |
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_CONTENTImageView: 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
OperationCanceledExceptionand 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. |
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. | 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 |
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 |
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 |
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.
|
Closing this for now - please create a new one if this is still needed |




































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)
PlatformInteropand applied it to file/URI/streamContextloads and streamImageViewloads.Target.SIZE_ORIGINAL; requests are now constrained to display bounds when valid metrics are available.Resource-path parity
Contextresource loader inPlatformInterop.FileImageSourceService.AndroidGetDrawableAsyncresource-ID fast path to use Glide viaPlatformInterop, so non-ImageView resource drawables get the same decode cap as other sources while preserving the existingImageView.SetImageResourcefast path.GetDrawableAsyncfailures now flow through the existing warning/rethrow path instead of silently returning null.Focused Android coverage
GetDrawableAsynccoverage for the bounded non-ImageView drawable path.