Skip to content

Commit

Permalink
[release/9.0.1xx-rc2] Move HybridWebView platform code to handlers (a…
Browse files Browse the repository at this point in the history
…ttempt 2) (#25041)

* Move HybridWebView platform code to handlers

And run everything through the mappers.

Fixes #24269

* Run Android tests

* `@android.webkit.JavascriptInterface` is missing!

By changing `HybridJavaScriptInterface` to a class, the JCW now says:

    @android.webkit.JavascriptInterface
    public void sendMessage (java.lang.String p0)

---------

Co-authored-by: Eilon Lipton <[email protected]>
Co-authored-by: Jonathan Peppers <[email protected]>
  • Loading branch information
3 people authored Oct 2, 2024
1 parent 673d90c commit 632fca6
Show file tree
Hide file tree
Showing 28 changed files with 379 additions and 221 deletions.
160 changes: 15 additions & 145 deletions src/Controls/src/Core/HybridWebView/HybridWebView.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Maui.Devices;
using System.Collections.Generic;

#if WINDOWS || ANDROID || IOS || MACCATALYST
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
#endif
using System.Threading.Tasks;

namespace Microsoft.Maui.Controls
{
Expand Down Expand Up @@ -39,37 +33,9 @@ public string? HybridRoot
set { SetValue(HybridRootProperty, value); }
}

void IHybridWebView.MessageReceived(string rawMessage)
void IHybridWebView.RawMessageReceived(string rawMessage)
{
if (string.IsNullOrEmpty(rawMessage))
{
throw new ArgumentException($"The raw message cannot be null or empty.", nameof(rawMessage));
}
var indexOfPipe = rawMessage.IndexOf("|", StringComparison.Ordinal);
if (indexOfPipe == -1)
{
throw new ArgumentException($"The raw message must contain a pipe character ('|').", nameof(rawMessage));
}

var messageType = rawMessage.Substring(0, indexOfPipe);
var messageContent = rawMessage.Substring(indexOfPipe + 1);

switch (messageType)
{
case "InvokeMethodCompleted":
{
var sections = messageContent.Split('|');
var taskId = sections[0];
var result = sections[1];
AsyncTaskCompleted(taskId, result);
}
break;
case "RawMessage":
RawMessageReceived?.Invoke(this, new HybridWebViewRawMessageReceivedEventArgs(messageContent));
break;
default:
throw new ArgumentException($"The message type '{messageType}' is not recognized.", nameof(rawMessage));
}
RawMessageReceived?.Invoke(this, new HybridWebViewRawMessageReceivedEventArgs(rawMessage));
}

/// <summary>
Expand All @@ -91,36 +57,12 @@ public void SendRawMessage(string rawMessage)
});
}

private int _invokeTaskId;
private Dictionary<string, TaskCompletionSource<string>> asyncTaskCallbacks = new Dictionary<string, TaskCompletionSource<string>>();

/// <summary>
/// Handler for when the an Async JavaScript task has completed and needs to notify .NET.
/// </summary>
private void AsyncTaskCompleted(string taskId, string result)
{
//Look for the callback in the list of pending callbacks.
if (!string.IsNullOrEmpty(taskId) && asyncTaskCallbacks.ContainsKey(taskId))
{
//Get the callback and remove it from the list.
var callback = asyncTaskCallbacks[taskId];
callback.SetResult(result);

//Remove the callback.
asyncTaskCallbacks.Remove(taskId);
}
}

