diff --git a/src/BlazorWebView/src/Maui/Tizen/BlazorWebViewHandler.Tizen.cs b/src/BlazorWebView/src/Maui/Tizen/BlazorWebViewHandler.Tizen.cs new file mode 100644 index 000000000000..1edb048f9602 --- /dev/null +++ b/src/BlazorWebView/src/Maui/Tizen/BlazorWebViewHandler.Tizen.cs @@ -0,0 +1,144 @@ +using System; +using System.IO; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Maui.Handlers; +using TChromium = Tizen.WebView.Chromium; +using TWebView = Tizen.WebView.WebView; + +namespace Microsoft.AspNetCore.Components.WebView.Maui +{ + public partial class BlazorWebViewHandler : ViewHandler + { + private const string AppOrigin = "app://0.0.0.0/"; + private const string BlazorInitScript = @" + window.__receiveMessageCallbacks = []; + window.__dispatchMessageCallback = function(message) { + window.__receiveMessageCallbacks.forEach(function(callback) { callback(message); }); + }; + window.external = { + sendMessage: function(message) { + window.webkit.messageHandlers.webwindowinterop.postMessage(message); + }, + receiveMessage: function(callback) { + window.__receiveMessageCallbacks.push(callback); + } + }; + + Blazor.start(); + + (function () { + window.onpageshow = function(event) { + if (event.persisted) { + window.location.reload(); + } + }; + })(); + "; + + private TizenWebViewManager? _webviewManager; + private WebViewExtension.InterceptRequestCallback? _interceptRequestCallback; + + private TWebView NativeWebView => NativeView.WebView; + + private bool RequiredStartupPropertiesSet => + //_webview != null && + HostPage != null && + Services != null; + + protected override WebViewContainer CreateNativeView() + { + TChromium.Initialize(); + Context!.CurrentApplication!.Terminated += (s, e) => TChromium.Shutdown(); + return new WebViewContainer(Context!.NativeParent); + } + + protected override void ConnectHandler(WebViewContainer nativeView) + { + _interceptRequestCallback = OnRequestInterceptCallback; + NativeWebView.LoadStarted += OnLoadStarted; + NativeWebView.LoadFinished += OnLoadFinished; + //NativeWebView.AddJavaScriptMessageHandler("BlazorHandler", PostMessageFromJS); + NativeWebView.SetInterceptRequestCallback(_interceptRequestCallback); + NativeWebView.GetSettings().JavaScriptEnabled = true; + + } + + protected override void DisconnectHandler(WebViewContainer nativeView) + { + base.DisconnectHandler(nativeView); + } + + private void StartWebViewCoreIfPossible() + { + if (!RequiredStartupPropertiesSet || + _webviewManager != null) + { + return; + } + if (NativeView == null) + { + throw new InvalidOperationException($"Can't start {nameof(BlazorWebView)} without native web view instance."); + } + + var assetConfig = Services!.GetRequiredService()!; + + // We assume the host page is always in the root of the content directory, because it's + // unclear there's any other use case. We can add more options later if so. + var contentRootDir = Path.GetDirectoryName(HostPage!) ?? string.Empty; + var hostPageRelativePath = Path.GetRelativePath(contentRootDir, HostPage!); + + var fileProvider = new ManifestEmbeddedFileProvider(assetConfig.AssetsAssembly, root: contentRootDir); + + _webviewManager = new TizenWebViewManager(this, NativeWebView, Services!, MauiDispatcher.Instance, fileProvider, hostPageRelativePath); + if (RootComponents != null) + { + foreach (var rootComponent in RootComponents) + { + // Since the page isn't loaded yet, this will always complete synchronously + _ = rootComponent.AddToWebViewManagerAsync(_webviewManager); + } + } + _webviewManager.Navigate("/"); + } + + private void OnRequestInterceptCallback(IntPtr context, IntPtr request, IntPtr userdata) + { + if (request == IntPtr.Zero) + { + throw new ArgumentNullException(nameof(request)); + } + + var url = NativeWebView.GetInterceptRequestUrl(request); + var urlScheme = url.Substring(0, url.IndexOf(':')); + + if (urlScheme == "app") + { + var allowFallbackOnHostPage = url.EndsWith("/"); + if (_webviewManager!.TryGetResponseContentInternal(url, allowFallbackOnHostPage, out var statusCode, out var statusMessage, out var content, out var headers)) + { + var contentType = headers["Content-Type"]; + var header = $"HTTP/1.0 200 OK\r\nContent-Type:{contentType}; charset=utf-8\r\nCache-Control:no-cache, max-age=0, must-revalidate, no-store\r\n\r\n"; + var body = new StreamReader(content).ReadToEnd(); + NativeWebView.SetInterceptRequestResponse(request, header, body, (uint)body.Length); + return; + } + } + + NativeWebView.IgnoreInterceptRequest(request); + } + + private void OnLoadStarted(object? sender, EventArgs e) + { + } + + private void OnLoadFinished(object? sender, EventArgs e) + { + NativeWebView.SetFocus(true); + + var url = NativeWebView.Url; + if (url == AppOrigin) + NativeWebView.Eval(BlazorInitScript); + } + } +} diff --git a/src/BlazorWebView/src/Maui/Tizen/TizenWebViewManager.cs b/src/BlazorWebView/src/Maui/Tizen/TizenWebViewManager.cs new file mode 100644 index 000000000000..a61fd57c4fe6 --- /dev/null +++ b/src/BlazorWebView/src/Maui/Tizen/TizenWebViewManager.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Extensions.FileProviders; +using TWebView = Tizen.WebView.WebView; + +namespace Microsoft.AspNetCore.Components.WebView.Maui +{ + public class TizenWebViewManager : WebViewManager + { + private const string AppOrigin = "app://0.0.0.0/"; + + private readonly BlazorWebViewHandler _blazorMauiWebViewHandler; + private readonly TWebView _webview; + + public TizenWebViewManager(BlazorWebViewHandler blazorMauiWebViewHandler, TWebView webview, IServiceProvider services, Dispatcher dispatcher, IFileProvider fileProvider, string hostPageRelativePath) + : base(services, dispatcher, new Uri(AppOrigin), fileProvider, hostPageRelativePath) + { + _blazorMauiWebViewHandler = blazorMauiWebViewHandler ?? throw new ArgumentNullException(nameof(blazorMauiWebViewHandler)); + _webview = webview ?? throw new ArgumentNullException(nameof(webview)); + + } + + internal bool TryGetResponseContentInternal(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out IDictionary headers) => + TryGetResponseContent(uri, allowFallbackOnHostPage, out statusCode, out statusMessage, out content, out headers); + + /// + protected override void NavigateCore(Uri absoluteUri) + { + } + + /// + protected override void SendMessage(string message) + { + } + } +} diff --git a/src/BlazorWebView/src/Maui/Tizen/WebViewContainer.cs b/src/BlazorWebView/src/Maui/Tizen/WebViewContainer.cs new file mode 100644 index 000000000000..61b39d78fec0 --- /dev/null +++ b/src/BlazorWebView/src/Maui/Tizen/WebViewContainer.cs @@ -0,0 +1,31 @@ +using System; +using Tizen.UIExtensions.ElmSharp; +using ElmSharp; +using TWebView = Tizen.WebView.WebView; + +namespace Microsoft.AspNetCore.Components.WebView.Maui +{ + public class WebViewContainer : WidgetLayout + { + public TWebView WebView { get; } + + public WebViewContainer(EvasObject parent) : base(parent) + { + WebView = new TWebView(parent); + SetContent(WebView); + AllowFocus(true); + Focused += OnFocused; + Unfocused += OnUnfocused; + } + + void OnFocused(object? sender, EventArgs e) + { + WebView.SetFocus(true); + } + + void OnUnfocused(object? sender, EventArgs e) + { + WebView.SetFocus(false); + } + } +} diff --git a/src/BlazorWebView/src/Maui/Tizen/WebViewExtension.cs b/src/BlazorWebView/src/Maui/Tizen/WebViewExtension.cs new file mode 100644 index 000000000000..672b21282ed4 --- /dev/null +++ b/src/BlazorWebView/src/Maui/Tizen/WebViewExtension.cs @@ -0,0 +1,69 @@ +using System; +using System.Runtime.InteropServices; +using TWebView = Tizen.WebView.WebView; + +namespace Microsoft.AspNetCore.Components.WebView.Maui +{ + public static class WebViewExtension + { + public const string ChromiumEwk = "libchromium-ewk.so"; + + public static void SetInterceptRequestCallback(this TWebView webView, InterceptRequestCallback callback) + { + var context = webView.GetContext(); + var handleField = context.GetType().GetField("_handle", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var contextHandle = (IntPtr?)handleField?.GetValue(context); + if (contextHandle != null) + ewk_context_intercept_request_callback_set(contextHandle.Value, callback, IntPtr.Zero); + } + +#pragma warning disable IDE0060 // Remove unused parameter + public static bool SetInterceptRequestResponse(this TWebView webView, IntPtr request, string header, string body, uint length) +#pragma warning restore IDE0060 // Remove unused parameter + { + return ewk_intercept_request_response_set(request, header, body, length); + } + +#pragma warning disable IDE0060 // Remove unused parameter + public static bool IgnoreInterceptRequest(this TWebView webView, IntPtr request) +#pragma warning restore IDE0060 // Remove unused parameter + { + return ewk_intercept_request_ignore(request); + } + +#pragma warning disable IDE0060 // Remove unused parameter + public static string GetInterceptRequestUrl(this TWebView webView, IntPtr request) +#pragma warning restore IDE0060 // Remove unused parameter + { + return Marshal.PtrToStringAnsi(_ewk_intercept_request_url_get(request)) ?? string.Empty; + } + + [DllImport(ChromiumEwk)] + internal static extern IntPtr ewk_view_context_get(IntPtr obj); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void InterceptRequestCallback(IntPtr context, IntPtr request, IntPtr userData); + + [DllImport(ChromiumEwk)] + internal static extern void ewk_context_intercept_request_callback_set(IntPtr context, InterceptRequestCallback callback, IntPtr userData); + + [DllImport(ChromiumEwk, EntryPoint = "ewk_intercept_request_url_get")] + internal static extern IntPtr _ewk_intercept_request_url_get(IntPtr request); + + [DllImport(ChromiumEwk, EntryPoint = "ewk_intercept_request_http_method_get")] + internal static extern IntPtr _ewk_intercept_request_http_method_get(IntPtr request); + + internal static string ewk_intercept_request_http_method_get(IntPtr request) + { + return Marshal.PtrToStringAnsi(_ewk_intercept_request_http_method_get(request)) ?? string.Empty; + } + + [DllImport(ChromiumEwk)] + internal static extern bool ewk_intercept_request_ignore(IntPtr request); + +#pragma warning disable CA2101 // Specify marshaling for P/Invoke string arguments + [DllImport(ChromiumEwk)] +#pragma warning restore CA2101 // Specify marshaling for P/Invoke string arguments + internal static extern bool ewk_intercept_request_response_set(IntPtr request, string header, string body, uint length); + } +}