[Android] Fix screenshot from webview content not working#30437
[Android] Fix screenshot from webview content not working#30437jsuarezruiz wants to merge 9 commits into
Conversation
🤖 AI Summary
📊 Review Session —
|
| Concern | Severity | Notes |
|---|---|---|
Thread.Sleep(50) on the calling thread |
High | CaptureAsync is typically invoked on the UI thread (Activity/DecorView). Sleeping the UI thread is an ANR/jank risk; also 50 ms is heuristic — no guarantee the hardware layer is rebuilt in software mode within 50 ms. |
SetLayerType(Software) applied to whole DecorView root |
High | When Render is called with the activity's DecorView root, this flips every view subtree to software rendering and then restores it. Causes a visible flash/relayout, expensive on a complex screen. |
view.IsHardwareAccelerated is true for ~all views on modern Android |
High | The new branch runs unconditionally on every screenshot, adding the 50 ms sleep + layer toggle every single time, even when the canvas path would already have worked. |
Doesn't actually use the GPU (no PixelCopy) |
Med | Despite the name "hardware-accelerated", the implementation is still a software canvas draw — it just toggles the layer type. The proper Android API for capturing hardware-accelerated content (incl. SurfaceView/TextureView and the WebView's SurfaceView on API ≥ 26) is PixelCopy.Request. |
view.RootView check on a view that itself may already be the root |
Med | When CaptureAsync(Activity) runs, view is already the DecorView root, so the early-return-on-null is moot. |
| Repro ViewModel uses a one-shot stream as factory | Med | ImageSource.FromStream(() => stream) captures the local stream — the MAUI image loading may dispose or fully read it once, leaving subsequent loads blank. This may be part of the originally observed bug rather than the platform code. |
Test uses Thread.Sleep(1000) + VerifyScreenshot instead of waiting for the result Image element |
Low | Test is timing-fragile. |
Build error risk: Controls.TestCases.HostApp adding Essentials.csproj reference |
Low | Already partly referenced through Maui meta-package; double reference can cause duplicate-type warnings if not handled. |
Acceptance criteria from issue
- Screenshot of a page containing a
WebViewin Release builds on Android renders into the resultImage. - No regression for non-WebView pages.
- No ANR / no UI thread jank.
- New UITest covers the scenario.
Notes for downstream phases
- The PR's core mechanic (toggle layer + sleep + canvas draw) is the same as
RenderUsingCanvasDrawingwith a thread sleep. The likely real root cause for the Release regression is the WebView's underlyingSurfaceViewno longer being captured byview.Draw(canvas)once hardware rendering is in use. - The Android-recommended fix is
PixelCopy.Request(API 24/26+), which copies pixels from the rendered surface directly. - Try-fix candidates should explore: PixelCopy-based capture; targeted WebView handling; layer-toggle without sleep (post-frame callback); and a "do nothing but fix the stream lifecycle" alternative.
🔧 Fix — Analysis & Comparison
Try-Fix Aggregate Narrative — PR #30437
Four independent candidate fixes were generated against origin/net10.0 for the Android screenshot-of-WebView regression (#30010). Each candidate loaded a different domain dimension from the maui-expert-reviewer rubric so the search space stays diverse:
| Candidate | Dimension | One-line summary | Same approach as PR? |
|---|---|---|---|
try-fix-1 |
Performance / UI-thread health | Async PreDraw wait, scope HW path to actual surface descendants, sample-based blank check | Same family (layer toggle) but no UI-thread sleep |
try-fix-2 |
Android API correctness | PixelCopy.Request (API ≥ 26) — the canonical Android API for snapshotting rendered surfaces |
Different (no layer mutation) |
try-fix-3 |
Targeted minimal fix | Walk the tree, draw each WebView on top of the canvas result |
Different (additive composite) |
try-fix-4 |
Architecture / reframe | Pre-fill the bitmap with the view background so transparent surface regions render opaque | Different (root-cause hypothesis) |
Gate context
The supplied Gate phase reported ❌ FAILED for the PR head. That signal applies to the PR's implementation and does not propagate to alternative candidates — but it tells us that the PR's mechanism (canvas draw with the entire DecorView forced to software layer + 50 ms sleep) does not reliably produce the snapshot baseline on Android.
This favors candidates that take a fundamentally different mechanism:
try-fix-2(PixelCopy) — independent of canvas draw entirely.try-fix-3(WebView composite) — independent of layer toggling.
…over candidates that just refine the PR's approach (try-fix-1) or that hinge on an unverified hypothesis (try-fix-4).
Recommended ordering
try-fix-2— PixelCopy is the textbook Android answer; highest confidence in correctness and lowest UI-thread risk.try-fix-3— minimal-blast-radius WebView-specific patch; safe even if PixelCopy isn't viable in this codebase for some reason.try-fix-1— useful as a perf hardening on top of whichever capture method ships, but doesn't change the underlying capture mechanic.try-fix-4— interesting hypothesis, lowest cost, but high risk of not addressing the actual symptom captured by the new UITest baseline.
See each candidate's try-fix-{N}/content.md for full diff and detailed reasoning.
📋 Report — Final Recommendation
Comparative Report — PR #30437
Candidate ranking matrix
| Candidate | Capture mechanism | UI-thread risk | API safety | Likely passes new UITest? | Notes |
|---|---|---|---|---|---|
pr |
Layer→Software toggle + Thread.Sleep(50) + canvas draw |
High — sleeps UI thread on every call | Low — IsHardwareAccelerated is true everywhere, so the slow path runs always |
❌ Gate FAILED | The gate signal is the most authoritative info we have. |
pr-plus-reviewer |
Same as pr but PreDraw wait + scoped toggle + better error logging + fixed VM stream |
Medium — still toggles layer; PreDraw wait replaces sleep | Better | Inherited risk from pr. Mechanism is the same family; while it improves perf and code health, it doesn't change the underlying capture method that the gate found insufficient. |
|
try-fix-1 |
Same family as pr, async PreDraw + descendant gate |
Low | Better | Same risk pattern as pr-plus-reviewer w.r.t. baseline. |
|
try-fix-2 |
PixelCopy.Request (API ≥ 26) |
None | High | Most likely ✅ — independent of the canvas-draw mechanic the gate exposed as broken. | Industry-standard Android API for hardware surface capture (WebView, SurfaceView, TextureView). |
try-fix-3 |
Canvas draw + per-WebView composite | None | High | Likely ✅ — synthesises the missing WebView region | Smallest blast radius; safe even if PixelCopy is unavailable. |
try-fix-4 |
Canvas draw + opaque background fill | None | High | Lowest — hypothesis-driven | Trivial change; if the bug is only transparent visibility this wins, but the snapshot baseline likely expects real WebView pixels. |
Reasoning
- The gate result is decisive. The submitted PR mechanism failed the regression test it ships with. Per the explicit rule "candidates that failed regression tests MUST be ranked lower than candidates that passed them",
pris ranked at the bottom by definition. pr-plus-revieweris the PR's mechanism with polish. Since the polish (no UI sleep, scoped layer toggle, fixed stream lifecycle) doesn't alter the capture method, it inherits the gate-failure risk. It improves PR quality but does not change the outcome the gate evaluated.try-fix-1is similarly a refinement of the same family; same caveat.try-fix-4is a one-line bet on a hypothesis that the bitmap is captured fine and only its transparency causes invisibility. The snapshot baseline (which presumably contains the WebView's HTML content rendered) makes this hypothesis less likely; ranked low.try-fix-3introduces per-WebView compositing — a controllable, narrow, deterministic addition that fills exactly the region that hardware surfaces leave blank in canvas draws. It is reliable and side-effect-free for non-WebView trees.try-fix-2(PixelCopy) is the canonical Android way to do this. It is async, doesn't manipulate the view tree, doesn't require any sleeps, and works for all hardware-surface views (WebView/SurfaceView/TextureView). It is the highest-confidence winner.
Winner: try-fix-2
try-fix-2 replaces the brittle "force software layer + sleep + canvas draw" with PixelCopy.Request(window, rect, bitmap, listener, handler) (API ≥ 26), which is Android's recommended way to snapshot hardware-rendered surfaces — including a WebView's SurfaceView. It:
- Eliminates the UI-thread
Thread.Sleep(50)and the global software-layer flip flagged by the expert reviewer. - Captures hardware-rendered surfaces correctly, which is the entire point of [Android] Loading the captured screenshot from webview content to Image control does not visible #30010.
- Falls back to today's canvas draw + drawing-cache paths on pre-26 devices or PixelCopy failure, preserving backward compatibility.
- Async by design, fits the existing
Task<IScreenshotResult>signature.
Comparison with second place (try-fix-3)
try-fix-3 is a strong runner-up — minimal API surface, no platform-version gates, easy to audit. The reasons it lost to try-fix-2:
try-fix-3is WebView-specific; identical regressions affecting other hardware surfaces (MapsMapView,VideoView, custom GL views) are not addressed.try-fix-2does not require walking the view tree on every screenshot.try-fix-2is the documented Android best practice;try-fix-3reuses the underlying canvas-draw mechanism the gate already flagged as insufficient.
Why no pr candidate wins
Both pr and pr-plus-reviewer keep the failed mechanism. Per the gate ranking rule they must rank below candidates with passing test signals.
Risk callouts on the winner
PixelCopy.Request(Window, ...)requires API 26. For API 24–25, only the surface-based overload exists; the candidate falls back to canvas-draw on those versions. (Same coverage as today.)- For
CaptureAsync(View)the window is obtained viaview.Context as Activity. CustomContextWrappers that don't expose the activity will fall back to the canvas path — same behavior as today, no regression. - PixelCopy on hardware-protected surfaces returns
ErrorSecure; the candidate returns null and the fallback path runs, identical to today's secure-window handling.
Recommendation to maintainers
Adopt try-fix-2 and additionally pull in the non-platform improvements from pr-plus-reviewer:
- Fix the repro page's
ImageSource.FromStream(() => stream)to useImageSource.FromStream(() => new MemoryStream(bytes)). - Replace
Thread.Sleep(1000)in the UITest withApp.WaitForElement("ResultImage").
|
Closing in favor of #35384, which replaces the software layer toggle approach with Android's Key changes from this PR:
Thank you @jsuarezruiz for identifying the issue and the initial fix — you're listed as co-author on the new PR. 🙏 |
### Description of Change Replaces the approach in #30437 with Android's canonical `PixelCopy` API (API 26+) for capturing hardware-accelerated surfaces, as recommended by the AI review. **Problem:** Calling `Screenshot.CaptureAsync()` on a page containing a `WebView` in Release builds on Android results in a blank/invisible image. The canvas-based `view.Draw()` approach cannot capture hardware-rendered surfaces like WebView's internal `SurfaceView`. **Solution:** Use `PixelCopy.Request(Window, Rect, Bitmap, ...)` which is the Android-recommended API for snapshotting rendered surfaces — including WebView, SurfaceView, TextureView, and other hardware-accelerated views. **Key improvements over #30437:** - **No UI thread blocking** — `PixelCopy` is callback-based and fully async, eliminating the `Thread.Sleep(50)` on the UI thread - **No layer type mutation** — avoids toggling the entire DecorView to software rendering (which caused visual flash and expensive relayout) - **Correct API** — `PixelCopy` is the documented Android best practice for hardware surface capture (API 26+) - **Graceful fallback** — falls back to existing canvas draw → drawing cache paths on pre-API-26 devices or PixelCopy failure - **Fixed test stream lifecycle** — uses `new MemoryStream(bytes)` factory pattern instead of capturing a one-shot stream - **Removed `Thread.Sleep` from UITest** — waits for UI elements instead - **C#-only test page** — per repo conventions, no unnecessary XAML - **No extra project references** — `Screenshot` is already accessible through MAUI meta-package Based on the work by @jsuarezruiz in #30437. ### Issues Fixed Fixes #30010 Supersedes #30437 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
### Description of Change Replaces the approach in #30437 with Android's canonical `PixelCopy` API (API 26+) for capturing hardware-accelerated surfaces, as recommended by the AI review. **Problem:** Calling `Screenshot.CaptureAsync()` on a page containing a `WebView` in Release builds on Android results in a blank/invisible image. The canvas-based `view.Draw()` approach cannot capture hardware-rendered surfaces like WebView's internal `SurfaceView`. **Solution:** Use `PixelCopy.Request(Window, Rect, Bitmap, ...)` which is the Android-recommended API for snapshotting rendered surfaces — including WebView, SurfaceView, TextureView, and other hardware-accelerated views. **Key improvements over #30437:** - **No UI thread blocking** — `PixelCopy` is callback-based and fully async, eliminating the `Thread.Sleep(50)` on the UI thread - **No layer type mutation** — avoids toggling the entire DecorView to software rendering (which caused visual flash and expensive relayout) - **Correct API** — `PixelCopy` is the documented Android best practice for hardware surface capture (API 26+) - **Graceful fallback** — falls back to existing canvas draw → drawing cache paths on pre-API-26 devices or PixelCopy failure - **Fixed test stream lifecycle** — uses `new MemoryStream(bytes)` factory pattern instead of capturing a one-shot stream - **Removed `Thread.Sleep` from UITest** — waits for UI elements instead - **C#-only test page** — per repo conventions, no unnecessary XAML - **No extra project references** — `Screenshot` is already accessible through MAUI meta-package Based on the work by @jsuarezruiz in #30437. ### Issues Fixed Fixes #30010 Supersedes #30437 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
### Description of Change Replaces the approach in #30437 with Android's canonical `PixelCopy` API (API 26+) for capturing hardware-accelerated surfaces, as recommended by the AI review. **Problem:** Calling `Screenshot.CaptureAsync()` on a page containing a `WebView` in Release builds on Android results in a blank/invisible image. The canvas-based `view.Draw()` approach cannot capture hardware-rendered surfaces like WebView's internal `SurfaceView`. **Solution:** Use `PixelCopy.Request(Window, Rect, Bitmap, ...)` which is the Android-recommended API for snapshotting rendered surfaces — including WebView, SurfaceView, TextureView, and other hardware-accelerated views. **Key improvements over #30437:** - **No UI thread blocking** — `PixelCopy` is callback-based and fully async, eliminating the `Thread.Sleep(50)` on the UI thread - **No layer type mutation** — avoids toggling the entire DecorView to software rendering (which caused visual flash and expensive relayout) - **Correct API** — `PixelCopy` is the documented Android best practice for hardware surface capture (API 26+) - **Graceful fallback** — falls back to existing canvas draw → drawing cache paths on pre-API-26 devices or PixelCopy failure - **Fixed test stream lifecycle** — uses `new MemoryStream(bytes)` factory pattern instead of capturing a one-shot stream - **Removed `Thread.Sleep` from UITest** — waits for UI elements instead - **C#-only test page** — per repo conventions, no unnecessary XAML - **No extra project references** — `Screenshot` is already accessible through MAUI meta-package Based on the work by @jsuarezruiz in #30437. ### Issues Fixed Fixes #30010 Supersedes #30437 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description of Change
This PR introduce a hardware-accelerated capture path on before falling back to canvas or drawing-cache rendering. Also, updated the screenshot sample from Essentials and added an UITest.
Issues Fixed
Fixes #30010