/// <summary>
/// Invokes a JavaScript method named <paramref name="methodName"/> and optionally passes in the parameter values
/// specified by <paramref name="paramValues"/>.
/// </summary>
/// <param name="methodName">The name of the JavaScript method to invoke.</param>
/// <param name="paramValues">Optional array of objects to be passed to the JavaScript method.</param>
/// <param name="paramJsonTypeInfos">Optional array of metadata about serializing the types of the parameters specified by <paramref name="paramValues"/>.</param>
/// <returns>A string containing the return value of the called method.</returns>
#if WINDOWS || ANDROID || IOS || MACCATALYST
public async Task<string?> InvokeJavaScriptAsync(string methodName, object?[]? paramValues, JsonTypeInfo?[]? paramJsonTypeInfos = null)
/// <inheritdoc/>
public async Task<TReturnType?> InvokeJavaScriptAsync<TReturnType>(
string methodName,
JsonTypeInfo<TReturnType> returnTypeJsonTypeInfo,
object?[]? paramValues = null,
JsonTypeInfo?[]? paramJsonTypeInfos = null)
{
if (string.IsNullOrEmpty(methodName))
{
Expand All @@ -139,57 +81,16 @@ private void AsyncTaskCompleted(string taskId, string result)
throw new ArgumentException($"The number of parameter values does not match the number of parameter JSON type infos.", nameof(paramValues));
}

// Create a callback for async JavaScript methods to invoke when they are done
var callback = new TaskCompletionSource<string>();
var currentInvokeTaskId = $"{_invokeTaskId++}";
asyncTaskCallbacks.Add(currentInvokeTaskId, callback);

var paramsValuesStringArray =
paramValues == null
? string.Empty
: string.Join(
", ",
paramValues.Select((v, i) => (v == null ? "null" : JsonSerializer.Serialize(v, paramJsonTypeInfos![i]!))));
var invokeResult = await Handler?.InvokeAsync(
nameof(IHybridWebView.InvokeJavaScriptAsync),
new HybridWebViewInvokeJavaScriptRequest(methodName, returnTypeJsonTypeInfo, paramValues, paramJsonTypeInfos))!;

await EvaluateJavaScriptAsync($"window.HybridWebView.InvokeMethod({currentInvokeTaskId}, {methodName}, [{paramsValuesStringArray}])");

return await callback.Task;
}
#else
public Task<string?> InvokeJavaScriptAsync(string methodName, object?[]? paramValues = null, object?[]? paramJsonTypeInfos = null)
{
_invokeTaskId++; // This is to avoid the compiler warning about the field not being used
throw new NotImplementedException();
}
#endif

/// <summary>
/// Invokes a JavaScript method named <paramref name="methodName"/> and optionally passes in the parameter values specified
/// by <paramref name="paramValues"/> by JSON-encoding each one.
/// </summary>
/// <typeparam name="TReturnType">The type of the return value to deserialize from JSON.</typeparam>
/// <param name="methodName">The name of the JavaScript method to invoke.</param>
/// <param name="returnTypeJsonTypeInfo">Metadata about deserializing the type of the return value specified by <typeparamref name="TReturnType"/>.</param>
/// <param name="paramValues">Optional array of objects to be passed to the JavaScript method by JSON-encoding each one.</param>
/// <param name="paramJsonTypeInfos">Optional array of metadata about serializing the types of the parameters specified by <paramref name="paramValues"/>.</param>
/// <returns>An object of type <typeparamref name="TReturnType"/> containing the return value of the called method.</returns>
#if WINDOWS || ANDROID || IOS || MACCATALYST
public async Task<TReturnType?> InvokeJavaScriptAsync<TReturnType>(string methodName, JsonTypeInfo<TReturnType?> returnTypeJsonTypeInfo, object?[]? paramValues = null, JsonTypeInfo?[]? paramJsonTypeInfos = null)
{
var stringResult = await InvokeJavaScriptAsync(methodName, paramValues, paramJsonTypeInfos);

if (stringResult is null)
if (invokeResult is null)
{
return default;
}
return JsonSerializer.Deserialize<TReturnType?>(stringResult, returnTypeJsonTypeInfo);
}
#else
public Task<TReturnType?> InvokeJavaScriptAsync<TReturnType>(string methodName, object returnTypeJsonTypeInfo, object?[]? paramValues, object?[]? paramJsonTypeInfos)
{
throw new NotImplementedException();
return (TReturnType)invokeResult;
}
#endif

/// <inheritdoc/>
public async Task<string?> EvaluateJavaScriptAsync(string script)
Expand All @@ -199,40 +100,9 @@ private void AsyncTaskCompleted(string taskId, string result)
return null;
}

