diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue34392.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue34392.cs new file mode 100644 index 000000000000..c0396d55e5f1 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue34392.cs @@ -0,0 +1,153 @@ +#if ANDROID +using Android.Webkit; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; +using AWebView = Android.Webkit.WebView; +#endif + +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 34392, "MAUI Handler not working with Custom WebView on Android (ShouldOverrideUrlLoading behavior)", PlatformAffected.Android)] +public class Issue34392 : TestContentPage +{ + public static Label StatusIndicator { get; private set; } + protected override void Init() + { + var grid = new Grid + { + RowDefinitions = new RowDefinitionCollection + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star } + } + }; + + var titleLabel = new Label + { + Text = "Custom WebViewClient Test", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + HorizontalOptions = LayoutOptions.Center, + Padding = new Thickness(5) + }; + grid.Add(titleLabel, 0, 0); + + var statusLabel = new Label + { + Text = "FAILED", + AutomationId = "StatusLabel", + FontSize = 14, + FontAttributes = FontAttributes.Bold, + HorizontalOptions = LayoutOptions.Center, + Padding = new Thickness(5) + }; + Grid.SetRow(statusLabel, 1); + grid.Add(statusLabel); + + var customWebView = new Issue34392_CustomWebView(); + Grid.SetRow(customWebView, 2); + grid.Add(customWebView); + + Content = grid; + + StatusIndicator = statusLabel; + + customWebView.CustomHeaders = new Dictionary + { + { "X-Custom-Header", "MyHeaderValue" }, + { "Authorization", "Bearer sample-token-12345" } + }; + + customWebView.Source = new HtmlWebViewSource + { + Html = "

Custom WebView Test

