Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue30010.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using Microsoft.Maui.Media;
#if ANDROID
using AColor = Android.Graphics.Color;
using BitmapFactory = Android.Graphics.BitmapFactory;
#endif

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 30010, "Loading the captured screenshot from webview content to Image control does not visible", PlatformAffected.Android)]
public class Issue30010 : ContentPage
{
readonly Image _resultImage;
readonly Label _statusLabel;
readonly Button _screenshotButton;

public Issue30010()
{
_statusLabel = new Label
{
Text = "Waiting for WebView to load...",
AutomationId = "StatusLabel",
Margin = new Thickness(12)
};

var webView = new WebView
{
HeightRequest = 200,
Source = new HtmlWebViewSource
{
Html = @"<HTML><BODY style=""margin:0;background:#00ff00;""><DIV style=""height:200px;background:#00ff00;color:#000;font-size:48px;"">WEBVIEW_MARKER</DIV></BODY></HTML>"
}
};
webView.Navigated += (s, e) =>
{
_statusLabel.Text = "WebView loaded";
_screenshotButton.IsEnabled = true;
};

_screenshotButton = new Button
{
Text = "Take Screenshot",
AutomationId = "TakeScreenshotButton",
IsEnabled = false
};
_screenshotButton.Clicked += OnTakeScreenshotClicked;

_resultImage = new Image
{
AutomationId = "ResultImage",
HeightRequest = 300
};

Content = new VerticalStackLayout
{
Children =
{
new Label { Text = "Issue 30010", FontAttributes = FontAttributes.Bold, Margin = new Thickness(12) },
_statusLabel,
webView,
_screenshotButton,
_resultImage
}
};
}

async void OnTakeScreenshotClicked(object sender, EventArgs e)
{
try
{
var screenshot = await Screenshot.CaptureAsync();
using var stream = await screenshot.OpenReadAsync(ScreenshotFormat.Png);

// Read into a byte array so the stream can be consumed multiple times
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
var bytes = ms.ToArray();
Comment on lines +70 to +76

if (!ContainsWebViewMarker(bytes))
{
_statusLabel.Text = "Error: Screenshot missing WebView marker";
return;
}

_resultImage.Source = ImageSource.FromStream(() => new MemoryStream(bytes));
_statusLabel.Text = "Screenshot captured";
}
catch (Exception ex)
{
_statusLabel.Text = $"Error: {ex.Message}";
}
}

static bool ContainsWebViewMarker(byte[] screenshot)
{
#if ANDROID
using var bitmap = BitmapFactory.DecodeByteArray(screenshot, 0, screenshot.Length);
if (bitmap is null)
return false;

var markerPixels = 0;
for (var y = 0; y < bitmap.Height; y += 4)
{
for (var x = 0; x < bitmap.Width; x += 4)
{
var color = new AColor(bitmap.GetPixel(x, y));
if (color.G > 200 && color.R < 80 && color.B < 80)
markerPixels++;

if (markerPixels > 100)
return true;
}
}
#endif

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#if ANDROID
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue30010 : _IssuesUITest
{
public Issue30010(TestDevice device) : base(device)
{
}

public override string Issue =>
"Loading the captured screenshot from webview content to Image control does not visible";

[Test]
[Category(UITestCategories.WebView)]
public void Issue30010_TakeScreenshotFunctionality()
{
// Wait for WebView to finish loading before tapping the button
App.WaitForTextToBePresentInElement("StatusLabel", "WebView loaded");
App.Tap("TakeScreenshotButton");

// Wait for the screenshot to be captured and validated by the test page
App.WaitForTextToBePresentInElement("StatusLabel", "Screenshot captured");
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.

[critical] Regression Prevention and Test Coverage — The known gate result showed this test passes without the product fix, so this assertion does not prove the WebView screenshot regression. Strengthen the repro/assertion so it fails on base, e.g. validate actual WebView-specific captured pixels/content rather than only waiting for the host page status label.

}
}
#endif
22 changes: 22 additions & 0 deletions src/Essentials/samples/Samples/View/ScreenshotPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@
<ScrollView Grid.Row="1">
<StackLayout Padding="12,0,12,12" Spacing="6">

<StackLayout Padding="12" Spacing="6">
<Label Text="Some Controls" />
<Switch OnColor="Orange"
ThumbColor="Green" />
<WebView HeightRequest="200">
<WebView.Source>
<HtmlWebViewSource>
<HtmlWebViewSource.Html>
<![CDATA[
<HTML>
<BODY>
<H1>.NET MAUI</H1>
<P>Welcome to WebView.</P>
</BODY>
</HTML>
]]>
</HtmlWebViewSource.Html>
</HtmlWebViewSource>
</WebView.Source>
</WebView>
</StackLayout>

<Button Text="Take Screenshot" Command="{Binding ScreenshotCommand}" />

<Button Text="Email Screenshot" Command="{Binding EmailCommand}" />
Expand Down
127 changes: 110 additions & 17 deletions src/Essentials/src/Screenshot/Screenshot.android.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#nullable enable

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Views;
using Java.Nio;
using Microsoft.Maui.ApplicationModel;
Expand All @@ -13,7 +16,7 @@ namespace Microsoft.Maui.Media
{
partial class ScreenshotImplementation : IPlatformScreenshot, IScreenshot
{
static IWindowManager WindowManager =>
static IWindowManager? WindowManager =>
Application.Context.GetSystemService(Context.WindowService) as IWindowManager;

public bool IsCaptureSupported => true;
Expand All @@ -23,41 +26,131 @@ public Task<IScreenshotResult> CaptureAsync()
if (WindowManager?.DefaultDisplay?.Flags.HasFlag(DisplayFlags.Secure) == true)
throw new UnauthorizedAccessException("Unable to take a screenshot of a secure window.");

var activity = ActivityStateManager.Default.GetCurrentActivity(true);
var activity = ActivityStateManager.Default.GetCurrentActivity(true)
?? throw new InvalidOperationException("Unable to find the current activity.");

return CaptureAsync(activity);
}

public Task<IScreenshotResult> CaptureAsync(Activity activity)
public async Task<IScreenshotResult> CaptureAsync(Activity activity)
{
var view = activity?.Window?.DecorView?.RootView;
var window = activity?.Window;
var view = window?.DecorView?.RootView;
if (view == null)
throw new InvalidOperationException("Unable to find the main window.");

return CaptureAsync(view);
var result = await CaptureAsync(view, window).ConfigureAwait(false);
return result ?? throw new InvalidOperationException("Unable to capture screenshot.");
}

public Task<IScreenshotResult> CaptureAsync(View view)
public async Task<IScreenshotResult?> CaptureAsync(View view)
{
return await CaptureAsync(view, GetActivity(view.Context)?.Window).ConfigureAwait(false);
}

async Task<IScreenshotResult?> CaptureAsync(View view, Window? window)
{
_ = view ?? throw new ArgumentNullException(nameof(view));

var bitmap = Render(view);
var result = bitmap is null ? null : new ScreenshotResult(bitmap);
var bitmap = await RenderAsync(view, window).ConfigureAwait(false);
return bitmap is null ? null : new ScreenshotResult(bitmap);
}

static async Task<Bitmap?> RenderAsync(View view, Window? window)
{
if (OperatingSystem.IsAndroidVersionAtLeast(26))
{
var bitmap = await RenderUsingPixelCopyAsync(view, window);
if (bitmap is not null)
return bitmap;
}

return RenderUsingCanvasDrawing(view) ?? RenderUsingDrawingCache(view);
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.

[major] Async and Threading Safety - If CaptureAsync(View) is called from a background thread and PixelCopy returns null, this fallback path runs View.Draw / drawing-cache work on that background thread. Android view rendering APIs must run on the UI thread; dispatch the fallback render to the main looper or fail before falling back.

}

static async Task<Bitmap?> RenderUsingPixelCopyAsync(View view, Window? window)
{
if (view.Width <= 0 || view.Height <= 0 || !view.IsAttachedToWindow)
return null;

if (window is null)
return null;

var bitmap = Bitmap.CreateBitmap(view.Width, view.Height, Bitmap.Config.Argb8888!);
if (bitmap is null)
return null;

var location = new int[2];
view.GetLocationInWindow(location);
var rect = new Rect(
location[0],
location[1],
location[0] + view.Width,
location[1] + view.Height);

var tcs = new TaskCompletionSource<Bitmap?>(TaskCreationOptions.RunContinuationsAsynchronously);

try
{
var listener = new PixelCopyFinishedListener(tcs, bitmap);
PixelCopy.Request(window, rect, bitmap,
listener,
new Handler(Looper.MainLooper!));
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.

[moderate] Android Platform Specifics — This allocates a new Android Handler/Java peer for every screenshot capture. MAUI's existing PixelCopy usage posts through the view's native handler; prefer view.Handler ?? new Handler(Looper.MainLooper!) so the callback is tied to the view looper and avoids unnecessary per-capture allocation.


try
{
return await tcs.Task.ConfigureAwait(true);
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.

[major] Async and Threading Safety — This waits indefinitely for the PixelCopy callback. If the window/surface is detached after PixelCopy.Request and no completion arrives, Screenshot.CaptureAsync never completes. Add a bounded wait/cancellation fallback and keep the continuation consistent with the rest of this file (ConfigureAwait(false)) while carefully handling bitmap ownership on timeout vs late success.

}
finally
{
GC.KeepAlive(listener);
}
}
catch (Exception)
{
bitmap.Dispose();
return null;
}
}

return Task.FromResult<IScreenshotResult>(result);
static Activity? GetActivity(Context? context)
{
while (context is ContextWrapper wrapper)
{
if (context is Activity activity)
return activity;
context = wrapper.BaseContext;
}
return context as Activity;
}

static Bitmap Render(View view)
sealed class PixelCopyFinishedListener : Java.Lang.Object, PixelCopy.IOnPixelCopyFinishedListener
{
var bitmap = RenderUsingCanvasDrawing(view);
readonly TaskCompletionSource<Bitmap?> _tcs;
readonly Bitmap _bitmap;

if (bitmap == null)
bitmap = RenderUsingDrawingCache(view);
public PixelCopyFinishedListener(TaskCompletionSource<Bitmap?> tcs, Bitmap bitmap)
{
_tcs = tcs;
_bitmap = bitmap;
}

return bitmap;
public void OnPixelCopyFinished(int copyResult)
{
// PixelCopy.SUCCESS == 0
if (copyResult == 0)
{
_tcs.TrySetResult(_bitmap);
}
else
{
_bitmap.Dispose();
_tcs.TrySetResult(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.

[major] Logic and Correctness - Returning null for any PixelCopy failure silently falls through to the old canvas/drawing-cache screenshot path, which is the path that cannot capture hardware-accelerated WebView content. On an API 26+ device where PixelCopy returns ERROR_SOURCE_NO_DATA/timeout, callers can still receive a successful but blank-prone screenshot instead of a retry or clear failure signal.

}
}
}

static Bitmap RenderUsingCanvasDrawing(View view)
static Bitmap? RenderUsingCanvasDrawing(View view)
{
try
{
Expand All @@ -67,7 +160,7 @@ static Bitmap RenderUsingCanvasDrawing(View view)
var height = view.Height;

var bitmap = Bitmap.CreateBitmap(width, height, Bitmap.Config.Argb8888);
if (bitmap == null)
if (bitmap is null)
return null;

using (var canvas = new Canvas(bitmap))
Expand All @@ -81,7 +174,7 @@ static Bitmap RenderUsingCanvasDrawing(View view)
}
}

static Bitmap RenderUsingDrawingCache(View view)
static Bitmap? RenderUsingDrawingCache(View view)
{
#pragma warning disable CS0618 // Type or member is obsolete
#pragma warning disable CA1416 // Validate platform compatibility
Expand Down
Loading