/** * Copyright (c) 2021 Vuplex Inc. All rights reserved. * * Licensed under the Vuplex Commercial Software Library License, you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * https://vuplex.com/commercial-library-license * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Only define BaseWebView.cs on supported platforms to avoid IL2CPP linking // errors on unsupported platforms. #if UNITY_EDITOR || UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX || UNITY_ANDROID || (UNITY_IOS && !VUPLEX_OMIT_IOS) || UNITY_WSA ||UNITY_WEBGL using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using UnityEngine; #if NET_4_6 || NET_STANDARD_2_0 using System.Threading.Tasks; #endif namespace Vuplex.WebView { /// /// The base `IWebView` implementation, which is extended for each platform. /// public abstract class BaseWebView : MonoBehaviour { public event EventHandler CloseRequested; public event EventHandler ConsoleMessageLogged { add { _consoleMessageLogged += value; if (_consoleMessageLogged.GetInvocationList().Length == 1) { _setConsoleMessageEventsEnabled(true); } } remove { _consoleMessageLogged -= value; if (_consoleMessageLogged.GetInvocationList().Length == 0) { _setConsoleMessageEventsEnabled(false); } } } public event EventHandler FocusedInputFieldChanged { add { _focusedInputFieldChanged += value; if (_focusedInputFieldChanged.GetInvocationList().Length == 1) { _setFocusedInputFieldEventsEnabled(true); } } remove { _focusedInputFieldChanged -= value; if (_focusedInputFieldChanged.GetInvocationList().Length == 0) { _setFocusedInputFieldEventsEnabled(false); } } } public event EventHandler LoadProgressChanged; public event EventHandler> MessageEmitted; public event EventHandler PageLoadFailed; public event EventHandler> TitleChanged; public event EventHandler UrlChanged; public event EventHandler> VideoRectChanged; public bool IsDisposed { get; protected set; } public bool IsInitialized { get; private set; } public List PageLoadScripts { get { return _pageLoadScripts; } } public float Resolution { get { return _numberOfPixelsPerUnityUnit; } } public Vector2 Size { get { return new Vector2(_width, _height); } } public virtual Vector2 SizeInPixels { get { return new Vector2(_nativeWidth, _nativeHeight); } } public Texture2D Texture { get { return _viewportTexture; } } public string Url { get; private set; } public Texture2D VideoTexture { get { return _videoTexture; } } public void Init(Texture2D texture, float width, float height) { Init(texture, width, height, null); } public virtual void Init(Texture2D viewportTexture, float width, float height, Texture2D videoTexture) { if (IsInitialized) { throw new InvalidOperationException("Init() cannot be called on a webview that has already been initialized."); } _viewportTexture = viewportTexture; _videoTexture = videoTexture; // Assign the game object a unique name so that the native view can send it messages. gameObject.name = String.Format("WebView-{0}", Guid.NewGuid().ToString()); _width = width; _height = height; Utils.ThrowExceptionIfAbnormallyLarge(_nativeWidth, _nativeHeight); IsInitialized = true; // Prevent the script from automatically being destroyed when a new scene is loaded. DontDestroyOnLoad(gameObject); } public virtual void Blur() { _assertValidState(); WebView_blur(_nativeWebViewPtr); } #if NET_4_6 || NET_STANDARD_2_0 public Task CanGoBack() { var task = new TaskCompletionSource(); CanGoBack(task.SetResult); return task.Task; } public Task CanGoForward() { var task = new TaskCompletionSource(); CanGoForward(task.SetResult); return task.Task; } #endif public virtual void CanGoBack(Action callback) { _assertValidState(); _pendingCanGoBackCallbacks.Add(callback); WebView_canGoBack(_nativeWebViewPtr); } public virtual void CanGoForward(Action callback) { _assertValidState(); _pendingCanGoForwardCallbacks.Add(callback); WebView_canGoForward(_nativeWebViewPtr); } #if NET_4_6 || NET_STANDARD_2_0 public Task CaptureScreenshot() { var task = new TaskCompletionSource(); CaptureScreenshot(task.SetResult); return task.Task; } #endif public virtual void CaptureScreenshot(Action callback) { var bytes = new byte[0]; try { var texture = _getReadableTexture(); bytes = ImageConversion.EncodeToPNG(texture); Destroy(texture); } catch (Exception e) { WebViewLogger.LogError("An exception occurred while capturing the screenshot: " + e); } callback(bytes); } public virtual void Click(Vector2 point) { _assertValidState(); int nativeX = (int) (point.x * _nativeWidth); int nativeY = (int) (point.y * _nativeHeight); WebView_click(_nativeWebViewPtr, nativeX, nativeY); } public virtual void Click(Vector2 point, bool preventStealingFocus) { // On most platforms, the regular `Click()` method // doesn't steal focus. Click(point); } public static void ClearAllData() { WebView_clearAllData(); } public static void CreateTexture(float width, float height, Action callback) { int nativeWidth = (int)(width * Config.NumberOfPixelsPerUnityUnit); int nativeHeight = (int)(height * Config.NumberOfPixelsPerUnityUnit); Utils.ThrowExceptionIfAbnormallyLarge(nativeWidth, nativeHeight); var texture = new Texture2D( nativeWidth, nativeHeight, TextureFormat.RGBA32, false, false ); #if UNITY_2020_2_OR_NEWER // In Unity 2020.2, Unity's internal TexturesD3D11.cpp class on Windows logs an error if // UpdateExternalTexture() is called on a Texture2D created from the constructor // rather than from Texture2D.CreateExternalTexture(). So, rather than returning // the original Texture2D created via the constructor, we return a copy created // via CreateExternalTexture(). This approach is only used for 2020.2 and newer because // it doesn't work in 2018.4 and instead causes a crash. texture = Texture2D.CreateExternalTexture( nativeWidth, nativeHeight, TextureFormat.RGBA32, false, false, texture.GetNativeTexturePtr() ); #endif // Invoke the callback asynchronously in order to match the async // behavior that's required for Android. Dispatcher.RunOnMainThread(() => callback(texture)); } public virtual void Copy() { _assertValidState(); _getSelectedText(text => GUIUtility.systemCopyBuffer = text); } public virtual void Cut() { _assertValidState(); _getSelectedText(text => { GUIUtility.systemCopyBuffer = text; HandleKeyboardInput("Backspace"); }); } public virtual void DisableViewUpdates() { _assertValidState(); WebView_disableViewUpdates(_nativeWebViewPtr); _viewUpdatesAreEnabled = false; } public virtual void Dispose() { _assertValidState(); IsDisposed = true; WebView_destroy(_nativeWebViewPtr); _nativeWebViewPtr = IntPtr.Zero; // To avoid a MissingReferenceException, verify that this script // hasn't already been destroyed prior to accessing gameObject. if (this != null) { Destroy(gameObject); } } public virtual void EnableViewUpdates() { _assertValidState(); if (_currentViewportNativeTexture != IntPtr.Zero) { _viewportTexture.UpdateExternalTexture(_currentViewportNativeTexture); } WebView_enableViewUpdates(_nativeWebViewPtr); _viewUpdatesAreEnabled = true; } #if NET_4_6 || NET_STANDARD_2_0 public Task ExecuteJavaScript(string javaScript) { var task = new TaskCompletionSource(); ExecuteJavaScript(javaScript, task.SetResult); return task.Task; } #else public void ExecuteJavaScript(string javaScript) { ExecuteJavaScript(javaScript, null); } #endif public virtual void ExecuteJavaScript(string javaScript, Action callback) { _assertValidState(); string resultCallbackId = null; if (callback != null) { resultCallbackId = Guid.NewGuid().ToString(); _pendingJavaScriptResultCallbacks[resultCallbackId] = callback; } WebView_executeJavaScript(_nativeWebViewPtr, javaScript, resultCallbackId); } public virtual void Focus() { _assertValidState(); WebView_focus(_nativeWebViewPtr); } #if NET_4_6 || NET_STANDARD_2_0 public Task GetRawTextureData() { var task = new TaskCompletionSource(); GetRawTextureData(task.SetResult); return task.Task; } #endif public virtual void GetRawTextureData(Action callback) { var bytes = new byte[0]; try { var texture = _getReadableTexture(); bytes = texture.GetRawTextureData(); Destroy(texture); } catch (Exception e) { WebViewLogger.LogError("An exception occurred while getting the raw texture data: " + e); } callback(bytes); } public static void GloballySetUserAgent(bool mobile) { var success = WebView_globallySetUserAgentToMobile(mobile); if (!success) { throw new InvalidOperationException(USER_AGENT_EXCEPTION_MESSAGE); } } public static void GloballySetUserAgent(string userAgent) { var success = WebView_globallySetUserAgent(userAgent); if (!success) { throw new InvalidOperationException(USER_AGENT_EXCEPTION_MESSAGE); } } public virtual void GoBack() { _assertValidState(); WebView_goBack(_nativeWebViewPtr); } public virtual void GoForward() { _assertValidState(); WebView_goForward(_nativeWebViewPtr); } public virtual void HandleKeyboardInput(string input) { _assertValidState(); WebView_handleKeyboardInput(_nativeWebViewPtr, input); } public virtual void LoadHtml(string html) { _assertValidState(); WebView_loadHtml(_nativeWebViewPtr, html); } public virtual void LoadUrl(string url) { _assertValidState(); WebView_loadUrl(_nativeWebViewPtr, _transformStreamingAssetsUrlIfNeeded(url)); } public virtual void LoadUrl(string url, Dictionary additionalHttpHeaders) { _assertValidState(); if (additionalHttpHeaders == null) { LoadUrl(url); } else { var headerStrings = additionalHttpHeaders.Keys.Select(key => String.Format("{0}: {1}", key, additionalHttpHeaders[key])).ToArray(); var newlineDelimitedHttpHeaders = String.Join("\n", headerStrings); WebView_loadUrlWithHeaders(_nativeWebViewPtr, url, newlineDelimitedHttpHeaders); } } public virtual void Paste() { _assertValidState(); var text = GUIUtility.systemCopyBuffer; foreach (var character in text) { HandleKeyboardInput(char.ToString(character)); } } public void PostMessage(string data) { var escapedString = data.Replace("'", "\\'").Replace("\n", "\\n"); ExecuteJavaScript(String.Format("vuplex._emit('message', {{ data: \'{0}\' }})", escapedString)); } public virtual void Reload() { _assertValidState(); WebView_reload(_nativeWebViewPtr); } public virtual void Resize(float width, float height) { if (IsDisposed || (width == _width && height == _height)) { return; } _width = width; _height = height; _resize(); } public virtual void Scroll(Vector2 delta) { _assertValidState(); var deltaX = (int)(delta.x * _numberOfPixelsPerUnityUnit); var deltaY = (int)(delta.y * _numberOfPixelsPerUnityUnit); WebView_scroll(_nativeWebViewPtr, deltaX, deltaY); } public virtual void Scroll(Vector2 scrollDelta, Vector2 point) { _assertValidState(); var deltaX = (int)(scrollDelta.x * _numberOfPixelsPerUnityUnit); var deltaY = (int)(scrollDelta.y * _numberOfPixelsPerUnityUnit); var pointerX = (int)(point.x * _nativeWidth); var pointerY = (int)(point.y * _nativeHeight); WebView_scrollAtPoint(_nativeWebViewPtr, deltaX, deltaY, pointerX, pointerY); } public virtual void SelectAll() { _assertValidState(); // If the focused element is an input with a select() method, then use that. // Otherwise, travel up the DOM until we get to the body or a contenteditable // element, and then select its contents. ExecuteJavaScript( @"(function() { var element = document.activeElement || document.body; while (!(element === document.body || element.getAttribute('contenteditable') === 'true')) { if (typeof element.select === 'function') { element.select(); return; } element = element.parentElement; } var range = document.createRange(); range.selectNodeContents(element); var selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); })();", null ); } public void SetResolution(float pixelsPerUnityUnit) { // Note: this method can be called prior to initialization. _numberOfPixelsPerUnityUnit = pixelsPerUnityUnit; _resize(); } public static void SetStorageEnabled(bool enabled) { WebView_setStorageEnabled(enabled); } public virtual void ZoomIn() { _assertValidState(); WebView_zoomIn(_nativeWebViewPtr); } public virtual void ZoomOut() { _assertValidState(); WebView_zoomOut(_nativeWebViewPtr); } EventHandler _consoleMessageLogged; protected IntPtr _currentViewportNativeTexture; #if (UNITY_STANDALONE_WIN && !UNITY_EDITOR) || UNITY_EDITOR_WIN ||UNITY_WEBGL protected const string _dllName = "VuplexWebViewWindows"; #elif (UNITY_STANDALONE_OSX && !UNITY_EDITOR) || UNITY_EDITOR_OSX protected const string _dllName = "VuplexWebViewMac"; #elif UNITY_WSA protected const string _dllName = "VuplexWebViewUwp"; #elif UNITY_ANDROID protected const string _dllName = "VuplexWebViewAndroid"; #else protected const string _dllName = "__Internal"; #endif EventHandler _focusedInputFieldChanged; FocusedInputFieldType _focusedInputFieldType = FocusedInputFieldType.None; protected float _height; // in Unity units Material _materialForBlitting; // Height in pixels. protected int _nativeHeight { get { // Height must be non-zero return Math.Max(1, (int)(_height * _numberOfPixelsPerUnityUnit)); } } protected IntPtr _nativeWebViewPtr = IntPtr.Zero; // Width in pixels. protected int _nativeWidth { get { // Width must be non-zero return Math.Max(1, (int)(_width * _numberOfPixelsPerUnityUnit)); } } protected float _numberOfPixelsPerUnityUnit = Config.NumberOfPixelsPerUnityUnit; List _pageLoadScripts = new List(); List> _pendingCanGoBackCallbacks = new List>(); List> _pendingCanGoForwardCallbacks = new List>(); protected Dictionary> _pendingJavaScriptResultCallbacks = new Dictionary>(); static readonly Regex STREAMING_ASSETS_URL_REGEX = new Regex(@"^streaming-assets:(//)?(.*)$", RegexOptions.IgnoreCase); const string USER_AGENT_EXCEPTION_MESSAGE = "Unable to set the User-Agent string, because a webview has already been created with the default User-Agent. On Windows and macOS, SetUserAgent() can only be called prior to creating any webviews."; Rect _videoRect = Rect.zero; protected Texture2D _videoTexture; protected bool _viewUpdatesAreEnabled = true; protected Texture2D _viewportTexture; protected float _width; // in Unity units protected void _assertValidState(){ if (!IsInitialized) { throw new InvalidOperationException("Methods cannot be called on an uninitialized webview. Please initialize it first with IWebView.Init()."); } if (IsDisposed) { throw new InvalidOperationException("Methods cannot be called on a disposed webview."); } } protected virtual Material _createMaterialForBlitting() { return Utils.CreateDefaultMaterial(); } Texture2D _getReadableTexture() { // https://support.unity3d.com/hc/en-us/articles/206486626-How-can-I-get-pixels-from-unreadable-textures- // Create a temporary RenderTexture of the same size as the texture RenderTexture tempRenderTexture = RenderTexture.GetTemporary( _nativeWidth, _nativeHeight, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Linear ); if (_materialForBlitting == null) { _materialForBlitting = _createMaterialForBlitting(); } // Use the version of Graphics.Blit() that accepts a material // so that any transformations needed are performed with the shader. Graphics.Blit(Texture,tempRenderTexture,_materialForBlitting); // Backup the currently set RenderTexture RenderTexture previousRenderTexture = RenderTexture.active; // Set the current RenderTexture to the temporary one we created RenderTexture.active = tempRenderTexture; // Create a new readable Texture2D to copy the pixels to it Texture2D readableTexture = new Texture2D(_nativeWidth, _nativeHeight); // Copy the pixels from the RenderTexture to the new Texture readableTexture.ReadPixels(new Rect(0, 0, tempRenderTexture.width, tempRenderTexture.height), 0, 0); readableTexture.Apply(); // Reset the active RenderTexture RenderTexture.active = previousRenderTexture; // Release the temporary RenderTexture RenderTexture.ReleaseTemporary(tempRenderTexture); return readableTexture; } void _getSelectedText(Action callback) { // window.getSelection() doesn't work on the content of