" + }; + } +} + +public class Issue34392_CustomWebView : Microsoft.Maui.Controls.WebView +{ + public static readonly BindableProperty CustomHeadersProperty = + BindableProperty.Create(nameof(CustomHeaders), typeof(Dictionary), typeof(Issue34392_CustomWebView), new Dictionary()); + + public Dictionary CustomHeaders + { + get => (Dictionary)GetValue(CustomHeadersProperty); + set => SetValue(CustomHeadersProperty, value); + } +} + +#if ANDROID +#nullable enable +public class Issue34392_CustomWebViewHandler : WebViewHandler +{ + protected override void ConnectHandler(AWebView platformView) + { + base.ConnectHandler(platformView); + + var customWebView = VirtualView as Issue34392_CustomWebView; + var headers = customWebView?.CustomHeaders ?? new Dictionary(); + + platformView.SetWebViewClient(new Issue34392_CustomWebViewClient(this, headers)); + } +} + +public class Issue34392_CustomWebViewClient : MauiWebViewClient +{ + private readonly Dictionary _headerParams; + private bool _alreadyRedirected; + + public Issue34392_CustomWebViewClient(WebViewHandler handler, Dictionary headers) + : base(handler) + { + _headerParams = headers; + } + + public override bool ShouldOverrideUrlLoading(AWebView? view, IWebResourceRequest? request) + { + var url = request?.Url?.ToString() ?? string.Empty; + + // Update UI to show this method was called + MainThread.BeginInvokeOnMainThread(() => + { + if (Issue34392.StatusIndicator != null) + { + Issue34392.StatusIndicator.Text = "SUCCESS"; + } + }); + + // Prevent infinite redirect loop — only load with custom headers once + if (_alreadyRedirected) + return true; + + _alreadyRedirected = true; + + if (_headerParams.Count > 0) + { + view?.LoadUrl(url, _headerParams); + } + else + { + view?.LoadUrl(url); + } + + return true; + } +} +#endif + +public static class Issue34392Extensions +{ + public static MauiAppBuilder Issue34392AddHandlers(this MauiAppBuilder builder) + { + builder.ConfigureMauiHandlers(handlers => + { +#if ANDROID + handlers.AddHandler(); +#endif + }); + + return builder; + } +} diff --git a/src/Controls/tests/TestCases.HostApp/MauiProgram.cs b/src/Controls/tests/TestCases.HostApp/MauiProgram.cs index beae20e666d9..de9e84a4adaa 100644 --- a/src/Controls/tests/TestCases.HostApp/MauiProgram.cs +++ b/src/Controls/tests/TestCases.HostApp/MauiProgram.cs @@ -41,7 +41,8 @@ public static MauiApp CreateMauiApp() .Issue18720DatePickerAddMappers() .Issue18720TimePickerAddMappers() .Issue28945AddMappers() - .Issue25436RegisterNavigationService(); + .Issue25436RegisterNavigationService() + .Issue34392AddHandlers(); #if IOS || MACCATALYST appBuilder.ConfigureCollectionViewHandlers(); diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34392.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34392.cs new file mode 100644 index 000000000000..cbe0fd8fe1e3 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34392.cs @@ -0,0 +1,26 @@ +#if ANDROID //WebViewClient is only used in Android +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues +{ + public class Issue34392 : _IssuesUITest + { + public Issue34392(TestDevice testDevice) : base(testDevice) + { + } + public override string Issue => "MAUI Handler not working with Custom WebView on Android (ShouldOverrideUrlLoading behavior)"; + + [Test] + [Category(UITestCategories.WebView)] + public void ShouldOverrideUrlLoading_Called() + { + // The label starts as "FAILED" and updates asynchronously after ShouldOverrideUrlLoading fires. + // Poll until the text changes to "SUCCESS" instead of reading a stale initial value. + var result = App.WaitForTextToBePresentInElement("StatusLabel", "SUCCESS", timeout: TimeSpan.FromSeconds(10)); + Assert.That(result, Is.True, "Expected ShouldOverrideUrlLoading to be called and update the status label text to 'SUCCESS', but it was not updated within the timeout."); + } + } +} +#endif diff --git a/src/Core/src/Handlers/WebView/WebViewHandler.Android.cs b/src/Core/src/Handlers/WebView/WebViewHandler.Android.cs index ef02fde79eb5..e5f6ebf61c5f 100644 --- a/src/Core/src/Handlers/WebView/WebViewHandler.Android.cs +++ b/src/Core/src/Handlers/WebView/WebViewHandler.Android.cs @@ -18,6 +18,9 @@ public partial class WebViewHandler : ViewHandler protected internal string? UrlCanceled { get; set; } + MauiWebViewClient? _webViewClient; + MauiWebChromeClient? _webChromeClient; + protected override AWebView CreatePlatformView() { var platformView = new MauiWebView(this, Context!) @@ -34,6 +37,13 @@ protected override AWebView CreatePlatformView() platformView.SetLayerType(global::Android.Views.LayerType.Software, null); } + // Create web clients once and store references + _webViewClient = new MauiWebViewClient(this); + platformView.SetWebViewClient(_webViewClient); + + _webChromeClient = new MauiWebChromeClient(this); + platformView.SetWebChromeClient(_webChromeClient); + return platformView; } @@ -54,20 +64,55 @@ public override void SetVirtualView(IView view) protected override void DisconnectHandler(AWebView platformView) { + // WebView.WebViewClient / WebView.WebChromeClient getters are only available on API 26+. + // On older API levels we fall back to the cached references we set in CreatePlatformView. + global::Android.Webkit.WebViewClient? currentWebViewClient = null; + WebChromeClient? currentWebChromeClient = null; + if (OperatingSystem.IsAndroidVersionAtLeast(26)) { - if (platformView.WebViewClient is MauiWebViewClient webViewClient) - webViewClient.Disconnect(); - - if (platformView.WebChromeClient is MauiWebChromeClient webChromeClient) - webChromeClient.Disconnect(); + currentWebViewClient = platformView.WebViewClient; + currentWebChromeClient = platformView.WebChromeClient; } platformView.SetWebViewClient(null!); platformView.SetWebChromeClient(null); - platformView.StopLoading(); + // Disconnect/dispose the clients that were actually active + if (OperatingSystem.IsAndroidVersionAtLeast(26)) + { + (currentWebViewClient as MauiWebViewClient)?.Disconnect(); + (currentWebChromeClient as MauiWebChromeClient)?.Disconnect(); + } + + // Also clean up originals if they were replaced by a custom handler + if (!ReferenceEquals(currentWebViewClient, _webViewClient)) + { + if (OperatingSystem.IsAndroidVersionAtLeast(26)) + { + _webViewClient?.Disconnect(); + } + + _webViewClient?.Dispose(); + } + + if (!ReferenceEquals(currentWebChromeClient, _webChromeClient)) + { + if (OperatingSystem.IsAndroidVersionAtLeast(26)) + { + _webChromeClient?.Disconnect(); + } + + _webChromeClient?.Dispose(); + } + + (currentWebViewClient as IDisposable)?.Dispose(); + (currentWebChromeClient as IDisposable)?.Dispose(); + + _webViewClient = null; + _webChromeClient = null; + base.DisconnectHandler(platformView); } @@ -81,18 +126,6 @@ public static void MapUserAgent(IWebViewHandler handler, IWebView webView) handler.PlatformView.UpdateUserAgent(webView); } - public static void MapWebViewClient(IWebViewHandler handler, IWebView webView) - { - if (handler is WebViewHandler platformHandler) - handler.PlatformView.SetWebViewClient(new MauiWebViewClient(platformHandler)); - } - - public static void MapWebChromeClient(IWebViewHandler handler, IWebView webView) - { - if (handler is WebViewHandler platformHandler) - handler.PlatformView.SetWebChromeClient(new MauiWebChromeClient(platformHandler)); - } - public static void MapWebViewSettings(IWebViewHandler handler, IWebView webView) { handler.PlatformView.UpdateSettings(webView, true, true); @@ -167,8 +200,7 @@ protected internal bool NavigatingCanceled(string? url) static void ProcessSourceWhenReady(IWebViewHandler handler, IWebView webView) { - //We want to load the source after making sure the mapper for webclients - //and settings were called already + //We want to load the source after making sure the mapper for settings was called already var platformHandler = handler as WebViewHandler; if (platformHandler == null || platformHandler._firstRun) return; diff --git a/src/Core/src/Handlers/WebView/WebViewHandler.cs b/src/Core/src/Handlers/WebView/WebViewHandler.cs index 00901214e0f2..9ea5c6ea41b0 100644 --- a/src/Core/src/Handlers/WebView/WebViewHandler.cs +++ b/src/Core/src/Handlers/WebView/WebViewHandler.cs @@ -29,8 +29,6 @@ public partial class WebViewHandler : IWebViewHandler [nameof(IView.Background)] = MapBackground, #endif #if __ANDROID__ - [nameof(WebViewClient)] = MapWebViewClient, - [nameof(WebChromeClient)] = MapWebChromeClient, [nameof(WebView.Settings)] = MapWebViewSettings #elif __IOS__ || MACCATALYST [nameof(IWebView.FlowDirection)] = MapFlowDirection, diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index 5a9c15041b88..fbd3db15af5e 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -284,6 +284,8 @@ static Microsoft.Maui.Platform.TimePickerExtensions.UpdateTime(this Microsoft.Ma static Microsoft.Maui.SafeAreaEdges.Container.get -> Microsoft.Maui.SafeAreaEdges virtual Microsoft.Maui.Handlers.DatePickerHandler2.CreateDatePickerDialog(int year, int month, int day) -> Google.Android.Material.DatePicker.MaterialDatePicker? virtual Microsoft.Maui.Handlers.TimePickerHandler2.CreateTimePickerDialog(int hour, int minute) -> Google.Android.Material.TimePicker.MaterialTimePicker? +*REMOVED*static Microsoft.Maui.Handlers.WebViewHandler.MapWebChromeClient(Microsoft.Maui.Handlers.IWebViewHandler! handler, Microsoft.Maui.IWebView! webView) -> void +*REMOVED*static Microsoft.Maui.Handlers.WebViewHandler.MapWebViewClient(Microsoft.Maui.Handlers.IWebViewHandler! handler, Microsoft.Maui.IWebView! webView) -> void Microsoft.Maui.ITab Microsoft.Maui.ITab.Icon.get -> Microsoft.Maui.IImageSource? Microsoft.Maui.ITab.IsEnabled.get -> bool