-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[Android] Fix screenshot from WebView content not working #35384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
febd7d4
0bc938e
a0c7b57
9d80ad5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| using Microsoft.Maui.Media; | ||
|
|
||
| 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><H1>.NET MAUI</H1><P>Welcome to WebView.</P></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(); | ||
|
|
||
| var stream = await screenshot.OpenReadAsync(ScreenshotFormat.Png); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [moderate] Screenshot stream is not disposed — The stream returned from |
||
|
|
||
| // 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
|
||
|
|
||
| _resultImage.Source = ImageSource.FromStream(() => new MemoryStream(bytes)); | ||
| _statusLabel.Text = "Screenshot captured"; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _statusLabel.Text = $"Error: {ex.Message}"; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| #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 | ||
| App.WaitForElement("StatusLabel"); | ||
| App.WaitForElement("TakeScreenshotButton"); | ||
|
|
||
| // The button is enabled only after WebView.Navigated fires | ||
| App.WaitForElement("TakeScreenshotButton"); | ||
| App.Tap("TakeScreenshotButton"); | ||
|
|
||
| // Wait for the screenshot to be captured and displayed | ||
| var statusLabel = App.WaitForElement("StatusLabel"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Test waits for pre-existing UI instead of capture completion — This waits for |
||
| App.WaitForElement("ResultImage"); | ||
|
|
||
|
|
||
| VerifyScreenshot("Issue30010TakeScreenshotFunctionality"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [moderate] Regression Prevention and Test Coverage — The test waits for the status label, but the app sets that label immediately after assigning |
||
| } | ||
| } | ||
| #endif | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [moderate] Sample WebView needs an explicit height — This sample adds a |
||
| <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}" /> | ||
|
|
||
| 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; | ||
|
|
@@ -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; | ||
|
|
@@ -23,41 +26,117 @@ 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; | ||
| if (view == null) | ||
| throw new InvalidOperationException("Unable to find the main window."); | ||
|
|
||
| return CaptureAsync(view); | ||
| var result = await CaptureAsync(view).ConfigureAwait(false); | ||
| return result ?? throw new InvalidOperationException("Unable to capture screenshot."); | ||
| } | ||
|
|
||
| public Task<IScreenshotResult> CaptureAsync(View view) | ||
| public async Task<IScreenshotResult?> CaptureAsync(View view) | ||
| { | ||
| _ = view ?? throw new ArgumentNullException(nameof(view)); | ||
|
|
||
| var bitmap = Render(view); | ||
| var result = bitmap is null ? null : new ScreenshotResult(bitmap); | ||
| var bitmap = await RenderAsync(view).ConfigureAwait(false); | ||
| return bitmap is null ? null : new ScreenshotResult(bitmap); | ||
| } | ||
|
|
||
| static async Task<Bitmap?> RenderAsync(View view) | ||
| { | ||
| if (OperatingSystem.IsAndroidVersionAtLeast(26)) | ||
| { | ||
| var bitmap = await RenderUsingPixelCopyAsync(view).ConfigureAwait(false); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Async and threading safety — If |
||
| if (bitmap is not null) | ||
| return bitmap; | ||
| } | ||
|
|
||
| return RenderUsingCanvasDrawing(view) ?? RenderUsingDrawingCache(view); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Async and Threading Safety - If |
||
| } | ||
|
|
||
| static Task<Bitmap?> RenderUsingPixelCopyAsync(View view) | ||
| { | ||
| if (view.Width <= 0 || view.Height <= 0 || !view.IsAttachedToWindow) | ||
| return Task.FromResult<Bitmap?>(null); | ||
|
|
||
| var window = GetActivity(view.Context)?.Window; | ||
| if (window is null) | ||
| return Task.FromResult<Bitmap?>(null); | ||
|
|
||
| var bitmap = Bitmap.CreateBitmap(view.Width, view.Height, Bitmap.Config.Argb8888!); | ||
| if (bitmap is null) | ||
| return Task.FromResult<Bitmap?>(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 | ||
| { | ||
| PixelCopy.Request(window, rect, bitmap, | ||
| new PixelCopyFinishedListener(tcs, bitmap), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Android callback lifetime — The |
||
| new Handler(Looper.MainLooper!)); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [moderate] Android Platform Specifics — This allocates a new Android |
||
| } | ||
| catch (Exception) | ||
| { | ||
| bitmap.Dispose(); | ||
| return Task.FromResult<Bitmap?>(null); | ||
| } | ||
|
|
||
| return Task.FromResult<IScreenshotResult>(result); | ||
| return tcs.Task; | ||
| } | ||
|
|
||
| static Bitmap Render(View view) | ||
| static Activity? GetActivity(Context? context) | ||
| { | ||
| var bitmap = RenderUsingCanvasDrawing(view); | ||
| while (context is ContextWrapper wrapper) | ||
| { | ||
| if (context is Activity activity) | ||
| return activity; | ||
| context = wrapper.BaseContext; | ||
| } | ||
| return context as Activity; | ||
| } | ||
|
|
||
| if (bitmap == null) | ||
| bitmap = RenderUsingDrawingCache(view); | ||
| sealed class PixelCopyFinishedListener : Java.Lang.Object, PixelCopy.IOnPixelCopyFinishedListener | ||
| { | ||
| readonly TaskCompletionSource<Bitmap?> _tcs; | ||
| readonly Bitmap _bitmap; | ||
|
|
||
| return bitmap; | ||
| public PixelCopyFinishedListener(TaskCompletionSource<Bitmap?> tcs, Bitmap bitmap) | ||
| { | ||
| _tcs = tcs; | ||
| _bitmap = bitmap; | ||
| } | ||
|
|
||
| public void OnPixelCopyFinished(int copyResult) | ||
| { | ||
| if (copyResult == (int)PixelCopyResult.Success) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] PixelCopy success check — |
||
| { | ||
| _tcs.TrySetResult(_bitmap); | ||
| } | ||
| else | ||
| { | ||
| _bitmap.Dispose(); | ||
| _tcs.TrySetResult(null); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Logic and Correctness - Returning |
||
| } | ||
| } | ||
| } | ||
|
|
||
| static Bitmap RenderUsingCanvasDrawing(View view) | ||
| static Bitmap? RenderUsingCanvasDrawing(View view) | ||
| { | ||
| try | ||
| { | ||
|
|
@@ -67,7 +146,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)) | ||
|
|
@@ -81,7 +160,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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Build & MSBuild — This file does not enable nullable annotations, but the new handler uses
object?. With repository-wide warnings-as-errors this produces CS8632 and blocks the Android test host build. Useobject senderhere or add#nullable enableto the file.