// Make all the platforms mimic Android's implementation, which is by far the most complete.
if (DeviceInfo.Platform != DevicePlatform.Android)
{
script = WebView.EscapeJsString(script);

if (DeviceInfo.Platform != DevicePlatform.WinUI)
{
// Use JSON.stringify() method to converts a JavaScript value to a JSON string
script = "try{JSON.stringify(eval('" + script + "'))}catch(e){'null'};";
}
else
{
script = "try{eval('" + script + "')}catch(e){'null'};";
}
}

string? result;

// Use the handler command to evaluate the JS
result = await Handler!.InvokeAsync(nameof(IHybridWebView.EvaluateJavaScriptAsync),
var result = await Handler!.InvokeAsync(nameof(IHybridWebView.EvaluateJavaScriptAsync),
new EvaluateJavaScriptAsyncRequest(script));

//if the js function errored or returned null/undefined treat it as null
if (result == "null")
{
result = null;
}
//JSON.stringify wraps the result in literal quotes, we just want the actual returned result
//note that if the js function returns the string "null" we will get here and not above
else if (result != null)
{
result = result.Trim('"');
}

return result;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) ->
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType?>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,7 @@ Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) ->
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType?>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,7 @@ Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) ->
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType?>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) ->
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, object?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, object! returnTypeJsonTypeInfo, object?[]? paramValues, object?[]? paramJsonTypeInfos) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) ->
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType?>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
3 changes: 1 addition & 2 deletions src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) ->
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, object?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, object! returnTypeJsonTypeInfo, object?[]? paramValues, object?[]? paramJsonTypeInfos) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ Microsoft.Maui.Controls.HybridWebView.EvaluateJavaScriptAsync(string! script) ->
Microsoft.Maui.Controls.HybridWebView.HybridRoot.get -> string?
Microsoft.Maui.Controls.HybridWebView.HybridRoot.set -> void
Microsoft.Maui.Controls.HybridWebView.HybridWebView() -> void
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync(string! methodName, object?[]? paramValues = null, object?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<string?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, object! returnTypeJsonTypeInfo, object?[]? paramValues, object?[]? paramJsonTypeInfos) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.InvokeJavaScriptAsync<TReturnType>(string! methodName, System.Text.Json.Serialization.Metadata.JsonTypeInfo<TReturnType>! returnTypeJsonTypeInfo, object?[]? paramValues = null, System.Text.Json.Serialization.Metadata.JsonTypeInfo?[]? paramJsonTypeInfos = null) -> System.Threading.Tasks.Task<TReturnType?>!
Microsoft.Maui.Controls.HybridWebView.RawMessageReceived -> System.EventHandler<Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs!>?
Microsoft.Maui.Controls.HybridWebView.SendRawMessage(string! rawMessage) -> void
Microsoft.Maui.Controls.HybridWebViewRawMessageReceivedEventArgs
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/WebView/WebView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ public IPlatformElementConfiguration<T, WebView> On<T>() where T : IConfigPlatfo
return _platformConfigurationRegistry.Value.On<T>();
}

internal static string EscapeJsString(string js)
private static string EscapeJsString(string js)
{
if (js == null)
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#if !ANDROID
using System;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
Expand Down Expand Up @@ -356,4 +355,3 @@ private async Task WaitForHybridWebViewLoaded(HybridWebView hybridWebView)
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import androidx.annotation.NonNull;

public interface HybridJavaScriptInterface {
void sendMessage(@NonNull String message);
public abstract class HybridJavaScriptInterface {
@android.webkit.JavascriptInterface
public abstract void sendMessage(@NonNull String message);
}
1 change: 1 addition & 0 deletions src/Core/src/Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<ProjectReference Include="..\..\Graphics\src\Graphics\Graphics.csproj" />
<PackageReference Include="System.Numerics.Vectors" Condition="$(TargetFramework.StartsWith('netstandard'))" />
<PackageReference Include="System.Text.Json" Condition="$(TargetFramework.StartsWith('netstandard'))" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.Contains('-windows'))">
<PackageReference Include="Microsoft.WindowsAppSDK" />
Expand Down
Loading

0 comments on commit 632fca6

Please sign in to comment.