From cdaeda47d2bd012921485afb0fe2eb380ef16c75 Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Tue, 23 Aug 2022 10:38:24 -0400 Subject: [PATCH] feat: Use in-app screenshots for android --- .../SamplesApp.Droid/MainActivity.cs | 64 ++++++++++++++++++- .../Extensions/AppExtensions.cs | 21 ++++++ .../SampleControlUITestBase.cs | 15 ++++- .../Android/MainActivity.Android.cs | 63 ++++++++++++++++++ 4 files changed, 160 insertions(+), 3 deletions(-) diff --git a/src/SamplesApp/SamplesApp.Droid/MainActivity.cs b/src/SamplesApp/SamplesApp.Droid/MainActivity.cs index e0619c0c7c27..d42bf2987a36 100644 --- a/src/SamplesApp/SamplesApp.Droid/MainActivity.cs +++ b/src/SamplesApp/SamplesApp.Droid/MainActivity.cs @@ -8,6 +8,9 @@ using Microsoft.Identity.Client; using Uno.UI.ViewManagement; using Uno.AuthenticationBroker; +using System.IO; +using System.Threading; +using Android.OS; namespace SamplesApp.Droid { @@ -31,7 +34,8 @@ namespace SamplesApp.Droid public class MainActivity : Windows.UI.Xaml.ApplicationActivity { private bool _onCreateEventInvoked = false; - + private HandlerThread _pixelCopyHandlerThread; + public MainActivity() { ApplicationViewHelper.GetBaseActivityEvents().Create += OnCreateEvent; @@ -61,6 +65,48 @@ protected override void OnStart() [Export("GetDisplayScreenScaling")] public string GetDisplayScreenScaling(string displayId) => App.GetDisplayScreenScaling(displayId); + /// + /// Returns a base64 encoded PNG file + /// + [Export("GetScreenshot")] + public string GetScreenshot(string displayId) + { + var rootView = Windows.UI.Xaml.Window.Current.MainContent as View; + + var bitmap = Android.Graphics.Bitmap.CreateBitmap(rootView.Width, rootView.Height, Android.Graphics.Bitmap.Config.Argb8888); + var locationOfViewInWindow = new int[2]; + rootView.GetLocationInWindow(locationOfViewInWindow); + + var xCoordinate = locationOfViewInWindow[0]; + var yCoordinate = locationOfViewInWindow[1]; + + var scope = new Android.Graphics.Rect( + xCoordinate, + yCoordinate, + xCoordinate + rootView.Width, + yCoordinate + rootView.Height + ); + + if (_pixelCopyHandlerThread == null) + { + _pixelCopyHandlerThread = new Android.OS.HandlerThread("ScreenshotHelper"); + _pixelCopyHandlerThread.Start(); + } + + var listener = new PixelCopyListener(); + + // PixelCopy.Request returns the actual rendering of the screen location + // for the app, incliing OpenGL content. + PixelCopy.Request(Window, scope, bitmap, listener, new Android.OS.Handler(_pixelCopyHandlerThread.Looper)); + + listener.WaitOne(); + + using var memoryStream = new MemoryStream(); + bitmap.Compress(Android.Graphics.Bitmap.CompressFormat.Png, 100, memoryStream); + + return Convert.ToBase64String(memoryStream.ToArray()); + } + [Export("SetFullScreenMode")] public void SetFullScreenMode(bool fullscreen) { @@ -81,6 +127,21 @@ protected override void OnActivityResult(int requestCode, Result resultCode, And base.OnActivityResult(requestCode, resultCode, data); AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(requestCode, resultCode, data); } + + class PixelCopyListener : Java.Lang.Object, PixelCopy.IOnPixelCopyFinishedListener + { + private ManualResetEvent _event = new ManualResetEvent(false); + + public void WaitOne() + { + _event.WaitOne(); + } + + public void OnPixelCopyFinished(int copyResult) + { + _event.Set(); + } + } } @@ -107,5 +168,6 @@ public class MsalActivity : BrowserTabActivity public class WebAuthenticationBrokerActivity : WebAuthenticationBrokerActivityBase { } + } diff --git a/src/SamplesApp/SamplesApp.UITests/Extensions/AppExtensions.cs b/src/SamplesApp/SamplesApp.UITests/Extensions/AppExtensions.cs index d35d75cc749c..fe0ca03dfc34 100644 --- a/src/SamplesApp/SamplesApp.UITests/Extensions/AppExtensions.cs +++ b/src/SamplesApp/SamplesApp.UITests/Extensions/AppExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Drawing; using System.Globalization; +using System.IO; using System.Linq; using System.Text; using Uno.UITest; @@ -36,6 +37,26 @@ float GetScaling() return 1f; } } +#endif + } + + public static FileInfo GetInAppScreenshot(this IApp app) + { +#if IS_RUNTIME_UI_TESTS + return null; +#else + var byte64Image = app.InvokeGeneric("browser:SampleRunner|GetScreenshot", "0")?.ToString(); + + var array = Convert.FromBase64String(byte64Image); + + var outputFile = Path.GetTempFileName(); + File.WriteAllBytes(outputFile, array); + + var finalPath = Path.ChangeExtension(outputFile, ".png"); + + File.Move(outputFile, finalPath); + + return new(finalPath); #endif } } diff --git a/src/SamplesApp/SamplesApp.UITests/SampleControlUITestBase.cs b/src/SamplesApp/SamplesApp.UITests/SampleControlUITestBase.cs index d6ccdaf45f86..44c3fedd6153 100644 --- a/src/SamplesApp/SamplesApp.UITests/SampleControlUITestBase.cs +++ b/src/SamplesApp/SamplesApp.UITests/SampleControlUITestBase.cs @@ -185,8 +185,7 @@ public ScreenshotInfo TakeScreenshot(string stepName, ScreenshotOptions options) } var title = GetCurrentStepTitle(stepName); - - var fileInfo = _app.Screenshot(title); + var fileInfo = GetNativeScreenshot(title); var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.Name); if (fileNameWithoutExt != title) @@ -219,6 +218,18 @@ public ScreenshotInfo TakeScreenshot(string stepName, ScreenshotOptions options) return new ScreenshotInfo(fileInfo, stepName); } + private FileInfo GetNativeScreenshot(string title) + { + if (AppInitializer.GetLocalPlatform() == Platform.Android) + { + return _app.GetInAppScreenshot(); + } + else + { + return _app.Screenshot(title); + } + } + private static string GetCurrentStepTitle(string stepName) => $"{TestContext.CurrentContext.Test.Name}_{stepName}" .Replace(" ", "_") diff --git a/src/SamplesApp/SamplesApp.net6mobile/Android/MainActivity.Android.cs b/src/SamplesApp/SamplesApp.net6mobile/Android/MainActivity.Android.cs index f94f7247b3d6..4ad56f2cda51 100644 --- a/src/SamplesApp/SamplesApp.net6mobile/Android/MainActivity.Android.cs +++ b/src/SamplesApp/SamplesApp.net6mobile/Android/MainActivity.Android.cs @@ -7,6 +7,8 @@ using Windows.UI.ViewManagement; using Microsoft.Identity.Client; using Uno.UI; +using System.Threading; +using Android.OS; namespace SamplesApp.Droid { @@ -26,6 +28,8 @@ namespace SamplesApp.Droid DataScheme = "uno-samples-test")] public class MainActivity : Windows.UI.Xaml.ApplicationActivity { + private HandlerThread _pixelCopyHandlerThread; + [Export("RunTest")] public string RunTest(string metadataName) => App.RunTest(metadataName); @@ -35,6 +39,49 @@ public class MainActivity : Windows.UI.Xaml.ApplicationActivity [Export("GetDisplayScreenScaling")] public string GetDisplayScreenScaling(string displayId) => App.GetDisplayScreenScaling(displayId); + /// + /// Returns a base64 encoded PNG file + /// + [Export("GetScreenshot")] + public string GetScreenshot(string displayId) + { + var rootView = Windows.UI.Xaml.Window.Current.MainContent as View; + + var bitmap = Android.Graphics.Bitmap.CreateBitmap(rootView.Width, rootView.Height, Android.Graphics.Bitmap.Config.Argb8888); + var locationOfViewInWindow = new int[2]; + rootView.GetLocationInWindow(locationOfViewInWindow); + + var xCoordinate = locationOfViewInWindow[0]; + var yCoordinate = locationOfViewInWindow[1]; + + var scope = new Android.Graphics.Rect( + xCoordinate, + yCoordinate, + xCoordinate + rootView.Width, + yCoordinate + rootView.Height + ); + + if (_pixelCopyHandlerThread == null) + { + _pixelCopyHandlerThread = new Android.OS.HandlerThread("ScreenshotHelper"); + _pixelCopyHandlerThread.Start(); + } + + var listener = new PixelCopyListener(); + + // PixelCopy.Request returns the actual rendering of the screen location + // for the app, incliing OpenGL content. + PixelCopy.Request(Window, scope, bitmap, listener, new Android.OS.Handler(_pixelCopyHandlerThread.Looper)); + + listener.WaitOne(); + + using var memoryStream = new System.IO.MemoryStream(); + bitmap.Compress(Android.Graphics.Bitmap.CompressFormat.Png, 100, memoryStream); + + return Convert.ToBase64String(memoryStream.ToArray()); + } + + [Export("SetFullScreenMode")] public void SetFullScreenMode(bool fullscreen) { @@ -57,6 +104,22 @@ protected override void OnActivityResult(int requestCode, Result resultCode, And AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(requestCode, resultCode, data); #endif } + + + class PixelCopyListener : Java.Lang.Object, PixelCopy.IOnPixelCopyFinishedListener + { + private ManualResetEvent _event = new ManualResetEvent(false); + + public void WaitOne() + { + _event.WaitOne(); + } + + public void OnPixelCopyFinished(int copyResult) + { + _event.Set(); + } + } } #if !NET6_0