From e168aca42f27c0246529a69fe92345f8d996f7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Sch=C3=A4rer?= Date: Fri, 10 Oct 2025 01:40:16 +0200 Subject: [PATCH 01/11] ! F: Implement webview reset --- .../AirConsole/scripts/Runtime/AirConsole.cs | 315 +++++++++++++----- CHANGELOG.md | 5 + 2 files changed, 232 insertions(+), 88 deletions(-) diff --git a/Assets/AirConsole/scripts/Runtime/AirConsole.cs b/Assets/AirConsole/scripts/Runtime/AirConsole.cs index ad73084a..76f1017e 100644 --- a/Assets/AirConsole/scripts/Runtime/AirConsole.cs +++ b/Assets/AirConsole/scripts/Runtime/AirConsole.cs @@ -1302,25 +1302,25 @@ private void OnDeviceStateChange(JObject msg) { try { int deviceId = (int)msg["device_id"]; - AllocateDeviceSlots(deviceId); JToken deviceData = msg["device_data"]; - if (deviceData != null && deviceData.HasValues) { - _devices[deviceId] = deviceData; - } else { - _devices[deviceId] = null; - } + + // Queue all _devices modifications to run on the Unity main thread to avoid race conditions + eventQueue.Enqueue(delegate() { + AllocateDeviceSlots(deviceId); + if (deviceData != null && deviceData.HasValues) { + _devices[deviceId] = deviceData; + } else { + _devices[deviceId] = null; + } - if (onDeviceStateChange != null) { - eventQueue.Enqueue(delegate() { - if (onDeviceStateChange != null) { - onDeviceStateChange(deviceId, GetDevice(_device_id)); - } - }); - } + if (onDeviceStateChange != null) { + onDeviceStateChange(deviceId, GetDevice(_device_id)); + } - if (Settings.debug.info) { - AirConsoleLogger.Log(() => $"AirConsole: saved devicestate of {deviceId}"); - } + if (Settings.debug.info) { + AirConsoleLogger.Log(() => $"AirConsole: saved devicestate of {deviceId}"); + } + }); } catch (Exception e) { if (Settings.debug.error) { AirConsoleLogger.LogError(() => e.Message); @@ -1417,52 +1417,51 @@ private void OnMessage(JObject msg) { } private void OnReady(JObject msg) { - if (webViewLoadingCanvas) { - Destroy(webViewLoadingCanvas.gameObject); - } - - // parse server_time_offset - _server_time_offset = (int)msg["server_time_offset"]; - - // parse device_id - _device_id = (int)msg["device_id"]; - - // parse location - _location = (string)msg["location"]; - - if (msg["translations"] != null) { - _translations = new Dictionary(); - JObject translationObject = (JObject)msg["translations"]; - if (translationObject != null) { - foreach (KeyValuePair keyValue in translationObject) { - string value = (string)keyValue.Value; - value = value.Replace("\\n", "\n"); - value = value.Replace("<", "<"); - value = value.Replace(">", ">"); - _translations.Add(keyValue.Key, value); + // Queue all state modifications to run on the Unity main thread to avoid race conditions + eventQueue.Enqueue(delegate() { + if (webViewLoadingCanvas) { + Destroy(webViewLoadingCanvas.gameObject); + } + + // parse server_time_offset + _server_time_offset = (int)msg["server_time_offset"]; + + // parse device_id + _device_id = (int)msg["device_id"]; + + // parse location + _location = (string)msg["location"]; + + if (msg["translations"] != null) { + _translations = new Dictionary(); + JObject translationObject = (JObject)msg["translations"]; + if (translationObject != null) { + foreach (KeyValuePair keyValue in translationObject) { + string value = (string)keyValue.Value; + value = value.Replace("\\n", "\n"); + value = value.Replace("<", "<"); + value = value.Replace(">", ">"); + _translations.Add(keyValue.Key, value); + } } } - } - // load devices - _devices.Clear(); - foreach (JToken data in (JToken)msg["devices"]) { - JToken assign = data; - if (data != null && !data.HasValues) { - assign = null; - } + // load devices + _devices.Clear(); + foreach (JToken data in (JToken)msg["devices"]) { + JToken assign = data; + if (data != null && !data.HasValues) { + assign = null; + } - _devices.Add(assign); - } + _devices.Add(assign); + } - if (onReady != null) { - eventQueue.Enqueue(delegate() { - if (onReady != null) { - onReady((string)msg["code"]); - } - }); - } + if (onReady != null) { + onReady((string)msg["code"]); + } + }); } private void OnDeviceProfileChange(JObject msg) { @@ -1537,6 +1536,98 @@ private void OnAdComplete(JObject msg) { } } + private void ResetCaches() { + AirConsoleLogger.LogDevelopment(() => "Resetting AirConsole caches"); + + // Clear device and player data + _devices.Clear(); + _players.Clear(); + + // Reset state variables + _device_id = 0; + _server_time_offset = 0; + _location = null; + _translations = null; + + // Reset safe area + _safeAreaWasSet = false; + _lastSafeAreaParameters = null; + SafeArea = new Rect(0, 0, Screen.width, Screen.height); + + // Clear event queue + eventQueue.Clear(); + + AirConsoleLogger.LogDevelopment(() => "AirConsole caches reset complete"); + } + + private void CleanupWebSocketListener() { + if (wsListener == null) { + return; + } + + AirConsoleLogger.LogDevelopment(() => "Cleaning up WebSocket listener"); + + // Unsubscribe all event handlers to prevent stale events + if (IsAndroidOrEditor) { + wsListener.onLaunchApp -= OnLaunchApp; + wsListener.onUnityWebviewResize -= OnUnityWebviewResize; + wsListener.onUnityWebviewPlatformReady -= OnUnityWebviewPlatformReady; + wsListener.OnUpdateContentProvider -= OnUpdateContentProvider; + wsListener.OnPlatformReady -= HandlePlatformReady; + } + + wsListener.OnSetSafeArea -= OnSetSafeArea; + wsListener.onReady -= OnReady; + wsListener.onClose -= OnClose; + wsListener.onMessage -= OnMessage; + wsListener.onDeviceStateChange -= OnDeviceStateChange; + wsListener.onConnect -= OnConnect; + wsListener.onDisconnect -= OnDisconnect; + wsListener.onCustomDeviceStateChange -= OnCustomDeviceStateChange; + wsListener.onDeviceProfileChange -= OnDeviceProfileChange; + wsListener.onAdShow -= OnAdShow; + wsListener.onAdComplete -= OnAdComplete; + wsListener.onGameEnd -= OnGameEnd; + wsListener.onHighScores -= OnHighScores; + wsListener.onHighScoreStored -= OnHighScoreStored; + wsListener.onPersistentDataStored -= OnPersistentDataStored; + wsListener.onPersistentDataLoaded -= OnPersistentDataLoaded; + wsListener.onPremium -= OnPremium; + wsListener.onPause -= OnPause; + wsListener.onResume -= OnResume; + + // Stop websocket server if in editor + StopWebsocketServer(); + + wsListener = null; + + AirConsoleLogger.LogDevelopment(() => "WebSocket listener cleanup complete"); + } + + private void RecreateWebView() { + if (string.IsNullOrEmpty(_webViewOriginalUrl) || string.IsNullOrEmpty(_webViewConnectionUrl)) { + AirConsoleLogger.LogDevelopment(() => "Cannot recreate webview - no URL stored"); + return; + } + + AirConsoleLogger.LogDevelopment(() => $"Recreating webview with URL: {_webViewOriginalUrl}"); + + // Cleanup websocket listener first to prevent stale events + CleanupWebSocketListener(); + + // Destroy the old webview + if (webViewObject != null) { + Destroy(webViewObject.gameObject); + webViewObject = null; + } + + // Reset webview manager + _webViewManager = null; + + // Recreate the webview with stored connection URL + CreateAndroidWebview(_webViewConnectionUrl); + } + private void OnGameEnd(JObject msg) { _webViewManager.RequestStateTransition(WebViewManager.WebViewState.FullScreen); @@ -1552,6 +1643,12 @@ private void OnGameEnd(JObject msg) { if (Settings.debug.info) { AirConsoleLogger.Log(() => "AirConsole: onGameEnd"); } + + // Reset all caches + ResetCaches(); + + // Recreate webview with original URL + RecreateWebView(); } catch (Exception e) { if (Settings.debug.error) { AirConsoleLogger.LogError(() => e.Message); @@ -1779,6 +1876,8 @@ internal static bool IsAndroidOrEditor { private JObject _lastSafeAreaParameters; private WebViewManager _webViewManager; private bool _logPlatformMessages; + private string _webViewConnectionUrl; + private string _webViewOriginalUrl; private IRuntimeConfigurator _runtimeConfigurator; @@ -1789,12 +1888,46 @@ private void StopWebsocketServer() { return; } + // Unregister event handlers before stopping to prevent race conditions + if (wsListener != null) { + wsListener.OnSetSafeArea -= OnSetSafeArea; + wsListener.onReady -= OnReady; + wsListener.onClose -= OnClose; + wsListener.onMessage -= OnMessage; + wsListener.onDeviceStateChange -= OnDeviceStateChange; + wsListener.onConnect -= OnConnect; + wsListener.onDisconnect -= OnDisconnect; + wsListener.onCustomDeviceStateChange -= OnCustomDeviceStateChange; + wsListener.onDeviceProfileChange -= OnDeviceProfileChange; + wsListener.onAdShow -= OnAdShow; + wsListener.onAdComplete -= OnAdComplete; + wsListener.onGameEnd -= OnGameEnd; + wsListener.onHighScores -= OnHighScores; + wsListener.onHighScoreStored -= OnHighScoreStored; + wsListener.onPersistentDataStored -= OnPersistentDataStored; + wsListener.onPersistentDataLoaded -= OnPersistentDataLoaded; + wsListener.onPremium -= OnPremium; + wsListener.onPause -= OnPause; + wsListener.onResume -= OnResume; + + if (IsAndroidOrEditor) { + wsListener.onLaunchApp -= OnLaunchApp; + wsListener.onUnityWebviewResize -= OnUnityWebviewResize; + wsListener.onUnityWebviewPlatformReady -= OnUnityWebviewPlatformReady; + wsListener.OnUpdateContentProvider -= OnUpdateContentProvider; + wsListener.OnPlatformReady -= HandlePlatformReady; + } + } + wsServer.Stop(); wsServer = null; } private void OnClose() { - _devices.Clear(); + // Queue the clear operation to run on the Unity main thread to avoid race conditions + eventQueue.Enqueue(delegate() { + _devices.Clear(); + }); } public static string GetUrl(StartMode mode) { @@ -1953,6 +2086,8 @@ private void PrepareWebviewOverlay() { private void CreateAndroidWebview(string connectionUrl) { AirConsoleLogger.LogDevelopment(() => $"CreateAndroidWebview with connection url {connectionUrl}"); if (webViewObject == null) { + _webViewConnectionUrl = connectionUrl; + webViewObject = new GameObject("WebViewObject").AddComponent(); if (Application.isPlaying) { DontDestroyOnLoad(webViewObject.gameObject); @@ -1961,16 +2096,12 @@ private void CreateAndroidWebview(string connectionUrl) { webViewObject.Init(ProcessJS, err => AirConsoleLogger.LogDevelopment(() => $"AirConsole WebView error: {err}"), httpError => AirConsoleLogger.LogDevelopment(() => $"AirConsole WebView HttpError: {httpError}"), - url => AirConsoleLogger.LogDevelopment(() => $"AirConsole WebView Loaded URL {url}"), + loadedUrl => AirConsoleLogger.LogDevelopment(() => $"AirConsole WebView Loaded URL {loadedUrl}"), started => AirConsoleLogger.LogDevelopment(() => $"AirConsole WebView started: {started}"), hooked => AirConsoleLogger.LogDevelopment(() => $"AirConsole WebView hooked: {hooked}"), cookies => AirConsoleLogger.LogDevelopment(() => $"AirConsole WebView cookies: {cookies}"), true, false); - if (IsAndroidRuntime && _pluginManager != null) { - _pluginManager.OnReloadWebview += () => webViewObject.Reload(); - _pluginManager.InitializeOfflineCheck(); - } #if UNITY_ANDROID string urlOverride = AndroidIntentUtils.GetIntentExtraString("base_url", string.Empty); string url = !string.IsNullOrEmpty(urlOverride) ? urlOverride : Settings.AIRCONSOLE_BASE_URL; @@ -1988,12 +2119,13 @@ private void CreateAndroidWebview(string connectionUrl) { url += "&game-version=" + androidGameVersion; url += "&unity-version=" + Application.unityVersion; + _webViewOriginalUrl = url; _webViewManager = new WebViewManager(webViewObject, defaultScreenHeight); webViewObject.SetVisibility(!Application.isEditor); AirConsoleLogger.LogDevelopment(() => $"Initial URL: {url}"); webViewObject.LoadURL(url); - + if (IsAndroidRuntime && _pluginManager != null) { _pluginManager.OnReloadWebview += () => webViewObject.LoadURL(url); _pluginManager.InitializeOfflineCheck(); @@ -2021,38 +2153,45 @@ private void OnLaunchApp(JObject msg) { AirConsoleLogger.LogDevelopment(() => $"OnLaunchApp for {msg} -> {gameId} -> {gameVersion}"); if (gameId != Application.identifier || gameVersion != instance.androidGameVersion) { - bool quitAfterLaunchIntent = false; // Flag used to force old pre v2.5 way of quitting + // Marshal to main thread since this is called from WebSocket background thread + eventQueue.Enqueue(() => { + StartCoroutine(HandleLaunchAppTransition(msg, gameId, gameVersion)); + }); + } + } - if (msg["quit_after_launch_intent"] != null) { - quitAfterLaunchIntent = msg.SelectToken("quit_after_launch_intent").Value(); - } + private System.Collections.IEnumerator HandleLaunchAppTransition(JObject msg, string gameId, string gameVersion) { + bool quitAfterLaunchIntent = false; // Flag used to force old pre v2.5 way of quitting - // Quit the Unity Player first and give it the time to close all the threads (Default) - if (!quitAfterLaunchIntent) { - Application.Quit(); - if (_pluginManager == null || !_pluginManager.IsAutomotive()) { - AirConsoleLogger.LogDevelopment(() => $"Quit and sleep for 2000ms"); - Thread.Sleep(2000); - } else { - AirConsoleLogger.LogDevelopment(() => $"Quit immediately"); - } - } + if (msg["quit_after_launch_intent"] != null) { + quitAfterLaunchIntent = msg.SelectToken("quit_after_launch_intent").Value(); + } - if (IsAndroidRuntime) { - LaunchNativeAirConsoleStore(msg, gameId, gameVersion); + // Quit the Unity Player first and give it the time to close all the threads (Default) + if (!quitAfterLaunchIntent) { + Application.Quit(); + if (_pluginManager == null || !_pluginManager.IsAutomotive()) { + AirConsoleLogger.LogDevelopment(() => $"Quit and wait for 2 seconds"); + yield return new WaitForSecondsRealtime(2.0f); + } else { + AirConsoleLogger.LogDevelopment(() => $"Quit immediately"); } + } - // Quitting after launch intent was the pre v2.5 way - if (quitAfterLaunchIntent) { - AirConsoleLogger.LogDevelopment(() => $"Quit after launch intent"); - Application.Quit(); - return; - } + if (IsAndroidRuntime) { + LaunchNativeAirConsoleStore(msg, gameId, gameVersion); + } - int timeout = _pluginManager != null && _pluginManager.IsAutomotive() ? 2000 : 2000; - Thread.Sleep(timeout); - FinishActivity(); + // Quitting after launch intent was the pre v2.5 way + if (quitAfterLaunchIntent) { + AirConsoleLogger.LogDevelopment(() => $"Quit after launch intent"); + Application.Quit(); + yield break; } + + float timeout = 2.0f; + yield return new WaitForSecondsRealtime(timeout); + FinishActivity(); } private static void LaunchNativeAirConsoleStore(JObject msg, string gameId, string gameVersion) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 38238c52..bf7aae7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ Release notes follow the [keep a changelog](https://keepachangelog.com/en/1.1.0/ - Android Target SDK: Increased to 35 to meet Google Play requirements per Nov 1, 2025. - Minimum Versions: The Unity minimum versions have been updated to match `CVE-2025-59489` fix versions. +- Android: After the last device disconnects, the webview is reset along the game state. + +### Added + +- **Webview Reset**: Added functionality to reset the webview, allowing users to clear its state and reload content as needed. ### Removed From 666ae5430e29be75c2ceaba2c5b118d6a6b95d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Sch=C3=A4rer?= Date: Fri, 10 Oct 2025 02:14:26 +0200 Subject: [PATCH 02/11] Refactor AirConsole.cs to improve code readability and maintainability by removing unnecessary whitespace and consolidating WebSocket event unsubscription logic. --- .../AirConsole/scripts/Runtime/AirConsole.cs | 109 ++++++++---------- 1 file changed, 45 insertions(+), 64 deletions(-) diff --git a/Assets/AirConsole/scripts/Runtime/AirConsole.cs b/Assets/AirConsole/scripts/Runtime/AirConsole.cs index 76f1017e..38d71476 100644 --- a/Assets/AirConsole/scripts/Runtime/AirConsole.cs +++ b/Assets/AirConsole/scripts/Runtime/AirConsole.cs @@ -116,9 +116,9 @@ public class AirConsole : MonoBehaviour { #if !DISABLE_AIRCONSOLE #region airconsole api - + // ReSharper disable MemberCanBePrivate.Global UnusedMember.Global - + /// /// Device ID of the screen /// @@ -1255,7 +1255,7 @@ protected void Update() { } } } - + private void ProcessEvents() { // dispatch event queue on main unity thread while (eventQueue.Count > 0) { @@ -1303,7 +1303,7 @@ private void OnDeviceStateChange(JObject msg) { try { int deviceId = (int)msg["device_id"]; JToken deviceData = msg["device_data"]; - + // Queue all _devices modifications to run on the Unity main thread to avoid race conditions eventQueue.Enqueue(delegate() { AllocateDeviceSlots(deviceId); @@ -1538,44 +1538,34 @@ private void OnAdComplete(JObject msg) { private void ResetCaches() { AirConsoleLogger.LogDevelopment(() => "Resetting AirConsole caches"); - + // Clear device and player data _devices.Clear(); _players.Clear(); - + // Reset state variables _device_id = 0; _server_time_offset = 0; _location = null; _translations = null; - + // Reset safe area _safeAreaWasSet = false; _lastSafeAreaParameters = null; SafeArea = new Rect(0, 0, Screen.width, Screen.height); - + // Clear event queue eventQueue.Clear(); - + AirConsoleLogger.LogDevelopment(() => "AirConsole caches reset complete"); } - private void CleanupWebSocketListener() { + private void UnsubscribeWebSocketEvents() { if (wsListener == null) { return; } - - AirConsoleLogger.LogDevelopment(() => "Cleaning up WebSocket listener"); - + // Unsubscribe all event handlers to prevent stale events - if (IsAndroidOrEditor) { - wsListener.onLaunchApp -= OnLaunchApp; - wsListener.onUnityWebviewResize -= OnUnityWebviewResize; - wsListener.onUnityWebviewPlatformReady -= OnUnityWebviewPlatformReady; - wsListener.OnUpdateContentProvider -= OnUpdateContentProvider; - wsListener.OnPlatformReady -= HandlePlatformReady; - } - wsListener.OnSetSafeArea -= OnSetSafeArea; wsListener.onReady -= OnReady; wsListener.onClose -= OnClose; @@ -1595,12 +1585,31 @@ private void CleanupWebSocketListener() { wsListener.onPremium -= OnPremium; wsListener.onPause -= OnPause; wsListener.onResume -= OnResume; - + + if (IsAndroidOrEditor) { + wsListener.onLaunchApp -= OnLaunchApp; + wsListener.onUnityWebviewResize -= OnUnityWebviewResize; + wsListener.onUnityWebviewPlatformReady -= OnUnityWebviewPlatformReady; + wsListener.OnUpdateContentProvider -= OnUpdateContentProvider; + wsListener.OnPlatformReady -= HandlePlatformReady; + } + } + + private void CleanupWebSocketListener() { + if (wsListener == null) { + return; + } + + AirConsoleLogger.LogDevelopment(() => "Cleaning up WebSocket listener"); + + // Unsubscribe all event handlers to prevent stale events + UnsubscribeWebSocketEvents(); + // Stop websocket server if in editor StopWebsocketServer(); - + wsListener = null; - + AirConsoleLogger.LogDevelopment(() => "WebSocket listener cleanup complete"); } @@ -1609,21 +1618,21 @@ private void RecreateWebView() { AirConsoleLogger.LogDevelopment(() => "Cannot recreate webview - no URL stored"); return; } - + AirConsoleLogger.LogDevelopment(() => $"Recreating webview with URL: {_webViewOriginalUrl}"); - + // Cleanup websocket listener first to prevent stale events CleanupWebSocketListener(); - + // Destroy the old webview if (webViewObject != null) { Destroy(webViewObject.gameObject); webViewObject = null; } - + // Reset webview manager _webViewManager = null; - + // Recreate the webview with stored connection URL CreateAndroidWebview(_webViewConnectionUrl); } @@ -1643,10 +1652,10 @@ private void OnGameEnd(JObject msg) { if (Settings.debug.info) { AirConsoleLogger.Log(() => "AirConsole: onGameEnd"); } - + // Reset all caches ResetCaches(); - + // Recreate webview with original URL RecreateWebView(); } catch (Exception e) { @@ -1889,35 +1898,7 @@ private void StopWebsocketServer() { } // Unregister event handlers before stopping to prevent race conditions - if (wsListener != null) { - wsListener.OnSetSafeArea -= OnSetSafeArea; - wsListener.onReady -= OnReady; - wsListener.onClose -= OnClose; - wsListener.onMessage -= OnMessage; - wsListener.onDeviceStateChange -= OnDeviceStateChange; - wsListener.onConnect -= OnConnect; - wsListener.onDisconnect -= OnDisconnect; - wsListener.onCustomDeviceStateChange -= OnCustomDeviceStateChange; - wsListener.onDeviceProfileChange -= OnDeviceProfileChange; - wsListener.onAdShow -= OnAdShow; - wsListener.onAdComplete -= OnAdComplete; - wsListener.onGameEnd -= OnGameEnd; - wsListener.onHighScores -= OnHighScores; - wsListener.onHighScoreStored -= OnHighScoreStored; - wsListener.onPersistentDataStored -= OnPersistentDataStored; - wsListener.onPersistentDataLoaded -= OnPersistentDataLoaded; - wsListener.onPremium -= OnPremium; - wsListener.onPause -= OnPause; - wsListener.onResume -= OnResume; - - if (IsAndroidOrEditor) { - wsListener.onLaunchApp -= OnLaunchApp; - wsListener.onUnityWebviewResize -= OnUnityWebviewResize; - wsListener.onUnityWebviewPlatformReady -= OnUnityWebviewPlatformReady; - wsListener.OnUpdateContentProvider -= OnUpdateContentProvider; - wsListener.OnPlatformReady -= HandlePlatformReady; - } - } + UnsubscribeWebSocketEvents(); wsServer.Stop(); wsServer = null; @@ -2061,7 +2042,7 @@ private void InitWebView() { private void PrepareWebviewOverlay() { webViewLoadingCanvas = new GameObject("WebViewLoadingCanvas").AddComponent(); webViewLoadingCanvas.renderMode = RenderMode.ScreenSpaceOverlay; - + webViewLoadingBG = new GameObject("WebViewLoadingBG").AddComponent(); webViewLoadingBG.color = Color.black; webViewLoadingBG.transform.SetParent(webViewLoadingCanvas.transform, true); @@ -2087,7 +2068,7 @@ private void CreateAndroidWebview(string connectionUrl) { AirConsoleLogger.LogDevelopment(() => $"CreateAndroidWebview with connection url {connectionUrl}"); if (webViewObject == null) { _webViewConnectionUrl = connectionUrl; - + webViewObject = new GameObject("WebViewObject").AddComponent(); if (Application.isPlaying) { DontDestroyOnLoad(webViewObject.gameObject); @@ -2127,7 +2108,7 @@ private void CreateAndroidWebview(string connectionUrl) { webViewObject.LoadURL(url); if (IsAndroidRuntime && _pluginManager != null) { - _pluginManager.OnReloadWebview += () => webViewObject.LoadURL(url); + _pluginManager.OnReloadWebview += () => webViewObject.LoadURL(url); _pluginManager.InitializeOfflineCheck(); } @@ -2138,7 +2119,7 @@ private void CreateAndroidWebview(string connectionUrl) { InitWebSockets(); } } - + private static int GetAndroidBundleVersionCode() { AndroidJavaObject ca = UnityAndroidObjectProvider.GetUnityActivity(); AndroidJavaObject packageManager = ca.Call("getPackageManager"); From 46f550d35a871ccf169953f2757382133e8c2cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Sch=C3=A4rer?= Date: Fri, 10 Oct 2025 02:28:55 +0200 Subject: [PATCH 03/11] . R: address feedback --- .../AirConsole/scripts/Runtime/AirConsole.cs | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/Assets/AirConsole/scripts/Runtime/AirConsole.cs b/Assets/AirConsole/scripts/Runtime/AirConsole.cs index 38d71476..92d134c1 100644 --- a/Assets/AirConsole/scripts/Runtime/AirConsole.cs +++ b/Assets/AirConsole/scripts/Runtime/AirConsole.cs @@ -1596,10 +1596,6 @@ private void UnsubscribeWebSocketEvents() { } private void CleanupWebSocketListener() { - if (wsListener == null) { - return; - } - AirConsoleLogger.LogDevelopment(() => "Cleaning up WebSocket listener"); // Unsubscribe all event handlers to prevent stale events @@ -1608,14 +1604,24 @@ private void CleanupWebSocketListener() { // Stop websocket server if in editor StopWebsocketServer(); - wsListener = null; + if (wsListener != null) { + wsListener = null; + } AirConsoleLogger.LogDevelopment(() => "WebSocket listener cleanup complete"); } private void RecreateWebView() { if (string.IsNullOrEmpty(_webViewOriginalUrl) || string.IsNullOrEmpty(_webViewConnectionUrl)) { - AirConsoleLogger.LogDevelopment(() => "Cannot recreate webview - no URL stored"); + string missing = ""; + if (string.IsNullOrEmpty(_webViewOriginalUrl) && string.IsNullOrEmpty(_webViewConnectionUrl)) { + missing = "both original and connection URLs"; + } else if (string.IsNullOrEmpty(_webViewOriginalUrl)) { + missing = "original URL"; + } else { + missing = "connection URL"; + } + AirConsoleLogger.LogDevelopment(() => $"Cannot recreate webview - missing {missing}"); return; } @@ -2083,13 +2089,14 @@ private void CreateAndroidWebview(string connectionUrl) { cookies => AirConsoleLogger.LogDevelopment(() => $"AirConsole WebView cookies: {cookies}"), true, false); -#if UNITY_ANDROID - string urlOverride = AndroidIntentUtils.GetIntentExtraString("base_url", string.Empty); - string url = !string.IsNullOrEmpty(urlOverride) ? urlOverride : Settings.AIRCONSOLE_BASE_URL; - AirConsoleLogger.LogDevelopment(() => $"BaseURL Override: {urlOverride}"); -#else - string url = Settings.AIRCONSOLE_BASE_URL; -#endif + string url; + if (IsAndroidRuntime) { + string urlOverride = AndroidIntentUtils.GetIntentExtraString("base_url", string.Empty); + url = !string.IsNullOrEmpty(urlOverride) ? urlOverride : Settings.AIRCONSOLE_BASE_URL; + AirConsoleLogger.LogDevelopment(() => $"BaseURL Override: {urlOverride}"); + } else { + url = Settings.AIRCONSOLE_BASE_URL; + } url += connectionUrl; if (IsAndroidRuntime) { From 1a0e960ffc26255945b6484456029e686e7577b7 Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Fri, 10 Oct 2025 14:18:07 +0200 Subject: [PATCH 04/11] ! R: Improve thread safety in game end handling --- .../AirConsole/scripts/Runtime/AirConsole.cs | 31 +++++++++---------- CHANGELOG.md | 1 + 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Assets/AirConsole/scripts/Runtime/AirConsole.cs b/Assets/AirConsole/scripts/Runtime/AirConsole.cs index 92d134c1..c4a40337 100644 --- a/Assets/AirConsole/scripts/Runtime/AirConsole.cs +++ b/Assets/AirConsole/scripts/Runtime/AirConsole.cs @@ -1603,24 +1603,23 @@ private void CleanupWebSocketListener() { // Stop websocket server if in editor StopWebsocketServer(); - - if (wsListener != null) { - wsListener = null; - } + wsListener = null; AirConsoleLogger.LogDevelopment(() => "WebSocket listener cleanup complete"); } private void RecreateWebView() { if (string.IsNullOrEmpty(_webViewOriginalUrl) || string.IsNullOrEmpty(_webViewConnectionUrl)) { - string missing = ""; - if (string.IsNullOrEmpty(_webViewOriginalUrl) && string.IsNullOrEmpty(_webViewConnectionUrl)) { - missing = "both original and connection URLs"; - } else if (string.IsNullOrEmpty(_webViewOriginalUrl)) { - missing = "original URL"; - } else { - missing = "connection URL"; + List missingComponents = new(); + if (string.IsNullOrEmpty(_webViewOriginalUrl)) { + missingComponents.Add("original URL"); + } + + if (string.IsNullOrEmpty(_webViewConnectionUrl)) { + missingComponents.Add("connection URL"); } + + string missing = string.Join(" and ", missingComponents); AirConsoleLogger.LogDevelopment(() => $"Cannot recreate webview - missing {missing}"); return; } @@ -1659,11 +1658,11 @@ private void OnGameEnd(JObject msg) { AirConsoleLogger.Log(() => "AirConsole: onGameEnd"); } - // Reset all caches - ResetCaches(); - - // Recreate webview with original URL - RecreateWebView(); + // Reset all caches and recreate webview on the main thread + eventQueue.Enqueue(delegate() { + ResetCaches(); + RecreateWebView(); + }); } catch (Exception e) { if (Settings.debug.error) { AirConsoleLogger.LogError(() => e.Message); diff --git a/CHANGELOG.md b/CHANGELOG.md index bf7aae7e..8d4f841f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Release notes follow the [keep a changelog](https://keepachangelog.com/en/1.1.0/ ### Added +- Unity Editor: Update to minimum versions to match `CVE-2025-59489` fix versions. - **Webview Reset**: Added functionality to reset the webview, allowing users to clear its state and reload content as needed. ### Removed From fe74cc8580fc92ea21133430ea0d4c3014a07801 Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Sun, 12 Oct 2025 19:58:03 +0200 Subject: [PATCH 05/11] . r: Integrate webview plugin --- .../scripts/Runtime/AirConsole.Runtime.asmdef | 2 +- Assets/AirConsole/unity-webview.meta | 8 + Assets/AirConsole/unity-webview/Editor.meta | 8 + .../Editor/UnityWebViewPostprocessBuild.cs | 539 +++++ .../UnityWebViewPostprocessBuild.cs.meta | 12 + .../Editor/Webview.Editor.asmdef | 18 + .../Editor/Webview.Editor.asmdef.meta | 7 + Assets/AirConsole/unity-webview/Plugins.meta | 8 + .../unity-webview/Plugins/Android.meta | 8 + .../WebViewPlugin-development.aar.tmpl | Bin 0 -> 39468 bytes .../WebViewPlugin-development.aar.tmpl.meta | 7 + .../Android/WebViewPlugin-release.aar.tmpl | Bin 0 -> 39681 bytes .../WebViewPlugin-release.aar.tmpl.meta | 7 + .../unity-webview/Plugins/Editor.meta | 8 + .../Editor/UnityWebViewPostprocessBuild.cs | 539 +++++ .../UnityWebViewPostprocessBuild.cs.meta | 11 + .../unity-webview/Plugins/WebView.bundle.meta | 33 + .../WebView.bundle/Contents/Info.plist | 48 + .../WebView.bundle/Contents/MacOS/WebView | Bin 0 -> 270288 bytes .../Contents/Resources/InfoPlist.strings | Bin 0 -> 92 bytes .../Contents/_CodeSignature/CodeResources | 128 ++ .../unity-webview/Plugins/WebViewObject.cs | 1661 ++++++++++++++ .../Plugins/WebViewObject.cs.meta | 11 + .../AirConsole/unity-webview/Plugins/iOS.meta | 8 + .../unity-webview/Plugins/iOS/WebView.mm | 1204 ++++++++++ .../unity-webview/Plugins/iOS/WebView.mm.meta | 33 + .../Plugins/iOS/WebViewWithUIWebView.mm | 1289 +++++++++++ .../Plugins/iOS/WebViewWithUIWebView.mm.meta | 33 + .../Plugins/unity-webview-webgl-plugin.jslib | 31 + .../unity-webview-webgl-plugin.jslib.meta | 32 + Assets/AirConsole/unity-webview/Runtime.meta | 8 + .../unity-webview/Runtime/VersionInfo.cs | 4 + .../unity-webview/Runtime/VersionInfo.cs.meta | 3 + .../unity-webview/Runtime/WebViewObject.cs | 1934 +++++++++++++++++ .../Runtime/WebViewObject.cs.meta | 12 + .../Runtime/Webview.Runtime.asmdef | 20 + .../Runtime/Webview.Runtime.asmdef.meta | 7 + Packages/manifest.json | 4 +- Packages/packages-lock.json | 16 +- 39 files changed, 7682 insertions(+), 19 deletions(-) create mode 100644 Assets/AirConsole/unity-webview.meta create mode 100644 Assets/AirConsole/unity-webview/Editor.meta create mode 100644 Assets/AirConsole/unity-webview/Editor/UnityWebViewPostprocessBuild.cs create mode 100644 Assets/AirConsole/unity-webview/Editor/UnityWebViewPostprocessBuild.cs.meta create mode 100644 Assets/AirConsole/unity-webview/Editor/Webview.Editor.asmdef create mode 100644 Assets/AirConsole/unity-webview/Editor/Webview.Editor.asmdef.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/Android.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-development.aar.tmpl create mode 100644 Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-development.aar.tmpl.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-release.aar.tmpl create mode 100644 Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-release.aar.tmpl.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/Editor.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/Editor/UnityWebViewPostprocessBuild.cs create mode 100644 Assets/AirConsole/unity-webview/Plugins/Editor/UnityWebViewPostprocessBuild.cs.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/WebView.bundle.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/Info.plist create mode 100755 Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/MacOS/WebView create mode 100644 Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/Resources/InfoPlist.strings create mode 100644 Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/_CodeSignature/CodeResources create mode 100644 Assets/AirConsole/unity-webview/Plugins/WebViewObject.cs create mode 100644 Assets/AirConsole/unity-webview/Plugins/WebViewObject.cs.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/iOS.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/iOS/WebView.mm create mode 100644 Assets/AirConsole/unity-webview/Plugins/iOS/WebView.mm.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/iOS/WebViewWithUIWebView.mm create mode 100644 Assets/AirConsole/unity-webview/Plugins/iOS/WebViewWithUIWebView.mm.meta create mode 100644 Assets/AirConsole/unity-webview/Plugins/unity-webview-webgl-plugin.jslib create mode 100644 Assets/AirConsole/unity-webview/Plugins/unity-webview-webgl-plugin.jslib.meta create mode 100644 Assets/AirConsole/unity-webview/Runtime.meta create mode 100644 Assets/AirConsole/unity-webview/Runtime/VersionInfo.cs create mode 100644 Assets/AirConsole/unity-webview/Runtime/VersionInfo.cs.meta create mode 100644 Assets/AirConsole/unity-webview/Runtime/WebViewObject.cs create mode 100644 Assets/AirConsole/unity-webview/Runtime/WebViewObject.cs.meta create mode 100644 Assets/AirConsole/unity-webview/Runtime/Webview.Runtime.asmdef create mode 100644 Assets/AirConsole/unity-webview/Runtime/Webview.Runtime.asmdef.meta diff --git a/Assets/AirConsole/scripts/Runtime/AirConsole.Runtime.asmdef b/Assets/AirConsole/scripts/Runtime/AirConsole.Runtime.asmdef index 7cacff24..8913fa27 100644 --- a/Assets/AirConsole/scripts/Runtime/AirConsole.Runtime.asmdef +++ b/Assets/AirConsole/scripts/Runtime/AirConsole.Runtime.asmdef @@ -4,7 +4,7 @@ "references": [ "Unity.TextMeshPro", "UnityEngine.UI", - "unity-webview" + "com.airconsole.unity-webview.runtime" ], "includePlatforms": [ "Android", diff --git a/Assets/AirConsole/unity-webview.meta b/Assets/AirConsole/unity-webview.meta new file mode 100644 index 00000000..066e9d62 --- /dev/null +++ b/Assets/AirConsole/unity-webview.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 65d2afe35499f42e092353ff43f147d9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Editor.meta b/Assets/AirConsole/unity-webview/Editor.meta new file mode 100644 index 00000000..439d487c --- /dev/null +++ b/Assets/AirConsole/unity-webview/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3e67d20066ca445cab1d878aa12db423 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Editor/UnityWebViewPostprocessBuild.cs b/Assets/AirConsole/unity-webview/Editor/UnityWebViewPostprocessBuild.cs new file mode 100644 index 00000000..cb840e4d --- /dev/null +++ b/Assets/AirConsole/unity-webview/Editor/UnityWebViewPostprocessBuild.cs @@ -0,0 +1,539 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using System.Collections; +using System.IO; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Text; +using System.Xml; +using System; +using UnityEditor.Android; +#if UNITY_2018_1_OR_NEWER +using UnityEditor.Build; +#endif +using UnityEditor.Callbacks; +using UnityEditor; +using UnityEngine; + +#if UNITY_2018_1_OR_NEWER +public class UnityWebViewPostprocessBuild : IPreprocessBuild, IPostGenerateGradleAndroidProject +#else +public class UnityWebViewPostprocessBuild +#endif +{ + private static bool nofragment = true; + + //// for android/unity 2018.1 or newer + //// cf. https://forum.unity.com/threads/android-hardwareaccelerated-is-forced-false-in-all-activities.532786/ + //// cf. https://github.com/Over17/UnityAndroidManifestCallback + +#if UNITY_2018_1_OR_NEWER + public void OnPreprocessBuild(BuildTarget buildTarget, string path) { + if (buildTarget == BuildTarget.Android) { + var dev = "Packages/com.airconsole.unity-webview/Assets/Plugins/Android/WebViewPlugin-development.aar.tmpl"; + var rel = "Packages/com.airconsole.unity-webview/Assets/Plugins/Android/WebViewPlugin-release.aar.tmpl"; + if (!File.Exists(dev) || !File.Exists(rel)) { + dev = "Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-development.aar.tmpl"; + rel = "Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-release.aar.tmpl"; + } + var src = (EditorUserBuildSettings.development) ? dev : rel; + //Directory.CreateDirectory("Temp/StagingArea/aar"); + //File.Copy(src, "Temp/StagingArea/aar/WebViewPlugin.aar", true); + Directory.CreateDirectory("Assets/Plugins/Android"); + File.Copy(src, "Assets/Plugins/Android/WebViewPlugin.aar", true); + } + } + + public void OnPostGenerateGradleAndroidProject(string basePath) { + var changed = false; + var androidManifest = new AndroidManifest(GetManifestPath(basePath)); + if (!nofragment) { + changed = (androidManifest.AddFileProvider(basePath) || changed); + { + var path = GetBuildGradlePath(basePath); + var lines0 = File.ReadAllText(path).Replace("\r\n", "\n").Replace("\r", "\n").Split(new[]{'\n'}); + { + var lines = new List(); + var independencies = false; + foreach (var line in lines0) { + if (line == "dependencies {") { + independencies = true; + } else if (independencies && line == "}") { + independencies = false; + lines.Add(" implementation 'androidx.core:core:1.6.0'"); + } else if (independencies) { + if (line.Contains("implementation(name: 'core") + || line.Contains("implementation(name: 'androidx.core.core") + || line.Contains("implementation 'androidx.core:core")) { + break; + } + } + lines.Add(line); + } + if (lines.Count > lines0.Length) { + File.WriteAllText(path, string.Join("\n", lines) + "\n"); + } + } + } + { + var path = GetGradlePropertiesPath(basePath); + var lines0 = ""; + var lines = ""; + if (File.Exists(path)) { + lines0 = File.ReadAllText(path).Replace("\r\n", "\n").Replace("\r", "\n") + "\n"; + lines = lines0; + } + if (!lines.Contains("android.useAndroidX=true")) { + lines += "android.useAndroidX=true\n"; + } + if (!lines.Contains("android.enableJetifier=true")) { + lines += "android.enableJetifier=true\n"; + } + if (lines != lines0) { + File.WriteAllText(path, lines); + } + } + } + changed = (androidManifest.SetExported(true) || changed); + changed = (androidManifest.SetWindowSoftInputMode("adjustPan") || changed); + changed = (androidManifest.SetHardwareAccelerated(true) || changed); +#if UNITYWEBVIEW_ANDROID_USES_CLEARTEXT_TRAFFIC + changed = (androidManifest.SetUsesCleartextTraffic(true) || changed); +#endif +#if UNITYWEBVIEW_ANDROID_ENABLE_CAMERA + changed = (androidManifest.AddCamera() || changed); + changed = (androidManifest.AddGallery() || changed); +#endif +#if UNITYWEBVIEW_ANDROID_ENABLE_MICROPHONE + changed = (androidManifest.AddMicrophone() || changed); +#endif + if (changed) { + androidManifest.Save(); + Debug.Log("unitywebview: adjusted AndroidManifest.xml."); + } + } +#endif + + public int callbackOrder { + get { + return 1; + } + } + + private string GetManifestPath(string basePath) { + var pathBuilder = new StringBuilder(basePath); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("src"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("main"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("AndroidManifest.xml"); + return pathBuilder.ToString(); + } + + private string GetBuildGradlePath(string basePath) { + var pathBuilder = new StringBuilder(basePath); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("build.gradle"); + return pathBuilder.ToString(); + } + + private string GetGradlePropertiesPath(string basePath) { + var pathBuilder = new StringBuilder(basePath); + if (basePath.EndsWith("unityLibrary")) { + pathBuilder.Append(Path.DirectorySeparatorChar).Append(".."); + } + pathBuilder.Append(Path.DirectorySeparatorChar).Append("gradle.properties"); + return pathBuilder.ToString(); + } + + //// for others + + [PostProcessBuild(100)] + public static void OnPostprocessBuild(BuildTarget buildTarget, string path) { +#if UNITY_2018_1_OR_NEWER + try { + File.Delete("Assets/Plugins/Android/WebViewPlugin.aar"); + File.Delete("Assets/Plugins/Android/WebViewPlugin.aar.meta"); + Directory.Delete("Assets/Plugins/Android"); + File.Delete("Assets/Plugins/Android.meta"); + Directory.Delete("Assets/Plugins"); + File.Delete("Assets/Plugins.meta"); + } catch (Exception) { + } +#else + if (buildTarget == BuildTarget.Android) { + string manifest = Path.Combine(Application.dataPath, "Plugins/Android/AndroidManifest.xml"); + if (!File.Exists(manifest)) { + string manifest0 = Path.Combine(Application.dataPath, "../Temp/StagingArea/AndroidManifest-main.xml"); + if (!File.Exists(manifest0)) { + Debug.LogError("unitywebview: cannot find both Assets/Plugins/Android/AndroidManifest.xml and Temp/StagingArea/AndroidManifest-main.xml. please build the app to generate Assets/Plugins/Android/AndroidManifest.xml and then rebuild it again."); + return; + } else { + File.Copy(manifest0, manifest, true); + } + } + var changed = false; + if (EditorUserBuildSettings.development) { + if (!File.Exists("Assets/Plugins/Android/WebView.aar") + || !File.ReadAllBytes("Assets/Plugins/Android/WebView.aar").SequenceEqual(File.ReadAllBytes("Assets/Plugins/Android/WebViewPlugin-development.aar.tmpl"))) { + File.Copy("Assets/Plugins/Android/WebViewPlugin-development.aar.tmpl", "Assets/Plugins/Android/WebView.aar", true); + changed = true; + } + } else { + if (!File.Exists("Assets/Plugins/Android/WebView.aar") + || !File.ReadAllBytes("Assets/Plugins/Android/WebView.aar").SequenceEqual(File.ReadAllBytes("Assets/Plugins/Android/WebViewPlugin-release.aar.tmpl"))) { + File.Copy("Assets/Plugins/Android/WebViewPlugin-release.aar.tmpl", "Assets/Plugins/Android/WebView.aar", true); + changed = true; + } + } + var androidManifest = new AndroidManifest(manifest); + if (!nofragment) { + changed = (androidManifest.AddFileProvider("Assets/Plugins/Android") || changed); + var files = Directory.GetFiles("Assets/Plugins/Android/"); + var found = false; + foreach (var file in files) { + if (Regex.IsMatch(file, @"^Assets/Plugins/Android/(androidx\.core\.)?core-.*.aar$")) { + found = true; + break; + } + } + if (!found) { + foreach (var file in files) { + var match = Regex.Match(file, @"^Assets/Plugins/Android/(core.*.aar).tmpl$"); + if (match.Success) { + var name = match.Groups[1].Value; + File.Copy(file, "Assets/Plugins/Android/" + name, true); + break; + } + } + } + } + changed = (androidManifest.SetWindowSoftInputMode("adjustPan") || changed); + changed = (androidManifest.SetHardwareAccelerated(true) || changed); +#if UNITYWEBVIEW_ANDROID_USES_CLEARTEXT_TRAFFIC + changed = (androidManifest.SetUsesCleartextTraffic(true) || changed); +#endif +#if UNITYWEBVIEW_ANDROID_ENABLE_CAMERA + changed = (androidManifest.AddCamera() || changed); + changed = (androidManifest.AddGallery() || changed); +#endif +#if UNITYWEBVIEW_ANDROID_ENABLE_MICROPHONE + changed = (androidManifest.AddMicrophone() || changed); +#endif +#if UNITY_5_6_0 || UNITY_5_6_1 + changed = (androidManifest.SetActivityName("net.gree.unitywebview.CUnityPlayerActivity") || changed); +#endif + if (changed) { + androidManifest.Save(); + Debug.LogError("unitywebview: adjusted AndroidManifest.xml and/or WebView.aar. Please rebuild the app."); + } + } +#endif + if (buildTarget == BuildTarget.iOS) { + string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj"; + var type = Type.GetType("UnityEditor.iOS.Xcode.PBXProject, UnityEditor.iOS.Extensions.Xcode"); + if (type == null) + { + Debug.LogError("unitywebview: failed to get PBXProject. please install iOS build support."); + return; + } + var src = File.ReadAllText(projPath); + //dynamic proj = type.GetConstructor(Type.EmptyTypes).Invoke(null); + var proj = type.GetConstructor(Type.EmptyTypes).Invoke(null); + //proj.ReadFromString(src); + { + var method = type.GetMethod("ReadFromString"); + method.Invoke(proj, new object[]{src}); + } + var target = ""; +#if UNITY_2019_3_OR_NEWER + //target = proj.GetUnityFrameworkTargetGuid(); + { + var method = type.GetMethod("GetUnityFrameworkTargetGuid"); + target = (string)method.Invoke(proj, null); + } +#else + //target = proj.TargetGuidByName("Unity-iPhone"); + { + var method = type.GetMethod("TargetGuidByName"); + target = (string)method.Invoke(proj, new object[]{"Unity-iPhone"}); + } +#endif + //proj.AddFrameworkToProject(target, "WebKit.framework", false); + { + var method = type.GetMethod("AddFrameworkToProject"); + method.Invoke(proj, new object[]{target, "WebKit.framework", false}); + } + var cflags = ""; + if (EditorUserBuildSettings.development) { + cflags += " -DUNITYWEBVIEW_DEVELOPMENT"; + } +#if UNITYWEBVIEW_IOS_ALLOW_FILE_URLS + cflags += " -DUNITYWEBVIEW_IOS_ALLOW_FILE_URLS"; +#endif + cflags = cflags.Trim(); + if (!string.IsNullOrEmpty(cflags)) { + // proj.AddBuildProperty(target, "OTHER_LDFLAGS", cflags); + var method = type.GetMethod("AddBuildProperty", new Type[]{typeof(string), typeof(string), typeof(string)}); + method.Invoke(proj, new object[]{target, "OTHER_CFLAGS", cflags}); + } + var dst = ""; + //dst = proj.WriteToString(); + { + var method = type.GetMethod("WriteToString"); + dst = (string)method.Invoke(proj, null); + } + File.WriteAllText(projPath, dst); + } + } +} + +internal class AndroidXmlDocument : XmlDocument { + private string m_Path; + protected XmlNamespaceManager nsMgr; + public readonly string AndroidXmlNamespace = "http://schemas.android.com/apk/res/android"; + + public AndroidXmlDocument(string path) { + m_Path = path; + using (var reader = new XmlTextReader(m_Path)) { + reader.Read(); + Load(reader); + } + nsMgr = new XmlNamespaceManager(NameTable); + nsMgr.AddNamespace("android", AndroidXmlNamespace); + } + + public string Save() { + return SaveAs(m_Path); + } + + public string SaveAs(string path) { + using (var writer = new XmlTextWriter(path, new UTF8Encoding(false))) { + writer.Formatting = Formatting.Indented; + Save(writer); + } + return path; + } +} + +internal class AndroidManifest : AndroidXmlDocument { + private readonly XmlElement ManifestElement; + private readonly XmlElement ApplicationElement; + + public AndroidManifest(string path) : base(path) { + ManifestElement = SelectSingleNode("/manifest") as XmlElement; + ApplicationElement = SelectSingleNode("/manifest/application") as XmlElement; + } + + private XmlAttribute CreateAndroidAttribute(string key, string value) { + XmlAttribute attr = CreateAttribute("android", key, AndroidXmlNamespace); + attr.Value = value; + return attr; + } + + internal XmlNode GetActivityWithLaunchIntent() { + return + SelectSingleNode( + "/manifest/application/activity[intent-filter/action/@android:name='android.intent.action.MAIN' and " + + "intent-filter/category/@android:name='android.intent.category.LAUNCHER']", + nsMgr); + } + + internal bool SetUsesCleartextTraffic(bool enabled) { + // android:usesCleartextTraffic + bool changed = false; + if (ApplicationElement.GetAttribute("usesCleartextTraffic", AndroidXmlNamespace) != ((enabled) ? "true" : "false")) { + ApplicationElement.SetAttribute("usesCleartextTraffic", AndroidXmlNamespace, (enabled) ? "true" : "false"); + changed = true; + } + return changed; + } + + // for api level 33 + internal bool SetExported(bool enabled) { + bool changed = false; + var activity = GetActivityWithLaunchIntent() as XmlElement; + if (activity.GetAttribute("exported", AndroidXmlNamespace) != ((enabled) ? "true" : "false")) { + activity.SetAttribute("exported", AndroidXmlNamespace, (enabled) ? "true" : "false"); + changed = true; + } + return changed; + } + + internal bool SetWindowSoftInputMode(string mode) { + bool changed = false; + var activity = GetActivityWithLaunchIntent() as XmlElement; + if (activity.GetAttribute("windowSoftInputMode", AndroidXmlNamespace) != mode) { + activity.SetAttribute("windowSoftInputMode", AndroidXmlNamespace, mode); + changed = true; + } + return changed; + } + + internal bool SetHardwareAccelerated(bool enabled) { + bool changed = false; + var activity = GetActivityWithLaunchIntent() as XmlElement; + if (activity.GetAttribute("hardwareAccelerated", AndroidXmlNamespace) != ((enabled) ? "true" : "false")) { + activity.SetAttribute("hardwareAccelerated", AndroidXmlNamespace, (enabled) ? "true" : "false"); + changed = true; + } + return changed; + } + + internal bool SetActivityName(string name) { + bool changed = false; + var activity = GetActivityWithLaunchIntent() as XmlElement; + if (activity.GetAttribute("name", AndroidXmlNamespace) != name) { + activity.SetAttribute("name", AndroidXmlNamespace, name); + changed = true; + } + return changed; + } + + internal bool AddFileProvider(string basePath) { + bool changed = false; + var authorities = PlayerSettings.applicationIdentifier + ".unitywebview.fileprovider"; + if (SelectNodes("/manifest/application/provider[@android:authorities='" + authorities + "']", nsMgr).Count == 0) { + var elem = CreateElement("provider"); + elem.Attributes.Append(CreateAndroidAttribute("name", "androidx.core.content.FileProvider")); + elem.Attributes.Append(CreateAndroidAttribute("authorities", authorities)); + elem.Attributes.Append(CreateAndroidAttribute("exported", "false")); + elem.Attributes.Append(CreateAndroidAttribute("grantUriPermissions", "true")); + var meta = CreateElement("meta-data"); + meta.Attributes.Append(CreateAndroidAttribute("name", "android.support.FILE_PROVIDER_PATHS")); + meta.Attributes.Append(CreateAndroidAttribute("resource", "@xml/unitywebview_file_provider_paths")); + elem.AppendChild(meta); + ApplicationElement.AppendChild(elem); + changed = true; + var xml = GetFileProviderSettingPath(basePath); + if (!File.Exists(xml)) { + Directory.CreateDirectory(Path.GetDirectoryName(xml)); + File.WriteAllText( + xml, + "\n" + + " \n" + + "\n"); + } + } + return changed; + } + + private string GetFileProviderSettingPath(string basePath) { + var pathBuilder = new StringBuilder(basePath); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("src"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("main"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("res"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("xml"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("unitywebview_file_provider_paths.xml"); + return pathBuilder.ToString(); + } + + internal bool AddCamera() { + bool changed = false; + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.CAMERA']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.CAMERA")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-feature[@android:name='android.hardware.camera']", nsMgr).Count == 0) { + var elem = CreateElement("uses-feature"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.hardware.camera")); + ManifestElement.AppendChild(elem); + changed = true; + } + // cf. https://developer.android.com/training/data-storage/shared/media#media-location-permission + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.ACCESS_MEDIA_LOCATION']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.ACCESS_MEDIA_LOCATION")); + ManifestElement.AppendChild(elem); + changed = true; + } + // cf. https://developer.android.com/training/package-visibility/declaring + if (SelectNodes("/manifest/queries", nsMgr).Count == 0) { + var elem = CreateElement("queries"); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/queries/intent/action[@android:name='android.media.action.IMAGE_CAPTURE']", nsMgr).Count == 0) { + var action = CreateElement("action"); + action.Attributes.Append(CreateAndroidAttribute("name", "android.media.action.IMAGE_CAPTURE")); + var intent = CreateElement("intent"); + intent.AppendChild(action); + var queries = SelectSingleNode("/manifest/queries") as XmlElement; + queries.AppendChild(intent); + changed = true; + } + return changed; + } + + internal bool AddGallery() { + bool changed = false; + // for api level 33 + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.READ_MEDIA_IMAGES']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.READ_MEDIA_IMAGES")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.READ_MEDIA_VIDEO']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.READ_MEDIA_VIDEO")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.READ_MEDIA_AUDIO']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.READ_MEDIA_AUDIO")); + ManifestElement.AppendChild(elem); + changed = true; + } + // cf. https://developer.android.com/training/package-visibility/declaring + if (SelectNodes("/manifest/queries", nsMgr).Count == 0) { + var elem = CreateElement("queries"); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/queries/intent/action[@android:name='android.media.action.GET_CONTENT']", nsMgr).Count == 0) { + var action = CreateElement("action"); + action.Attributes.Append(CreateAndroidAttribute("name", "android.media.action.GET_CONTENT")); + var intent = CreateElement("intent"); + intent.AppendChild(action); + var queries = SelectSingleNode("/manifest/queries") as XmlElement; + queries.AppendChild(intent); + changed = true; + } + return changed; + } + + internal bool AddMicrophone() { + bool changed = false; + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.MICROPHONE']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.MICROPHONE")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-feature[@android:name='android.hardware.microphone']", nsMgr).Count == 0) { + var elem = CreateElement("uses-feature"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.hardware.microphone")); + ManifestElement.AppendChild(elem); + changed = true; + } + // cf. https://github.com/gree/unity-webview/issues/679 + // cf. https://github.com/fluttercommunity/flutter_webview_plugin/issues/138#issuecomment-559307558 + // cf. https://stackoverflow.com/questions/38917751/webview-webrtc-not-working/68024032#68024032 + // cf. https://stackoverflow.com/questions/40236925/allowing-microphone-accesspermission-in-webview-android-studio-java/47410311#47410311 + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.MODIFY_AUDIO_SETTINGS']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.MODIFY_AUDIO_SETTINGS")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.RECORD_AUDIO']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.RECORD_AUDIO")); + ManifestElement.AppendChild(elem); + changed = true; + } + return changed; + } +} +#endif diff --git a/Assets/AirConsole/unity-webview/Editor/UnityWebViewPostprocessBuild.cs.meta b/Assets/AirConsole/unity-webview/Editor/UnityWebViewPostprocessBuild.cs.meta new file mode 100644 index 00000000..b090b4b0 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Editor/UnityWebViewPostprocessBuild.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 622ef80e351c84dd7bc9fa7e98037fec +timeCreated: 1535029316 +licenseType: Pro +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Editor/Webview.Editor.asmdef b/Assets/AirConsole/unity-webview/Editor/Webview.Editor.asmdef new file mode 100644 index 00000000..4db17dbd --- /dev/null +++ b/Assets/AirConsole/unity-webview/Editor/Webview.Editor.asmdef @@ -0,0 +1,18 @@ +{ + "name": "com.airconsole.unity-webview.editor", + "rootNamespace": "", + "references": [ + "com.airconsole.unity-webview.runtime" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/AirConsole/unity-webview/Editor/Webview.Editor.asmdef.meta b/Assets/AirConsole/unity-webview/Editor/Webview.Editor.asmdef.meta new file mode 100644 index 00000000..c20763bd --- /dev/null +++ b/Assets/AirConsole/unity-webview/Editor/Webview.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a2b2fe4dcc4344e349cf958f3fb62ffa +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins.meta b/Assets/AirConsole/unity-webview/Plugins.meta new file mode 100644 index 00000000..a772e932 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3fab9ef1899dc4bf18586c6a01172d62 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/Android.meta b/Assets/AirConsole/unity-webview/Plugins/Android.meta new file mode 100644 index 00000000..a55a2c27 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/Android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dbc2200488e7d45189f9a082caf5d637 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-development.aar.tmpl b/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-development.aar.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..23bdad7db48c711e81286b9e4c18998173e7e2d2 GIT binary patch literal 39468 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 i|BUg!u_eJO8UK%nt1F?R{X>QN+*O}F7Y5GViu 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

Td7*q9v9)2``5LvbopE-O-UWpvtjPMfC{C?A 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�oWTE1L}i38?AUVHTdTw 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 literal 0 HcmV?d00001 diff --git a/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-release.aar.tmpl.meta b/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-release.aar.tmpl.meta new file mode 100644 index 00000000..d4aa3543 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-release.aar.tmpl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 69e600137811c42778f78793d1022369 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/Editor.meta b/Assets/AirConsole/unity-webview/Plugins/Editor.meta new file mode 100644 index 00000000..4afc4293 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5415c2bc4488c42739b36767bc7f8c83 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/Editor/UnityWebViewPostprocessBuild.cs b/Assets/AirConsole/unity-webview/Plugins/Editor/UnityWebViewPostprocessBuild.cs new file mode 100644 index 00000000..cb840e4d --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/Editor/UnityWebViewPostprocessBuild.cs @@ -0,0 +1,539 @@ +#if UNITY_EDITOR +using System.Collections.Generic; +using System.Collections; +using System.IO; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Text; +using System.Xml; +using System; +using UnityEditor.Android; +#if UNITY_2018_1_OR_NEWER +using UnityEditor.Build; +#endif +using UnityEditor.Callbacks; +using UnityEditor; +using UnityEngine; + +#if UNITY_2018_1_OR_NEWER +public class UnityWebViewPostprocessBuild : IPreprocessBuild, IPostGenerateGradleAndroidProject +#else +public class UnityWebViewPostprocessBuild +#endif +{ + private static bool nofragment = true; + + //// for android/unity 2018.1 or newer + //// cf. https://forum.unity.com/threads/android-hardwareaccelerated-is-forced-false-in-all-activities.532786/ + //// cf. https://github.com/Over17/UnityAndroidManifestCallback + +#if UNITY_2018_1_OR_NEWER + public void OnPreprocessBuild(BuildTarget buildTarget, string path) { + if (buildTarget == BuildTarget.Android) { + var dev = "Packages/com.airconsole.unity-webview/Assets/Plugins/Android/WebViewPlugin-development.aar.tmpl"; + var rel = "Packages/com.airconsole.unity-webview/Assets/Plugins/Android/WebViewPlugin-release.aar.tmpl"; + if (!File.Exists(dev) || !File.Exists(rel)) { + dev = "Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-development.aar.tmpl"; + rel = "Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-release.aar.tmpl"; + } + var src = (EditorUserBuildSettings.development) ? dev : rel; + //Directory.CreateDirectory("Temp/StagingArea/aar"); + //File.Copy(src, "Temp/StagingArea/aar/WebViewPlugin.aar", true); + Directory.CreateDirectory("Assets/Plugins/Android"); + File.Copy(src, "Assets/Plugins/Android/WebViewPlugin.aar", true); + } + } + + public void OnPostGenerateGradleAndroidProject(string basePath) { + var changed = false; + var androidManifest = new AndroidManifest(GetManifestPath(basePath)); + if (!nofragment) { + changed = (androidManifest.AddFileProvider(basePath) || changed); + { + var path = GetBuildGradlePath(basePath); + var lines0 = File.ReadAllText(path).Replace("\r\n", "\n").Replace("\r", "\n").Split(new[]{'\n'}); + { + var lines = new List(); + var independencies = false; + foreach (var line in lines0) { + if (line == "dependencies {") { + independencies = true; + } else if (independencies && line == "}") { + independencies = false; + lines.Add(" implementation 'androidx.core:core:1.6.0'"); + } else if (independencies) { + if (line.Contains("implementation(name: 'core") + || line.Contains("implementation(name: 'androidx.core.core") + || line.Contains("implementation 'androidx.core:core")) { + break; + } + } + lines.Add(line); + } + if (lines.Count > lines0.Length) { + File.WriteAllText(path, string.Join("\n", lines) + "\n"); + } + } + } + { + var path = GetGradlePropertiesPath(basePath); + var lines0 = ""; + var lines = ""; + if (File.Exists(path)) { + lines0 = File.ReadAllText(path).Replace("\r\n", "\n").Replace("\r", "\n") + "\n"; + lines = lines0; + } + if (!lines.Contains("android.useAndroidX=true")) { + lines += "android.useAndroidX=true\n"; + } + if (!lines.Contains("android.enableJetifier=true")) { + lines += "android.enableJetifier=true\n"; + } + if (lines != lines0) { + File.WriteAllText(path, lines); + } + } + } + changed = (androidManifest.SetExported(true) || changed); + changed = (androidManifest.SetWindowSoftInputMode("adjustPan") || changed); + changed = (androidManifest.SetHardwareAccelerated(true) || changed); +#if UNITYWEBVIEW_ANDROID_USES_CLEARTEXT_TRAFFIC + changed = (androidManifest.SetUsesCleartextTraffic(true) || changed); +#endif +#if UNITYWEBVIEW_ANDROID_ENABLE_CAMERA + changed = (androidManifest.AddCamera() || changed); + changed = (androidManifest.AddGallery() || changed); +#endif +#if UNITYWEBVIEW_ANDROID_ENABLE_MICROPHONE + changed = (androidManifest.AddMicrophone() || changed); +#endif + if (changed) { + androidManifest.Save(); + Debug.Log("unitywebview: adjusted AndroidManifest.xml."); + } + } +#endif + + public int callbackOrder { + get { + return 1; + } + } + + private string GetManifestPath(string basePath) { + var pathBuilder = new StringBuilder(basePath); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("src"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("main"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("AndroidManifest.xml"); + return pathBuilder.ToString(); + } + + private string GetBuildGradlePath(string basePath) { + var pathBuilder = new StringBuilder(basePath); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("build.gradle"); + return pathBuilder.ToString(); + } + + private string GetGradlePropertiesPath(string basePath) { + var pathBuilder = new StringBuilder(basePath); + if (basePath.EndsWith("unityLibrary")) { + pathBuilder.Append(Path.DirectorySeparatorChar).Append(".."); + } + pathBuilder.Append(Path.DirectorySeparatorChar).Append("gradle.properties"); + return pathBuilder.ToString(); + } + + //// for others + + [PostProcessBuild(100)] + public static void OnPostprocessBuild(BuildTarget buildTarget, string path) { +#if UNITY_2018_1_OR_NEWER + try { + File.Delete("Assets/Plugins/Android/WebViewPlugin.aar"); + File.Delete("Assets/Plugins/Android/WebViewPlugin.aar.meta"); + Directory.Delete("Assets/Plugins/Android"); + File.Delete("Assets/Plugins/Android.meta"); + Directory.Delete("Assets/Plugins"); + File.Delete("Assets/Plugins.meta"); + } catch (Exception) { + } +#else + if (buildTarget == BuildTarget.Android) { + string manifest = Path.Combine(Application.dataPath, "Plugins/Android/AndroidManifest.xml"); + if (!File.Exists(manifest)) { + string manifest0 = Path.Combine(Application.dataPath, "../Temp/StagingArea/AndroidManifest-main.xml"); + if (!File.Exists(manifest0)) { + Debug.LogError("unitywebview: cannot find both Assets/Plugins/Android/AndroidManifest.xml and Temp/StagingArea/AndroidManifest-main.xml. please build the app to generate Assets/Plugins/Android/AndroidManifest.xml and then rebuild it again."); + return; + } else { + File.Copy(manifest0, manifest, true); + } + } + var changed = false; + if (EditorUserBuildSettings.development) { + if (!File.Exists("Assets/Plugins/Android/WebView.aar") + || !File.ReadAllBytes("Assets/Plugins/Android/WebView.aar").SequenceEqual(File.ReadAllBytes("Assets/Plugins/Android/WebViewPlugin-development.aar.tmpl"))) { + File.Copy("Assets/Plugins/Android/WebViewPlugin-development.aar.tmpl", "Assets/Plugins/Android/WebView.aar", true); + changed = true; + } + } else { + if (!File.Exists("Assets/Plugins/Android/WebView.aar") + || !File.ReadAllBytes("Assets/Plugins/Android/WebView.aar").SequenceEqual(File.ReadAllBytes("Assets/Plugins/Android/WebViewPlugin-release.aar.tmpl"))) { + File.Copy("Assets/Plugins/Android/WebViewPlugin-release.aar.tmpl", "Assets/Plugins/Android/WebView.aar", true); + changed = true; + } + } + var androidManifest = new AndroidManifest(manifest); + if (!nofragment) { + changed = (androidManifest.AddFileProvider("Assets/Plugins/Android") || changed); + var files = Directory.GetFiles("Assets/Plugins/Android/"); + var found = false; + foreach (var file in files) { + if (Regex.IsMatch(file, @"^Assets/Plugins/Android/(androidx\.core\.)?core-.*.aar$")) { + found = true; + break; + } + } + if (!found) { + foreach (var file in files) { + var match = Regex.Match(file, @"^Assets/Plugins/Android/(core.*.aar).tmpl$"); + if (match.Success) { + var name = match.Groups[1].Value; + File.Copy(file, "Assets/Plugins/Android/" + name, true); + break; + } + } + } + } + changed = (androidManifest.SetWindowSoftInputMode("adjustPan") || changed); + changed = (androidManifest.SetHardwareAccelerated(true) || changed); +#if UNITYWEBVIEW_ANDROID_USES_CLEARTEXT_TRAFFIC + changed = (androidManifest.SetUsesCleartextTraffic(true) || changed); +#endif +#if UNITYWEBVIEW_ANDROID_ENABLE_CAMERA + changed = (androidManifest.AddCamera() || changed); + changed = (androidManifest.AddGallery() || changed); +#endif +#if UNITYWEBVIEW_ANDROID_ENABLE_MICROPHONE + changed = (androidManifest.AddMicrophone() || changed); +#endif +#if UNITY_5_6_0 || UNITY_5_6_1 + changed = (androidManifest.SetActivityName("net.gree.unitywebview.CUnityPlayerActivity") || changed); +#endif + if (changed) { + androidManifest.Save(); + Debug.LogError("unitywebview: adjusted AndroidManifest.xml and/or WebView.aar. Please rebuild the app."); + } + } +#endif + if (buildTarget == BuildTarget.iOS) { + string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj"; + var type = Type.GetType("UnityEditor.iOS.Xcode.PBXProject, UnityEditor.iOS.Extensions.Xcode"); + if (type == null) + { + Debug.LogError("unitywebview: failed to get PBXProject. please install iOS build support."); + return; + } + var src = File.ReadAllText(projPath); + //dynamic proj = type.GetConstructor(Type.EmptyTypes).Invoke(null); + var proj = type.GetConstructor(Type.EmptyTypes).Invoke(null); + //proj.ReadFromString(src); + { + var method = type.GetMethod("ReadFromString"); + method.Invoke(proj, new object[]{src}); + } + var target = ""; +#if UNITY_2019_3_OR_NEWER + //target = proj.GetUnityFrameworkTargetGuid(); + { + var method = type.GetMethod("GetUnityFrameworkTargetGuid"); + target = (string)method.Invoke(proj, null); + } +#else + //target = proj.TargetGuidByName("Unity-iPhone"); + { + var method = type.GetMethod("TargetGuidByName"); + target = (string)method.Invoke(proj, new object[]{"Unity-iPhone"}); + } +#endif + //proj.AddFrameworkToProject(target, "WebKit.framework", false); + { + var method = type.GetMethod("AddFrameworkToProject"); + method.Invoke(proj, new object[]{target, "WebKit.framework", false}); + } + var cflags = ""; + if (EditorUserBuildSettings.development) { + cflags += " -DUNITYWEBVIEW_DEVELOPMENT"; + } +#if UNITYWEBVIEW_IOS_ALLOW_FILE_URLS + cflags += " -DUNITYWEBVIEW_IOS_ALLOW_FILE_URLS"; +#endif + cflags = cflags.Trim(); + if (!string.IsNullOrEmpty(cflags)) { + // proj.AddBuildProperty(target, "OTHER_LDFLAGS", cflags); + var method = type.GetMethod("AddBuildProperty", new Type[]{typeof(string), typeof(string), typeof(string)}); + method.Invoke(proj, new object[]{target, "OTHER_CFLAGS", cflags}); + } + var dst = ""; + //dst = proj.WriteToString(); + { + var method = type.GetMethod("WriteToString"); + dst = (string)method.Invoke(proj, null); + } + File.WriteAllText(projPath, dst); + } + } +} + +internal class AndroidXmlDocument : XmlDocument { + private string m_Path; + protected XmlNamespaceManager nsMgr; + public readonly string AndroidXmlNamespace = "http://schemas.android.com/apk/res/android"; + + public AndroidXmlDocument(string path) { + m_Path = path; + using (var reader = new XmlTextReader(m_Path)) { + reader.Read(); + Load(reader); + } + nsMgr = new XmlNamespaceManager(NameTable); + nsMgr.AddNamespace("android", AndroidXmlNamespace); + } + + public string Save() { + return SaveAs(m_Path); + } + + public string SaveAs(string path) { + using (var writer = new XmlTextWriter(path, new UTF8Encoding(false))) { + writer.Formatting = Formatting.Indented; + Save(writer); + } + return path; + } +} + +internal class AndroidManifest : AndroidXmlDocument { + private readonly XmlElement ManifestElement; + private readonly XmlElement ApplicationElement; + + public AndroidManifest(string path) : base(path) { + ManifestElement = SelectSingleNode("/manifest") as XmlElement; + ApplicationElement = SelectSingleNode("/manifest/application") as XmlElement; + } + + private XmlAttribute CreateAndroidAttribute(string key, string value) { + XmlAttribute attr = CreateAttribute("android", key, AndroidXmlNamespace); + attr.Value = value; + return attr; + } + + internal XmlNode GetActivityWithLaunchIntent() { + return + SelectSingleNode( + "/manifest/application/activity[intent-filter/action/@android:name='android.intent.action.MAIN' and " + + "intent-filter/category/@android:name='android.intent.category.LAUNCHER']", + nsMgr); + } + + internal bool SetUsesCleartextTraffic(bool enabled) { + // android:usesCleartextTraffic + bool changed = false; + if (ApplicationElement.GetAttribute("usesCleartextTraffic", AndroidXmlNamespace) != ((enabled) ? "true" : "false")) { + ApplicationElement.SetAttribute("usesCleartextTraffic", AndroidXmlNamespace, (enabled) ? "true" : "false"); + changed = true; + } + return changed; + } + + // for api level 33 + internal bool SetExported(bool enabled) { + bool changed = false; + var activity = GetActivityWithLaunchIntent() as XmlElement; + if (activity.GetAttribute("exported", AndroidXmlNamespace) != ((enabled) ? "true" : "false")) { + activity.SetAttribute("exported", AndroidXmlNamespace, (enabled) ? "true" : "false"); + changed = true; + } + return changed; + } + + internal bool SetWindowSoftInputMode(string mode) { + bool changed = false; + var activity = GetActivityWithLaunchIntent() as XmlElement; + if (activity.GetAttribute("windowSoftInputMode", AndroidXmlNamespace) != mode) { + activity.SetAttribute("windowSoftInputMode", AndroidXmlNamespace, mode); + changed = true; + } + return changed; + } + + internal bool SetHardwareAccelerated(bool enabled) { + bool changed = false; + var activity = GetActivityWithLaunchIntent() as XmlElement; + if (activity.GetAttribute("hardwareAccelerated", AndroidXmlNamespace) != ((enabled) ? "true" : "false")) { + activity.SetAttribute("hardwareAccelerated", AndroidXmlNamespace, (enabled) ? "true" : "false"); + changed = true; + } + return changed; + } + + internal bool SetActivityName(string name) { + bool changed = false; + var activity = GetActivityWithLaunchIntent() as XmlElement; + if (activity.GetAttribute("name", AndroidXmlNamespace) != name) { + activity.SetAttribute("name", AndroidXmlNamespace, name); + changed = true; + } + return changed; + } + + internal bool AddFileProvider(string basePath) { + bool changed = false; + var authorities = PlayerSettings.applicationIdentifier + ".unitywebview.fileprovider"; + if (SelectNodes("/manifest/application/provider[@android:authorities='" + authorities + "']", nsMgr).Count == 0) { + var elem = CreateElement("provider"); + elem.Attributes.Append(CreateAndroidAttribute("name", "androidx.core.content.FileProvider")); + elem.Attributes.Append(CreateAndroidAttribute("authorities", authorities)); + elem.Attributes.Append(CreateAndroidAttribute("exported", "false")); + elem.Attributes.Append(CreateAndroidAttribute("grantUriPermissions", "true")); + var meta = CreateElement("meta-data"); + meta.Attributes.Append(CreateAndroidAttribute("name", "android.support.FILE_PROVIDER_PATHS")); + meta.Attributes.Append(CreateAndroidAttribute("resource", "@xml/unitywebview_file_provider_paths")); + elem.AppendChild(meta); + ApplicationElement.AppendChild(elem); + changed = true; + var xml = GetFileProviderSettingPath(basePath); + if (!File.Exists(xml)) { + Directory.CreateDirectory(Path.GetDirectoryName(xml)); + File.WriteAllText( + xml, + "\n" + + " \n" + + "\n"); + } + } + return changed; + } + + private string GetFileProviderSettingPath(string basePath) { + var pathBuilder = new StringBuilder(basePath); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("src"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("main"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("res"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("xml"); + pathBuilder.Append(Path.DirectorySeparatorChar).Append("unitywebview_file_provider_paths.xml"); + return pathBuilder.ToString(); + } + + internal bool AddCamera() { + bool changed = false; + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.CAMERA']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.CAMERA")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-feature[@android:name='android.hardware.camera']", nsMgr).Count == 0) { + var elem = CreateElement("uses-feature"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.hardware.camera")); + ManifestElement.AppendChild(elem); + changed = true; + } + // cf. https://developer.android.com/training/data-storage/shared/media#media-location-permission + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.ACCESS_MEDIA_LOCATION']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.ACCESS_MEDIA_LOCATION")); + ManifestElement.AppendChild(elem); + changed = true; + } + // cf. https://developer.android.com/training/package-visibility/declaring + if (SelectNodes("/manifest/queries", nsMgr).Count == 0) { + var elem = CreateElement("queries"); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/queries/intent/action[@android:name='android.media.action.IMAGE_CAPTURE']", nsMgr).Count == 0) { + var action = CreateElement("action"); + action.Attributes.Append(CreateAndroidAttribute("name", "android.media.action.IMAGE_CAPTURE")); + var intent = CreateElement("intent"); + intent.AppendChild(action); + var queries = SelectSingleNode("/manifest/queries") as XmlElement; + queries.AppendChild(intent); + changed = true; + } + return changed; + } + + internal bool AddGallery() { + bool changed = false; + // for api level 33 + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.READ_MEDIA_IMAGES']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.READ_MEDIA_IMAGES")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.READ_MEDIA_VIDEO']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.READ_MEDIA_VIDEO")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.READ_MEDIA_AUDIO']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.READ_MEDIA_AUDIO")); + ManifestElement.AppendChild(elem); + changed = true; + } + // cf. https://developer.android.com/training/package-visibility/declaring + if (SelectNodes("/manifest/queries", nsMgr).Count == 0) { + var elem = CreateElement("queries"); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/queries/intent/action[@android:name='android.media.action.GET_CONTENT']", nsMgr).Count == 0) { + var action = CreateElement("action"); + action.Attributes.Append(CreateAndroidAttribute("name", "android.media.action.GET_CONTENT")); + var intent = CreateElement("intent"); + intent.AppendChild(action); + var queries = SelectSingleNode("/manifest/queries") as XmlElement; + queries.AppendChild(intent); + changed = true; + } + return changed; + } + + internal bool AddMicrophone() { + bool changed = false; + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.MICROPHONE']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.MICROPHONE")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-feature[@android:name='android.hardware.microphone']", nsMgr).Count == 0) { + var elem = CreateElement("uses-feature"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.hardware.microphone")); + ManifestElement.AppendChild(elem); + changed = true; + } + // cf. https://github.com/gree/unity-webview/issues/679 + // cf. https://github.com/fluttercommunity/flutter_webview_plugin/issues/138#issuecomment-559307558 + // cf. https://stackoverflow.com/questions/38917751/webview-webrtc-not-working/68024032#68024032 + // cf. https://stackoverflow.com/questions/40236925/allowing-microphone-accesspermission-in-webview-android-studio-java/47410311#47410311 + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.MODIFY_AUDIO_SETTINGS']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.MODIFY_AUDIO_SETTINGS")); + ManifestElement.AppendChild(elem); + changed = true; + } + if (SelectNodes("/manifest/uses-permission[@android:name='android.permission.RECORD_AUDIO']", nsMgr).Count == 0) { + var elem = CreateElement("uses-permission"); + elem.Attributes.Append(CreateAndroidAttribute("name", "android.permission.RECORD_AUDIO")); + ManifestElement.AppendChild(elem); + changed = true; + } + return changed; + } +} +#endif diff --git a/Assets/AirConsole/unity-webview/Plugins/Editor/UnityWebViewPostprocessBuild.cs.meta b/Assets/AirConsole/unity-webview/Plugins/Editor/UnityWebViewPostprocessBuild.cs.meta new file mode 100644 index 00000000..2d862b52 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/Editor/UnityWebViewPostprocessBuild.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0b2f5f306eb6e4afcbc074e6efccc188 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/WebView.bundle.meta b/Assets/AirConsole/unity-webview/Plugins/WebView.bundle.meta new file mode 100644 index 00000000..09bec84a --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/WebView.bundle.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 5e6d20ceb4c28439b803c8423db284d5 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 1 + settings: + DefaultValueInitialized: true + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: AnyCPU + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/Info.plist b/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/Info.plist new file mode 100644 index 00000000..186615dc --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/Info.plist @@ -0,0 +1,48 @@ + + + + + BuildMachineOSBuild + 24G231 + CFBundleDevelopmentRegion + English + CFBundleExecutable + WebView + CFBundleIdentifier + net.gree.unitywebview.WebView + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + WebView + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 25A352 + DTPlatformName + macosx + DTPlatformVersion + 26.0 + DTSDKBuild + 25A352 + DTSDKName + macosx26.0 + DTXcode + 2601 + DTXcodeBuild + 17A400 + LSMinimumSystemVersion + 10.13 + + diff --git a/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/MacOS/WebView b/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/MacOS/WebView new file mode 100755 index 0000000000000000000000000000000000000000..19f2a22e8fb8552e26819596f69ee8fab9ff21a0 GIT binary patch literal 270288 zcmeFad3=;b@;}})lY|5UlPH6rBH@yN90@WSB7uY?Fp&vF!sUTsNG8chl8JM0h#*R$ zj*sKwjqa+T>kYfCC+e!<`@Kelx)3RO-7nErb!bVY^FxzZP-?aFT> z+N8h5OE?i!mJ-OaND%8(j+2TCug|~K8wGXcceq}n_9_Z0$MZ{8{E`%2@~fzI)-^gk zfOX~9)*uOQRZOT%UyBg^J3_vi%E}7o%1URWufpe8+Ld2bqpVq{6h!4t{GyCyrlO+K z-QbNWuq(gzizMBZiV2kyy6}iER#do`UQk(4S?BP28yxk{uKX6Qk<^;1q?dIbU1e4L z>Yct?Vt0i8K2-cRpC~!#82@TZq$>BzCEsPH$ea5Yp_xEiY6AnMAm>_J&Gq!dNvt~%3sQ%dv9h6z(qQB*Lm zAP%F?uPg=YQYLZfKTV-4Dr(#@EBS4H8x%dwSJh>P(0mxKna+3 zo+ISjpyWwb^McCp{OC(XteyDPyGdy-Uq!XQq4EfR8~-7R$rsTdmE-xPNO1J?$a&FO zf14CPUHMdw=NDz9GT>JoZzx^IW7ETuuthPU^3nOl=2@M~>+8yI$>WmWK-E5#yYNd> zC{ENoYxKB%?(X=lRQxhkB`U}B>l4omdBx9F@2GK-DR-4`lj5h32bGUz$GTt9omE$U z_bGlEw9w#>%JK3^XzWw*^E&H1&T6>(uKe0wlC?N~j6&IDvQ>X(fZRuQI@=sYNKJmPU39&AVPzQu)1fuajLS(?1Z3qcL@D`+- z(6J?-3$X|bEzJpC8Z{l}8w2C<+U z)Cqu5J6CS&{ce^k_vdXxcNtFq_4c$8fTa??UI&SuS7qd?l_MzEbj|@-1{%x*grKWfZtdd6yv7^$;<%nS~f@IEuV8-2Mg_ zJ$l|9^TG)+yyBk|$6tt#P=efkuV+G?YbpKHiZH%leAO!Cf#g5ZBiWTMmzdTLYwKLE z@|07?XRB&N-?<$-TwP~+Ike-a$G6mVyy|+dfx(PB+IST-HvYzV6;6;i$U=N;<&#FVf3?7cJTYpM=K9#v_)8Hxa=ufJ3c%0W;ViGBQUD%h{6Xon5GW zOI+p23}0Pl3lx?`ET>AU-9+V$qdFFFd*IPV>VdY}24WkOnFuIpQd_;KIQy_Y;9Vq3 zOORb6v$e?9ayD-TF+C}c=`(kbz{PEly^?UoINY5Q*8_^%7L%MNUzikfvRuu_8JO)SL?5`LS6Lt*nZF0KR-rZvp(a^zrI zK=9|M(&7eUQf*YxlqC#}LHERL=F zi_~;rf5(-qc|yXp2B5KUf}}kdvGj^7ej2g-a0{>#bWW2f6CT5#Is7>#oE%Q&+)4a- z8hw)B5zEu4irjXrdos5zV!2=DFyeQ}9F%h-^^AhMvpz+rpruMqu!v$Zbpe%O_r=8mJI+<)OPiJIrQaa@M3+Q!hG2Y2(&QjQA!3c z;fk%1*MOc!UMY}RiX>v0LkKO;Mlj7<@)(k!wUIX;&dhe%wMO!p zZ#>CW*P5e1AJPyCMoYHIP#C)0~qK1B%@hjDlbvl>l)`3SXzW$|gM3Ogx= z;Bv*>B~k2+?*JB+Hc=9w#SQGGUZWxI$o0${u_Gqn3gjuwMF|qS^b${)bD2pbv~XUR z`GPG$EqZjS>`{?a-AGVJUV9c>L-7m+ChQVJ7SttaZ$>IthrvXSj=Vuhk3SdLXS|I9UaV^icu{Fj#sn&Rw_K$c5F1TBTP5cS75utcGHMXgzf1I>fuwY^}7CmIskZr(lAUlv5+yZjg3d$L1R(qKEe$V#e zCPt+F25LjgTLE?;*$$G3WiG*>_jl$}b=q#-2RK%q3mr%(%4;neL0eOzg1v*hKv~id zTln;N3O0JDOQ@Ktit=Ou zgZ<5;Jd_Et^2gV9^)i=!ejkjdB=82#06_kpBp(cN%jssnWogrGa0d-y4~I-c%%^t#eqi54I6wIEk-OTQ1jc zGH5?MwrcztBBe74EJ9;N4{i&PxIbBh(x(8bOo%eiL!R#6vz4c1C$@uMpupgvyww1jr~2)Q!l5N}fS$ z5Y8U>AY!?UnLjNzA+X5E=)58?Pp-pS(MykW3^dRt01vX-NieTVa$Q+AX&@%Ze)c()k2>)k=9}YaJT>0G_KqLL`PLM&s7oj}t z{}uIjrIK13O#Tr(Cm{*l1t=K5MK#D0K11Ax3iiO7Z2&*dU|0jrBLX|^0cU%}GFb7u zoB5GXD*QWDko%#?HOweitr_^dcXc_|+EVjA%+Te&%wbpy!l+PG^ep>W_03^zVe2jM-Eyg3hX{}%lXNzgKg zhwN?IGA*Enpyc=M)HZdaj%eVcsHYnB^i<|bo9b-jIFMU&pG-D!@L0D}aSo8- zdEqHYU})jllxjQ&;6+w~0`|#Tc|Bafd=M2msN$*N@m!9C$07}%K*P(G*P!pyKmqBX zD67nE4<`|lOnEpUFy7FRSzSHa`p<3T%V@n#qH1u=7=z*efT87r7)rha&q|>7h-Kf^ zq<5NCsXzl$zXeIqvH-Rav1|vHMkk+Y@uXS+v_7eBWh^AQg)*9TI&i3Z2CDL;x`@Gf zPr)?+I~G{PKLi8EFLA=mek$<>f&>?Dv;|CTm$Z3?F;0Qgq>Uu97qN{L4lKbc04qBc z709Y^a3GD1_yh{EQ9uX^C;l7?S!l4&gVgRC>=|*A_N4|NtX%@b&11izS^q`ope4xC z{)cSw1<;Ueb0M4T?E%nW(Ab4+0Twf3D-+>(mey@pHzh4Hk*heYQD4i^xLnL_F>gn< zh{HW-trDEViD9S#X1}Hl)_4{mY>h4axt-i58Q!tsetM+yqJ~-mmwQc=9yvrvO3sqxm0@Q_6(wtX#|p>t+z6 z|4fGdOD6C$I{=49?15h+mPdhQ1H*{@1NqWs^b9)^^sJh!$-k9FyPRwk&k0bI1iJ*3 zq|;uCC)~1TT?!f2AtW~r21|9rA6SanR&y)fLEz@}(pg-0$P}l-H zs3nUcyA5Pe0rg$8{&1ga>H8}wLn`K}uig>{ z_C26zh^izDWU&t*So6E)rhmr<_=X1o3tH|bH-&RDu%K? zvFO~L$Vfhe-NXleg=o;fh-EDi1uX}OG0VJ|P|{?V5)PXGtcX&IX;HP9@sPQQv0z__ zOsTeYoeAKYy5-w;N!Hx{hy+b+oGZf`6P{K^WWhf~1`7sz+L{=v;5f&a5T=6dY zb;VoR!UY_u-+#$xp?w_!0ImkW#rk#OGigt%)5@JThMYBc@@oi z18S1>{RNc7bA;l_F8&fA+4|Np7S^{08KtT-iH|PQVpaX)0;z|frBe4=)@;ax}lCqe~_Ny`^t1qpGc;)sltY>-PZGd;J#jU3>i(%rf+P zD>)d=di$NS*Y_x9U9Qk(O!=`o(Q-l4C*hJQq|T_vt)s6#PSuv;@o=%2KUjs0mJiA z@+F$}@y)WE&#Kxp;{~`ANbcrl#-f{>kfClWFX$p(x&Rj|775%{ngo}jz8*TAudts? z(-rYol7L<1M9?W$dAcHd7GzOH9M5fndzj=Uv~4(A@R5*`fvRWC`6+=wE% z^=tEDvA1-XTHaYa9j)iw0ok!XqHYHJ&O-t}H6Qh|v4OxdiPq3tsEn?npVgqH9@7C3 zQ2XWL#dk+WjzJY&!jq7vW{>Zt*>`w8SUv zFskUy-itgnd)Jg$Z}}q4ui(8@w)+(E(nk82v{fC?97YA%Lp#vW#yFCPwpf_>ie@ip zcvECuLM+q~&RF83S;sLuoGz%N(&+Ps1CWjUpQR5dc=hHr`j<7#BJ|@K-AKBfW)k8l zYDOFR0+Z^J+M|-*c^gZ*9xk?vq?u7k9|Mgp>1D(R&p(;n(Ij<%A|`1$(|;eD6E7)W z*;)TNHn%v1dATI;3-mh?gnUcsP55s#3Ja0r!kN~R^=k$SbJI=(_bu->63mZv5Jw}< z7JGmmWgfVgIJAap(UcsGlRySY#PXa%l>$Xvv5(eK4=B_`M$wI>O@K%NplfPfTf7Jk z7SHy#`-)I?1 z9}JfuZ#`*}4p^Cjb8(s>7%RXEXS(w&=d4iVm{IbDoK#BEV zao7Y7JdSy%N%YOP5~B|ymctiGQPxJ=I1e?UHg=+K0SkE|mN$`sdH|1DUSuGSa=BgZ z&~^f8upZ5i4eH?gB?T=w{D1>H7J53F+3A!+TU^U)E!KC^Q2njc6ilHCl~H~g<*uVw zLMZ6%)f`HtDdZ6h7Ep-3r(m9<>M>L`XnA-Yar_T06=|yCMpaR>o(vjYlV7hAqT?hC zEb1<&y1azF2PpbNa^dLK-1HU8NykO+M=Z}05|YehhB$U53M_V9mnm*j6eB?)=gBF= ziLCcBpm@E4eXK>EBY8e6YI>KZ1#j~2zJ&&Gq2jXtbgD~^|2x2S!6#E?wWqKm&rcJ+ zWhwH0A-mM9Ng#(VT6q~@T_k{zM88_HEa0c4pzIGyo$-N{(1`h0(unHt%Yaa=1yl>y zD$nA>W*xgtVRNElPNjCKvo``IWrQ*}pukIUTjVte3rIiEnVbKLrb;zGo=P}T7%gE&$0*WUql~sQBY6t+?F!kmi-DD! zk*gR>Yb(w_UM(kgJ$khd(9l!jS9*tn1a4>8QpAnuc4@APf5lxI^I3~}nsKIA) z=w4g+#Bl%cG1x8AksZ&i)loFfu6insCpTwoXRN7n@K=H zq2oA#6Q@(_cK&SP$S-u9HExCD?OG|>4z@sAUSv5b;T4W0D3X#(2G+crZYBggG9E#{ zz(hFz1S8f|UK;rd&6<59jn{*O#<@QVx*crAy7XC z7#qY^WJ;UyR2Ozkdh;XItG|6cRbQ{F_ldUoJmAcwR-q1Mlcd%Xf(I=ZTtvL#kLdV> zBZGWJ*9JPdxGVrEk0=a$7_gv6;~B`w)uo*W4mR1BkXRR(h6hqTrqC}I4D7T@a{!6; z-y=sAhwZezYoj7Rnm~LBrrUKiIB!8-nMjl~H~wkp#;myUCZpfSkJK+I!z5XQY8>B7 z_rv$xuE#5zBOHLCY)ThSas9&>J9>Q#C%~#~quIC+KuE{X$zM zojecCf3*U=ER5E`rCToO$p?goM`-`&kMTSMch z8s8+o9vB|otAN62@r1aX^0Hs6Rn27pg-tTb3MEM~BbLR0Vleeibv!tMn6%Q=YAp?X z3x%J-6f`rbe=h-#lr1c;uu$BsegXfl$}6th=w|g>^wC1s zst?dd8+v`)q;(x4QH?mV{Zqdl61U@dgFIHiEIFp8Af14_klrg3pD2-Ok`a(w;*OQk8W<4$pM)hLShTtmchX! z-ZelRyMYYmPh7h-bR4=WC+t?hZNVFfi!B(W8x{rI(|BdX)4%3?lyTpaYwg%(2sGP? zWApRoCI?9v0uXik=;&;P`wG+BBd{ReI!N_cZaWASZT zkQq!)_9fGJ%o_(9t+@@N*|iaG;W0J!+NM299C7Z`KrC8Avp@zTMAoi7O6>U-+Xp!J zR|I320PAnF)W=GjPO(l=Br^s15O61%c*R)?Y*R~t`39OQn84?t%<~m@<<&7Rf=7Re zEwalt`8Mh_1R#x>o5$c&>Jc4*_GeP(J82o~KR3RELiXlX^f8>?%Ibyk;Z~RtDn~3I z){`2z?}!7rkZ7j5Q280f?fyaL$KAfYm2(~S0!f{3mOBSTT(amfq0ahy}B$@C}LDD~}wuTm>Z3qwG7XU)D#G)Jf zwSgC&`_~aQYr5%Ok}%lJ^ijKIy9%)NVVoZVO9=ZglH%+yU^YCPv>;Q#I8&hrsJpU!2miQSb(ntoYrg7(l#wcba9UqLw zb&F^55w^f1gfn7~hb};UdF&(m8?h{6j>1dj()4`y#G2k$$B=8ail+An1O$g+~ zx?F@vcsK}CqZgJ2FQrML<-H5rD9(o3to*#WwYVK5@KqmPK&ojCEdy(c$%Dn)N?Hkp zO4gIWhqMydN%u~-E7E)>rE3b?a1Wk=SjJ)QcTk>A)E~eM!7_o1PuY=<1};QOo4+tO zA)*om>kz-2n*zux5ub%@5HW`mB^Vli0#88rK2g4Ew=Gi_aeGX})mprW7Daj+lfoI% z`Bqw-i2rpd5$OeZhzR*!gdniMQrgsG>>*zc#KZ9(TCc~n8k`};yM+uxvkqgi5P02B ztf0AU*%ma{5)UQYQx^*dOC%P%LT7u}3KfA_s$bJ{~N&}xGmhKOReE$NGf`0#s;)FK} z%#ZEqd^&^uM{!%nV)p;Ye9M{o_V5+tw>0YuwWJ6*W4hZyfu-)==pBh&;WVCb6eey4 zVJQN6vVNmg4g2XHC3w+t7}|jPP|k-`B)g1W&XmV^ag6Z%uBjBjE-qNcea?gS%`@Bdy;n+k+r3nS&N9kc0Vz&K=Rk zYRZ(!kox88xft~nOfjq@*YP8aaVK!yPk7JR8oCV}*f8@xgi-K1f$Ox`SGI;O14og%EfkursN_In@9d~HeSx>kZ z-LwS_o9T<7AWz;@hpzt|7r=kkW@fGb!DMbS!<(=F!Dw#6Vu_5w+)Q690@M5n zBVD9bPAbxt(bh3k2S(=1c5L&;yoA7%Qk(?N$^Z`i>feOs@PY?_-;%90VFb7dXkqa1 zaq8_JD1sjsMJ$;tT5D(MD>5Kmy?@ZYYNaEU!ppM@OKg7(;-I_;!ogWUihv# z{lpQ}^nGKak8|{n5Gts!fy=?mhTz6R*WZOe|tUX8+IHk=}UcoNma=}z8aG9%pA z2P1ZYZ!D4hT8u`dqY7;Uv1;!Qka58MHzLyp{~Zaf2K3~oyVW8s+>gx&>qQet)lLYT zt>xUz{*VW}yH3ZI_*w`x)y+*c2AYm9l?GY^FBagd2?E6kIrF(TzjJjXnTuxKcPY7x zfeddAZDNV@I-pJ9JZo8KiUA^)?!RtQw{?OZlK#Sv_6Jzja2);wY%}uWgAvD~X*ZS@Qd%QtWi!DM)Aa%qOK;SJY zJH0{CmnI11afi9@@})9AumEpODt&Kr@B8CgjZs!PQ5loxMRph#DXF&|1-ZG4^NLxJx{v;pKap*)a>f%puL@^6m zE6g-MKC4K+eevxjI0>@TD=YLq3*K(gUm|<5H~7PTl}~wnF^Sh2IuBf-TfAEj)#gn_ zo6zv%V8V_W-sM3>cq#~4E!NGzVm9zR3If})7+rwCKVn?s1zvW-9}&2)JAuJabP;&C zlH);ejR_n?n-DlDPT-%;lRdi(gi_!`Yhwbx2W-a{QiW!E~_e15Op2mcj@rUXWXuc_sB`k-!QWD(JcRdoCy$<*c6M5bfe0i!bSs8nIu)jat?q6>m$bP;EnUo z>8-`hk%E8H$QPOC94OJ2P?--n_=3k5!gKIX0?t;MKe$`k_tMRQF+vsBx%s-2~ zR=REd>Kx(gT{OAUXKqRXAAQkP5#rb2f|FkgblyA%jRc%sL1snF*p(r6rz0C>R@$@qkZakN9H@1wx`Ozq+iv(5|@eV9d? zTh>#-;Z{Szv&6{UTtcTcnl}q7#+T+~hOQb&-6+f|(wwhnKX021A8`Z3y!gc|boEm> zJa%J!`+~DQ$mdTb0rL}J1wTHA+&CSvAWL364O0)k<(`9^8tIc`W6)s zDJz*#hC#oXOr*ek_KbY+`rsm3Yqx+EnffHO%(1U^FW{tQ3caoYmI4Dy5SbEfMuuHm ziQP!zV+0R6w;l0%A@upCLm7(mwxTiiwfLH)|3oY& zfd;EhJI3$?WN8d}?~K`BMiVZ&!2ykWObOu*Eu@7CJ_y7S%Lgc7B8T3$#OYE!ro%sYECwfTL4r9YXDBlH$uw8n)oVF)p9Jsyf(B;?3g%&0Dqu8( zCFU{DVZM!NGcVk09zA~()#m3u+abUlkm|-8eZYc7EPorSf^BF9*H-1`NJk0vgc^*$vN(X1h^cL5`@5q2RXhq{JxwDG|k zVf_iX9D|NG6KLYT%77V}d-C%Qumq%y;QeXnMZhuaBP2JBJj_ClzkJy$ZttUB{KE;w z;rSSOJjLHb7b@fcgg)K|wV3fDo4T5t zI=M?z&kz+gWjV5`J9XTT^JE9VJEn`F>!&Pz$dCiCVLbpP3)&hwAGHhCt|0J{jJ02J)T}S+prs6=>k0>S&>X6T zM;bJ~FF{IaR?h>?W|ajr7R9x5Y?Y<$C6)UMF8vgK@eHg z$(KKP_M<~vhyVk5UwT(%M$f4n7%F&<&n~27oEH_~|J7mCcc{khIPVOiqURa6)#ITbvZ+fGMNh~FeUM@?B&gOl=6ONmo(r6Kf% znPB{E6o>J}0Mq2hNr1Kk*1*HJJ<&vT1GH>Bzx1U=Fcv-++XikAz;K z>NF(hNIrOO(s}xJ+{=vR5$wS^YG!~<#PT1##Pj4elxUv3(pUDl>+J!)h5cf2U=O}J zF)$xr^Y9&Rw-lM5apD{x?;3PB^l#LJ{Bt3HQJ@O@e(dm*ivp8IKuPuBj&9;Gr6|x* zq!qtjq?OXM73m8W!WW=s7Aps~32>JiwQBu{SjMuL--j}lnDpJ5bfY2fSjpqP3#lQ@ zwx4G+&)2z(6(US!ntgbiY~&{>kloYL@ecB2`}Ga)J)**l+ z{sEY#@F48_$)3rX!m>gpl;u`o6x0n0y!Q)Iw!7Alg{W!fZo+|#?o1Dden5$xDcj~$ z&_|!Ja8Qp1*oltdM)!v@K(9Nf@-jrg*ptyW%eKzUz@B^>o|5}U@=Y`xSpHnH;}cxY zz;^6cOvKP=ifi!2s(7yYgFhtQMs(u<`L9is6XR(d81nb2Mjs-;MZadRJk74Tlkt?> zx6B?0*r^suv9_h&Ow0$@lO?uiCU; zXKD#_Q{4^+MPCw`L*J-4r6lN&lxQ=P?UN7r`xH-Jk>(%T@n^()#5K`2^ia63eR9Z? z%xr9dcKhTue{!id4R!r(XzSU5P*Uh^Y{O}YT2UaKG$z|L?4xF;m1y*>Fp-v(y}rSo zqa|#iZoS&E{(!-j(#D`~%!?pV2Y^L^kc2~JpvOk#jc^Llw1Z~vH`tWgM`ZZSILUt1 zJbvejB!0BH=FDJfJvF)=GGnxUF?mu;g7YJId!+dd^NlUkjQRW>)#lvZExo~PEIbml z{L{2=%;zPB`r|;H&mM;Ev!%Rjn|#P-o_UCZ%n!h^&oUt_TKPk(#%8W^rDHgKa`7 zd%FF^8w7R>QlE0Yr7OUDATtzb!xwh#2<4fZ&x4B23{LqDU=g|pylmPTwt$JQC?-w2y_tZn_2ox(S+R7}=N(1O@0(u5j9zqNDz@AdvzzggE zWB-HUBwJw8&hTgaHQu40@HPhfdi_0kC@tbkvume^vLP{o7+lT9)~{1ZGoPe}#sCDD!Qbzo zS>*RH><^`aM#hAppW!MfB;19Rt)4wdh@Mg6Q18_uBH%HvF7J#i^P5fYRWX&`Z49UV<2=9y6nKDr5S{NSoE08;=kkDh@q zjZZ9)*$iZ7$ZQs}v_knO%bHIWpUbEbVa2qc10mM#&*d8)T_cmNvLQjILN$dXHz+GS~(EFF-gdRaOoOKw?W-xOr3_i39T_CclBE^0 zlrKxGWT{M+R?E^7Sz04Y^ynQtFOj8nvUI5|ZIGq)vb0H-nq_I5EL|Z>EwU7lrFL1O z9V8?>AWK16IwVVfk|p{94zzx)ETzfP^|F*9OE=0=7E1V5a`y!|ZnQy1WMoqh!^;Nq zW^Ja#GUYnHX&k~P3TClCbPc6iqJKIibm=nWPsM3D*_m_-I6OuWI`Re(L`L4kpBwq} zR{s18f8NfYoA~oi{`@O{&gRcD{`@2K_b}ys{P_TXZsE_r@#i-Fe270E;m^nTb31=N z$)Eq^&u93vg+F)l=WhPo!=G*VvA}i zA=_D$|4ioHlz(65ucG{3nZKLzdt`n$<)4)KgOvZ9%qPReb=)cQLnwcf%%4vAt7LvQ z<=4vmTFQH5K1lg$&PO7KRB@^hrzTd3oP5|_#E>qsaqlVpYJ;$w*NSuJK$#4G(J-yg z)_&s5kwUoBTk%!ghOt6yF{A_bw80|8Ue11KfD^peXt}GO*k!cb)=%s=rr+F8d~ckI z(~CA!|10~6_e>`V@snxDV|~S438SvWAztN$EfK z6;CA%_^hvZH|f}S`-(4^1nAzT)a+#KoJE{Q%sUk}1T~DgBXsHzoC- zeZ{9Kknj55=K%0U@4-S`)#n2P@}H;v32Hok`oDXL*A3~{_Yxl)(ga!{z~x5EwY|ijjmHV`kg?yN zdWpAKfOFYzTlhk>>04CG&i%HD3 z95jjg)c=^oDl`xRe-r}kRY#HxNpq3s_ zFz@Xx_9URk}d^%m=rP;p}tfwv~1%1cR9 zC7d*1OK)*$FW_$JMY!#~`ab|^dX2iTxA>;lfO~q2Kr*`aKr&IcB@bxtE#6N)L5Lrd z#{+P0%7B-9lZ9jOQi$SU%7B0M7R|kB9fV$fF$fs^SPKpzW*u`~Z&8!c)LUF@NIje) zZZkj#Oj4lUGo*f*B0>bX(s&X8_ZSC!lp_9V1lx8aQG9G1^81SzY@lXOP?nxkucN0dvkRmQi9MF~`HYTFtwnPFymxwAK zCsLL5NdxwzU|s|Fk0ipiC-vW*B0fqQwKGL%y#}0`dkuu;D^e$y1G{8DY6__(KQz}K-sBX*0h%XijMY-b_Xdc&(MZ|dWK|y-)4uE@ zt}#sk;C0jBoqfd5rqq@`B9M@BppUpap&t<@r0?w`_9vw7hD{{EbFNG54e(Wob|Lm9 z606IS;8`{&9gpmDNvZ$rBmSK<0eM)>!(=rnclQxHdSMd3+iMWOzxGPK3znQbT!^j7 zROij))MxsLP_hO2%TfkyM#Cv7xAYNzOM&NlGllB>l+yp^KJbnwBfqP6{~P;=|Mc!3 z>?6MGy-U~SBw@wxL-zlVQJgdA6Ql4#&?^o7-!#ID%zE31xz!gxly;>N3;3yC z++~v?8O5u~Da*EOq8hgb`t45;Ye)P$L0oH`a7`i{!qATr#6e^K?-Rs&(}0cyaffNp z)6{w1JAb& zF!Ne!lJKcwx)9ToYsHxpxjc~PS-(4tVm{*EXAQ^xYQUlia?lPgde68cK|E#}@Q(!X zZ&Ui;6U3LMArB{rV1iBOcbY)f*6$OOsIt6ig6B@X+a&&BNWIqtH$`FBOUAw!_jinW z_*p&^f$uUcQnH*<)MRK%M02TMCtwBT!g^Ec;RJDyi3^V;q<#yHB`6z96YnGl{F|Z7 zjszSkLB=6sk`R+-;GR;GA@u`aWvJT)^Df zW=MJ1h{)iE=vW*h3J@%(-)0a?QvYNSb;YWU6U7uEru5lt5ND-gH58I5M}A@2H`-%-jtmPT=3A8>h=|W{O(m! z&>&WyOjQV$JaEudkv&x5Ckrt-WgTLkxl#PMer4he^b@q{h#4quNIsY#_L{au`;sc4 zt>gOSh+*(N{CDN97MVFaU#4j)J%mYypUe^YRIXs$_d?*-oG>6=gVcz0HBuAOi;*TE zrN2a#CaJWSN|RNZqSD?f?W59EmG)I>Kb4wQnx@ipl^&zgV^!K;r52SAQ0Z|h9jMYl zDm`AMC#dv9l@3#r%0QawNHM;fI?Sx_q^=Z@I!zRaNNs`rP$%ofrC@UZ2hBsB(I8 zmpkhG&Ro&pTtO5%Qx7`s06&1b>v%5xaColpNKPcJZQi^x$#I6uSy$y1UZ<}Rk~$lF zq!lz@SLa?a*IDCSNvJZ9yAmocbGz$+@^~DpNL0ObZ>_`QtXfDvZQAIlbc&@eU%jJI z$RB&2;c?d&VN7xr*ygIK6)U9j#Zq_GDnUQ??Ow6aRaaN)UJknPl`B_PRH4frKZdHp z%Of0BJ5)TS`{A8BZ0y3Zv&SyzT0@G{WpL!sxR<${UK&Q~jvRz6t`#ALcu5e<#k=~<2ut9E-}8)Xh(ZLYhK zOv;-}HsM_9qY-JKK_y=EK`zHGI#9$9vhiTRT)i%z6Pk3)!>_jUfblQN&hfZlz@TYt z1T(!47_+%B^xRq})QyH5e(H)e5-1kG);@g|eqj_fkR{m)-Sv&`1`NMGPSlcs{PerD zYUmca@UEB~ts+;|42P?3p1T-5@yzho)j@Wr6LKuYPrBAQX=W^4<#T$=oSwOus+Eq0 zneOS1%4IS)!|hq&@Kg!@v1oic9UiH1uPPjA`9RQ&f$FMVHGU6IPA<7U_1<}IjET=( zN#+R(^bk{y)x%owxpnCz5_W}N$?2u$6{@i@Kr&W&rMqzzVEzW5{G7h37=!Gjb{lG> zwUAM}*`Lsef)Z`}@l(|eawe@r&F;FvFgeR1cNhJkdEEiX%D+$OZisfS3+t6GUus~!G2A6bN)5eiW1 zXn@o80AJ_yIpqLS^@df_abOO|@mTJvbe6eRI_u`r*ky?-cRdD4RCydL3Vda57xY&R zU%>J&$yuMP0ud=qJK}|A! zHhUZmHE>%@A!QTQb$)Mc^mlL3hkB=|!G}WEIDG|mb;{y%1&W>WM_SDS>GeUVT#l%b zv#G|dPo*05JG_O`w@^o>vmY*@AE2M>Y=mQTz;!vRT`M_TD{5i#a-?eAULRJVx$KUh zR$h&wOOoV6!(UeCXmEOS8)+E5fDRm?)uynt}1wPCAOCP^WXlE?#1CIZ8pjxd@I5 zDk(&n=Vtd!M7DXQb{K@079-SF5T&wCa1~-nB1K*4sADH-M=$G~CGcuK?qseYw>6zt zIf0&1-02ndZok)Afx5= zW183deU7Dd@W)ueDDcCF9?eqdfY{MlkG;%3&`%x=b$R(}7SAx)FrL(Hs zzf^j09fKJPo9tZbLdKB~+p#2;!B4gHel+alFm;0vaP|$Xbhih=5aLDhy^V-T1nd;k zroJ5OOC{nLK)V1|z{@GFN5gd@W8cM5Xv~cXs1Xjsi8atm`Q?a#VSdmphMkmIfQh`^ zII(DM;Xon%ja!OBa4X|#1} zR#&K`k|kvhBw&n0w*(z_WWcM)<)W*WF16sIGP`|{AXqKuEd*9?eVajG1f*4%(6Ql{ zFhqgKP&})zu;o5lqTwtlppXgA4&m`Ps0hL9Sgs-zgwDu!t|~2DUnK;J?z*~#wN59T zt=CysjgN*Z-4o}oVxI(wy;0``C&fY#c|%nxc>@_ltin=M=X0EmK>;jE<165R-0QlC zydJGww9P8)6h0~t?Ez_XP*B4`3CJiQf%<5jq*}VzT#)M;fn|<%nwaN|v$?9^#yERs zG!!asAp3@=LKW%jIE$?q!8w2AZkWN!PO6nZK>afK!gFYmBxba;E@|+_^XJ%(aMB<; z30~gdX!O>)5rNVA%0$ssol5guzB=aum)Et_Rfmbl6bxJH@X$^MqcJaTt)?omEj?4i z$LO;T=K;C&&s5N@3MMF_k*ClB^<~tB{&m?ccSf=^Bj$q_{jiR%y~J|2liR>F&QfJtaW&uatJfGEKu_i#SPGG}ZEsj2wLsv95Vb9fNW zvXKgZV--vq@dAxJrpb}x(LFS`%2frMa=Ml~JGq!#v77>C{g@#ayWd(`zh%5qSc|X$ zu>@k&TDQNh3U1cDqKLdb@Xo4vc5E5c0Yxr0wScL!B2rH0e!(_klY`a>cY}(0W}qIH zWdvh7t14$D0_HL|*6vkM`VsXAPn+%NhUAY{gy8zx1!wcWGMUSM*6Tb<^U6&?u0}f9n#_+(_M@txoTX z5S&yxaq2^n2^m}gtv8e@b9(AsUNRx{MjZ>$2FOKLJ$;p04}Qx)6#G&r#8K#IB;)-< z41Kf}_)W9J0vuav3gB4bhZ>x9G?P>~fNfG25sxx_o!IeI^wj^~F`S1pxsrxPe@sRU zc0EoD)OiQt|5tIb*J_ALSca`nJ&sBcIQ%cYmTqdMdmiukF}QSgW_Njg6v4sY)YTzA zz*6gTH2CO@t-Fvy<;V> zYN{B|2Bs1_6jVdcb$*iwta%$m0?tt<*ov6y%YaD%U_H{?aa1IwkHY9sLhllQSckC) zlfk09us*~RJ{-p%K{2PY631zz9SZZ0Kx5_SgXd^tlCK1VZ8gpc;wlw6VcT$teh(=s zCU$WHrqpt>>?7cERN_f7g=0$W!~~em;}ehVWG-D0N2x`RisGo%!TWD9EA6&>i09*Z zP&aEDIuDYBbs3~1^f6z?YDyb0N7}lvz4Bu!iVhd@xsNV!IR{y~*I|?d@lL_#Ry!6s zd<$Xd)17#r(Mjh7?Zkip#dJb;znIXcjw};&C@U+M`h|fT~{2j;E>};yCf7QGO!I*@T2U4TbEaGmxL!$|yCmzT$7RU1cn5Q&cGP{(Ea zq?1WmVtH5CGy=*Yh$qaS25onRUkFVSoaDrsO%G{@DSmMf*$;*1_(*?LhVD?+SU@eq z(_s*e>hrYJ_bkImF4f;o%%ffC1dY?aY=kY4j>L%jv?O>w4;&>tR`K@vIT|QDQFo_w* z5yC_R$XM{$jYP2y7SPlAVUi_JK4n_dF#DFv=@>OO0Q5NxQ?I1aI`y-UzfsCWH6Lh!2mUx-T7BT~x*?*Q=N$n3kv=UAsXA$8}be_$)HmXsIgB ze3?-dIHS6*!}$K`Fy^lp;D_{89#$zA1zulO6Y)S=SL1lA@rkF1!%Ese5Q@-jh_(v` zN38;`x~BfeYCP#;5f{xyb`5cj|E_7N5sq&;PI}hsQKgR?^{zZ<-wT<#?H#qj8)^_h zLGh*mOl)tB=pbq+@%+hTh=*J!y6d=ZQ1P|cw7MgXvFgr7z(VfBs5koR6VI2bM=82D z*Lds*=hH&30kMpo)F5h_d z0AHQCd&kFfACG;ve&2?aGg%)N-cKfDj!M|*E#DEF-WdD8=ZVS zwE{^-=?#t2g^yBH%45HbQ}Qx0x#{r=GG1P^$e5auH9BKj#;CEQGIBFUjT)VCO2#NZ zwol`{&@i3-aBzq8c^&+^oe?9x9HPR~HLw zU3FC~2vo9WbS6G=muxv6h~r?WcqK04=+6@Q&X||Ni5fgnYpCM89XNvEIWjFPe^}?s zy~A>_orJLqb;%0%hL_HA`J&Xb%VQ7TqO{9%@~36t&RNC$@`|!KdaabG_b-i!QoIjLgp)nJ*x65npSg0-az|iUt`?X0T6&Eec!JlW2`BlV01w=bibhZye> z;Py9Onh?qO$Ey@v&PQs*|Er4<&mvHIpMdxz6 zQU${A>hXa;D#!nA15H&U-u>t&1bu(oF?d=0YNSEDf_NU@8Tt=ieY*ka(|EP?5v0TM zDr~+7{P2cOJJMcwySBd<@1^4X&apmxw@sE1S$H?&C#3mEFU-dKQb=Ax*;jG%ZN+)`>{P3o~hQ91H^i+JGU=#o%28*moH0L%l^} zNgrVxnTq$>(u6@v!xv*&@D}rN=)+)<^w?m0Pt!1wuynXcczd`ot{)+c<1h|RjKSAq z@!H8aAwHcTjH|MQ@$78TYtBSr@I&w8tU~0Tj=GcaIw}lmOrA)%Cr=nwL)VYuO_tu! zaU%4)X@(GUON8-u==2`DF#N+VOedBK(-qL^>v#vbc@E?)6Na)f$at1Wda$&lsTo~Hsi-hwQh=dmCdHUHR(Ra4!wfh{Abme)%(6CsTo?I*v|GiiwK^;AR zJsSA`MFVd&NVXrT^pHybtI}k*#P?TehDyh(G)JXHDlJp#c`B_{sZXWrRC={a>AUaf z@2@J|rqX|^v`wY2tMq_MKUe7wDotpVe9}~UqDnJWI!UGYmR|m|t8|e{{qQ~Ue;KOa zy+0n>DPKk<^8NI;=^rxHKk*73|9t{KM1S9@bhFN{%NmoxRQMra9k0r93AdMkwT1rD z9+Ij4nWHjW#>mWDDvh>BPBrc?OXcZ12k39KN>6=2mU#hHSxSD3KFXgK+_=AdmG8QHWJ>a9N(zxL}Gf7COfmA>t6kSkhp$ND_3RMs=V8eAM zg-9ZS1QMEc6btsmWfeQRx{~X6r%S|7}8#mA`ij9Yfr($H+f)knpz% zJy!bP5PGcm{afgr!$NN<|G|TVpCa|$l&{m?(uxA&ZWlM_ZNZD%ByQe28ox$dLV5UI zgdf{*(~iTdp3}}ZzC&}_J+SO7qe48F+^k*BJNN|f!*Evp;DjC%&fZ5us8&4;hc+&{ zOtWp5aEwr23I78Ze%n==<1-h&@CuFJr5cv+Gz6VAV-oErTNk`G4Tk3u*0H1RU&H~DVz+2pIq zN0V>HJw@zb^1~HYEgdv^o()gC&keJTMYfDi2PIHzA0|% zZ8|owm+!mlCRVIp|99WIN(|9pd%bl1;oV&Hv(|BEZ! zXGFhS&(IipJ{Eqro*BaLPS+hV@_mJr6F2{VV#KqX0?~iBD_xre-Yf2@cW8Un zyWB@zu%R3LS3)=C;rKOL&jsQ>CT_F1t>r(Kz%1`g_=m^z4;r$pX2B1?(_pmwiQo%e z_*0~xEO+5Eg#SSozK8IecGQH@>jv%n*u{UuUp0QfU4~P;--x}BbK$QRJ??luDEQef z{w(Q#-SN3v!mV)ezZ1~myW5XNG4wnaLrVm%I26h#q%+ zdRgq|#&44Go!btdNVsnNlQH5mHU_@`h0)`GX$<~vC0%YiuZqF%yD_@{wK4d|OZjx` z-xdR39YfExF~WT{2LI_X_-kU|vt!@~#faxqG5C*@dgo5>-(&Ef5<~xv82t4y_)}uo zVPOpZJu&p#G5CAO&_6nco=2pfyd>^t;&%F*E4oANxfKH6BJL(} zcf`PdDfrI!XnlrnguvA<*XXSm`~&}&xCz=kmU|v=#`(_qJXvFn_isO6E6RRSyYdYQ z4Mh2$4cs5_d-zgKd{OMpJwZN)U7%evPURjUpQA3(?r!1d9w8t1xPyCxeBA91_X_#A z$4A^d0494tW~?*Ne1k`W^0l^0`*@^btMW z3+1!^a_t)Xb8nQ-`GP-I_%}_~_zPBO*YI=il+Sn44^9;RF;g_YgT%+ok1GYA<-)HN z{GY`>rkva>_;MHiCBYBBQio&szZLv1S84ooi4XU7`Me_dGQo3?m(S5Mo;^qK-0S6Y ziSVy9>6)$aF9)=1(mPr3oh9E5o_oT4`UdzP`{AOe=NTI2r*ioiJ9E#N&xw*QV`uIm z^Ep=RX43n*;75pk)`))YHS=-LkDvUr#^=D{RC)888r~}7oBKuH2Q=q{+l2nHj4L*| z=-Y*Un56qP7yVtKr%L=jbkUoH{;Swwmy5nj=vRpTeGYmm;6Q=|7GMX!ss!w_Xsw8-#v})Pwt_-T7L|$A`jyp}5aWc-M*B zUEF=b|A2&BDEPjj_f!ezpAv4agmaegO%?sc;%*f^V+4L&`23QNL851*v@?mOeG<8^ zq`uFP@Xb2#49R!1{dUl!r-+`Lh5n+r z6{4?~@Y$kglekY_uI1NBI!_h%3<*C+;`t9L4`+y;OQoKkFLt<1_^TxzCyCxM63;E7 z$0OnXOZZIx+FkTNAa*nBy8OyHpU# z`j(5HCUKvZa*!hWjteNWSm#RoPLp(8C+?MEpDJ-p|84qf(?6U3*z~_YN<5Mz+;2o* zv$!>aUnhDW7dx5$b-UQ*JxRwHf%i&!w@CWW6nd(p>+hoX7=f#W?`F|ISmf`L{Jl}! zKS(_GiTfwv|Bu+kBk9>B_WD@#w2|=N7JL3t+`fW;PtyB~$eHo*!@_6A!DhT?#&>3X zX2xS?{AI>lW_)GFHD(-R#w{;PcxL?3S70-~Fyjd`PB7yLvwm#Wi_Li4j8m=u@-h31 z2NN^~)*gg!`xCJ=gyUB}uHoi`=nK3OE*A@0_Ix}e8z1NVE(__RAo;9^17yXfKEIEN|v2TM98iRGAYhvJy{?yGzQ*`$=qdx}Uni%+EQMYERc;Zgn0&xq(EfBXr+yZe6 z{9kW@&b{hA#k~t^vWp647V7^q3tRpIhiBI$pEj;$@XW%Z@kRNE@K;b&P<&W^O=?ko zO>#{Rf5n~hXBNyZJSG2hg$~zoS`=J32gIrEf7f5}@cdchW{xXfoIf-6U%_bwC+8PG z?wNoTrq-4Ef>xP-o@u0)#Hg|G*?^VwY!!J!>z#HD6CO$n6Uz@@xjJe+9{$h zpRod|k%8lNKfd*6#iY1mwOv9w5G8idWxgZ9i%H9^Kuvvm9&XveT~O-2Q+<2ZwDP4u zSb^=2#)>sw@6~r;2UbsZM{L^6mMdy;NfQzuGe*;9@-A=lC4s=URv25}(v&MzBMhS7 zh2G@soV+#JoOo_;vD+AO`zVp$;9Kt@w4(xjc8%m9t!2D`i=GA^;egkUFI*xWrxfC zWhG@5C>I+;Jew19z@j+tGWAScaBKyd9~P6aj46`&yhsbIu4Asxg!b9fX4VbKtzKN` zuRhDKuE!oXWc(85pCaWBE-jhkSKpQK3+6W-QTJ;-9@>>3kI_|6F{62Cp60Rxc>wZNvU0gTwS?A>z|!qT1nA?7H%a zRn?^>RqCQ?7{>b~)wWRa{%qMiA=cx&;p(B){Qg>!{ACsM{Bsxcb13zBbCy@l0eW$% zzZCDOqw3UWvvT5sW5x?g5uH-?xq+gI1%;sxD3BP|__<%cw*qTuFc*)yN22u_VD8CQ z?;GQjCV{<9tGe{*>lI*wP|;b_bpjyQQaqC1-6BvWOTID3N}_SP%8=37h^|hoTtrnUR%(5FcJ42Sb^lo$e{H?d!x%o z;RF~7g=9gUZXVR<9g56tzgD2>DPy+HhiBJ!%k#^9AhXLCRa=3z-y3Q@2V<;jQ7Y0& z3zs1h{@JP;D4(hXcvrdJ18WveD8ePu{I*oRwQ#jZg`tWKE-ZrC&}*5S(sf}1*BVdf zJpaVdZRQHkPhe2PT94{%rdH1-v`#}aad~$cZ^Y+i)Z^5By!aYKdF|u{`epiB@^197 z(w)43_c!>_zTbaF@#CZ2`Vea@DQ_9G&EyNJLohVcdk+pDph;OLh zKV(R*A1%?cI)BX~#C$wj1#98Eio;*Fti+EO_xbUQa)0@>NHhO_XYB@zN&SYW;in&3(=Isn!3hEVk_sFv?N z@uOY1S(PHIW76@t8ZxYj#-J}KRD%QZtPSy~Z*)~J=V#bRSQjFgPxau=P8NDz97=|b zAqL(wMKP$xouK7t`H+(#s&q0ldAoQnN!#2cG?8d{Ny0~OL;VkR>3j7hC(YN_J{KZCs-?$vR#c-+0^Ls)LMT^u$mQEuSje0-}0J|JS}v8*J&x@qqtWq z?ltOvK>gpV@Y~h@ZR-DK1mNfhp7dB=8SFmlQ5UsX_@HfD7I66(-6{GrK~`zBZ)?rN zJ^AN5zO!_br{xnzcR*hehA!p2tkk;46Z&+^eV(unqCDtXYQ61Au-2&nq7h!@DeoM(fd^p$Ht>F{S3D(xF9^}I9>i^aF zZ+R!F54u^qeCj(1_%v&R^$tvwVD09Yf4#gE#kxE}u35u3TNA9Co+J5k{I|UN8)w$v z6VxZB^p}0CeGw-8A`2Fs^55u{+E=V?^P^B&wreHbVd3R zvP}%jhcOGAbZb{aq4jHm-1dzxOD9;rDXrK1D-_|4i6+8NC87Y-S+^%vTmJ&**3By9 z%^nX%QMkX~+6m-pCB8zXm*4oYeo17fxu5)sbO$u!+qKGS>lj}j+Z%k*o1ZSxH%Lp; zZ%OWUBr%&|U6s>64(VeOW0k-U;rZvC@Og7q_lY@a_T?4wK8PM^M6 z312iXv|dWUN7>LSEw?^Q7{A1P-ptyWFwuG^aWbwzvUVl%))joG8*>u<8MNz@ipniJ zX{NQQjlQ@S-z&GgYJ_F|)8{IJe(Uu%xG$u8_A=X(*|}5O-#Y&0JN-jXeR9`Moszn~ zo^pM{J?)M;=7=LUv|YN?_Sl|{?XU4&>fP(R-qYmy*>mK^#A^~BOnfBa3h#MG_w3oX zZ{NqfZzcTZyCw0V#FxD*6WYz2_loyH&jwHCTb>&{-+Pko^b}>LchAVq=%Svk$7FWt z0w|+P*RC0>JaaBi%-!z2#FzJ_XKEj|ce6X4#l-lx1oVDtXf!vdT4t_}!n-nTls zla@E{S)C!UdB5s1z&$(xD;X}IuZ3>Tdmf*v<;^+IQw28fEd>NN_XIpHuz7#!eSu#& zTE1<$XWqLX zBe1zYV2;4%z4=Q7HunYG3z+fY-8Ota5ZIh!O-s{sbAEM%z~a-*qjUfr@-bs z=<@=bbD;YL=3PI0#%JmHnDd|)3Cz2H_`D{tIT!jLfq54YpPtz8XL@)i5T7D}$H@85 z1p=FMqE`uQ&WElQ*qjU9BCt6R`n|yB9B9w3Iy`g!^8|s-xzFVSoAaJG0cL&W-9mgi zbki{J7~(UcyN2ICOT*;?x1X!w0f%Y&pX5M7P~fIYO@A2ml=^ed*6`nZXgFz}hTjsn zNZ?-q(_ZF&k&K>z6ZCyjl=y?dyjP0PD1mvu6rU*q^PVX_X9~>wruftd%zLN!tPq&@ zPw}}yVBSN;r%_)@3rDHH%G&~--^#UIU43YSA4F?(J=43;v%tJZi_b!Vd7l=aO9bY4aGt;q3S1-bM}#{EtYwdC{@(=-2s{A&Ch02$E)uw2;0py_E$~{v9n)~XnZVlw zHupMwEwDMClzgP-H|KqZ32e^$%n{g}_X!AW&ikwt*qrNm+wjYIozy>Q{pS461cA-_ z*~!Kbift zesj*JKwxve=K_IKH|zX*$nXpNzTp?R%TZdNIiFJ?usQF2w!r2*)^!4#bKx5WHs``W z6WE*!PsBKg`EAa99SOKcR?GYOXA=u_m=JyxJl@n1vdAT{99o2epB0H z5MEd4dROb~O1K-^Wr1@AP8GON;B0|C5Szn)@|*(t90S0;-vw_3e3q|8fWCqFXU5+% z2H+hK2TVGuQ0SZ>;b|^9<;{4V<9Es%nEaGqh94ioly}l8Z^ri=?^E8u|N5@}2SHLzwbTI_1rLgYyr{8<_l* z2U5N7yJI_1s$hw~xI z8<_kI|2KN@(4oAOPI)t5;{1v71|~n{PsfiBVahw{lsEG$&bKIUVDeM`L;UyAZO z|4usP&HRw_MammEmb{Zrc{88n{F3qpHu6b`10TYSzmraRGymj#l=229f3);F>6AC~ zRnA{2Z(t+;y377fI_1s$mh)YeevC8t82K*V7MSrj{G?Of%!fHYro4ekXZ-I;43~G( zDR1V_oKI8Uz(yX;rg{i7d?%gqX1>k&H{}gXeunRschV_u=I5NRQ{KQv9>Wav5N7yJ zI_1rLp7VRk8<_kI-!1Q?Q{K$~xgJ1y0~`6_F8ev@lsD@ITtA?^fsOo975kR*=cH5K ztS@lAf$|29CGVtD-mFJ(eS-1^HuCQN!%3&SS-;?V2IUPLOWsMRyjkzy`Um9=Y~)8u z1tdJpMW?)3AK`ilnoHuF!?E;NDm%5ly}l8Z`NP9 z9z%Hplb`Z?>A^#X@=iMC&3X;jZzykI@>BkL{P+;2ypv9Ov%bUi9?Bb-{FHCPj}KwW zJL!}+>p@%}qP&60Px+tm<3pJ8PCDhy`VrTYC~si$Qy#-`^$@1KlTP^#d794kC(0X` z{FLA5YQLOx%A55mu2)gsz(&4Z?+5T?A7PIY>BziEfq`ZO2&$yrLDt}HoA^#1nu|_(v!2WKUCJAn{Ip-P zD|{!N@@D;)>%o*au#s;E9efBgd?%gqlV>Zk#r0##8<_kIf1W0X6Y-Fbjq9c zcdo}%-oQrw2A8~(PI-PSfX_akmN&4G|6Y?r>37m8KfYKqJe{EB4Q%9P_!N?N(kVYh z!&Q{G9Z{E8Ay|4Hm;VDeKQ-MV@RQ{G9Z z{OS@-|3KsoOn%CvTBwIG<(+iOuPf2?Cb7SP$sa9zC!O+JMZTk?-@ry5(-QR%X82Az z<=-jM^b;j~1CyWOyXBp9%I_BW3XwN(EO{rL^1q4v60x6wW63+|li2S1>Z(t+uUO#ctDSv{<-z4$|HuCQIvy)EwB_cmi>W_hq zyu1E6>6E`(GFHG*w4U5-fe#; zo$_5p9%r&t`VDO4-Q~|or+lu+pC{=za4dNzo$@%As-A2~zk!Xs+kQ?u<@rq=KC?yM zz(#(RCWrbzC!O+UznuH$%zpzL`8h6mC!O+Uf1Ug7lsB-EzZm6;58-JpI_1rNJoo1* zZ(#EG#V;2>K7=Xnq*LDP-*Z2o@&+b9(#$2vgokr@T2&!1D!^H!%4r|0RBW2vgokr+ktOka-?~@&+b9 z<)`AuhcM-xbjq9a3Ov6+c>|N5@_q2*LzwbTI_1sz2A+4Iyn)G2`G70`4W0YNIiQ<| z`^86M55~D)oMVV^xnJyrxnE3v=YFx1&i!I1%>7~~+!>K`!rU)*!rU)*!VkJ&?(ZIn zH8Si4t_jeV) zz}(+e`~q`-SHW8~KlgVDJNI`9JNI`9JNI`9JNI`9JNI`9oBiFDDAzeUDD|Jk+zB7+ zg15T((Oib{-|eDvzu2jd`^8R}`^8R}`^8R}`^8R}`@=^fQ0M+IVdwrZ;aK~_OkXDq z2F$%Sj6Y#>PYlyb*xVn(XZ8aY{n0qZxA-~3C70)dHuEjcF9@6Y1IL?$&3KaIU&3a5 z%kdatGv4C-fUp_=bG|{?%qKYiCT!-{oDUN=^Igub37h#f=L>|*d|(1#fGIlmmUWVb z&Gx%hVijUIFw)cE_kvF zegQS*Mf_gIZ##ZF@cS9RU+@d!_Zxl(@UzhGdGYh%mw;a)er@n;i(fnZ+T+&&zmE8I z!Y>uS&iJL_myTZ+e%bhS#jhKF-SImNzr*qCfnQJjUPCzS-!t&b#IG6fulVi9?|1x? zfcpteUV;Br{9eLuAATmDhmxlMx6_hig})|V7dXQ`E>L=P?68-%UtoKtSzd2F4_{X%4G+>VzOTVrc^jj=T@=L?Rl@r7P} zY>g@GrO4J8m&yy9t@7x1IS3 zRm>Pv{eETyv5;?Cs<$+wlgx{dkyQ0=ro4C=gX(;%GlIpuJQzW6yh|8C!)yHJJ*{do+T^*Co{}q){n+t2BZTdbKiw zf{POP_GJ`&#CxYvc+`ui5%`&oS4AUchad8B0rD*NOh_Fj5qTU-J7ofCq{Rv1>?=J$LS53e<_#G3vy)kcT zH7IU$*cB)-h~p~Dcnw6!qDYg9I4)e^oB3{exq``!o?d!3U%7UpotH7WX^|H(xjAA) zLA3*}TWOS;7B0pM*%5J&>uB7{BW}HkUTWB|5cRqlkfPkxBiQI+qT`8n#}%;ZY9F|~*ax>x9pqLaaKyOc z#!&R#HUhaWwGpho%?3b}i?o1sU2YSG(N|$bxslA=edE0JM*NYt-9#twnwtoU^Bx#Y zbY5(uG0|?c(G>Ri99xF2ve9UBQCb)zH`;{Zna%xyJvc}-2kU1kH$2#w%#%6PHWBE)>c zqLTUL9Egmrt48}$UcO-Tnbv?aVh-1HDlwirwX&{a9PXu>i#wPO!oC=HD{@wW`+4Z- z8^=b^sf3xz$}5-f2CK@NIvIu@L|3iiH37FC&D!!ht=h03MEAflTsfx--r};lGg+B& zXP#;shAv)Ey$JW#l+Qs8sF_?@Ic=tYSVc)~#pKHI=MxZweWAA~HpHv7?bo3uYzr;_ZE6SRjvO3mzomNZ0(C%|2q}rbxw%=le?uZLxe=ISBQPT)Fhd4Mq9SzAp^*t18kwM>k=7bIG$Im1BV#`_GG0R?<25ugUc(~eH7qh-!y@A~EK=9- zNL|Atbq$ZyH9Rtn!z0r;JTixdM<#oCWU_}xMs-AFR7XTcbwp&mMnpzpL}VmJL`Gs% zWP(OT>KYYU{1KLkIhp49)e79qj2HK56Sc@}wanj>;WY zg~}`ARD7=iop+UguKFrN3*Mn}*kHe`;`qzaXB*Y}^C5acC5mE)0>){$f5}M1C}zl5 z&lwpt^x-2+=z2Bee=-ikMmp5^%P|^TGGv4@k?dXe9_fgiBl0f6|0x5E7#0?Iq%9ob z9UHQ?NpGv{?&yv3FUcJy>6$#hWNvxyk)GbuW~ybA-a|%2Pvgj#@l*SFQ}YYPPxa3z znm%iK;qQil1FcP$C3}3DMFFkYJFQw8o@h##;gb{EB+b& zsgq~Mpy}+<@%GO;ea85i(RFCPqVW@E;nR__QviEo&^OK1&tzl6Wag zt_+R25zjo~Bkv@q zBkP!hCu#|dRxceguAFUEzr-PS6?W*25tl=fb;aqBWK`TtOWLNkPC}bEdI!{ zAKrJH0t{vHi7MR!#mM{!8%EdWXzyBzb}ZJG@*JJRQHb3Ps@<1^r7;WmK))WwxU;}C z2vK?-2dVX@C5$n$q=u1W7a9j!lzdcMxcCIl608^^yJk-vGSXk`-m=q#!Ptbuw*G>$ za_-H5GuOYM7RxTVWpnZ08pKm3wS(s4OFx6=lrO;|ZOwe_C=TLAW6rqpB{{RO`G@mA zgRoo4_3!Gc@&Q=8sark(8!1ch^}9heRg32$h(RZolnt6$y?7A<8e}=9X#Z*y-A)ab3Vj9aHWY^og$e`U{KW@Jnr`BK9 zC)gFPFaG1SxbHC@q=)+)cnIHM;X5RnX1xvfG!pU4v^+(5IbI8I5**62F2uB;ylx;q zAW=S0Wr5_tK(U5(`4JOVpHh14)qR)sPEIVI*K1p^r~W-|edAxRDL(thv2R`3WA*Z! z`f2x-cU-*e>ckV)tSg#!cHrsfyZvoI^Xtd{J*zNy*(ookel+lwF74;OcEZVfs~^4b zvZ|MdKmFN@mz1ok{Aagos=6$_x$}| zPs#lLn97%5wD)9h@Q!`xi&Yhy3LYBMQ0ZO1V8Y>?!zbTJ>#q&E_^xRi%v6B|JMKb zw>IAR(y8AK`ncU~|LlHo)=9&TS-!TNx9a9G`(J!_)ql2MboRiqfm1#ER-O6Ok(JNn zeA;!{GY@Ylth>iE;oG#=pT6uL|4IMbaXA|o&+zmaxc7k8k(4TVA=p_n7{NO}g&5F&Df(^w+n_S3h^};CIG6_U)Qi9$nP*)OXkJ zOFiwq;uD(}Z@u)~DOrE_o^e!e(T88;KDPUZ&z2nV(HR+^Ui+VvhwTe@&0cw+dfuvj zPh9u)o|3-7dDksy-|zBkhfcb#Ytu1DzO%5?yo;V&R-W5#_L*-@T2pyZhp7)8Huu&K z`W-&EPu9Yh9{uU-hi@+V`?&3E_7AzBc~pH;-?THYoa|Y;^PHqlj(q>thmKuv#aBmu z(`V~5>t1`zGbVlQyDtUj^|rVC1M?l-FK3)7PCf8v}$JvY44`P3`g z91-j}ac)6}^H#re-*3Nu_wDVyp8eNv?K zC*61Yr}y2vbMm`0`@G!#jNIEkUir>PhkcfJ)%#a|eNstzug?#A{={ZzH?*N@xLA9@QbRK?<+xCP=Ch+80Tfw%?Y7KmFQZh^Q3;ueToAZ~%U1>zQn zTOe+MxCP=Ch+80Tfw%?Y7KmFQZh^Q3;ueToAZ~%U1>zQnTOe+MxCP=Ch+80Tfw%?Y z7KmFQZh^Q3;ueToAZ~%U1>zQnTOe+MxCP=Ch+80Tfw%?Y7KmFQZh^Q3;ueToAZ~%U z1>zQnTOe+MxCP=Ch+80Tfw%?Y7KmFQZh^Q3;ueToAZ~%U1>zQnTOe+MxCP=Ch+80T zfw%?Y7KmFQZh^Q3;ueToAZ~%U1>zQnTOe+MxCP=Ch+80Tfw%?Y7KmFQZUL7CUi!RG=Ju!Zut6n`Lene zUbnt_gvn>*4DAxeN8?(a1S`-2Fg~?)i%V-mD7U_Cf7YaRDrJ^M*r^X`Qu&!u+^itjF)4TUO@ySiC!aPXx5&)VA6s9F|^<4*mE0`8~vz40h{Fy-KrI zh!Vo;Yk(mQ;U2$V*{`gsq_%bezP{+zx9l3tFKR6V8!hg*iay$p>LTptyH-oyd6;$$ zd+ks_Z@cYN*&&u}~PRHk(6@9b%j{+>N3#NMhSS(ZVL%FMN zDJ8M>1;DGG&Ee%Z3CO;hqW=5+^Q)Q2$~ym9ix-qd#4iAOKIYSQgeA!epQ8S^{EIBd z;ptrodG&~~2s`x&HRM3wSCQWSIJMHv*@|ww^H<({2(l*4AWx(mb+{OsPz~u zU1`|!3@#jI9QiZ(>-Sfc_-mKXFRiZf*VHYFsV`O3P`@*sEWg7{P;gEAWEwqp{8Bwy z(MU-sVW&QaD&-Y@wdGZd%FjZ#=GHeUMeBQ4^bvOIL*EfCUsd}_jc$E8U9>)vUcyd& zh*UIvwTsa`M$aMr@zm1J4<6FnC3o8hiOM2e?)tkck8 zGY{Cd%$VV^wgbQRT91`H!Lr&BhaaDb!1I?2|8lseDs1$BGp_Mir$ixj2ae&aY4@L= z!QV!naoh3UKaXF(xRE+@@Y88vsNIww!C*kKHQ^{Ysr1Xk?)NnOs7se0cFbOD(9Gqv zb>;I1O|2|lRI+IKpb3jGDp*>*XkP80!jeU$)e8omWiW$I!8f5RQPBtG3k@3FdNVx> zpx)D(UVNB%N@ZPiy_yz9ua0@EdJC(|s!O7)RU~ND@hygH>~O%dt&?RP<7veX6RH<4 zKqWxRqbV;O)It~iBOUrJ>vKSe_u|?`gQ_Y^`D4#DFn=IE*Iii(qIzgYmNOGCYwUJ3 zwc&DQM;|?Kh=`FtJe;X$`oj5j)($LihUCt#s{K~JXiIHE0gES3sq23Bd z{Iy+gXiaZn^`i0?+eNc2%2^mslyEHT9@r`GVBt($R8mt>Symf66vKfq_#BBJ>(9pN z9&0t+{sObU^Eq7Lb#HpCoGA+Rzx3Z`pa1Ox&ssZvobGAdf3CN2{|c+&N1tUk`MmbY z>DHymHE5B}K4YJ|VubIn51hs-A)&o zF3QcY-$WeupZj#uGJ;*~SXe#>uzk z2b=nM8&THx(%xxS;Ps{zR>N0_%eVN^Ci~#t0^W*2g~5IvYXjx1jQrrsz%R+PRuNx3 z$cMa2wm$)m`SQKEAHY@oo%4g6;Xj-FN96^JkFpg18;0IBKe!S8QqqqE{YXuJ3Ao~c zdBF<@TET7f=fai2Il=vUpIS+%DZm% zo#4CCvPPg!-22O2Z5uE?P0xehTGO_n0B%7}yN0W}>auhPcz-c*8IT|RrJWUAuENQ( z`yeiT@tYyxnQ-Q)aMBTGhTRK#N?~)BRhCZ`*CF}A3h3zPEA4RwerTo#dGPb*7x(Cc z-%juz4V@JoVcU+{wsqhy2K*7r8Ok{05JbFA&(iQ>!219rUG+^P@`JRKS7-}B`_0&6 zO*`1*FvP99-4=0cf_oFT?Z# z{{`aB_?-kDjGxYf_6V~(!t82)1Gy%+c@jo}H&B%92xF_mf42CQjy3HY7$4d@5#=8B zjCok!G&(=HvrS%btmM~F;48YoZe6t9js)E7vDPR1@!y}N}@n=ha}fTzC}{$=nt zAsxv`hu2#85dCKYKgwerOFrgDXOZd0@_ohl{hqwy9$!PJDu?6qgU=!ErQm&lacqk? zw$*V&dj%VGv)`uME-(0|f-~*cm78I2S8lrfs&d(uPDVR63GLZLv}+U4zKw6(S3RzA zcb~%fX!le*pN@K#hF>#mtGc z_XF-6qIa{6zoX#LmnvDh7(S#u`Rv{F6+*k1Z(}}@UxvGd?D&|05IDGnX?1d=H)9FWvz8ag~6Nwx_`_9Uhxbm z3@UrC@iwrJPe8x!&U418Blwx`>Fx7We{G?iXFIr4#W~&n8DU`BGmK@x2VT?fWg^VE zlHbHLzjq?7C#bT3F$v<#Jp4hq;p3M7bPgZ4d<7ob#f)2+-WE8`-l=qC*muJ{Q0U$9 z>)Zs?*F@CYB-GzF7>8gi;`27BbWcY7QD)Yl9qEN1+nv8aj^*$@xQssrAqsvAuo;uR z0Kc+VP6xF6Y4$p#gEB9RjJBnwH41GjXpFOJ)2HPJ&lSB-1J68WpXftf`2y_}%lkUu zo4~hF_%?+2G8Eqj!uNOJmCmC4;8_wD8VIEmQ)*@QuKupgE-6kxKbwtlb65LfWy37{WYF0k z&Qxx;eG=WCdBN!l&a_WdZiYQYx#{*~<)+ya;qE_o63W#?l&=XWXXDW?jB7->Q~klG zzATJ!b$$LLcv((0+|?clm~Az2Y_ByQeYTSiaR|}iKN>XJr9WJ@>1_WwwnN@1dA1?c zV_eoLtX_!jOuM^NH^y#RN?*F2q1-e(O}Sb2TdAnWsqCLU_Iqfba^UZTdPrPbxSZ1{ z+oS$ml!0;ov%Vess#B2mwy;4v*rGjb(gAg?Bj#4VhF>sWFl~W_vhX$Xn0aL(Jl3hO za?mqy{DDH0rDr`29JlLwo@RfII1Nv-hB4fXl6>9%jLHwb3j9vk`2q0i`pL0yntd*G zGmL+OZghMAzl!(i7)wi8`UE)2`K7FoN9P6lYWOFyjIuuTany%DNPQ?7tLwv^i4D8{ zkcj$_VQ&?EO*4}kVL#M|EZc(apNHlJYmgTzeAsO$%AWFt2A}CBj;8s z9UH)7(!p`}ORDT<*nbCJw>KSCTXc=lJ3l{oG1@lTfIO7NT)WN{Dg9=h0Zv2h0Sk;8#oSa67aNn5#pxDEL~yqY`cfp{7TUEcrz3Du6DB6`V!zd zPM>6KDs6yjV>&6Du1kKVyS8ab5^TygCd00QoXV5#9J{62Re(AFX=0mG$Tp|DeJaAB zTM8QMa}VA4?t3k43T& zE?01a=^{M?oia$*QDJbleI(#a$g`evT&Q%TtmP>AEW3-r!EW7wW1B-B^3(kR_QfKV zYKO4;qHtK6)q0TLj!I94-CntAc3b79+b?%Q|LXJZX!a){-HAwl66!npSJYcwPCd34 zysTRl$Zref{4>a0gM4DR3a88Y_JW*_4P&L8{{|jC=3$;>+MQK7&$Pb=9_4d;!5WtH zbo&joAxa0z`8M#dypo5qC|}!EoyEFBKloX`l2LY(J#8AA+FAS2hi&iwX#)B%PvaVl z4c~=aQx?lsru~))UzN3Vdk0`u_E^^Xu&kxoPoSJ^z<5!Gk2VNlvTPx4T^e4IxV!*4 zUAEXBW!p=Y-Lmb+7#8w=BVFYEBXqO!g6rub{D+jAWv^2%$3hcf+X=Aoc$6*lEf~8r zb~59iZ2Rxv^C7>OcgpV2eJ5q1`wqI$eVg#GFVT6EX|D#3Wl_WF_Np)#3Jln3E^5f2~YlYqQV#CncL*3CTj<=|spmrEOP1z_q9z`mBL|G??IrA;#JQs8x4 zn`zg9#_$%weWx4xeApMq3-*pg-KEXH6&0!fRCdkkbkIlx?3a^s_+M zd6A{^VwU8^sle0bCo4C@o~hh4dzhr*G{8(l1;RG*J^{EJFyFZW@ykKnbiQ|Lm@Z*W z5gF=bJ3UH;m0|ypa*#Pz(!;)(Wp-?cU&>HUD&n4HA1C^b1>M+)ZSE0DUzXhuc-reo z<)+!ch<%P0eViAVI3EsNv^e*cFnWlLiLeJFD_y=EHBu_K>V}qmw;#3FDN&|ejcv79K0f7kcVk9 zaeY+c`WWzkg-u6DIneE!u_NP=X+I%4HiFmWA?pk4I_ZtTuX2{ZoGuNV-!aYig~HYA zb2mx2w*pVw>_2xr>hCzz<3iNu0`yf_r(WS{Sk+OlU0{zS9s6SG-SA_(LO2UQw%N=> z_9tB9pd0FM5GSrZaNNmpsp(s2vkMTebDXR7WZKJsXZxe!47)B2PP1!NeADe*xIa#R z3hfQn%F);VfH5h@qw|30oV}7R=JET_%}2VntSv`6!uc`2$<7P@N%&{eMVn;!Pl@2y z`ezEi;hP@82fgWe!3i#X1>ifB_VPH$vAw(zaWU=OBT^qmgQm+)nthSv>u}&$7Jv_H z;V1v&?pvGTQ@qmYKtpn_+iTZko-0mU2A-Gps=f+myA=z^V2EadWnDnG#kP zkuhc4k}~F_9Ar!z$NI@Il0y8ZzUlVDEBb!#5Z+#V4StrjZ-HkU_lu!4Pi(MN^gRYwwQ(-nJ}r757d{hD)^+Op2k@$mo2}cpXGQM=F8+H&Pb2Wr+BieM z6Le>JxlV(6uhJ7tu+Q(TAR!JD-(dAV4b*$@*>qWr-6*8ucdl&rJJCnXr zw{a^($A#cE<&JIKe5E7Pt^t0PGhDQBOGIW-C|upfVGlutTLye$IF~|YyNC|Ji={h%QEf!FgU|LJ`7H?N2z$G z+rPEP*y`EHeMmE|oWybLooNlbjO}vL8itDvhCm0~53~09iPG7!b_Tr-J&|4=$29ve z3G?Ga3A0yvSeV%oCK~v#Fm+iE57X-i^8mtP{GMT$KF^MyPxYXU#(wL$KFkyJo-5Ou zfiZLw_5z|75qAFroBi*znEeFEaQ*wLD%I@=l>d&wha3-6&~YLh;Rzf$Dwb- z9Q>p9#ts?w``~HGt2Fy2@{T*7}y8V8b7MK3Q|}+INB9l#x5&H+}7`@H1|^uueOvt+k$cbU?;T zw}8fWp%3EAytolC=kmu2{1?EwzIL@Qho61?rIf)M_`BGLV%u>m+NLj5+00hse6DL^ zpXl}5#da5yAM~YgPxD$Ensf7l*{)%pU% zp9#8N+u&THtL=eZ*nX_VJ^{<92l25yW8cOR-*i=v7v=}e`o}mEcg}~gp2GHy<%oKZ z59y`Ny4pX$4h(l7@Ju7^d^G$f9Ih=g9NPN`;2Cxw<)+)cl*=*}eXQ3NWvzKcUT~RD z+k9{gn{({NxMe~g^Q#?P=CMkbbKaYi+0aSsM;`LB|K_}R6X5L!4rH;6U50ShV0=&i z(ZY{)V8x%K;*xG3A^0HhwDWGnL9O+`7JGoB9d^Tg411l_!?ha5@f*=&!rcQooln{J zN1$y=v_>G_+pU&(uPa!S+3>0K_vCTL`zplySd@#qB<=@3jpmerRp|xq` z%_Y!9oE{Tp+X29w4^W2vrN(!)&vEe?9h@VW@zg@_usl~QH{G7E+%$WhaoeK*Bx&7z(xX;buCy6V5y735^04au@6 zNLeX>tk%tSkSu#IVA6*wH`5*hccp4iP?u2#bv;L&QFPYpaY%C^(q4c$9Qrt{!}M)u z)^f7!Uf^SXF+VuIp*?6{Z5QnMC_U+Rmgvh+Zif9%+t__z6Z(~o;G;c{ft_f-RKQv% z_B(N2%371vuo*g-cNS>0N2anzn%z@md=443%cz68T{ivCZ;;phi(db01V7ujuSM@y z%8l$lKW}UL_)Pm#@R&6*)8~B%9PMGsz`IIMx_vX;L+Mj>oV@lc;5TLJMfi>W=i#R< zwxN!$O|sVOwDDX>n!OhF(!$=wB%Uer=ra(s(b-ZjpMosQ1IOBd1Z((a=qNS@t!`&9JXhZn}Mia%1-|7a=dq7~ul&((W40w$A}<@)x0C5qnFtyVWl*coyg;Z_3~|c}+cQuourg*27gA)9urMW8TaY z*SRNu65_ARb-F!G=q2Kwpj@nn!;OrKj>kCA7!T-K6+pNd=)d1VxY!$CZ(yE_*eUJx z80rH5bKue8X4->AmV8_bp$(~raV9_AKPWc??SVdT$nykgQV(cjrvGP@GuEHOM20q` zY_@XK?e|lxRlEBO)Q(43`6gf4TI!b?BaWq6k2 z0CcN!80agHMg6DWoB`uryNUPiKF?PDf^`!o%socNp%`O4)TN>WpzCv+3D|4wfOV^m zX!|;$4NOH_*ct7A7wk1AsJ#bO&wAwrS=ZM3x;67$=O>7ZO4sLBHR{9tIL~nZ8l2U5 z2JvM*`v`Q}i+j+Yp&v=WxvfRW59Uh_>hms~nduIFhanH>r|zDf?t2iwS?tpvhx{&_ z)9SkXJcO5K|4sBh4?OE6$8-xMzqbI#I?eEjuMqqb!0WLN$ACDi1Domk+0Fh3XsnaA za z&2Gk?$h!WpS%0K!tS5O-zK7{z*(!p4PlBCiz~0kg_i5PMIuUzY32JYv0`g{_%(6>A z-DGij#_D|VFl=46v+d<7>@0g}7(U&uRroY}VHiHc9s(HQ{df7kLg)K?uwk@(FO>Z8 zLw6|O`{;cC5j5H@59|EQ>#nd#x3GNg8p`+MAQ*(Q^yo5{r6=&Q1q<4h=Aj=y=Xmu(sM30U_SHP zP;B`Vc$kk(o$`WT!>`BcUF|R7--tFM9p#%kdm?Sr54*45<&7Rb`IsMHN&ovXbTFTI zCgt-&6MmKopXG;fd<#6P{GvR(911_ne$EkoCiEVS@Tvc8gue~pS42ra=Y8Bupx&oV z*x0N5yia)8Tq_{YBj90qY(hD^-x2n`@UND%bF9tq1En5yCjB3v1umvP!)~PiF8Z;D z0Dm_4hQVI!7j6K|dXB=O^89+`X5013v72QLe&2P)P0X*Upvdb6ug`h zFyC3WS+?jmF7=-Wo*>E;=d{a3_i=E4o%=ZSMvmi?(f03ZW36YpO}XLvBJHsVax4p2 zH{YI9#(8v_eE>Mp=D}syO6Gjh((UbH$2p+sHV9{}P`1hM2ab7;{hSblJ+X zr-QaS!5TIaVXzMV445)SMn>W@4X|#f_1yhK;hO~4l+!=LPuq-!%P||{#rEKIw9(W# zRnc+&2QKGa?lJzabGNQ#|EFa$>@txXqvSH|zRFFvdn-3`jzIbD$t~kBwgIY4AwM!8 zWAwS#$I4B&KU6O5 z(lS!D;q3r7y$260UQDs#?r4RrKtx9fF}XYTD* za!C)%TGoT-I4MtFo`Zb{dGCZjaKKHV8(lw1y;zHIoI0F5KMKzst?>Ls^5F&t56@%n z?vpS-+IdW-lX2aQ^&$2n$$2?Feo||hUb_Z5XgAd9b=F1sK`bh$JSLv?YZA|P@QjvE zo%g6$=ZV^wGmGtco~XWmuQoVOgm%WC)c>dBtW7x6h&r8SpAH?W9^m}PuCdmW9AnJ_ zj^%*kDc1WYq=n}wrU2iRmKU5%7v+DVax?AmaNXwsclQ}L-<$(fc?kLAM1GXW9}AcL zuF2z}3ZHKOl!SgZ#`*^BkUThV(^A+1>-!7WBw4e1rdSJ$vCi2u(OTFm#hUfgW%p1yeB=Wy?R*5y?Xepkv$He>{!5ay@fVe30YMxU`zJbpGceY2JohhI1hf> zhUYw%vW&pp0vyYXyZz=G&ErW{I@?k9i_AyzKLUQnNyF*(LrOl)u7k_>sbalH#ki^Z zl47lI#`{#i;C(8t{|8ZLennpW4f4)9Gg0DCJjMvE@#91J~s$WIj{N*ZZIqU0u;MABJ=d*PE%(R=4uRQNm z1{&*W30>4vKV9^1XDT<_K0~=#_UUk=z0YCZNy8XwIm-4Z+P47j?3lX#Z1%ZlRry z8lNPI&#%xCD?Z;K9*oa-;Bm(1dBmqrl=$3qkoXwe@O>tyZ9bFw@=hypc~Ro>sw*yc zLzZRrY2ahE1sfA1$9QZb*hjPcgwFFp&;8KD^&HmCAJG<3=USy3Ylv_U6ejHWW@z&K zJx8a^-`%Gz-oI>zae8}0%yNZ1EtE)9k;9{CRLE0snMcYvJ15mEE3BvKD6Eb4NFhh1Q{rC8PZI$L|FE=HXX? zvbYUxOeNmexq|MJyiH?0ne)(R%zD{d)`M~Zyi3@Ky1!(JwJ9&9szFgH*x+~4R7{(q4b{?kto;#D(IPe3? zVJyV=jXzW4JpJCle(FVgz8!Sh=~cMSeN?2aW$p_;v(C!6JPTgdUAA46eOl>Aw=aUb zyU*ho_eRch&H6RpuX+T0x=l;BABLZLiW993s_d@7yTgd@;nE&I2)Y^r0MB(M3ogeg zTx(P1WC-RVN94`t`Q`v@$Uc+)#qfXbExkR_D!tu{F_aI#q|^l%3#Qo?>QgWLIQ}^o zZ5wrtk~Z$I2#-3R1k8EtO2DM+wW3V>62NOvb}yB9T@Uxp-UW@;omRsY@UI1}KlG+} zJfrCE`3&2KO#3R(=+-GW-Cm^J$ocE;K3l8iB^cXg*%jd9oRu~*F7=d(e6i^3tz562 zpNu(^)$?uVTx~XJwD(-N9LELF&J2frsFV4KaU#}8(n30?h)y$CV?8i=eY&Eh*#*Lr z0M{ATSilTxDqIs*mndOPRPt%|VC8Zg895gmBRqc)p4~o-oBU6pZ4N(Q!?u1U)=*f+ zdV-hr=NPzaz4@a!SIAGaW--q!%(YCHxzrGU2s;7`!Dx5IoS8ExtA3-bPAGe8yPP(Zm;JWD)$>0_5aGuW26lW0MD=ja+YE_;0&aJ@x7K|Bv=hI;nFr- zyQCh*hwH11$6UaSf4OqQ_raL&#>QugoU!rg!rvXPvyL*3)*4TP2m1d6S~1p7=_h|N z)>Y|0h5iEVXWWW=YyS-TcUp>v_9@-0RSTbdKWw0M}1HM_=>>%Ep%n&)+5?}4-($czb)1Fz_$BQ31=aza z_|t$lI^RM#>!IU5{HQw!9fmeRaO;61Z70G--@-Xwy#?EU?C|%2PV(#kkGpJsC}nUZ z(#^ck^&4l|0UwUMV7a^jewME@;OhFFZeI(3OZ`r>s{ym@ILn?hujVRxy4@eHI}Qxj z!d|asS@$RJy|*Yi$J$hYcIHRK`Q*Llzrl9nQ{*$}xH#*yY8C3-hThh9`RIS#ZK5;G z9v)pj+9S+peeO@lXNHZlW1BJ)@`9%z%*|e`%bu3g(Hc2;V6t^&5A?m?t;N3a3rW_F zk)A9pAzSI(w`xXN{=}QL2lk$|yY1UIkNha}mP#+iP0N=x@5Y+)FziFFhfBFDld!(w z$=Y)#c(3e|w+a4v7ol&RO2WGh&KITrh3%hTuflHPewK7#Sl7H^;1dnfnqk(ZOx z1MTjtZNFal2FG=n3-9>E+xDxk+T79YlaAJR{fAt6b3acq-o5D6BgN`}drwcw9?wNr z+)UdRz}`y`|7}Vp`76dHTkOE}R|2Lj*l+KG9kRs^{a}Zch(ikOaD~^xIwt7Q*S14` z^Ob}>m=`S{*}M|*-v+xUC*}odt53WsDvV^YM^CXw3hdGEPHp3U9g?r2yh(FKM=RJ% zjsG|XYdKHMaR$o++w&mK&M~i5d!fojntd5uvzBgQ{<4^ug0&;L^XB(=`I28^-uCOY?dG1z)_2z6l{d3~ zqD-&4!)xtXb#dTk#=*Dmz$|Cjs=kE1v%~&d*S&uG&VOTNinSr|{rPX406ka>vWt;l z70AClQekWsG+ZOp7aHJsxb!#lj z{v@Q~YUKL~p7vMrygv0`DEg0r{tKbIAMk!px~dmbQ{KG!%Et6ts=#;E@}rvDp?tN& zUKMrBM;O#u0XyFheXLJUi`+jU_sWL!Tb}l`-Ger6)&j`CwtRl`wPkkNgtd$H$k$C4_lu-mY75)>kxtaFS?v<7 z9rI9EIQHJ-OR-sZnf{|ww%t7Mo@5*m&kJ6?d|q>VIB{(?u9x7BAg-Op^%vX#;@WH65Wx*6u7k#n65R2` zb=0_g!Hp-blg3RJ+=;}cYTQYJJDIr7_t(MxtM2NG^9_2x&YKb_+PD9}jhhb~*lX2~ zZP)@AeGcg9_E6*z%R?dDwJXkS?vMQFk38*wV#Y;GY5B zRMc_H+w*PZM;jdSdu~r{v0FC#ZVM0Wko`0Kc;8F; ze}I34`1isO9SZ*q{C^hzSMXmU{(r-Nhxk8-|7r1m3jdel{}}!@9bp%e*F+9XZeFciq&Hl=AAn)HuQm`pM^$Dgi52DO%UA7A+dJXpyQ#>ssAv-|utIb7t<`naM4?`~E)fzQ4Ko+&t%dp65A# zp7ZyfbLU*i-(kbwX~Q40;qS8H@3!IZvEh%~@b}vA_u26G+wc$A@DJMX583b!+wdoB z_(yE`M{W4WZ1~4*_$O@mCvEtrZ1|#a^7`$v;TPNROKkXV8~!vKeyI(Ax((lB!=Gux zFSFs#vf-E8@T+Y2)i(TvHvAeJeuE9a$%fx-!*8|Wx7+a7*znid@H=ey0ULg=4L@YV zkJ|7PHvB;w{stTVunm8s4S$mjf3ppL#D>4chQHN@zs-g}YQx`d!{1@U-)X}iv*GWu z;qSKL@3G;J+wk|=@b}s9_uKFf*zgb9@DJJW58Lo3Z1_iP_(yH{$87k=ZTKf__$O`n zr)>CQk}{P4S$0Tf7ph<(T2Y%Hy`_>c-0Qy z(fgOnX zJIe@vJKQtjrjC6N+~d&wE!+p;4txaPEJQfR;BJN62e$S=}-58Si9d-}8+{vo() zesKEq<_`(+c6JZK{WiEq;lBrAw!^=hbY<5q; zJ%inc;l7C7hv1&d?t^gueU`G-0l5Fb?)`B8irxF*{xQ4v!u?%#kHh@}yZ6BTWp?j| z`$=~1g8NZ+kHP&BcJGAyL3Zze`#yG4N=i@#&ja0uwsRrcFJ0SUlTWbBZutL=%W*UO ze~Uvb=-LGL&)H4=hHj^W@c%aZ(>U@qc6UHVx32*FpWrad;r|(^1tLYzMBC=saFRmCB(utDPU>0_eh?oAF+<4%ayb}jdkqqStq z*~B6^Tfy{AQ^nqb(qmJ_;exVFXN%3QncL15W3FjJ>~)pEdBEj9FjX9LxpC;) zDOc$je!5gR9o=_f4VcFZOE*pxn~IRBEk*6{I9fDQh|N>naBiF89-b<8PC@z(PAP-O zsVRtYOYvq0+@ojh#-2X%J%>JmT3xoeKy4`#-n0~BKY;%-s zEfPB%?xTfbH%u<}N{<5$_mM&|K~WudluQ(gP0lj>G<(ch0}p4(zCv-z3Et*{l0AiD zte|)&w8?r03QBeqilYT?{GM^xRkEcJ)+-$@6x&@}kvUvQ>j&`49tO{19PO~S9AP-7 z;eduw2h6+6QL=jqa^)VKA`Up3gxKmV**pd11+jz9Y5S&#qfR$|wRqB5j4D&$hR4=| zl1)>Pf|89>#J+;6gHyz*g3=RsIp0-!q)6;^wPFZ#mFz7No2ZDk7nY6{i9LmG{Ct0Z z;Z9}mTV=f}K3E_crtK{dYuvjEL@y$Ux`8%1O2+VNnFDr&Ax7ZgEJY!VI;m*JoV(fn zA9adZm^_pmD;6~+IESR!y}wwjMZu%s;5Xu!wy#)hcX;*|!@%xc#o{2<%wv*y%27O4 zEH*mfvCTOR(qm3H-o+dz10Qgb`h?Sib77ESoWLf#r%wdUug~N3qyf1nEOXGvRTn$h{4LOo7L?DKhO-+*^_MDdR8<%K8*4 zBr0LBScM=^fdw3|E*IHtklh=Ypu1l7KcPLkN-^q`LH za=KAH_L5x=O7A1iV!Ydj#tXRuQrueL-Ze$+AOq|!AjN$J9@H1q$DOD(2@Tq(1S|2qtK0mzIGSF0EY@m`dFbGCz_lhb8Rk~36HTN z_W@K{vdDoVnf4+#D)xyYSu>;!@Y*Kkk|iCY8Eq6cvEmD4KQ|q37`3|54|brTO1WJA zCsg}!A3RG0-22WF3HR=^(CMLik2=Z@pC$HDQ#vf28*$V-8j*V!PAhPhgLlN~*$m+V zH{J!_R!}kq|AG?u?=NuUG?T*x?vb;^F>1&gUF1FDLedYoC{$Dzz#7CVdJvA2kH4i~xclll{qiRyB^81Pw~J#Qe1kZSnMb+J%|!2#!KaFlew}wxzTdl-FPKG0JA6D+nvb% z!X4Bfy0Lh!{^fupf7}8yBjFr+3$d|dk?8o^-;h9wgCG0+0O;_X#I#>p9{SryTBsPO;fJDibHmb)gUoi+e>wlPo{g zC(i`Gxp*te3~>gEfkJClJWceXR%^+&0q;y>mWGcn7+*g9r8fG>k@~Z(>J@Ib0}FJ z&2m_wWOlfb?kN4n1?EIjw@@iyTtSz)+u`0^fNEJP(}5~+(lPBgnXU|7J}M{W3-9Yw zI!XoE(2WSzxKZlSX=p|psSu@ot1urZSu1MFSr!RvD#5_gPWEt=oOEDZj7r|+4O^VY zVLRuv!vz?n-1ynywt~_L*teh+)|WE#L^GbvO7<6^8o}C|9d4BHcA3^)ju2Xtvjq9r z>4eltj6=v+0hOH8Eo%tD9%JafTqvw4+~}@HVKOKAqcy>BhYJu0((Oi1L%mJO376RI zTBFh@kcaZJVJGs5F}xQ!L10H5sAnh}701;==I;>7v=@W@UPm#CcR!8Mhvevg*rBSP zvW-Uw4@SG0(2=%E$5#m?eQhz?NT3AGC0a7-6dMrICWmVqO2UDOgVZ};_C;?G% zBBoQA7BfAI=~Sj?Gc93y4%2g)p2yV9^n9ilFnt@-X-qFE%qXVET5Z9;R0^ox$`frZbsd&Ga2iuVGro^jfCZF}kr zWm?5_9@F_u7ci}6dLvUW)0>zsWO_5xTbSO;w1#Od(>kX0OdFUsGF`-UF;kqcEw8hg zYEs|QwmKLNMAnPdhJRgKS1cG!EHmP9e~-~%gp3}4!Vs&MrujGd!-0?y6D@7aJMS^N z62c+sR~wzHg2wvRP=8M_426|TOxi8}fgl13M#4!?9c$8K%7tR(ie}M%)vOi59||os zhT@{VW=2a}TOt+=_sp1e>#P;C>P1&1obU(3aTT*ywBHW-KrkMS#0{~seNnX%7r}5a zu{xOOReAFYe;`obA5TR3Rv7p88}USw;SU%w?|?tlZ+J!6SWgl<&=@lAM|kkcmT_;` z-v_0b3W-wN)ZX5zLR=IyLV>u58;N?P)CecYDu_N5imYE@^caK0YK=v@VB*$DBm`D0 z<{zS@>e%DG{+JP19f_@rNBv!f=nN+M{815?c32dP^fjPNyzJc+?CBNjmGMPqBrqh_ z%hFsO4270O1|T=PYjDsPK$c_uC@NoEmT=PSF!93d#e2()S*vHY%vxpDP|2|rjw+hS zx}XuKVx;V-g7AvoL?T)rAy!)=5`$4YyE@wH(LFPUT-8yRVwbKI$;bZs6@h4s1(*ph^w+o78Hetw8_Fib&UrT z1}y1sABrN;D3wJq6zlR>5EU3Q(I^7bc>s3>D!RAVfVmNozn`+ADhUjWeAErqN)0$E zZS|49Xe5l{*X2Yp38X)c6m!w74h9S^t%hJ=kv|w}k2E4Du|@r%5YlcKNJnQswKZxP zokIyD-fF~Fps9BG!;2$z{;qY(wI~u>?~esUSI8faDhkarehiq^cziLNi-7q@QZ#NVmm)h+cO9O4(j8 z5>95$67*m&kt!Ke$N{9Bvd|vvGnP>Kp;KuKhP#Z48yA#&MZoCn?`bgNvfWYX2aIli ze<(pULbV9?SmqC-(~W^2G7^R=KvEA6sg46}*c8S5)`s2NeWBzDwuq!T*hdx8}P-9iYPmhtP4TZSIdj(*c zJG0iTQoTL|xyunfsx|dQbW`f#*y>f^LK&%({qT}L6w(SKijK{XuFL2S4oYXQ=tY%R zCDj{=Colr7klhi?Do3N_kfg$);%^Q4!$#a2rDBXzcfd~S)V=6uskyiL$+X_MB;+9r z-HXU;L(yKp7yTzf@yAdGp-8NaMvN93o>VuD?4TgMroKamtAa@;O6D`HdbxOFC}b@2 z$Jco~(ZwMBUer+PSM{K(M~{?`>>Q{gPW2`!=Hg^gdC`-EF{)9k_TCqX^m*3@1L(=Q zU+wpM;|91?^-}6>k^Weh;SGjqu7N5>?Tm(jWx7*Z9u5tq#8Z<1@=M6FCWjG=#*75U z4p|-JV8$Eo?f0XLV@G?0Ya%?BVh~;4T~GZf@sm-J+n~Y8#9XdgSGcEbD4sC-Y7@nosdWt;D6I@vU}3D3(AM9ndU4G`3q_qwM;0+T^O5yf39HFZx9a_<*r~(R6+)o14@;v* zVwenJzDRv<6mt@RdWvS#*M{+>3-cHFT0GXHm*Y?;5$J)ep8sv6C@t(U1p%iJ>KEFL7i_57JRJFDVJeIOcuhiRhZ3{W2%KEUpG!9=dqIg|h7SA%e+jn|y@8 z=&c_!$Qu*V02*|v_>~`KfpE}#R*$f42^yl&S<-|;5!pIKtUt^%gt&izXDFCD!=0|G zYPcRH1Yjf-THR|H=xpOgs2l&n0o@aOhh(3G6vvaD6FMoG2oetmmQin@CJ{pzib4th zJ5ea`OUfi_<%B#QypMW48o6jfU!OL8WG{LKq=$pr9yuw27)?lEJ{l)UOD9_bHLj7j za?z6}+PUO41q0~Dr1Q4qRH!jb^&34ER8(Ilop=-@f9cMA8kXK%PrA83fPO3b!aHb? zq`>IOdPz8L4qwjgh^Hzj7T6r4($-lL;bJ@-%gTzM6F*f0n1^bf)YKN!pw? zS)J9Sj;7483kqy;EYgoBOSv9k#LPpAdV;2C(sb|$?N9W_Y50RP5^YA`fcnBu)=W$f za=OQf^AjE!n4VXfF}zbdPd?^t-gA)E2rn9n=ivDN-RT}dJDtR=M%<{kR}0s8dF8z)0(2ykdYR z%zBx@i>KdS8o$+irM?%_2FxWeN9~RDhXUwkBkLQew+G({v^V3Cffp#ec&LRRuN9GT z(({G7k!l&3YC=55qDhV>+mS(S-?fYXoEW5R9IoM|^tNPCk&FsUi}t$1Zd> zZ#9q=vY(9`lc)v*s5pJWL@LRqT&aZs6N^f*QH^HF94H}l?*lT6{*b*qrta}xlUT}* z$udVDnzdN7)HYK}bcKkOnw z&JJmCPuVUdn<|9d(!6@tfXPW0t@_Z+geqJujW^_IHDZ0iI8`C!hL?rt0Vqhdx^9Ta zgIpDagI@-N`0M>qs=Q|uV1gb6a#lMGz^S3879A`4p|BC6mc-KmJSJI^I7#uP1t*(g ztnYs=z;>+3EeS{aQ)xtD_hGew*E@*+R+Ecot#C4ht$6h5!%_(*4sRvbs+(FIX_wFZ zC|p`QYYxT}G=oEb6AEE|fT1?w4<~4iE!&ij8vxer>S-MWJ(UnoHNaNKqtofv5sP(BCaBY}Ydr;9c*3Xvf#W3oH8hDxTtp!1K^Gr3< zR@kyCQt32?C$kwzv_`PFi~O`<%9-%?)5;dQDNOI!t|SAS2k)b41V5-|FzKra6MLZcsNPP_ zid5>8ezYi|vjk6!!x)6A!Xmqm31Qht^~%CN^uHCJP6pgPi{o2;*Ln~G+KtL3_nPH~%@ zWa(Z(3-A<4>aP|40}I?c(gm&LNA z#3sTfgQ?~y25MPYZm=~5g0z5v6mTo5SYEJd=2nsyoHdnIc#ohwg}@SZtigdHARR6#uNq+X?M&~!-`CQ1 z2Xb=r1L=x2Gtjc-hR+O&G_~u>Vgo7}lt3gyqZ!!d&GvMGFlxX{dxn@n$xmf@z~tmO zCfSHY#;YmgDt@v_Rk;|j`rS&NstC-2HMhdrR{wipN%E(3V$7yZ+8G?4$w%db`Eq`w zx5|(mQcX>$)%0`*l1BQnS?Xt&8I&&0Z(88VETs9Gtgj-GHISCXDD+zw4%FOwiz)kW zRkuzmvzaDSPVT&`Jf%`WrlVAt5_*ft#Fj8o4-D+%VU0n}q1ke&;O17!!k`fEmEq(n z2itH|vJgnBa%3bXIe|<~9H zWY1tKLaiYgg`M3HsnALvZ9#7rz%5XsBt1&#&v}f+Mvp$` za7jHWk-fT_#nY;k3D5wVBYs*uk5Tj#6<4in8A(ofTH0E$oYu{Y#j^U(lOsp$x{CRA z=&&lQgL5l`bLYpdt17Rfd4vj%*vMo0yzg#gS_=d#<_9YY$*#SQroO6IBhA=#l~u9p zXfRRv(0&41R5!K13Z%QTDp(0WrLX)DK$}dLjt?=Y_*CV-m6E5DdN=0Tf?}kQWx7qq zF&QfLct0a;;weScW&<%=+S1=NnIZ1PtTMy*E>S0im#O{fhM-(8_s51*80@X;r4+=w zDd`)hGel7IW4RD71juyzdEh3GC`*Q~10V3mJXqZGBOL)={F;qN z;@FVhzbw0k*mu&F{LQq%9F4>hcU^O>3N_g6DO1`vd%}2~PKQb{T0>Qv5{)6F4>JZ8 zst!hr&!$+Al52EC*XYD2CA!qIpXd6UJ!LZL+3Yi0y=dXN#Z!K*=T^@(v##-YJ=a`w zt>=2rHT`&ex*-k=)7sz7;RpvydGEzL80NfVcZZW0uN!Lj_t0){*)=3}&9!%y-=)$+ zaJG7NvA#DL3dn@OBoAC$hW~h%YycbLSQu&?#EUq3FHycT7N_Y%4|deT0r_qRmLOys zxwX7zM*7RWGpg}8c{f_F;J2*s;&A8EU?M5r(w5qJO9>BD*W6l;x6XVk+kCCd+m@^m zjg5`7Jb{2`*|KGxAzYrOCU0M#Hy-!gci(;D>YAIct`SIdgM6`#0IgvX#nwN)NJdUx zUAXz``vPLBs7F;@9@Z~^flUve+K{(F?g+IAnQA8ZJ-Taqyuz6&OIpGgm`Wdq675At@yPI z=tK>8pv86gy&7m$J$`8eTHOd)9Q0kfSco;C9ZQft(Bn(-V>VFNGNcQ1a|^-;Jqdae zG_V|D{S(rKpZJu6R^N`_2Z0_2?FDUULpY!%u>N+?-JrWcCsyJIPN0ER$S3F_&Dej9ffcS2Y$^{0JDiQhj7(6gkz0EoK-#rI>o{@^DLz4Ea9Aaj&QV| zBc?Q+FAB@wCY)mzi=vKF;oLkO!Cfv25|<-?S0Eo(2V>!WpQ*&uc10!FZ)`9jg?MlDWb)I9C*JuM&kj<_qVh zYEgK=E6(n?8NZCXMHC;uRX9%FDhhgQM8UxtQRs%Pci?WQ7mjAwyQD!B?Ls9TY!r?S zjlx;B2x(h{w8QoXaW^%I!j9X7YtwC_B(PKzooW`2v1Q0Z3-a0`&cQcL+a0jO+oy72F!<;*X5Tb7TCVE9h;ACE|EUYjLSEzO&*<96^aUNBrJwXj zDYQ$?XKW%ez5^W2$#D}0PWj_D&>HlUbVZpa>5DMN1S({@47C%pXc} zltc6X&V0jC-kKla^1djC{{76yu$gvg{SPpolbe*${4d(@zsmfgq^5Fc{a-QP`nxgB z{{!M1G6J+i`l=I6>^ z9rG>4m(*`yzB@k<87*>4lwmhQ#i`>0MqgdmH!CS z<4h?LHEAiygS-Xu7RXy5Z-Kl8@)pQjAa8-Z1@aciTOeBPeV?$=&+-~}PFCCWgwJl0! z!G{zr_>`hk7|-~q!e98XqOW{J(P_*-{-nbH$+Y0J3ZKpNBBnE$&SQGBg$^)oVyeSw zv*5Li157p6@qd8*HFc=u2>p9uhT^jL2in(5R7ND?*t z`{f?CqhqI%qhk=LJgGeBH_!VY#y?!^S?<4EKKPn)GJF-X`mgh`<9>y8K8g!ee)e!a z%NgrjTJ1Q%`S>Z*6HLoLr^4-H`aILKo>KlTOml_%3iI8cSMuwbj#=oBELiik|I{(1 zuhVxI<42kPm}%)3lw7X#{P1bTe}iek7Zskv^nFbCFr8ppze~w=G1YpHG5#vkSD3z; z!@uZDDx3(SI@kYU?oS7cbj?uZ zVEylb$GNy0xShYYT}izBWkq#6Z({!6A6NdbSoqD%U-)6g|GkCZ%KY*zihs(&Z)g6c ztY6IMmFW1_F#ieWPdD-12y!j+pV_LypUZqgWPuLmU&Z6VJd-~4V*%#7SijlA?`8f^ zKSUu4akqsZVt(mIv-6|Oe}VIF)lV@0`^>kde~|gJIsAT(--+m`?vM@sC&oqH?+&VL z2*LT)^=hA`ULCj8uieF}z7??EceuP77{AWg$5@Q0h~CY3I^!*jn;AdLxP<9C_Wv-` zzp(!+j4L>vV~iUZU;L1Y?@FdOFkQg3f$42bS2K+=9boz;rq^)1FL3zZWB;Er{Uy`a znNH*S_bk)jF#S)aQ@MQg{AD4BU&H>3nMRpzX8I`8Cz&2#`U|GNW9sJmHJ#~oOmAX( z8`D)x<4pg7>HC;|nCa6@_cMKk?eHq&Uoc(&&&uxGnSPn+513x?F~zTCx}5X9lJVV4 z?`OK1>BCHSGkt~Wai;fvT!lNx^bw|?XZkIs2bsRg^e;@$;r^hM={uN?F#QD6Pcwaz z=>wep60UzQvi~8be_$#;q0+a6=?bQIGyMwF7nr`xwD6Nk?|i10GQF1RN~UX>_A%YW z^s7vN!1U)#|AXmNp3hy)w1M;SH0SG1#!bwRGu^;+JJY9_KFd_k6Mw?^w@feIrt)(W z)9-RTt&F>vu4non(~mNJgz39DU7u(CEYkx_f5POgn=_Bm_EB5bT9Aq5bp~5vd zoVPPQ#_4^9%jq=By~@2BLTFrDh z>;07Root^M84oc3O~zx4ujOzbVth8^?=Y@odbDrUMI*Ej`knt^^zt&w@v$J(6dzfGW;zbJ1q0&FY8`l*7`ArU*^hg-{E&@ z$#km2ugRgOrGL-i@6I82(qezDr^7#xLr=@)Y2E^P3*;@3w?N(kc?;w%khehI0(lGM zEs(cB-U4|GIYgRON zOs|a=Db*yM!QNN-- z>Y;nMusB-U)<|iHmYVdMe>e~_L~O@9tm$cKL$Jo8NUYDF zNXR_rOh`-HqF~5a<`4UONO0dIf=%u1t@V+}x}ZS`hJ2PZk<3ce6(b&vgyYCmO=jAb z^(XwDAp{Z&hI_>D(Mh6g>F?{LWNn&6rZpz3&XRCVZF(Y9t z4n;csp}J6{YaLq0*C^B!vn}E8TF1P!ChhaZiRJ6+UFYlR_s0T=IWQCoz`MWG*BJ~4 zP@_glsj1aQqm(QJiqv$XFEIiu7T48BLXlWo)Zdk@g4YETeg5c@K2-k|MpVSMW@&tl z1L$%luh@%Y{%CKoD=zz|K?ol=8#S%_Y-wAKG)LBp;Z)rm-Qh&Kvx=F!x&wdXP&0Z* z1R9C0i_1(7x9YIcE31xn)G=~NMr5^Asn852NjCLCVt8~CiRMU`A3a!RSKHFYodid- zFWDA!Qd4b;B^!cWG!*z_NL6vB6xuGj5we5NSHB2GLL~_&RI9HK`Qz~wdWaJDZYmtV zymaK!8eje5)<`g%xWkB{YZ*I{G^QHgsgY%ALAT%AOR?M`>%Vl#+Al;*z>M{aRv00} zkFIe<#ENAcvbxmA444aJ4@Rlgy@nA|l?X!a^J{#IjYNHa3?pbldbbDrq<*O!Fq$Jh z6qrYXHp3t5>TUHWdeQwi1Y_urBC((mUlMMJ^!bC~IEj`~J6joEk9y@pSKY5dD^K<) z6`@E^5ZzHM7KznYHnuM9BCTr8&)zZA%brKXYP8r~pRb~#(ubaBFyV{Fkmg480wUH# z0=}+6zt0~++eWs1#$cBbl_NR$t&}>&PZ)#21nI0v>U75pi+o3tm*|b5oj|--29fFq zy8Xcr`A6a3Cu1cvh#p+_s!5+=N7AdG24z`NG%0C~pr;VAO*#4f@m>-b$tJMENc6|T za!@Cktx1^xhLTuhXtm*AM~u-V!w92H)ZB)ux3mX^ ztsh-&C9!rVS)3>G95;DVDCD=#;#Zq81F;X}W(O>}Jd_lTlO@s8A{gxv#zZ8s9kqyEzA5+~WJuY;G%tPd850zCwX`Ab&SuE2vv zeOg$v#p@39fF?EPkJOga;kbC*C5^{c+vyTce|T|3eAI;wBvvn;hSu&UD9}AJ z=)I4V`;ZK0mpPo@nZr>X#tG<7kW2w)E7Y%xqpo`KR~J__v9%D@Lxy@VmEhflI>BEr zLcHLy^?iE{@*Fn zyH%!z`X8~is4p^L)SI*ZXpx*=Z7+iTr1&wZdRT^y2N^Z0{h(-Bq(5#b@Csx%N~x{# z{|Lzzte}bXUy6_pMxLXE&6Lsi z7U3bIhaMlqM~YS$;>n^m@ywK!(EzpZ{fJ-A#KcntmMZ8I|29R4k;`4fj_r=pl494P zv&RY=_B!?yl@u4dN={BWS}=T8d3jmc&f-L3J7hLZJ>)#$*jcdIvCpy9G3{i*rh)^m zLj}hi(OGWy%$YNHIybr|3ii213b!~M=w@5#CR9u&oCaOA8+G&QS&cT z_|L!;-#FuG=PSIAaTTykFXLXu2N^%ec!KeE#)san!avA({7Qw3E>PjuyhGt@8All} zXFSR{!gwF!j{sA8jx&Cd@$~CeczbDuutqxOu+9KVW=_@o_D`K=Cg?xl#J7s}-KhxS8=<#tmM@-^6(TO$vX3 z@$_31-p}~NtqLD!JW-?YxfiMUT5A=)mhlG0jf^)l4lv%v_yNY_j6cEn2;-+27uTuq zk20>RSGXMYQ0BK$;Sl4}#R@;mxMPXJuP{EoRN<+px1{e{rtr;-hg%eWC*#eG2N<7Z z{87e%#vaChWn9Df9UhgQ4#xG2hZ!4; zM;YJGc%1Pgj3*dB!&n4_ybdw;Fn*J94dba-a{P?1Vm!=vKI2iwEsVz*_cESfJj_@a zD*lHVdl-M7aSh||Fz#UdCgWkog)>z8Mj2npc%1PP#uJQt7>jNd{{xIYj6cb^hVkbZ zcQAgQ@i61RG9G1o@l`54P{>YPLV)D#qiCTNzI<4lou$ z72mrVdl)~=xQ6kIj5`?rjPWqz6O2b0m%M}X&-iA>6O30e7Wb(5gN!|l-_N*)@h2H~ zFuwU375*^eLB^wuw=y1Q{0+tvjNN5QU#wH{FJtUs9AR9;_=Ai)7;k4h%=ksdql{l; zJkHp8t%`4g@pQ%_q~c%7*uyx=xQ6j*#vP2OUB~%nyqNJQ;~vK2j6cYDg7FTtK8t<6*`N7>_dkJmYc3FEXBBJi%CmRs4Ts>|yMlrQ)k$JcDru z;~K`pj5`>QGTy{^obh(X6O3PCEFvoYUoiGCKE=3(@p(6J{u!4s9%fw2c$D#-jK>-G zGoE0)nXx={2uXN?u_vn1^Gn7xj88J|V0`gxE`P=~j7J&2oAEf~&oG`~{0+v~Y?9Zn z7<(8O&tdyBu3+53xRdcP<3Yxwj6cM9obgV^6O6yl7z?HHI?C9?`0R3ypYc_UI~d>0 zc$o2Bj7J&Y$9SCaKQo?S{4!$^SMi@_>|s2kg5zgg!?=U7pYbr`_c9)3Jj!^S@mCp7 zF#Z8!ec;p|8G8~cJ?B?){EV+-+`+ho@i5~a#-ohi&3K&g*&f$;?6-!m5LRs0vuQ{j6U&t+W0xS4SW<8H>ojPGYW%J>P!%vYu3_BCxP$S1jE5O-V?4_ECyd7#|B3MgWA_4%e@Mk&#n{96UdAJzbE}oSJ}+`iP zxKV{yGpy3Ti17}_!-R{3*u(fEjQ2C%&iDx9FEiftZXvIIjE^$@3FG4TDE{vlZ(;1f zb1%g=^Sz2cjd3sIYZwo2RD3Vvjqg)<8RIRCI~Z?g9A~_T@q>&HGX4bP0}rb3b}}x0 zzrxQku44Rej6;lHW4wX!Nygh4pM8_c-%-X_FfQAq!mnW5!MK6(7~|E9_cOkS@nOaf zFh0q6gz@x$RN+6ycn#w(Gv3Jf+l;p|KFoLzw^h3mF&xlZvm5aXI6g z7&kF)Vcg5u&v+x_1mo?D-_LkA<4-c)%lLDQ4>BHSJi+(}j88KDHSiVZ4U0|VD!o_T zjQpG@#16(c0TX{8;|Ca@Wc(=0mp-rbpJjeEW9KbOznAe%jJGl#X1tH_PR1u0|AcYb z3o3littz}VjGGBxFf2M=RQx{1y^Oapjxv6p@gU>hF&<`oaSif+AJVZ!*^B|NV)vK6n4}dKJDt@3D@tJ~zFOu|7Bb z!;A%=BRK|q#U)se;d3m1P583mOx1YP$s*54D@0(|ae zIFs`t#eafeIKbX*7AEl=cEd{Ui_e|mlV z74T=8F!5*Nb}4bG*<#_7zFtqK^>xzMSnCg4^v!(I*X!@J9#8rjYyIsOeKVi*^?E(6 z-;=(^TEEnyZ|0M}Uf-wne$v-i>z7#c&3w|=`vJ5+K>8YM{bGy0nNRw9|A6)rNMB>E zZ?(UfPx^Ylf%YFrUt_I*s8A(-`(r&mnz{iJwj1%qRT=93Sof zkiN!R--U4KBHUu(lfK>`qWvP$*O=rferx{CeA3tZNwmL2`WkEflNS4%`J}J+pJ+dd z^flJ{N6CSU2&Lc5Cw;wNMf+EzuQAC}`iI#7gh}7bCw;xYMf+W(uQAD!emQQs2$Q~< zPx^X4jP}P!Ut^Nbrf=qxzTQ8h{WQ|oSnHQj-{#`eYMqb zulMI@zmD`Z*7~z7_A~QIU+?G9{vPRTto0K}16_nE{boMt>-|644;1(p*4H?fzL`(@dOwo(Cnk|+C}z)crn(l_%-U+<67ekti|O!B0^lN`8+ zkiMBu`g%W=_E%;8@!U-p$&>zO0=TrkmM2X5djFO7V@Y3Q;*FfPn+V3TOjkP|4l^0=(-^?d{y&p{b!!rMPZl_D@pCkuk z(%16jP5OHOnD&!NUt{7^{x^{W7ZK7o^GRRtH`D$z>1#~#q~GeqKU{=K-^?d{y+2L+ z)ugX6$&>yea^NCD`er`q>-}un-zI&HNuKnN;iii)>6`haKT$(mlppJBto5Ucozyq; zNnh`m)BZWdud&vjWzjeDNqFfP|+W#kgjY*z#t@>s@>Ff6c=zRgw*EpBHnNRxq zeFA#Ffb=!i`m@M^iwLFP%qM;Q{sFy@K>8Y!Jf+=bslR4E=~v^qg|708mA=MWe}Wvi zh*11yKI!ZC8|ZxpieF=rr}&#J@tgUiuiuBD_ajJOW34}8(Kqu+U%x*=?^BSz##� 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 a!Y*1dX%G66ZqL#C$|bw0ZoP5@jOGW1ArT7z literal 0 HcmV?d00001 diff --git a/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/_CodeSignature/CodeResources b/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/_CodeSignature/CodeResources new file mode 100644 index 00000000..f4d2e431 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/_CodeSignature/CodeResources @@ -0,0 +1,128 @@ + + + + + files + + Resources/InfoPlist.strings + + MiLKDDnrUKr4EmuvhS5VQwxHGK8= + + + files2 + + Resources/InfoPlist.strings + + hash2 + + Oc8u4Ht7Mz58F50L9NeYpbcq9qTlhPUeZCcDu/pPyCg= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Assets/AirConsole/unity-webview/Plugins/WebViewObject.cs b/Assets/AirConsole/unity-webview/Plugins/WebViewObject.cs new file mode 100644 index 00000000..4d519703 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/WebViewObject.cs @@ -0,0 +1,1661 @@ +/* + * Copyright (C) 2011 Keijiro Takahashi + * Copyright (C) 2012 GREE, Inc. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +using UnityEngine; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; +#if UNITY_2018_4_OR_NEWER +using UnityEngine.Networking; +#endif +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX +using System.IO; +using System.Text.RegularExpressions; +using UnityEngine.EventSystems; +using UnityEngine.Rendering; +using UnityEngine.UI; +#endif +#if UNITY_ANDROID +using UnityEngine.Android; +#endif + +using Callback = System.Action; + +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX +public class UnitySendMessageDispatcher +{ + public static void Dispatch(string name, string method, string message) + { + GameObject obj = GameObject.Find(name); + if (obj != null) + obj.SendMessage(method, message); + } +} +#endif + +public class WebViewObject : MonoBehaviour +{ + Callback onJS; + Callback onError; + Callback onHttpError; + Callback onStarted; + Callback onLoaded; + Callback onHooked; + Callback onCookies; + bool paused; + bool visibility; + bool alertDialogEnabled; + bool scrollBounceEnabled; + int mMarginLeft; + int mMarginTop; + int mMarginRight; + int mMarginBottom; + bool mMarginRelative; + float mMarginLeftComputed; + float mMarginTopComputed; + float mMarginRightComputed; + float mMarginBottomComputed; + bool mMarginRelativeComputed; +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + public GameObject canvas; + Image bg; + IntPtr webView; + Rect rect; + Texture2D texture; + byte[] textureDataBuffer; + string inputString = ""; + bool hasFocus; +#elif UNITY_IPHONE + IntPtr webView; +#elif UNITY_ANDROID + AndroidJavaObject webView; + + bool mVisibility; + int mKeyboardVisibleHeight; + float mResumedTimestamp; + int mLastScreenHeight; +#if UNITYWEBVIEW_ANDROID_ENABLE_NAVIGATOR_ONLINE + float androidNetworkReachabilityCheckT0 = -1.0f; + NetworkReachability? androidNetworkReachability0 = null; +#endif + + void OnApplicationPause(bool paused) + { + this.paused = paused; + if (webView == null) + return; + // if (!paused && mKeyboardVisibleHeight > 0) + // { + // webView.Call("SetVisibility", false); + // mResumedTimestamp = Time.realtimeSinceStartup; + // } + webView.Call("OnApplicationPause", paused); + } + + void Update() + { + // NOTE: + // + // When OnApplicationPause(true) is called and the app is in closing, webView.Call(...) + // after that could cause crashes because underlying java instances were closed. + // + // This has not been cleary confirmed yet. However, as Update() is called once after + // OnApplicationPause(true), it is likely correct. + // + // Base on this assumption, we do nothing here if the app is paused. + // + // cf. https://github.com/gree/unity-webview/issues/991#issuecomment-1776628648 + // cf. https://docs.unity3d.com/2020.3/Documentation/Manual/ExecutionOrder.html + // + // In between frames + // + // * OnApplicationPause: This is called at the end of the frame where the pause is detected, + // effectively between the normal frame updates. One extra frame will be issued after + // OnApplicationPause is called to allow the game to show graphics that indicate the + // paused state. + // + if (paused) + return; + if (webView == null) + return; +#if UNITYWEBVIEW_ANDROID_ENABLE_NAVIGATOR_ONLINE + var t = Time.time; + if (t - 1.0f >= androidNetworkReachabilityCheckT0) + { + androidNetworkReachabilityCheckT0 = t; + var androidNetworkReachability = Application.internetReachability; + if (androidNetworkReachability0 != androidNetworkReachability) + { + androidNetworkReachability0 = androidNetworkReachability; + webView.Call("SetNetworkAvailable", androidNetworkReachability != NetworkReachability.NotReachable); + } + } +#endif + if (mResumedTimestamp != 0.0f && Time.realtimeSinceStartup - mResumedTimestamp > 0.5f) + { + mResumedTimestamp = 0.0f; + webView.Call("SetVisibility", mVisibility); + } + if (Screen.height != mLastScreenHeight) + { + mLastScreenHeight = Screen.height; + webView.Call("EvaluateJS", "(function() {var e = document.activeElement; if (e != null && e.tagName.toLowerCase() != 'body') {e.blur(); e.focus();}})()"); + } + for (;;) { + if (webView == null) + break; + var s = webView.Call("GetMessage"); + if (s == null) + break; + var i = s.IndexOf(':', 0); + if (i == -1) + continue; + switch (s.Substring(0, i)) { + case "CallFromJS": + CallFromJS(s.Substring(i + 1)); + break; + case "CallOnError": + CallOnError(s.Substring(i + 1)); + break; + case "CallOnHttpError": + CallOnHttpError(s.Substring(i + 1)); + break; + case "CallOnLoaded": + CallOnLoaded(s.Substring(i + 1)); + break; + case "CallOnStarted": + CallOnStarted(s.Substring(i + 1)); + break; + case "CallOnHooked": + CallOnHooked(s.Substring(i + 1)); + break; + case "CallOnCookies": + CallOnCookies(s.Substring(i + 1)); + break; + case "SetKeyboardVisible": + SetKeyboardVisible(s.Substring(i + 1)); + break; + case "RequestFileChooserPermissions": + RequestFileChooserPermissions(); + break; + } + } + } + + /// Called from Java native plugin to set when the keyboard is opened + public void SetKeyboardVisible(string keyboardVisibleHeight) + { + if (BottomAdjustmentDisabled()) + { + return; + } + var keyboardVisibleHeight0 = mKeyboardVisibleHeight; + var keyboardVisibleHeight1 = Int32.Parse(keyboardVisibleHeight); + if (keyboardVisibleHeight0 != keyboardVisibleHeight1) + { + mKeyboardVisibleHeight = keyboardVisibleHeight1; + SetMargins(mMarginLeft, mMarginTop, mMarginRight, mMarginBottom, mMarginRelative); + } + } + + /// Called from Java native plugin to request permissions for the file chooser. + public void RequestFileChooserPermissions() + { + var permissions = new List(); + using (var version = new AndroidJavaClass("android.os.Build$VERSION")) + { + if (version.GetStatic("SDK_INT") >= 33) + { + if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_IMAGES")) + { + permissions.Add("android.permission.READ_MEDIA_IMAGES"); + } + if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_VIDEO")) + { + permissions.Add("android.permission.READ_MEDIA_VIDEO"); + } + if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_AUDIO")) + { + permissions.Add("android.permission.READ_MEDIA_AUDIO"); + } + } + else + { + if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageRead)) + { + permissions.Add(Permission.ExternalStorageRead); + } + if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite)) + { + permissions.Add(Permission.ExternalStorageWrite); + } + } + } + if (!Permission.HasUserAuthorizedPermission(Permission.Camera)) + { + permissions.Add(Permission.Camera); + } + if (permissions.Count > 0) + { +#if UNITY_2020_2_OR_NEWER + var grantedCount = 0; + var deniedCount = 0; + var callbacks = new PermissionCallbacks(); + callbacks.PermissionGranted += (permission) => + { + grantedCount++; + if (grantedCount + deniedCount == permissions.Count) + { + StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count)); + } + }; + callbacks.PermissionDenied += (permission) => + { + deniedCount++; + if (grantedCount + deniedCount == permissions.Count) + { + StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count)); + } + }; + callbacks.PermissionDeniedAndDontAskAgain += (permission) => + { + deniedCount++; + if (grantedCount + deniedCount == permissions.Count) + { + StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count)); + } + }; + Permission.RequestUserPermissions(permissions.ToArray(), callbacks); +#else + StartCoroutine(RequestFileChooserPermissionsCoroutine(permissions.ToArray())); +#endif + } + else + { + StartCoroutine(CallOnRequestFileChooserPermissionsResult(true)); + } + } + +#if UNITY_2020_2_OR_NEWER +#else + int mRequestPermissionPhase; + + IEnumerator RequestFileChooserPermissionsCoroutine(string[] permissions) + { + foreach (var permission in permissions) + { + mRequestPermissionPhase = 0; + Permission.RequestUserPermission(permission); + // waiting permission dialog that may not be opened. + for (var i = 0; i < 8 && mRequestPermissionPhase == 0; i++) + { + yield return new WaitForSeconds(0.25f); + } + if (mRequestPermissionPhase == 0) + { + // permission dialog was not opened. + continue; + } + while (mRequestPermissionPhase == 1) + { + yield return new WaitForSeconds(0.3f); + } + } + yield return new WaitForSeconds(0.3f); + var granted = 0; + foreach (var permission in permissions) + { + if (Permission.HasUserAuthorizedPermission(permission)) + { + granted++; + } + } + StartCoroutine(CallOnRequestFileChooserPermissionsResult(granted == permissions.Length)); + } + + void OnApplicationFocus(bool hasFocus) + { + if (hasFocus) + { + if (mRequestPermissionPhase == 1) + { + mRequestPermissionPhase = 2; + } + } + else + { + if (mRequestPermissionPhase == 0) + { + mRequestPermissionPhase = 1; + } + } + } +#endif + + private IEnumerator CallOnRequestFileChooserPermissionsResult(bool granted) + { + for (var i = 0; i < 3; i++) + { + yield return null; + } + webView.Call("OnRequestFileChooserPermissionsResult", granted); + } + + public int AdjustBottomMargin(int bottom) + { + if (BottomAdjustmentDisabled()) + { + return bottom; + } + else if (mKeyboardVisibleHeight <= 0) + { + return bottom; + } + else + { + int keyboardHeight = 0; + using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (var activity = unityClass.GetStatic("currentActivity")) + using (var player = activity.Get("mUnityPlayer")) + using (var view = player.Call("getView")) + using (var rect = new AndroidJavaObject("android.graphics.Rect")) + { + if (view.Call("getGlobalVisibleRect", rect)) + { + int h0 = rect.Get("bottom"); + view.Call("getWindowVisibleDisplayFrame", rect); + int h1 = rect.Get("bottom"); + keyboardHeight = h0 - h1; + } + } + return (bottom > keyboardHeight) ? bottom : keyboardHeight; + } + } + + private bool BottomAdjustmentDisabled() + { +#if UNITYWEBVIEW_ANDROID_FORCE_MARGIN_ADJUSTMENT_FOR_KEYBOARD + return false; +#else + return + !Screen.fullScreen + || ((Screen.autorotateToLandscapeLeft || Screen.autorotateToLandscapeRight) + && (Screen.autorotateToPortrait || Screen.autorotateToPortraitUpsideDown)); +#endif + } +#else + IntPtr webView; +#endif + + void Awake() + { + alertDialogEnabled = true; + scrollBounceEnabled = true; + mMarginLeftComputed = -9999; + mMarginTopComputed = -9999; + mMarginRightComputed = -9999; + mMarginBottomComputed = -9999; + } + + public bool IsKeyboardVisible + { + get + { +#if !UNITY_EDITOR && UNITY_ANDROID + return mKeyboardVisibleHeight > 0; +#elif !UNITY_EDITOR && UNITY_IPHONE + return TouchScreenKeyboard.visible; +#else + return false; +#endif + } + } + +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + [DllImport("WebView")] + private static extern string _CWebViewPlugin_GetAppPath(); + [DllImport("WebView")] + private static extern IntPtr _CWebViewPlugin_InitStatic( + bool inEditor, bool useMetal); + [DllImport("WebView")] + private static extern IntPtr _CWebViewPlugin_Init( + string gameObject, bool transparent, bool zoom, int width, int height, string ua, bool separated); + [DllImport("WebView")] + private static extern int _CWebViewPlugin_Destroy(IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_SetRect( + IntPtr instance, int width, int height); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_SetVisibility( + IntPtr instance, bool visibility); + [DllImport("WebView")] + private static extern bool _CWebViewPlugin_SetURLPattern( + IntPtr instance, string allowPattern, string denyPattern, string hookPattern); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_LoadURL( + IntPtr instance, string url); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_LoadHTML( + IntPtr instance, string html, string baseUrl); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_EvaluateJS( + IntPtr instance, string url); + [DllImport("WebView")] + private static extern int _CWebViewPlugin_Progress( + IntPtr instance); + [DllImport("WebView")] + private static extern bool _CWebViewPlugin_CanGoBack( + IntPtr instance); + [DllImport("WebView")] + private static extern bool _CWebViewPlugin_CanGoForward( + IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_GoBack( + IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_GoForward( + IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_Reload( + IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_SendMouseEvent(IntPtr instance, int x, int y, float deltaY, int mouseState); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_SendKeyEvent(IntPtr instance, int x, int y, string keyChars, ushort keyCode, int keyState); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_Update(IntPtr instance, bool refreshBitmap, int devicePixelRatio); + [DllImport("WebView")] + private static extern int _CWebViewPlugin_BitmapWidth(IntPtr instance); + [DllImport("WebView")] + private static extern int _CWebViewPlugin_BitmapHeight(IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_Render(IntPtr instance, IntPtr textureBuffer); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_AddCustomHeader(IntPtr instance, string headerKey, string headerValue); + [DllImport("WebView")] + private static extern string _CWebViewPlugin_GetCustomHeaderValue(IntPtr instance, string headerKey); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_RemoveCustomHeader(IntPtr instance, string headerKey); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_ClearCustomHeader(IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_ClearCookies(); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_SaveCookies(); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_GetCookies(IntPtr instance, string url); + [DllImport("WebView")] + private static extern string _CWebViewPlugin_GetMessage(IntPtr instance); +#elif UNITY_IPHONE + [DllImport("__Internal")] + private static extern IntPtr _CWebViewPlugin_Init(string gameObject, bool transparent, bool zoom, string ua, bool enableWKWebView, int wkContentMode, bool wkAllowsLinkPreview, bool wkAllowsBackForwardNavigationGestures, int radius); + [DllImport("__Internal")] + private static extern int _CWebViewPlugin_Destroy(IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetMargins( + IntPtr instance, float left, float top, float right, float bottom, bool relative); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetVisibility( + IntPtr instance, bool visibility); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetScrollbarsVisibility( + IntPtr instance, bool visibility); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetAlertDialogEnabled( + IntPtr instance, bool enabled); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetScrollBounceEnabled( + IntPtr instance, bool enabled); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetInteractionEnabled( + IntPtr instance, bool enabled); + [DllImport("__Internal")] + private static extern bool _CWebViewPlugin_SetURLPattern( + IntPtr instance, string allowPattern, string denyPattern, string hookPattern); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_LoadURL( + IntPtr instance, string url); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_LoadHTML( + IntPtr instance, string html, string baseUrl); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_EvaluateJS( + IntPtr instance, string url); + [DllImport("__Internal")] + private static extern int _CWebViewPlugin_Progress( + IntPtr instance); + [DllImport("__Internal")] + private static extern bool _CWebViewPlugin_CanGoBack( + IntPtr instance); + [DllImport("__Internal")] + private static extern bool _CWebViewPlugin_CanGoForward( + IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_GoBack( + IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_GoForward( + IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_Reload( + IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_AddCustomHeader(IntPtr instance, string headerKey, string headerValue); + [DllImport("__Internal")] + private static extern string _CWebViewPlugin_GetCustomHeaderValue(IntPtr instance, string headerKey); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_RemoveCustomHeader(IntPtr instance, string headerKey); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_ClearCustomHeader(IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_ClearCookies(); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SaveCookies(); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_GetCookies(IntPtr instance, string url); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetBasicAuthInfo(IntPtr instance, string userName, string password); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_ClearCache(IntPtr instance, bool includeDiskFiles); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetSuspended(IntPtr instance, bool suspended); +#elif UNITY_WEBGL + [DllImport("__Internal")] + private static extern void _gree_unity_webview_init(string name); + [DllImport("__Internal")] + private static extern void _gree_unity_webview_setMargins(string name, int left, int top, int right, int bottom); + [DllImport("__Internal")] + private static extern void _gree_unity_webview_setVisibility(string name, bool visible); + [DllImport("__Internal")] + private static extern void _gree_unity_webview_loadURL(string name, string url); + [DllImport("__Internal")] + private static extern void _gree_unity_webview_evaluateJS(string name, string js); + [DllImport("__Internal")] + private static extern void _gree_unity_webview_destroy(string name); +#endif + + public static bool IsWebViewAvailable() + { +#if !UNITY_EDITOR && UNITY_ANDROID + using (var plugin = new AndroidJavaObject("net.gree.unitywebview.CWebViewPlugin")) + { + return plugin.CallStatic("IsWebViewAvailable"); + } +#else + return true; +#endif + } + + public void Init( + Callback cb = null, + Callback err = null, + Callback httpErr = null, + Callback ld = null, + Callback started = null, + Callback hooked = null, + Callback cookies = null, + bool transparent = false, + bool zoom = true, + string ua = "", + int radius = 0, + // android + int androidForceDarkMode = 0, // 0: follow system setting, 1: force dark off, 2: force dark on + // ios + bool enableWKWebView = true, + int wkContentMode = 0, // 0: recommended, 1: mobile, 2: desktop + bool wkAllowsLinkPreview = true, + bool wkAllowsBackForwardNavigationGestures = true, + // editor + bool separated = false) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + _CWebViewPlugin_InitStatic( + Application.platform == RuntimePlatform.OSXEditor, + SystemInfo.graphicsDeviceType == GraphicsDeviceType.Metal); +#endif + onJS = cb; + onError = err; + onHttpError = httpErr; + onStarted = started; + onLoaded = ld; + onHooked = hooked; + onCookies = cookies; +#if UNITY_WEBGL +#if !UNITY_EDITOR + _gree_unity_webview_init(name); +#endif +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.init", name); +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + Debug.LogError("Webview is not supported on this platform."); +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + { + var uri = new Uri(_CWebViewPlugin_GetAppPath()); + var info = File.ReadAllText(uri.LocalPath + "Contents/Info.plist"); + if (Regex.IsMatch(info, @"CFBundleGetInfoString\s*Unity version [5-9]\.[3-9]") + && !Regex.IsMatch(info, @"NSAppTransportSecurity\s*\s*NSAllowsArbitraryLoads\s*\s*")) { + Debug.LogWarning("WebViewObject: NSAppTransportSecurity isn't configured to allow HTTP. If you need to allow any HTTP access, please shutdown Unity and invoke:\n/usr/libexec/PlistBuddy -c \"Add NSAppTransportSecurity:NSAllowsArbitraryLoads bool true\" /Applications/Unity/Unity.app/Contents/Info.plist"); + } + } +#if UNITY_EDITOR_OSX + // if (string.IsNullOrEmpty(ua)) { + // ua = @"Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53"; + // } +#endif + webView = _CWebViewPlugin_Init( + name, + transparent, + zoom, + Screen.width, + Screen.height, + ua +#if UNITY_EDITOR + , separated +#else + , false +#endif + ); + rect = new Rect(0, 0, Screen.width, Screen.height); +#elif UNITY_IPHONE + webView = _CWebViewPlugin_Init(name, transparent, zoom, ua, enableWKWebView, wkContentMode, wkAllowsLinkPreview, wkAllowsBackForwardNavigationGestures, radius); +#elif UNITY_ANDROID + webView = new AndroidJavaObject("net.gree.unitywebview.CWebViewPlugin"); +#if UNITY_2021_1_OR_NEWER + webView.SetStatic("forceBringToFront", true); +#endif + webView.Call("Init", name, transparent, zoom, androidForceDarkMode, ua, radius); +#else + Debug.LogError("Webview is not supported on this platform."); +#endif + } + + protected virtual void OnDestroy() + { +#if UNITY_WEBGL +#if !UNITY_EDITOR + _gree_unity_webview_destroy(name); +#endif +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.destroy", name); +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + if (bg != null) { + Destroy(bg.gameObject); + } + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_Destroy(webView); + webView = IntPtr.Zero; + Destroy(texture); +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_Destroy(webView); + webView = IntPtr.Zero; +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("Destroy"); + webView.Dispose(); + webView = null; +#endif + } + + public void Pause() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + //TODO: UNSUPPORTED +#elif UNITY_IPHONE + // NOTE: this suspends media playback only. + if (webView == null) + return; + _CWebViewPlugin_SetSuspended(webView, true); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("Pause"); +#endif + } + + public void Resume() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + //TODO: UNSUPPORTED +#elif UNITY_IPHONE + // NOTE: this resumes media playback only. + _CWebViewPlugin_SetSuspended(webView, false); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("Resume"); +#endif + } + + // Use this function instead of SetMargins to easily set up a centered window + // NOTE: for historical reasons, `center` means the lower left corner and positive y values extend up. + public void SetCenterPositionWithScale(Vector2 center, Vector2 scale) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#else + float left = (Screen.width - scale.x) / 2.0f + center.x; + float right = Screen.width - (left + scale.x); + float bottom = (Screen.height - scale.y) / 2.0f + center.y; + float top = Screen.height - (bottom + scale.y); + SetMargins((int)left, (int)top, (int)right, (int)bottom); +#endif + } + + public void SetMargins(int left, int top, int right, int bottom, bool relative = false) + { +#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return; +#elif UNITY_WEBPLAYER || UNITY_WEBGL +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + if (webView == IntPtr.Zero) + return; +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; +#elif UNITY_ANDROID + if (webView == null) + return; +#endif + + mMarginLeft = left; + mMarginTop = top; + mMarginRight = right; + mMarginBottom = bottom; + mMarginRelative = relative; + float ml, mt, mr, mb; +#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_WEBPLAYER || UNITY_WEBGL + ml = left; + mt = top; + mr = right; + mb = bottom; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + ml = left; + mt = top; + mr = right; + mb = bottom; +#elif UNITY_IPHONE + if (relative) + { + float w = (float)Screen.width; + float h = (float)Screen.height; + ml = left / w; + mt = top / h; + mr = right / w; + mb = bottom / h; + } + else + { + ml = left; + mt = top; + mr = right; + mb = bottom; + } +#elif UNITY_ANDROID + if (relative) + { + float w = (float)Screen.width; + float h = (float)Screen.height; + int iw = Display.main.systemWidth; + int ih = Display.main.systemHeight; + if (!Screen.fullScreen) + { + using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (var activity = unityClass.GetStatic("currentActivity")) + using (var player = activity.Get("mUnityPlayer")) + using (var view = player.Call("getView")) + using (var rect = new AndroidJavaObject("android.graphics.Rect")) + { + view.Call("getDrawingRect", rect); + iw = rect.Call("width"); + ih = rect.Call("height"); + } + } + ml = left / w * iw; + mt = top / h * ih; + mr = right / w * iw; + mb = AdjustBottomMargin((int)(bottom / h * ih)); + } + else + { + ml = left; + mt = top; + mr = right; + mb = AdjustBottomMargin(bottom); + } +#endif + bool r = relative; + + if (ml == mMarginLeftComputed + && mt == mMarginTopComputed + && mr == mMarginRightComputed + && mb == mMarginBottomComputed + && r == mMarginRelativeComputed) + { + return; + } + mMarginLeftComputed = ml; + mMarginTopComputed = mt; + mMarginRightComputed = mr; + mMarginBottomComputed = mb; + mMarginRelativeComputed = r; + +#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.setMargins", name, (int)ml, (int)mt, (int)mr, (int)mb); +#elif UNITY_WEBGL && !UNITY_EDITOR + _gree_unity_webview_setMargins(name, (int)ml, (int)mt, (int)mr, (int)mb); +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + int width = (int)(Screen.width - (ml + mr)); + int height = (int)(Screen.height - (mb + mt)); + _CWebViewPlugin_SetRect(webView, width, height); + rect = new Rect(left, bottom, width, height); + UpdateBGTransform(); +#elif UNITY_IPHONE + _CWebViewPlugin_SetMargins(webView, ml, mt, mr, mb, r); +#elif UNITY_ANDROID + webView.Call("SetMargins", (int)ml, (int)mt, (int)mr, (int)mb); +#endif + } + + public void SetVisibility(bool v) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + if (bg != null) + { + bg.gameObject.active = v; + } +#endif + if (GetVisibility() && !v) + { + EvaluateJS("if (document && document.activeElement) document.activeElement.blur();"); + } +#if UNITY_WEBGL +#if !UNITY_EDITOR + _gree_unity_webview_setVisibility(name, v); +#endif +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.setVisibility", name, v); +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetVisibility(webView, v); +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetVisibility(webView, v); +#elif UNITY_ANDROID + if (webView == null) + return; + mVisibility = v; + webView.Call("SetVisibility", v); +#endif + visibility = v; + } + + public bool GetVisibility() + { + return visibility; + } + + public void SetScrollbarsVisibility(bool v) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetScrollbarsVisibility(webView, v); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetScrollbarsVisibility", v); +#else + // TODO: UNSUPPORTED +#endif + } + + public void EnableWebviewDebugging(bool enabled) { +#if UNITY_ANDROID && !(UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + if (webView == null) { + return; + } + + webView.Call("enableWebViewDebugging", enabled); +#else + Debug.Log($"EnableWebviewDebugging({enabled}) not implemented on {Application.platform}"); +#endif + } + + public void SetInteractionEnabled(bool enabled) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetInteractionEnabled(webView, enabled); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetInteractionEnabled", enabled); +#else + // TODO: UNSUPPORTED +#endif + } + + public void SetAlertDialogEnabled(bool e) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetAlertDialogEnabled(webView, e); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetAlertDialogEnabled", e); +#else + // TODO: UNSUPPORTED +#endif + alertDialogEnabled = e; + } + + public bool GetAlertDialogEnabled() + { + return alertDialogEnabled; + } + + public void SetScrollBounceEnabled(bool e) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetScrollBounceEnabled(webView, e); +#elif UNITY_ANDROID + // TODO: UNSUPPORTED +#else + // TODO: UNSUPPORTED +#endif + scrollBounceEnabled = e; + } + + public bool GetScrollBounceEnabled() + { + return scrollBounceEnabled; + } + + public void SetCameraAccess(bool allowed) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + // TODO: UNSUPPORTED +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetCameraAccess", allowed); +#else + // TODO: UNSUPPORTED +#endif + } + + public void SetMicrophoneAccess(bool allowed) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + // TODO: UNSUPPORTED +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetMicrophoneAccess", allowed); +#else + // TODO: UNSUPPORTED +#endif + } + + public bool SetURLPattern(string allowPattern, string denyPattern, string hookPattern) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return false; + return _CWebViewPlugin_SetURLPattern(webView, allowPattern, denyPattern, hookPattern); +#elif UNITY_ANDROID + if (webView == null) + return false; + return webView.Call("SetURLPattern", allowPattern, denyPattern, hookPattern); +#endif + } + + public void LoadURL(string url) + { + if (string.IsNullOrEmpty(url)) + return; +#if UNITY_WEBGL +#if !UNITY_EDITOR + _gree_unity_webview_loadURL(name, url); +#endif +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.loadURL", name, url); +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_LoadURL(webView, url); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("LoadURL", url); +#endif + } + + public void LoadHTML(string html, string baseUrl) + { + if (string.IsNullOrEmpty(html)) + return; + if (string.IsNullOrEmpty(baseUrl)) + baseUrl = ""; +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_LoadHTML(webView, html, baseUrl); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("LoadHTML", html, baseUrl); +#endif + } + + public void EvaluateJS(string js) + { +#if UNITY_WEBGL +#if !UNITY_EDITOR + _gree_unity_webview_evaluateJS(name, js); +#endif +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.evaluateJS", name, js); +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_EvaluateJS(webView, js); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("EvaluateJS", js); +#endif + } + + public int Progress() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED + return 0; +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return 0; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return 0; + return _CWebViewPlugin_Progress(webView); +#elif UNITY_ANDROID + if (webView == null) + return 0; + return webView.Get("progress"); +#endif + } + + public bool CanGoBack() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return false; + return _CWebViewPlugin_CanGoBack(webView); +#elif UNITY_ANDROID + if (webView == null) + return false; + return webView.Get("canGoBack"); +#endif + } + + public bool CanGoForward() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return false; + return _CWebViewPlugin_CanGoForward(webView); +#elif UNITY_ANDROID + if (webView == null) + return false; + return webView.Get("canGoForward"); +#endif + } + + public void GoBack() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_GoBack(webView); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("GoBack"); +#endif + } + + public void GoForward() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_GoForward(webView); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("GoForward"); +#endif + } + + public void Reload() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_Reload(webView); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("Reload"); +#endif + } + + public void CallOnError(string error) + { + if (onError != null) + { + onError(error); + } + } + + public void CallOnHttpError(string error) + { + if (onHttpError != null) + { + onHttpError(error); + } + } + + public void CallOnStarted(string url) + { + if (onStarted != null) + { + onStarted(url); + } + } + + public void CallOnLoaded(string url) + { + if (onLoaded != null) + { + onLoaded(url); + } + } + + public void CallFromJS(string message) + { + if (onJS != null) + { +#if !UNITY_ANDROID +#if UNITY_2018_4_OR_NEWER + message = UnityWebRequest.UnEscapeURL(message); +#else // UNITY_2018_4_OR_NEWER + message = WWW.UnEscapeURL(message); +#endif // UNITY_2018_4_OR_NEWER +#endif // !UNITY_ANDROID + onJS(message); + } + } + + public void CallOnHooked(string message) + { + if (onHooked != null) + { +#if !UNITY_ANDROID +#if UNITY_2018_4_OR_NEWER + message = UnityWebRequest.UnEscapeURL(message); +#else // UNITY_2018_4_OR_NEWER + message = WWW.UnEscapeURL(message); +#endif // UNITY_2018_4_OR_NEWER +#endif // !UNITY_ANDROID + onHooked(message); + } + } + + public void CallOnCookies(string cookies) + { + if (onCookies != null) + { + onCookies(cookies); + } + } + + public void AddCustomHeader(string headerKey, string headerValue) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_AddCustomHeader(webView, headerKey, headerValue); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("AddCustomHeader", headerKey, headerValue); +#endif + } + + public string GetCustomHeaderValue(string headerKey) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED + return null; +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return null; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return null; + return _CWebViewPlugin_GetCustomHeaderValue(webView, headerKey); +#elif UNITY_ANDROID + if (webView == null) + return null; + return webView.Call("GetCustomHeaderValue", headerKey); +#endif + } + + public void RemoveCustomHeader(string headerKey) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_RemoveCustomHeader(webView, headerKey); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("RemoveCustomHeader", headerKey); +#endif + } + + public void ClearCustomHeader() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_ClearCustomHeader(webView); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("ClearCustomHeader"); +#endif + } + + public void ClearCookies() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_ClearCookies(); +#elif UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("ClearCookies"); +#endif + } + + + public void SaveCookies() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SaveCookies(); +#elif UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("SaveCookies"); +#endif + } + + + public void GetCookies(string url) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_GetCookies(webView, url); +#elif UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("GetCookies", url); +#else + //TODO: UNSUPPORTED +#endif + } + + public void SetBasicAuthInfo(string userName, string password) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + //TODO: UNSUPPORTED +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetBasicAuthInfo(webView, userName, password); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetBasicAuthInfo", userName, password); +#endif + } + + public void ClearCache(bool includeDiskFiles) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_IPHONE && !UNITY_EDITOR + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_ClearCache(webView, includeDiskFiles); +#elif UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("ClearCache", includeDiskFiles); +#endif + } + + + public void SetTextZoom(int textZoom) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_IPHONE && !UNITY_EDITOR + //TODO: UNSUPPORTED +#elif UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("SetTextZoom", textZoom); +#endif + } + +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + void OnApplicationFocus(bool focus) + { + if (!focus) + { + hasFocus = false; + } + } + + void Start() + { + if (canvas != null) + { + var g = new GameObject(gameObject.name + "BG"); + g.transform.parent = canvas.transform; + bg = g.AddComponent(); + UpdateBGTransform(); + } + } + + void Update() + { + if (bg != null) { + bg.transform.SetAsLastSibling(); + } + if (hasFocus) { + inputString += Input.inputString; + } + for (;;) { + if (webView == IntPtr.Zero) + break; + string s = _CWebViewPlugin_GetMessage(webView); + if (s == null) + break; + var i = s.IndexOf(':', 0); + if (i == -1) + continue; + switch (s.Substring(0, i)) { + case "CallFromJS": + CallFromJS(s.Substring(i + 1)); + break; + case "CallOnError": + CallOnError(s.Substring(i + 1)); + break; + case "CallOnHttpError": + CallOnHttpError(s.Substring(i + 1)); + break; + case "CallOnLoaded": + CallOnLoaded(s.Substring(i + 1)); + break; + case "CallOnStarted": + CallOnStarted(s.Substring(i + 1)); + break; + case "CallOnHooked": + CallOnHooked(s.Substring(i + 1)); + break; + case "CallOnCookies": + CallOnCookies(s.Substring(i + 1)); + break; + } + } + if (webView == IntPtr.Zero || !visibility) + return; + bool refreshBitmap = (Time.frameCount % bitmapRefreshCycle == 0); + _CWebViewPlugin_Update(webView, refreshBitmap, devicePixelRatio); + if (refreshBitmap) { + { + var w = _CWebViewPlugin_BitmapWidth(webView); + var h = _CWebViewPlugin_BitmapHeight(webView); + if (texture == null || texture.width != w || texture.height != h) { + bool isLinearSpace = QualitySettings.activeColorSpace == ColorSpace.Linear; + texture = new Texture2D(w, h, TextureFormat.RGBA32, false, !isLinearSpace); + texture.filterMode = FilterMode.Bilinear; + texture.wrapMode = TextureWrapMode.Clamp; + textureDataBuffer = new byte[w * h * 4]; + } + } + if (textureDataBuffer.Length > 0) { + var gch = GCHandle.Alloc(textureDataBuffer, GCHandleType.Pinned); + _CWebViewPlugin_Render(webView, gch.AddrOfPinnedObject()); + gch.Free(); + texture.LoadRawTextureData(textureDataBuffer); + texture.Apply(); + } + } + } + + void UpdateBGTransform() + { + if (bg != null) { + bg.rectTransform.anchorMin = Vector2.zero; + bg.rectTransform.anchorMax = Vector2.zero; + bg.rectTransform.pivot = Vector2.zero; + bg.rectTransform.position = rect.min; + bg.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rect.size.x); + bg.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rect.size.y); + } + } + + public int bitmapRefreshCycle = 1; + public int devicePixelRatio = 1; + + void OnGUI() + { + if (webView == IntPtr.Zero || !visibility) + return; + switch (Event.current.type) { + case EventType.MouseDown: + case EventType.MouseUp: + hasFocus = rect.Contains(Input.mousePosition); + break; + } + switch (Event.current.type) { + case EventType.MouseMove: + case EventType.MouseDown: + case EventType.MouseDrag: + case EventType.MouseUp: + case EventType.ScrollWheel: + if (hasFocus) { + Vector3 p; + p.x = Input.mousePosition.x - rect.x; + p.y = Input.mousePosition.y - rect.y; + { + int mouseState = 0; + if (Input.GetButtonDown("Fire1")) { + mouseState = 1; + } else if (Input.GetButton("Fire1")) { + mouseState = 2; + } else if (Input.GetButtonUp("Fire1")) { + mouseState = 3; + } + //_CWebViewPlugin_SendMouseEvent(webView, (int)p.x, (int)p.y, Input.GetAxis("Mouse ScrollWheel"), mouseState); + _CWebViewPlugin_SendMouseEvent(webView, (int)p.x, (int)p.y, Input.mouseScrollDelta.y, mouseState); + } + } + break; + case EventType.Repaint: + while (!string.IsNullOrEmpty(inputString)) { + var keyChars = inputString.Substring(0, 1); + var keyCode = (ushort)inputString[0]; + inputString = inputString.Substring(1); + if (!string.IsNullOrEmpty(keyChars) || keyCode != 0) { + Vector3 p; + p.x = Input.mousePosition.x - rect.x; + p.y = Input.mousePosition.y - rect.y; + _CWebViewPlugin_SendKeyEvent(webView, (int)p.x, (int)p.y, keyChars, keyCode, 1); + } + } + if (texture != null) { + Matrix4x4 m = GUI.matrix; + GUI.matrix + = Matrix4x4.TRS( + new Vector3(0, Screen.height, 0), + Quaternion.identity, + new Vector3(1, -1, 1)); + Graphics.DrawTexture(rect, texture); + GUI.matrix = m; + } + break; + } + } +#endif +} diff --git a/Assets/AirConsole/unity-webview/Plugins/WebViewObject.cs.meta b/Assets/AirConsole/unity-webview/Plugins/WebViewObject.cs.meta new file mode 100644 index 00000000..32948634 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/WebViewObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d4d2b188f50df4b299eb714ef4360ee9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/iOS.meta b/Assets/AirConsole/unity-webview/Plugins/iOS.meta new file mode 100644 index 00000000..352898e8 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a53a54acdc5d64291aa49766bb494025 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/iOS/WebView.mm b/Assets/AirConsole/unity-webview/Plugins/iOS/WebView.mm new file mode 100644 index 00000000..0d720044 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/iOS/WebView.mm @@ -0,0 +1,1204 @@ +/* + * Copyright (C) 2011 Keijiro Takahashi + * Copyright (C) 2012 GREE, Inc. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#if !(__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0) + +#import +#import + +// NOTE: we need extern without "C" before unity 4.5 +//extern UIViewController *UnityGetGLViewController(); +extern "C" UIViewController *UnityGetGLViewController(); +extern "C" void UnitySendMessage(const char *, const char *, const char *); + +// cf. https://stackoverflow.com/questions/26383031/wkwebview-causes-my-view-controller-to-leak/33365424#33365424 +@interface WeakScriptMessageDelegate : NSObject + +@property (nonatomic, weak) id scriptDelegate; + +- (instancetype)initWithDelegate:(id)scriptDelegate; + +@end + +@implementation WeakScriptMessageDelegate + +- (instancetype)initWithDelegate:(id)scriptDelegate +{ + self = [super init]; + if (self) { + _scriptDelegate = scriptDelegate; + } + return self; +} + +- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message +{ + [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message]; +} + +@end + +@protocol WebViewProtocol +@property (nonatomic, getter=isOpaque) BOOL opaque; +@property (nullable, nonatomic, copy) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; +@property (nonatomic, getter=isHidden) BOOL hidden; +@property (nonatomic) CGRect frame; +@property (nullable, nonatomic, weak) id navigationDelegate; +@property (nullable, nonatomic, weak) id UIDelegate; +@property (nullable, nonatomic, readonly, copy) NSURL *URL; +- (void)load:(NSURLRequest *)request; +- (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl; +- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler; +@property (nonatomic, readonly) BOOL canGoBack; +@property (nonatomic, readonly) BOOL canGoForward; +- (void)goBack; +- (void)goForward; +- (void)reload; +- (void)stopLoading; +- (void)setScrollbarsVisibility:(BOOL)visibility; +- (void)setScrollBounce:(BOOL)enable; +@end + +@interface WKWebView(WebViewProtocolConformed) +@end + +@implementation WKWebView(WebViewProtocolConformed) + +- (void)load:(NSURLRequest *)request +{ + WKWebView *webView = (WKWebView *)self; + NSURL *url = [request URL]; + if ([url.absoluteString hasPrefix:@"file:"]) { + NSURL *top = [NSURL URLWithString:[[url absoluteString] stringByDeletingLastPathComponent]]; + [webView loadFileURL:url allowingReadAccessToURL:top]; + } else { + [webView loadRequest:request]; + } +} + +- (NSURLRequest *)constructionCustomHeader:(NSURLRequest *)originalRequest with:(NSDictionary *)headerDictionary +{ + NSMutableURLRequest *convertedRequest = originalRequest.mutableCopy; + for (NSString *key in [headerDictionary allKeys]) { + [convertedRequest setValue:headerDictionary[key] forHTTPHeaderField:key]; + } + return (NSURLRequest *)[convertedRequest copy]; +} + +- (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl +{ + WKWebView *webView = (WKWebView *)self; + [webView loadHTMLString:html baseURL:baseUrl]; +} + +- (void)setScrollbarsVisibility:(BOOL)visibility +{ + WKWebView *webView = (WKWebView *)self; + webView.scrollView.showsHorizontalScrollIndicator = visibility; + webView.scrollView.showsVerticalScrollIndicator = visibility; +} + +- (void)setScrollBounce:(BOOL)enable +{ + WKWebView *webView = (WKWebView *)self; + webView.scrollView.bounces = enable; +} + +@end + +@interface CWebViewPlugin : NSObject +{ + UIView *webView; + NSString *gameObjectName; + NSMutableDictionary *customRequestHeader; + BOOL alertDialogEnabled; + NSRegularExpression *allowRegex; + NSRegularExpression *denyRegex; + NSRegularExpression *hookRegex; + NSString *basicAuthUserName; + NSString *basicAuthPassword; +} +@end + +@implementation CWebViewPlugin + +static WKProcessPool *_sharedProcessPool; +static NSMutableArray *_instances = [[NSMutableArray alloc] init]; + +- (id)initWithGameObjectName:(const char *)gameObjectName_ transparent:(BOOL)transparent zoom:(BOOL)zoom ua:(const char *)ua enableWKWebView:(BOOL)enableWKWebView contentMode:(WKContentMode)contentMode allowsLinkPreview:(BOOL)allowsLinkPreview allowsBackForwardNavigationGestures:(BOOL)allowsBackForwardNavigationGestures radius:(int)radius +{ + self = [super init]; + + gameObjectName = [NSString stringWithUTF8String:gameObjectName_]; + customRequestHeader = [[NSMutableDictionary alloc] init]; + alertDialogEnabled = true; + allowRegex = nil; + denyRegex = nil; + hookRegex = nil; + basicAuthUserName = nil; + basicAuthPassword = nil; + UIView *view = UnityGetGLViewController().view; + if (enableWKWebView && [WKWebView class]) { + if (_sharedProcessPool == NULL) { + _sharedProcessPool = [[WKProcessPool alloc] init]; + } + WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; + WKUserContentController *controller = [[WKUserContentController alloc] init]; + [controller addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"unityControl"]; + [controller addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"saveDataURL"]; + NSString *str = @"\ +window.Unity = { \ + call: function(msg) { \ + window.webkit.messageHandlers.unityControl.postMessage(msg); \ + }, \ + saveDataURL: function(fileName, dataURL) { \ + window.webkit.messageHandlers.saveDataURL.postMessage(fileName + '\t' + dataURL); \ + } \ +}; \ +"; + if (!zoom) { + str = [str stringByAppendingString:@"\ +(function() { \ + var meta = document.querySelector('meta[name=viewport]'); \ + if (meta == null) { \ + meta = document.createElement('meta'); \ + meta.name = 'viewport'; \ + } \ + meta.content += ((meta.content.length > 0) ? ',' : '') + 'user-scalable=no'; \ + var head = document.getElementsByTagName('head')[0]; \ + head.appendChild(meta); \ +})(); \ +" + ]; + } + WKUserScript *script + = [[WKUserScript alloc] initWithSource:str injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; + [controller addUserScript:script]; + configuration.userContentController = controller; + configuration.allowsInlineMediaPlayback = true; + if (@available(iOS 10.0, *)) { + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } else { + if (@available(iOS 9.0, *)) { + configuration.requiresUserActionForMediaPlayback = NO; + } else { + configuration.mediaPlaybackRequiresUserAction = NO; + } + } + configuration.websiteDataStore = [WKWebsiteDataStore defaultDataStore]; + configuration.processPool = _sharedProcessPool; + if (@available(iOS 13.0, *)) { + configuration.defaultWebpagePreferences.preferredContentMode = contentMode; + } +#if UNITYWEBVIEW_IOS_ALLOW_FILE_URLS + // cf. https://stackoverflow.com/questions/35554814/wkwebview-xmlhttprequest-with-file-url/44365081#44365081 + try { + [configuration.preferences setValue:@TRUE forKey:@"allowFileAccessFromFileURLs"]; + } + catch (NSException *ex) { + } + try { + [configuration setValue:@TRUE forKey:@"allowUniversalAccessFromFileURLs"]; + } + catch (NSException *ex) { + } +#endif + WKWebView *wkwebView = [[WKWebView alloc] initWithFrame:view.frame configuration:configuration]; +#if UNITYWEBVIEW_DEVELOPMENT + NSOperatingSystemVersion version = { 16, 4, 0 }; + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { + wkwebView.inspectable = true; + } +#endif + wkwebView.allowsLinkPreview = allowsLinkPreview; + wkwebView.allowsBackForwardNavigationGestures = allowsBackForwardNavigationGestures; + webView = wkwebView; + webView.UIDelegate = self; + webView.navigationDelegate = self; + if (radius > 0) { + webView.layer.cornerRadius = radius; + webView.layer.masksToBounds = YES; + } + if (ua != NULL && strcmp(ua, "") != 0) { + ((WKWebView *)webView).customUserAgent = [[NSString alloc] initWithUTF8String:ua]; + } + // cf. https://rick38yip.medium.com/wkwebview-weird-spacing-issue-in-ios-13-54a4fc686f72 + // cf. https://stackoverflow.com/questions/44390971/automaticallyadjustsscrollviewinsets-was-deprecated-in-ios-11-0 + if (@available(iOS 11.0, *)) { + ((WKWebView *)webView).scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } else { + //UnityGetGLViewController().automaticallyAdjustsScrollViewInsets = false; + } + } else { + webView = nil; + return self; + } + if (transparent) { + webView.opaque = NO; + webView.backgroundColor = [UIColor clearColor]; + } + webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + webView.hidden = YES; + + [webView addObserver:self forKeyPath: @"loading" options: NSKeyValueObservingOptionNew context:nil]; + + [view addSubview:webView]; + + return self; +} + +- (void)dispose +{ + if (webView != nil) { + UIView *webView0 = webView; + webView = nil; + if ([webView0 isKindOfClass:[WKWebView class]]) { + webView0.UIDelegate = nil; + webView0.navigationDelegate = nil; + [((WKWebView *)webView0).configuration.userContentController removeScriptMessageHandlerForName:@"saveDataURL"]; + [((WKWebView *)webView0).configuration.userContentController removeScriptMessageHandlerForName:@"unityControl"]; + } + [webView0 stopLoading]; + [webView0 removeFromSuperview]; + [webView0 removeObserver:self forKeyPath:@"loading"]; + } + basicAuthPassword = nil; + basicAuthUserName = nil; + hookRegex = nil; + denyRegex = nil; + allowRegex = nil; + customRequestHeader = nil; + gameObjectName = nil; +} + ++ (void)resetSharedProcessPool +{ + // cf. https://stackoverflow.com/questions/33156567/getting-all-cookies-from-wkwebview/49744695#49744695 + _sharedProcessPool = [[WKProcessPool alloc] init]; + [_instances enumerateObjectsUsingBlock:^(CWebViewPlugin *obj, NSUInteger idx, BOOL *stop) { + if ([obj->webView isKindOfClass:[WKWebView class]]) { + WKWebView *webView = (WKWebView *)obj->webView; + webView.configuration.processPool = _sharedProcessPool; + } + }]; +} + ++ (void)clearCookies +{ + [CWebViewPlugin resetSharedProcessPool]; + + // cf. https://dev.classmethod.jp/smartphone/remove-webview-cookies/ + NSString *libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject; + NSString *cookiesPath = [libraryPath stringByAppendingPathComponent:@"Cookies"]; + NSString *webKitPath = [libraryPath stringByAppendingPathComponent:@"WebKit"]; + [[NSFileManager defaultManager] removeItemAtPath:cookiesPath error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:webKitPath error:nil]; + + NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + if (cookieStorage == nil) { + // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 + cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; + } + [[cookieStorage cookies] enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { + [cookieStorage deleteCookie:cookie]; + }]; + + NSOperatingSystemVersion version = { 9, 0, 0 }; + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { + // cf. https://stackoverflow.com/questions/46465070/how-to-delete-cookies-from-wkhttpcookiestore/47928399#47928399 + NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; + [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes + modifiedSince:date + completionHandler:^{}]; + } +} + ++ saveCookies +{ + [CWebViewPlugin resetSharedProcessPool]; +} + +- (void)getCookies:(const char *)url +{ + NSOperatingSystemVersion version = { 9, 0, 0 }; + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { + NSURL *nsurl = [NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]]; + WKHTTPCookieStore *cookieStore = WKWebsiteDataStore.defaultDataStore.httpCookieStore; + [cookieStore + getAllCookies:^(NSArray *array) { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + [formatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss zzz"]; + NSMutableString *result = [NSMutableString string]; + [array enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { + if ([cookie.domain isEqualToString:nsurl.host]) { + [result appendString:[NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value]]; + if ([cookie.domain length] > 0) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Domain=%@", cookie.domain]]; + } + if ([cookie.path length] > 0) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Path=%@", cookie.path]]; + } + if (cookie.expiresDate != nil) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Expires=%@", [formatter stringFromDate:cookie.expiresDate]]]; + } + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Version=%zd", cookie.version]]; + [result appendString:[NSString stringWithFormat:@"\n"]]; + } + }]; + UnitySendMessage([gameObjectName UTF8String], "CallOnCookies", [result UTF8String]); + }]; + } else { + [CWebViewPlugin resetSharedProcessPool]; + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + [formatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss zzz"]; + NSMutableString *result = [NSMutableString string]; + NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + if (cookieStorage == nil) { + // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 + cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; + } + [[cookieStorage cookiesForURL:[NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]]] + enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { + [result appendString:[NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value]]; + if ([cookie.domain length] > 0) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Domain=%@", cookie.domain]]; + } + if ([cookie.path length] > 0) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Path=%@", cookie.path]]; + } + if (cookie.expiresDate != nil) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Expires=%@", [formatter stringFromDate:cookie.expiresDate]]]; + } + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Version=%zd", cookie.version]]; + [result appendString:[NSString stringWithFormat:@"\n"]]; + }]; + UnitySendMessage([gameObjectName UTF8String], "CallOnCookies", [result UTF8String]); + } +} + +- (void)userContentController:(WKUserContentController *)userContentController + didReceiveScriptMessage:(WKScriptMessage *)message { + + // Log out the message received + //NSLog(@"Received event %@", message.body); + if ([message.name isEqualToString:@"unityControl"]) { + UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[NSString stringWithFormat:@"%@", message.body] UTF8String]); + } else if ([message.name isEqualToString:@"saveDataURL"]) { + NSRange range = [message.body rangeOfString:@"\t"]; + if (range.location == NSNotFound) { + return; + } + NSString *fileName = [[message.body substringWithRange:NSMakeRange(0, range.location)] lastPathComponent]; + NSString *dataURL = [message.body substringFromIndex:(range.location + 1)]; + range = [dataURL rangeOfString:@"data:"]; + if (range.location != 0) { + return; + } + NSString *tmp = [dataURL substringFromIndex:[@"data:" length]]; + range = [tmp rangeOfString:@";"]; + if (range.location == NSNotFound) { + return; + } + NSString *base64data = [tmp substringFromIndex:(range.location + 1 + [@"base64," length])]; + NSString *type = [tmp substringWithRange:NSMakeRange(0, range.location)]; + NSData *data = [[NSData alloc] initWithBase64EncodedString:base64data options:0]; + NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + path = [path stringByAppendingString:@"/Downloads"]; + BOOL isDir; + NSError *err = nil; + if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) { + if (!isDir) { + return; + } + } else { + [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&err]; + if (err != nil) { + return; + } + } + NSString *prefix = [path stringByAppendingString:@"/"]; + path = [prefix stringByAppendingString:fileName]; + int count = 0; + while ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + count++; + NSString *name = [fileName stringByDeletingPathExtension]; + NSString *ext = [fileName pathExtension]; + if (ext.length == 0) { + path = [NSString stringWithFormat:@"%@%@ (%d)", prefix, name, count]; + } else { + path = [NSString stringWithFormat:@"%@%@ (%d).%@", prefix, name, count, ext]; + } + } + [data writeToFile:path atomically:YES]; + } + + /* + // Then pull something from the device using the message body + NSString *version = [[UIDevice currentDevice] valueForKey:message.body]; + + // Execute some JavaScript using the result? + NSString *exec_template = @"set_headline(\"received: %@\");"; + NSString *exec = [NSString stringWithFormat:exec_template, version]; + [webView evaluateJavaScript:exec completionHandler:nil]; + */ +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if (webView == nil) + return; + + if ([keyPath isEqualToString:@"loading"] && [[change objectForKey:NSKeyValueChangeNewKey] intValue] == 0 + && [webView URL] != nil) { + UnitySendMessage( + [gameObjectName UTF8String], + "CallOnLoaded", + [[[webView URL] absoluteString] UTF8String]); + + } +} + +- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView +{ + UnitySendMessage([gameObjectName UTF8String], "CallOnError", "webViewWebContentProcessDidTerminate"); +} + +- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error +{ + UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); +} + +- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error +{ + UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); +} + +- (WKWebView *)webView:(WKWebView *)wkWebView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures +{ + // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c + if (!navigationAction.targetFrame.isMainFrame) { + [wkWebView loadRequest:navigationAction.request]; + } + return nil; +} + +- (void)webView:(WKWebView *)wkWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + if (webView == nil) { + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + NSURL *nsurl = [navigationAction.request URL]; + NSString *url = [nsurl absoluteString]; + BOOL pass = YES; + if (allowRegex != nil && [allowRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { + pass = YES; + } else if (denyRegex != nil && [denyRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { + pass = NO; + } + if (!pass) { + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + if ([url rangeOfString:@"//itunes.apple.com/"].location != NSNotFound) { + [[UIApplication sharedApplication] openURL:nsurl]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } else if ([url hasPrefix:@"unity:"]) { + UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[url substringFromIndex:6] UTF8String]); + decisionHandler(WKNavigationActionPolicyCancel); + return; + } else if (hookRegex != nil && [hookRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { + UnitySendMessage([gameObjectName UTF8String], "CallOnHooked", [url UTF8String]); + decisionHandler(WKNavigationActionPolicyCancel); + return; + } else if (![url hasPrefix:@"about:blank"] // for loadHTML(), cf. #365 + && ![url hasPrefix:@"about:srcdoc"] // for iframe srcdoc attribute + && ![url hasPrefix:@"file:"] + && ![url hasPrefix:@"http:"] + && ![url hasPrefix:@"https:"]) { + if([[UIApplication sharedApplication] canOpenURL:nsurl]) { + [[UIApplication sharedApplication] openURL:nsurl]; + } + decisionHandler(WKNavigationActionPolicyCancel); + return; + } else if (navigationAction.navigationType == WKNavigationTypeLinkActivated + && (!navigationAction.targetFrame || !navigationAction.targetFrame.isMainFrame)) { + // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c + [webView load:navigationAction.request]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } else { + if (navigationAction.targetFrame != nil && navigationAction.targetFrame.isMainFrame) { + // If the custom header is not attached, give it and make a request again. + if (![self isSetupedCustomHeader:[navigationAction request]]) { + NSLog(@"navi ... %@", navigationAction); + [wkWebView loadRequest:[self constructionCustomHeader:navigationAction.request]]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + } + } + UnitySendMessage([gameObjectName UTF8String], "CallOnStarted", [url UTF8String]); + decisionHandler(WKNavigationActionPolicyAllow); +} + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { + + if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { + + NSHTTPURLResponse * response = (NSHTTPURLResponse *)navigationResponse.response; + if (response.statusCode >= 400) { + UnitySendMessage([gameObjectName UTF8String], "CallOnHttpError", [[NSString stringWithFormat:@"%d", response.statusCode] UTF8String]); + } + + } + decisionHandler(WKNavigationResponsePolicyAllow); +} + +// alert +- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler +{ + if (!alertDialogEnabled) { + completionHandler(); + return; + } + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction: [UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + completionHandler(); + }]]; + [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; +} + +// confirm +- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler +{ + if (!alertDialogEnabled) { + completionHandler(NO); + return; + } + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + completionHandler(YES); + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + completionHandler(NO); + }]]; + [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; +} + +// prompt +- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler +{ + if (!alertDialogEnabled) { + completionHandler(nil); + return; + } + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" + message:prompt + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.text = defaultText; + }]; + [alertController addAction:[UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + NSString *input = ((UITextField *)alertController.textFields.firstObject).text; + completionHandler(input); + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + completionHandler(nil); + }]]; + [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; +} + +- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler +{ + NSURLSessionAuthChallengeDisposition disposition; + NSURLCredential *credential; + if (basicAuthUserName && basicAuthPassword && [challenge previousFailureCount] == 0) { + disposition = NSURLSessionAuthChallengeUseCredential; + credential = [NSURLCredential credentialWithUser:basicAuthUserName password:basicAuthPassword persistence:NSURLCredentialPersistenceForSession]; + } else { + disposition = NSURLSessionAuthChallengePerformDefaultHandling; + credential = nil; + } + completionHandler(disposition, credential); +} + +- (BOOL)isSetupedCustomHeader:(NSURLRequest *)targetRequest +{ + // Check for additional custom header. + for (NSString *key in [customRequestHeader allKeys]) { + if (![[[targetRequest allHTTPHeaderFields] objectForKey:key] isEqualToString:[customRequestHeader objectForKey:key]]) { + return NO; + } + } + return YES; +} + +- (NSURLRequest *)constructionCustomHeader:(NSURLRequest *)originalRequest +{ + NSMutableURLRequest *convertedRequest = originalRequest.mutableCopy; + for (NSString *key in [customRequestHeader allKeys]) { + [convertedRequest setValue:customRequestHeader[key] forHTTPHeaderField:key]; + } + return (NSURLRequest *)[convertedRequest copy]; +} + +- (void)setMargins:(float)left top:(float)top right:(float)right bottom:(float)bottom relative:(BOOL)relative +{ + if (webView == nil) + return; + UIView *view = UnityGetGLViewController().view; + CGRect frame = webView.frame; + CGRect screen = view.bounds; + if (relative) { + frame.size.width = floor(screen.size.width * (1.0f - left - right)); + frame.size.height = floor(screen.size.height * (1.0f - top - bottom)); + frame.origin.x = floor(screen.size.width * left); + frame.origin.y = floor(screen.size.height * top); + } else { + CGFloat scale = 1.0f / [self getScale:view]; + frame.size.width = floor(screen.size.width - scale * (left + right)); + frame.size.height = floor(screen.size.height - scale * (top + bottom)); + frame.origin.x = floor(scale * left); + frame.origin.y = floor(scale * top); + } + webView.frame = frame; +} + +- (CGFloat)getScale:(UIView *)view +{ + if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) + return view.window.screen.nativeScale; + return view.contentScaleFactor; +} + +- (void)setVisibility:(BOOL)visibility +{ + if (webView == nil) + return; + webView.hidden = visibility ? NO : YES; +} + +- (void)setInteractionEnabled:(BOOL)enabled +{ + if (webView == nil) + return; + webView.userInteractionEnabled = enabled; +} + +- (void)setAlertDialogEnabled:(BOOL)enabled +{ + alertDialogEnabled = enabled; +} + +- (void)setScrollbarsVisibility:(BOOL)visibility +{ + if (webView == nil) + return; + [webView setScrollbarsVisibility:visibility]; +} + +- (void)setScrollBounceEnabled:(BOOL)enabled +{ + if (webView == nil) + return; + [webView setScrollBounce:enabled]; +} + +- (BOOL)setURLPattern:(const char *)allowPattern and:(const char *)denyPattern and:(const char *)hookPattern +{ + NSError *err = nil; + NSRegularExpression *allow = nil; + NSRegularExpression *deny = nil; + NSRegularExpression *hook = nil; + if (allowPattern == nil || *allowPattern == '\0') { + allow = nil; + } else { + allow + = [NSRegularExpression + regularExpressionWithPattern:[NSString stringWithUTF8String:allowPattern] + options:0 + error:&err]; + if (err != nil) { + return NO; + } + } + if (denyPattern == nil || *denyPattern == '\0') { + deny = nil; + } else { + deny + = [NSRegularExpression + regularExpressionWithPattern:[NSString stringWithUTF8String:denyPattern] + options:0 + error:&err]; + if (err != nil) { + return NO; + } + } + if (hookPattern == nil || *hookPattern == '\0') { + hook = nil; + } else { + hook + = [NSRegularExpression + regularExpressionWithPattern:[NSString stringWithUTF8String:hookPattern] + options:0 + error:&err]; + if (err != nil) { + return NO; + } + } + allowRegex = allow; + denyRegex = deny; + hookRegex = hook; + return YES; +} + +- (void)loadURL:(const char *)url +{ + if (webView == nil) + return; + NSString *urlStr = [NSString stringWithUTF8String:url]; + NSURL *nsurl = [NSURL URLWithString:urlStr]; + NSURLRequest *request = [NSURLRequest requestWithURL:nsurl]; + [webView load:request]; +} + +- (void)loadHTML:(const char *)html baseURL:(const char *)baseUrl +{ + if (webView == nil) + return; + NSString *htmlStr = [NSString stringWithUTF8String:html]; + NSString *baseStr = [NSString stringWithUTF8String:baseUrl]; + NSURL *baseNSUrl = [NSURL URLWithString:baseStr]; + [webView loadHTML:htmlStr baseURL:baseNSUrl]; +} + +- (void)evaluateJS:(const char *)js +{ + if (webView == nil) + return; + NSString *jsStr = [NSString stringWithUTF8String:js]; + [webView evaluateJavaScript:jsStr completionHandler:^(NSString *result, NSError *error) {}]; +} + +- (int)progress +{ + if (webView == nil) + return 0; + if ([webView isKindOfClass:[WKWebView class]]) { + return (int)([(WKWebView *)webView estimatedProgress] * 100); + } else { + return 0; + } +} + +- (BOOL)canGoBack +{ + if (webView == nil) + return false; + return [webView canGoBack]; +} + +- (BOOL)canGoForward +{ + if (webView == nil) + return false; + return [webView canGoForward]; +} + +- (void)goBack +{ + if (webView == nil) + return; + [webView goBack]; +} + +- (void)goForward +{ + if (webView == nil) + return; + [webView goForward]; +} + +- (void)reload +{ + if (webView == nil) + return; + [webView reload]; +} + +- (void)addCustomRequestHeader:(const char *)headerKey value:(const char *)headerValue +{ + NSString *keyString = [NSString stringWithUTF8String:headerKey]; + NSString *valueString = [NSString stringWithUTF8String:headerValue]; + + [customRequestHeader setObject:valueString forKey:keyString]; +} + +- (void)removeCustomRequestHeader:(const char *)headerKey +{ + NSString *keyString = [NSString stringWithUTF8String:headerKey]; + + if ([[customRequestHeader allKeys]containsObject:keyString]) { + [customRequestHeader removeObjectForKey:keyString]; + } +} + +- (void)clearCustomRequestHeader +{ + [customRequestHeader removeAllObjects]; +} + +- (const char *)getCustomRequestHeaderValue:(const char *)headerKey +{ + NSString *keyString = [NSString stringWithUTF8String:headerKey]; + NSString *result = [customRequestHeader objectForKey:keyString]; + if (!result) { + return NULL; + } + + const char *s = [result UTF8String]; + char *r = (char *)malloc(strlen(s) + 1); + strcpy(r, s); + return r; +} + +- (void)setBasicAuthInfo:(const char *)userName password:(const char *)password +{ + basicAuthUserName = [NSString stringWithUTF8String:userName]; + basicAuthPassword = [NSString stringWithUTF8String:password]; +} + +- (void)clearCache:(BOOL)includeDiskFiles +{ + if (webView == nil) + return; + NSMutableSet *types = [NSMutableSet setWithArray:@[WKWebsiteDataTypeMemoryCache]]; + if (includeDiskFiles) { + [types addObject:WKWebsiteDataTypeDiskCache]; + } + NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; + [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:types modifiedSince:date completionHandler:^{}]; +} + +- (void)setAllMediaPlaybackSuspended:(BOOL)suspended +{ + NSOperatingSystemVersion version = { 15, 0, 0 }; + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { + if ([webView isKindOfClass:[WKWebView class]]) { + [(WKWebView *)webView setAllMediaPlaybackSuspended:suspended completionHandler:nil]; + } + } +} +@end + +extern "C" { + void *_CWebViewPlugin_Init(const char *gameObjectName, BOOL transparent, BOOL zoom, const char *ua, BOOL enableWKWebView, int contentMode, BOOL allowsLinkPreview, BOOL allowsBackForwardNavigationGestures, int radius); + void _CWebViewPlugin_Destroy(void *instance); + void _CWebViewPlugin_SetMargins( + void *instance, float left, float top, float right, float bottom, BOOL relative); + void _CWebViewPlugin_SetVisibility(void *instance, BOOL visibility); + void _CWebViewPlugin_SetInteractionEnabled(void *instance, BOOL enabled); + void _CWebViewPlugin_SetAlertDialogEnabled(void *instance, BOOL visibility); + void _CWebViewPlugin_SetScrollbarsVisibility(void *instance, BOOL visibility); + void _CWebViewPlugin_SetScrollBounceEnabled(void *instance, BOOL enabled); + BOOL _CWebViewPlugin_SetURLPattern(void *instance, const char *allowPattern, const char *denyPattern, const char *hookPattern); + void _CWebViewPlugin_LoadURL(void *instance, const char *url); + void _CWebViewPlugin_LoadHTML(void *instance, const char *html, const char *baseUrl); + void _CWebViewPlugin_EvaluateJS(void *instance, const char *url); + int _CWebViewPlugin_Progress(void *instance); + BOOL _CWebViewPlugin_CanGoBack(void *instance); + BOOL _CWebViewPlugin_CanGoForward(void *instance); + void _CWebViewPlugin_GoBack(void *instance); + void _CWebViewPlugin_GoForward(void *instance); + void _CWebViewPlugin_Reload(void *instance); + void _CWebViewPlugin_AddCustomHeader(void *instance, const char *headerKey, const char *headerValue); + void _CWebViewPlugin_RemoveCustomHeader(void *instance, const char *headerKey); + void _CWebViewPlugin_ClearCustomHeader(void *instance); + void _CWebViewPlugin_ClearCookies(); + void _CWebViewPlugin_SaveCookies(); + void _CWebViewPlugin_GetCookies(void *instance, const char *url); + const char *_CWebViewPlugin_GetCustomHeaderValue(void *instance, const char *headerKey); + void _CWebViewPlugin_SetBasicAuthInfo(void *instance, const char *userName, const char *password); + void _CWebViewPlugin_ClearCache(void *instance, BOOL includeDiskFiles); + void _CWebViewPlugin_SetSuspended(void *instance, BOOL suspended); +} + +void *_CWebViewPlugin_Init(const char *gameObjectName, BOOL transparent, BOOL zoom, const char *ua, BOOL enableWKWebView, int contentMode, BOOL allowsLinkPreview, BOOL allowsBackForwardNavigationGestures, int radius) +{ + if (! (enableWKWebView && [WKWebView class])) + return nil; + WKContentMode wkContentMode = WKContentModeRecommended; + switch (contentMode) { + case 1: + wkContentMode = WKContentModeMobile; + break; + case 2: + wkContentMode = WKContentModeDesktop; + break; + default: + wkContentMode = WKContentModeRecommended; + break; + } + CWebViewPlugin *webViewPlugin = [[CWebViewPlugin alloc] initWithGameObjectName:gameObjectName transparent:transparent zoom:zoom ua:ua enableWKWebView:enableWKWebView contentMode:wkContentMode allowsLinkPreview:allowsLinkPreview allowsBackForwardNavigationGestures:allowsBackForwardNavigationGestures radius:radius]; + [_instances addObject:webViewPlugin]; + return (__bridge_retained void *)webViewPlugin; +} + +void _CWebViewPlugin_Destroy(void *instance) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge_transfer CWebViewPlugin *)instance; + [_instances removeObject:webViewPlugin]; + [webViewPlugin dispose]; + webViewPlugin = nil; +} + +void _CWebViewPlugin_SetMargins( + void *instance, float left, float top, float right, float bottom, BOOL relative) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setMargins:left top:top right:right bottom:bottom relative:relative]; +} + +void _CWebViewPlugin_SetVisibility(void *instance, BOOL visibility) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setVisibility:visibility]; +} + +void _CWebViewPlugin_SetInteractionEnabled(void *instance, BOOL enabled) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setInteractionEnabled:enabled]; +} + +void _CWebViewPlugin_SetAlertDialogEnabled(void *instance, BOOL enabled) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setAlertDialogEnabled:enabled]; +} + +void _CWebViewPlugin_SetScrollbarsVisibility(void *instance, BOOL visibility) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setScrollbarsVisibility:visibility]; +} + +void _CWebViewPlugin_SetScrollBounceEnabled(void *instance, BOOL enabled) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setScrollBounceEnabled:enabled]; +} + +BOOL _CWebViewPlugin_SetURLPattern(void *instance, const char *allowPattern, const char *denyPattern, const char *hookPattern) +{ + if (instance == NULL) + return NO; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + return [webViewPlugin setURLPattern:allowPattern and:denyPattern and:hookPattern]; +} + +void _CWebViewPlugin_LoadURL(void *instance, const char *url) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin loadURL:url]; +} + +void _CWebViewPlugin_LoadHTML(void *instance, const char *html, const char *baseUrl) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin loadHTML:html baseURL:baseUrl]; +} + +void _CWebViewPlugin_EvaluateJS(void *instance, const char *js) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin evaluateJS:js]; +} + +int _CWebViewPlugin_Progress(void *instance) +{ + if (instance == NULL) + return 0; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + return [webViewPlugin progress]; +} + +BOOL _CWebViewPlugin_CanGoBack(void *instance) +{ + if (instance == NULL) + return false; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + return [webViewPlugin canGoBack]; +} + +BOOL _CWebViewPlugin_CanGoForward(void *instance) +{ + if (instance == NULL) + return false; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + return [webViewPlugin canGoForward]; +} + +void _CWebViewPlugin_GoBack(void *instance) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin goBack]; +} + +void _CWebViewPlugin_GoForward(void *instance) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin goForward]; +} + +void _CWebViewPlugin_Reload(void *instance) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin reload]; +} + +void _CWebViewPlugin_AddCustomHeader(void *instance, const char *headerKey, const char *headerValue) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin addCustomRequestHeader:headerKey value:headerValue]; +} + +void _CWebViewPlugin_RemoveCustomHeader(void *instance, const char *headerKey) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin removeCustomRequestHeader:headerKey]; +} + +void _CWebViewPlugin_ClearCustomHeader(void *instance) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin clearCustomRequestHeader]; +} + +void _CWebViewPlugin_ClearCookies() +{ + [CWebViewPlugin clearCookies]; +} + +void _CWebViewPlugin_SaveCookies() +{ + [CWebViewPlugin saveCookies]; +} + +void _CWebViewPlugin_GetCookies(void *instance, const char *url) +{ + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin getCookies:url]; +} + +const char *_CWebViewPlugin_GetCustomHeaderValue(void *instance, const char *headerKey) +{ + if (instance == NULL) + return NULL; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + return [webViewPlugin getCustomRequestHeaderValue:headerKey]; +} + +void _CWebViewPlugin_SetBasicAuthInfo(void *instance, const char *userName, const char *password) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setBasicAuthInfo:userName password:password]; +} + +void _CWebViewPlugin_ClearCache(void *instance, BOOL includeDiskFiles) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin clearCache:includeDiskFiles]; +} + +void _CWebViewPlugin_SetSuspended(void *instance, BOOL suspended) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setAllMediaPlaybackSuspended:suspended]; +} +#endif // !(__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0) diff --git a/Assets/AirConsole/unity-webview/Plugins/iOS/WebView.mm.meta b/Assets/AirConsole/unity-webview/Plugins/iOS/WebView.mm.meta new file mode 100644 index 00000000..fb91d132 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/iOS/WebView.mm.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 2cabb4f60971742a28f3bd04e65de504 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/iOS/WebViewWithUIWebView.mm b/Assets/AirConsole/unity-webview/Plugins/iOS/WebViewWithUIWebView.mm new file mode 100644 index 00000000..57b7f300 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/iOS/WebViewWithUIWebView.mm @@ -0,0 +1,1289 @@ +/* + * Copyright (C) 2011 Keijiro Takahashi + * Copyright (C) 2012 GREE, Inc. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0 + +#import +#import + +// NOTE: we need extern without "C" before unity 4.5 +//extern UIViewController *UnityGetGLViewController(); +extern "C" UIViewController *UnityGetGLViewController(); +extern "C" void UnitySendMessage(const char *, const char *, const char *); + +// cf. https://stackoverflow.com/questions/26383031/wkwebview-causes-my-view-controller-to-leak/33365424#33365424 +@interface WeakScriptMessageDelegate : NSObject + +@property (nonatomic, weak) id scriptDelegate; + +- (instancetype)initWithDelegate:(id)scriptDelegate; + +@end + +@implementation WeakScriptMessageDelegate + +- (instancetype)initWithDelegate:(id)scriptDelegate +{ + self = [super init]; + if (self) { + _scriptDelegate = scriptDelegate; + } + return self; +} + +- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message +{ + [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message]; +} + +@end + +@protocol WebViewProtocol +@property (nonatomic, getter=isOpaque) BOOL opaque; +@property (nullable, nonatomic, copy) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; +@property (nonatomic, getter=isHidden) BOOL hidden; +@property (nonatomic, getter=isUserInteractionEnabled) BOOL userInteractionEnabled; +@property (nonatomic) CGRect frame; +@property (nonatomic, readonly, strong) UIScrollView *scrollView; +@property (nullable, nonatomic, assign) id delegate; +@property (nullable, nonatomic, weak) id navigationDelegate; +@property (nullable, nonatomic, weak) id UIDelegate; +@property (nullable, nonatomic, readonly, copy) NSURL *URL; +- (void)load:(NSURLRequest *)request; +- (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl; +- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler; +@property (nonatomic, readonly) BOOL canGoBack; +@property (nonatomic, readonly) BOOL canGoForward; +- (void)goBack; +- (void)goForward; +- (void)reload; +- (void)stopLoading; +- (void)setScrollbarsVisibility:(BOOL)visibility; +- (void)setScrollBounce:(BOOL)enable; +@end + +@interface WKWebView(WebViewProtocolConformed) +@end + +@implementation WKWebView(WebViewProtocolConformed) + +@dynamic delegate; + +- (void)load:(NSURLRequest *)request +{ + WKWebView *webView = (WKWebView *)self; + NSURL *url = [request URL]; + if ([url.absoluteString hasPrefix:@"file:"]) { + NSURL *top = [NSURL URLWithString:[[url absoluteString] stringByDeletingLastPathComponent]]; + [webView loadFileURL:url allowingReadAccessToURL:top]; + } else { + [webView loadRequest:request]; + } +} + +- (NSURLRequest *)constructionCustomHeader:(NSURLRequest *)originalRequest with:(NSDictionary *)headerDictionary +{ + NSMutableURLRequest *convertedRequest = originalRequest.mutableCopy; + for (NSString *key in [headerDictionary allKeys]) { + [convertedRequest setValue:headerDictionary[key] forHTTPHeaderField:key]; + } + return (NSURLRequest *)[convertedRequest copy]; +} + +- (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl +{ + WKWebView *webView = (WKWebView *)self; + [webView loadHTMLString:html baseURL:baseUrl]; +} + +- (void)setScrollbarsVisibility:(BOOL)visibility +{ + WKWebView *webView = (WKWebView *)self; + webView.scrollView.showsHorizontalScrollIndicator = visibility; + webView.scrollView.showsVerticalScrollIndicator = visibility; +} + +- (void)setScrollBounce:(BOOL)enable +{ + WKWebView *webView = (WKWebView *)self; + webView.scrollView.bounces = enable; +} + +@end + +@interface UIWebView(WebViewProtocolConformed) +@end + +@implementation UIWebView(WebViewProtocolConformed) + +@dynamic navigationDelegate; +@dynamic UIDelegate; + +- (NSURL *)URL +{ + return [NSURL URLWithString:[self stringByEvaluatingJavaScriptFromString:@"document.URL"]]; +} + +- (void)load:(NSURLRequest *)request +{ + UIWebView *webView = (UIWebView *)self; + [webView loadRequest:request]; +} + +- (void)loadHTML:(NSString *)html baseURL:(NSURL *)baseUrl +{ + UIWebView *webView = (UIWebView *)self; + [webView loadHTMLString:html baseURL:baseUrl]; +} + +- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler +{ + NSString *result = [self stringByEvaluatingJavaScriptFromString:javaScriptString]; + if (completionHandler) { + completionHandler(result, nil); + } +} + +- (void)setScrollbarsVisibility:(BOOL)visibility +{ + UIWebView *webView = (UIWebView *)self; + webView.scrollView.showsHorizontalScrollIndicator = visibility; + webView.scrollView.showsVerticalScrollIndicator = visibility; +} + +- (void)setScrollBounce:(BOOL)enable +{ + UIWebView *webView = (UIWebView *)self; + webView.scrollView.bounces = enable; +} + +@end + +@interface CWebViewPlugin : NSObject +{ + UIView *webView; + NSString *gameObjectName; + NSMutableDictionary *customRequestHeader; + BOOL alertDialogEnabled; + NSRegularExpression *allowRegex; + NSRegularExpression *denyRegex; + NSRegularExpression *hookRegex; + NSString *basicAuthUserName; + NSString *basicAuthPassword; +} +@end + +@implementation CWebViewPlugin + +static WKProcessPool *_sharedProcessPool; +static NSMutableArray *_instances = [[NSMutableArray alloc] init]; + +- (id)initWithGameObjectName:(const char *)gameObjectName_ transparent:(BOOL)transparent zoom:(BOOL)zoom ua:(const char *)ua enableWKWebView:(BOOL)enableWKWebView contentMode:(WKContentMode)contentMode allowsLinkPreview:(BOOL)allowsLinkPreview allowsBackForwardNavigationGestures:(BOOL)allowsBackForwardNavigationGestures radius:(int)radius +{ + self = [super init]; + + gameObjectName = [NSString stringWithUTF8String:gameObjectName_]; + customRequestHeader = [[NSMutableDictionary alloc] init]; + alertDialogEnabled = true; + allowRegex = nil; + denyRegex = nil; + hookRegex = nil; + basicAuthUserName = nil; + basicAuthPassword = nil; + UIView *view = UnityGetGLViewController().view; + if (enableWKWebView && [WKWebView class]) { + if (_sharedProcessPool == NULL) { + _sharedProcessPool = [[WKProcessPool alloc] init]; + } + WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; + WKUserContentController *controller = [[WKUserContentController alloc] init]; + [controller addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"unityControl"]; + [controller addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"saveDataURL"]; + NSString *str = @"\ +window.Unity = { \ + call: function(msg) { \ + window.webkit.messageHandlers.unityControl.postMessage(msg); \ + }, \ + saveDataURL: function(fileName, dataURL) { \ + window.webkit.messageHandlers.saveDataURL.postMessage(fileName + '\t' + dataURL); \ + } \ +}; \ +"; + if (!zoom) { + str = [str stringByAppendingString:@"\ +(function() { \ + var meta = document.querySelector('meta[name=viewport]'); \ + if (meta == null) { \ + meta = document.createElement('meta'); \ + meta.name = 'viewport'; \ + } \ + meta.content += ((meta.content.length > 0) ? ',' : '') + 'user-scalable=no'; \ + var head = document.getElementsByTagName('head')[0]; \ + head.appendChild(meta); \ +})(); \ +" + ]; + } + WKUserScript *script + = [[WKUserScript alloc] initWithSource:str injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; + [controller addUserScript:script]; + configuration.userContentController = controller; + configuration.allowsInlineMediaPlayback = true; + if (@available(iOS 10.0, *)) { + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } else { + if (@available(iOS 9.0, *)) { + configuration.requiresUserActionForMediaPlayback = NO; + } else { + configuration.mediaPlaybackRequiresUserAction = NO; + } + } + configuration.websiteDataStore = [WKWebsiteDataStore defaultDataStore]; + configuration.processPool = _sharedProcessPool; + if (@available(iOS 13.0, *)) { + configuration.defaultWebpagePreferences.preferredContentMode = contentMode; + } +#if UNITYWEBVIEW_IOS_ALLOW_FILE_URLS + // cf. https://stackoverflow.com/questions/35554814/wkwebview-xmlhttprequest-with-file-url/44365081#44365081 + try { + [configuration.preferences setValue:@TRUE forKey:@"allowFileAccessFromFileURLs"]; + } + catch (NSException *ex) { + } + try { + [configuration setValue:@TRUE forKey:@"allowUniversalAccessFromFileURLs"]; + } + catch (NSException *ex) { + } +#endif + WKWebView *wkwebView = [[WKWebView alloc] initWithFrame:view.frame configuration:configuration]; +#if UNITYWEBVIEW_DEVELOPMENT + NSOperatingSystemVersion version = { 16, 4, 0 }; + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { + wkwebView.inspectable = true; + } +#endif + wkwebView.allowsLinkPreview = allowsLinkPreview; + wkwebView.allowsBackForwardNavigationGestures = allowsBackForwardNavigationGestures; + webView = wkwebView; + webView.UIDelegate = self; + webView.navigationDelegate = self; + if (ua != NULL && strcmp(ua, "") != 0) { + ((WKWebView *)webView).customUserAgent = [[NSString alloc] initWithUTF8String:ua]; + } + // cf. https://rick38yip.medium.com/wkwebview-weird-spacing-issue-in-ios-13-54a4fc686f72 + // cf. https://stackoverflow.com/questions/44390971/automaticallyadjustsscrollviewinsets-was-deprecated-in-ios-11-0 + if (@available(iOS 11.0, *)) { + ((WKWebView *)webView).scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + } else { + //UnityGetGLViewController().automaticallyAdjustsScrollViewInsets = false; + } + } else { + if (ua != NULL && strcmp(ua, "") != 0) { + [[NSUserDefaults standardUserDefaults] + registerDefaults:@{ @"UserAgent": [[NSString alloc] initWithUTF8String:ua] }]; + } + UIWebView *uiwebview = [[UIWebView alloc] initWithFrame:view.frame]; + uiwebview.allowsInlineMediaPlayback = YES; + uiwebview.mediaPlaybackRequiresUserAction = NO; + webView = uiwebview; + webView.delegate = self; + } + if (transparent) { + webView.opaque = NO; + webView.backgroundColor = [UIColor clearColor]; + } + if (radius > 0) { + webView.layer.cornerRadius = radius; + webView.layer.masksToBounds = YES; + } + webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + webView.hidden = YES; + + [webView addObserver:self forKeyPath: @"loading" options: NSKeyValueObservingOptionNew context:nil]; + + [view addSubview:webView]; + + return self; +} + +- (void)dispose +{ + if (webView != nil) { + UIView *webView0 = webView; + webView = nil; + if ([webView0 isKindOfClass:[WKWebView class]]) { + webView0.UIDelegate = nil; + webView0.navigationDelegate = nil; + [((WKWebView *)webView0).configuration.userContentController removeScriptMessageHandlerForName:@"saveDataURL"]; + [((WKWebView *)webView0).configuration.userContentController removeScriptMessageHandlerForName:@"unityControl"]; + } else { + webView0.delegate = nil; + } + [webView0 stopLoading]; + [webView0 removeFromSuperview]; + [webView0 removeObserver:self forKeyPath:@"loading"]; + } + basicAuthPassword = nil; + basicAuthUserName = nil; + hookRegex = nil; + denyRegex = nil; + allowRegex = nil; + customRequestHeader = nil; + gameObjectName = nil; +} + ++ (void)resetSharedProcessPool +{ + // cf. https://stackoverflow.com/questions/33156567/getting-all-cookies-from-wkwebview/49744695#49744695 + _sharedProcessPool = [[WKProcessPool alloc] init]; + [_instances enumerateObjectsUsingBlock:^(CWebViewPlugin *obj, NSUInteger idx, BOOL *stop) { + if ([obj->webView isKindOfClass:[WKWebView class]]) { + WKWebView *webView = (WKWebView *)obj->webView; + webView.configuration.processPool = _sharedProcessPool; + } + }]; +} + ++ (void)clearCookies +{ + [CWebViewPlugin resetSharedProcessPool]; + + // cf. https://dev.classmethod.jp/smartphone/remove-webview-cookies/ + NSString *libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject; + NSString *cookiesPath = [libraryPath stringByAppendingPathComponent:@"Cookies"]; + NSString *webKitPath = [libraryPath stringByAppendingPathComponent:@"WebKit"]; + [[NSFileManager defaultManager] removeItemAtPath:cookiesPath error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:webKitPath error:nil]; + + NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + if (cookieStorage == nil) { + // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 + cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; + } + [[cookieStorage cookies] enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { + [cookieStorage deleteCookie:cookie]; + }]; + + NSOperatingSystemVersion version = { 9, 0, 0 }; + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { + // cf. https://stackoverflow.com/questions/46465070/how-to-delete-cookies-from-wkhttpcookiestore/47928399#47928399 + NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; + [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes + modifiedSince:date + completionHandler:^{}]; + } +} + ++ saveCookies +{ + [CWebViewPlugin resetSharedProcessPool]; +} + +- (void)getCookies:(const char *)url +{ + NSOperatingSystemVersion version = { 9, 0, 0 }; + if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:version]) { + NSURL *nsurl = [NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]]; + WKHTTPCookieStore *cookieStore = WKWebsiteDataStore.defaultDataStore.httpCookieStore; + [cookieStore + getAllCookies:^(NSArray *array) { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + [formatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss zzz"]; + NSMutableString *result = [NSMutableString string]; + [array enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { + if ([cookie.domain isEqualToString:nsurl.host]) { + [result appendString:[NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value]]; + if ([cookie.domain length] > 0) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Domain=%@", cookie.domain]]; + } + if ([cookie.path length] > 0) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Path=%@", cookie.path]]; + } + if (cookie.expiresDate != nil) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Expires=%@", [formatter stringFromDate:cookie.expiresDate]]]; + } + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Version=%zd", cookie.version]]; + [result appendString:[NSString stringWithFormat:@"\n"]]; + } + }]; + UnitySendMessage([gameObjectName UTF8String], "CallOnCookies", [result UTF8String]); + }]; + } else { + [CWebViewPlugin resetSharedProcessPool]; + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + [formatter setDateFormat:@"EEE, dd MMM yyyy HH:mm:ss zzz"]; + NSMutableString *result = [NSMutableString string]; + NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; + if (cookieStorage == nil) { + // cf. https://stackoverflow.com/questions/33876295/nshttpcookiestorage-sharedhttpcookiestorage-comes-up-empty-in-10-11 + cookieStorage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"Cookies"]; + } + [[cookieStorage cookiesForURL:[NSURL URLWithString:[[NSString alloc] initWithUTF8String:url]]] + enumerateObjectsUsingBlock:^(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) { + [result appendString:[NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value]]; + if ([cookie.domain length] > 0) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Domain=%@", cookie.domain]]; + } + if ([cookie.path length] > 0) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Path=%@", cookie.path]]; + } + if (cookie.expiresDate != nil) { + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Expires=%@", [formatter stringFromDate:cookie.expiresDate]]]; + } + [result appendString:[NSString stringWithFormat:@"; "]]; + [result appendString:[NSString stringWithFormat:@"Version=%zd", cookie.version]]; + [result appendString:[NSString stringWithFormat:@"\n"]]; + }]; + UnitySendMessage([gameObjectName UTF8String], "CallOnCookies", [result UTF8String]); + } +} + +- (void)userContentController:(WKUserContentController *)userContentController + didReceiveScriptMessage:(WKScriptMessage *)message { + + // Log out the message received + //NSLog(@"Received event %@", message.body); + if ([message.name isEqualToString:@"unityControl"]) { + UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[NSString stringWithFormat:@"%@", message.body] UTF8String]); + } else if ([message.name isEqualToString:@"saveDataURL"]) { + NSRange range = [message.body rangeOfString:@"\t"]; + if (range.location == NSNotFound) { + return; + } + NSString *fileName = [[message.body substringWithRange:NSMakeRange(0, range.location)] lastPathComponent]; + NSString *dataURL = [message.body substringFromIndex:(range.location + 1)]; + range = [dataURL rangeOfString:@"data:"]; + if (range.location != 0) { + return; + } + NSString *tmp = [dataURL substringFromIndex:[@"data:" length]]; + range = [tmp rangeOfString:@";"]; + if (range.location == NSNotFound) { + return; + } + NSString *base64data = [tmp substringFromIndex:(range.location + 1 + [@"base64," length])]; + NSString *type = [tmp substringWithRange:NSMakeRange(0, range.location)]; + NSData *data = [[NSData alloc] initWithBase64EncodedString:base64data options:0]; + NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + path = [path stringByAppendingString:@"/Downloads"]; + BOOL isDir; + NSError *err = nil; + if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) { + if (!isDir) { + return; + } + } else { + [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&err]; + if (err != nil) { + return; + } + } + NSString *prefix = [path stringByAppendingString:@"/"]; + path = [prefix stringByAppendingString:fileName]; + int count = 0; + while ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + count++; + NSString *name = [fileName stringByDeletingPathExtension]; + NSString *ext = [fileName pathExtension]; + if (ext.length == 0) { + path = [NSString stringWithFormat:@"%@%@ (%d)", prefix, name, count]; + } else { + path = [NSString stringWithFormat:@"%@%@ (%d).%@", prefix, name, count, ext]; + } + } + [data writeToFile:path atomically:YES]; + } + + /* + // Then pull something from the device using the message body + NSString *version = [[UIDevice currentDevice] valueForKey:message.body]; + + // Execute some JavaScript using the result? + NSString *exec_template = @"set_headline(\"received: %@\");"; + NSString *exec = [NSString stringWithFormat:exec_template, version]; + [webView evaluateJavaScript:exec completionHandler:nil]; + */ +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if (webView == nil) + return; + + if ([keyPath isEqualToString:@"loading"] && [[change objectForKey:NSKeyValueChangeNewKey] intValue] == 0 + && [webView URL] != nil) { + UnitySendMessage( + [gameObjectName UTF8String], + "CallOnLoaded", + [[[webView URL] absoluteString] UTF8String]); + + } +} + +- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView +{ + UnitySendMessage([gameObjectName UTF8String], "CallOnError", "webViewWebContentProcessDidTerminate"); +} + +- (void)webView:(UIWebView *)uiWebView didFailLoadWithError:(NSError *)error +{ + UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); +} + +- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error +{ + UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); +} + +- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error +{ + UnitySendMessage([gameObjectName UTF8String], "CallOnError", [[error description] UTF8String]); +} + +- (void)webViewDidFinishLoad:(UIWebView *)uiWebView { + if (webView == nil) + return; + // cf. http://stackoverflow.com/questions/10996028/uiwebview-when-did-a-page-really-finish-loading/15916853#15916853 + if ([[uiWebView stringByEvaluatingJavaScriptFromString:@"document.readyState"] isEqualToString:@"complete"] + && [webView URL] != nil) { + UnitySendMessage( + [gameObjectName UTF8String], + "CallOnLoaded", + [[[webView URL] absoluteString] UTF8String]); + } +} + +- (BOOL)webView:(UIWebView *)uiWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType +{ + if (webView == nil) + return YES; + + NSURL *nsurl = [request URL]; + NSString *url = [nsurl absoluteString]; + BOOL pass = YES; + if (allowRegex != nil && [allowRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { + pass = YES; + } else if (denyRegex != nil && [denyRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { + pass = NO; + } + if (!pass) { + return NO; + } + if ([url rangeOfString:@"//itunes.apple.com/"].location != NSNotFound) { + [[UIApplication sharedApplication] openURL:nsurl]; + return NO; + } else if ([url hasPrefix:@"unity:"]) { + UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[url substringFromIndex:6] UTF8String]); + return NO; + } else if (hookRegex != nil && [hookRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { + UnitySendMessage([gameObjectName UTF8String], "CallOnHooked", [url UTF8String]); + return NO; + } else { + if (![self isSetupedCustomHeader:request]) { + [uiWebView loadRequest:[self constructionCustomHeader:request]]; + return NO; + } + UnitySendMessage([gameObjectName UTF8String], "CallOnStarted", [url UTF8String]); + return YES; + } +} + +- (WKWebView *)webView:(WKWebView *)wkWebView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures +{ + // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c + if (!navigationAction.targetFrame.isMainFrame) { + [wkWebView loadRequest:navigationAction.request]; + } + return nil; +} + +- (void)webView:(WKWebView *)wkWebView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + if (webView == nil) { + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + NSURL *nsurl = [navigationAction.request URL]; + NSString *url = [nsurl absoluteString]; + BOOL pass = YES; + if (allowRegex != nil && [allowRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { + pass = YES; + } else if (denyRegex != nil && [denyRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { + pass = NO; + } + if (!pass) { + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + if ([url rangeOfString:@"//itunes.apple.com/"].location != NSNotFound) { + [[UIApplication sharedApplication] openURL:nsurl]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } else if ([url hasPrefix:@"unity:"]) { + UnitySendMessage([gameObjectName UTF8String], "CallFromJS", [[url substringFromIndex:6] UTF8String]); + decisionHandler(WKNavigationActionPolicyCancel); + return; + } else if (hookRegex != nil && [hookRegex firstMatchInString:url options:0 range:NSMakeRange(0, url.length)]) { + UnitySendMessage([gameObjectName UTF8String], "CallOnHooked", [url UTF8String]); + decisionHandler(WKNavigationActionPolicyCancel); + return; + } else if (![url hasPrefix:@"about:blank"] // for loadHTML(), cf. #365 + && ![url hasPrefix:@"about:srcdoc"] // for iframe srcdoc attribute + && ![url hasPrefix:@"file:"] + && ![url hasPrefix:@"http:"] + && ![url hasPrefix:@"https:"]) { + if([[UIApplication sharedApplication] canOpenURL:nsurl]) { + [[UIApplication sharedApplication] openURL:nsurl]; + } + decisionHandler(WKNavigationActionPolicyCancel); + return; + } else if (navigationAction.navigationType == WKNavigationTypeLinkActivated + && (!navigationAction.targetFrame || !navigationAction.targetFrame.isMainFrame)) { + // cf. for target="_blank", cf. http://qiita.com/ShingoFukuyama/items/b3a1441025a36ab7659c + [webView load:navigationAction.request]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } else { + if (navigationAction.targetFrame != nil && navigationAction.targetFrame.isMainFrame) { + // If the custom header is not attached, give it and make a request again. + if (![self isSetupedCustomHeader:[navigationAction request]]) { + NSLog(@"navi ... %@", navigationAction); + [wkWebView loadRequest:[self constructionCustomHeader:navigationAction.request]]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + } + } + UnitySendMessage([gameObjectName UTF8String], "CallOnStarted", [url UTF8String]); + decisionHandler(WKNavigationActionPolicyAllow); +} + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler { + + if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) { + + NSHTTPURLResponse * response = (NSHTTPURLResponse *)navigationResponse.response; + if (response.statusCode >= 400) { + UnitySendMessage([gameObjectName UTF8String], "CallOnHttpError", [[NSString stringWithFormat:@"%d", response.statusCode] UTF8String]); + } + + } + decisionHandler(WKNavigationResponsePolicyAllow); +} + +// alert +- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler +{ + if (!alertDialogEnabled) { + completionHandler(); + return; + } + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction: [UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + completionHandler(); + }]]; + [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; +} + +// confirm +- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler +{ + if (!alertDialogEnabled) { + completionHandler(NO); + return; + } + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addAction:[UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + completionHandler(YES); + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + completionHandler(NO); + }]]; + [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; +} + +// prompt +- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *))completionHandler +{ + if (!alertDialogEnabled) { + completionHandler(nil); + return; + } + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" + message:prompt + preferredStyle:UIAlertControllerStyleAlert]; + [alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.text = defaultText; + }]; + [alertController addAction:[UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + NSString *input = ((UITextField *)alertController.textFields.firstObject).text; + completionHandler(input); + }]]; + [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction *action) { + completionHandler(nil); + }]]; + [UnityGetGLViewController() presentViewController:alertController animated:YES completion:^{}]; +} + +- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler +{ + NSURLSessionAuthChallengeDisposition disposition; + NSURLCredential *credential; + if (basicAuthUserName && basicAuthPassword && [challenge previousFailureCount] == 0) { + disposition = NSURLSessionAuthChallengeUseCredential; + credential = [NSURLCredential credentialWithUser:basicAuthUserName password:basicAuthPassword persistence:NSURLCredentialPersistenceForSession]; + } else { + disposition = NSURLSessionAuthChallengePerformDefaultHandling; + credential = nil; + } + completionHandler(disposition, credential); +} + +- (BOOL)isSetupedCustomHeader:(NSURLRequest *)targetRequest +{ + // Check for additional custom header. + for (NSString *key in [customRequestHeader allKeys]) { + if (![[[targetRequest allHTTPHeaderFields] objectForKey:key] isEqualToString:[customRequestHeader objectForKey:key]]) { + return NO; + } + } + return YES; +} + +- (NSURLRequest *)constructionCustomHeader:(NSURLRequest *)originalRequest +{ + NSMutableURLRequest *convertedRequest = originalRequest.mutableCopy; + for (NSString *key in [customRequestHeader allKeys]) { + [convertedRequest setValue:customRequestHeader[key] forHTTPHeaderField:key]; + } + return (NSURLRequest *)[convertedRequest copy]; +} + +- (void)setMargins:(float)left top:(float)top right:(float)right bottom:(float)bottom relative:(BOOL)relative +{ + if (webView == nil) + return; + UIView *view = UnityGetGLViewController().view; + CGRect frame = webView.frame; + CGRect screen = view.bounds; + if (relative) { + frame.size.width = floor(screen.size.width * (1.0f - left - right)); + frame.size.height = floor(screen.size.height * (1.0f - top - bottom)); + frame.origin.x = floor(screen.size.width * left); + frame.origin.y = floor(screen.size.height * top); + } else { + CGFloat scale = 1.0f / [self getScale:view]; + frame.size.width = floor(screen.size.width - scale * (left + right)); + frame.size.height = floor(screen.size.height - scale * (top + bottom)); + frame.origin.x = floor(scale * left); + frame.origin.y = floor(scale * top); + } + webView.frame = frame; +} + +- (CGFloat)getScale:(UIView *)view +{ + if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) + return view.window.screen.nativeScale; + return view.contentScaleFactor; +} + +- (void)setVisibility:(BOOL)visibility +{ + if (webView == nil) + return; + webView.hidden = visibility ? NO : YES; +} + +- (void)setInteractionEnabled:(BOOL)enabled +{ + if (webView == nil) + return; + webView.userInteractionEnabled = enabled; +} + +- (void)setAlertDialogEnabled:(BOOL)enabled +{ + alertDialogEnabled = enabled; +} + +- (void)setScrollbarsVisibility:(BOOL)visibility +{ + if (webView == nil) + return; + [webView setScrollbarsVisibility:visibility]; +} + +- (void)setScrollBounceEnabled:(BOOL)enabled +{ + if (webView == nil) + return; + [webView setScrollBounce:enabled]; +} + +- (BOOL)setURLPattern:(const char *)allowPattern and:(const char *)denyPattern and:(const char *)hookPattern +{ + NSError *err = nil; + NSRegularExpression *allow = nil; + NSRegularExpression *deny = nil; + NSRegularExpression *hook = nil; + if (allowPattern == nil || *allowPattern == '\0') { + allow = nil; + } else { + allow + = [NSRegularExpression + regularExpressionWithPattern:[NSString stringWithUTF8String:allowPattern] + options:0 + error:&err]; + if (err != nil) { + return NO; + } + } + if (denyPattern == nil || *denyPattern == '\0') { + deny = nil; + } else { + deny + = [NSRegularExpression + regularExpressionWithPattern:[NSString stringWithUTF8String:denyPattern] + options:0 + error:&err]; + if (err != nil) { + return NO; + } + } + if (hookPattern == nil || *hookPattern == '\0') { + hook = nil; + } else { + hook + = [NSRegularExpression + regularExpressionWithPattern:[NSString stringWithUTF8String:hookPattern] + options:0 + error:&err]; + if (err != nil) { + return NO; + } + } + allowRegex = allow; + denyRegex = deny; + hookRegex = hook; + return YES; +} + +- (void)loadURL:(const char *)url +{ + if (webView == nil) + return; + NSString *urlStr = [NSString stringWithUTF8String:url]; + NSURL *nsurl = [NSURL URLWithString:urlStr]; + NSURLRequest *request = [NSURLRequest requestWithURL:nsurl]; + [webView load:request]; +} + +- (void)loadHTML:(const char *)html baseURL:(const char *)baseUrl +{ + if (webView == nil) + return; + NSString *htmlStr = [NSString stringWithUTF8String:html]; + NSString *baseStr = [NSString stringWithUTF8String:baseUrl]; + NSURL *baseNSUrl = [NSURL URLWithString:baseStr]; + [webView loadHTML:htmlStr baseURL:baseNSUrl]; +} + +- (void)evaluateJS:(const char *)js +{ + if (webView == nil) + return; + NSString *jsStr = [NSString stringWithUTF8String:js]; + [webView evaluateJavaScript:jsStr completionHandler:^(NSString *result, NSError *error) {}]; +} + +- (int)progress +{ + if (webView == nil) + return 0; + if ([webView isKindOfClass:[WKWebView class]]) { + return (int)([(WKWebView *)webView estimatedProgress] * 100); + } else { + return 0; + } +} + +- (BOOL)canGoBack +{ + if (webView == nil) + return false; + return [webView canGoBack]; +} + +- (BOOL)canGoForward +{ + if (webView == nil) + return false; + return [webView canGoForward]; +} + +- (void)goBack +{ + if (webView == nil) + return; + [webView goBack]; +} + +- (void)goForward +{ + if (webView == nil) + return; + [webView goForward]; +} + +- (void)reload +{ + if (webView == nil) + return; + [webView reload]; +} + +- (void)addCustomRequestHeader:(const char *)headerKey value:(const char *)headerValue +{ + NSString *keyString = [NSString stringWithUTF8String:headerKey]; + NSString *valueString = [NSString stringWithUTF8String:headerValue]; + + [customRequestHeader setObject:valueString forKey:keyString]; +} + +- (void)removeCustomRequestHeader:(const char *)headerKey +{ + NSString *keyString = [NSString stringWithUTF8String:headerKey]; + + if ([[customRequestHeader allKeys]containsObject:keyString]) { + [customRequestHeader removeObjectForKey:keyString]; + } +} + +- (void)clearCustomRequestHeader +{ + [customRequestHeader removeAllObjects]; +} + +- (const char *)getCustomRequestHeaderValue:(const char *)headerKey +{ + NSString *keyString = [NSString stringWithUTF8String:headerKey]; + NSString *result = [customRequestHeader objectForKey:keyString]; + if (!result) { + return NULL; + } + + const char *s = [result UTF8String]; + char *r = (char *)malloc(strlen(s) + 1); + strcpy(r, s); + return r; +} + +- (void)setBasicAuthInfo:(const char *)userName password:(const char *)password +{ + basicAuthUserName = [NSString stringWithUTF8String:userName]; + basicAuthPassword = [NSString stringWithUTF8String:password]; +} +@end + +extern "C" { + void *_CWebViewPlugin_Init(const char *gameObjectName, BOOL transparent, BOOL zoom, const char *ua, BOOL enableWKWebView, int contentMode, BOOL allowsLinkPreview, BOOL allowsBackForwardNavigationGestures, int radius); + void _CWebViewPlugin_Destroy(void *instance); + void _CWebViewPlugin_SetMargins( + void *instance, float left, float top, float right, float bottom, BOOL relative); + void _CWebViewPlugin_SetVisibility(void *instance, BOOL visibility); + void _CWebViewPlugin_SetInteractionEnabled(void *instance, BOOL enabled); + void _CWebViewPlugin_SetAlertDialogEnabled(void *instance, BOOL visibility); + void _CWebViewPlugin_SetScrollbarsVisibility(void *instance, BOOL visibility); + void _CWebViewPlugin_SetScrollBounceEnabled(void *instance, BOOL enabled); + BOOL _CWebViewPlugin_SetURLPattern(void *instance, const char *allowPattern, const char *denyPattern, const char *hookPattern); + void _CWebViewPlugin_LoadURL(void *instance, const char *url); + void _CWebViewPlugin_LoadHTML(void *instance, const char *html, const char *baseUrl); + void _CWebViewPlugin_EvaluateJS(void *instance, const char *url); + int _CWebViewPlugin_Progress(void *instance); + BOOL _CWebViewPlugin_CanGoBack(void *instance); + BOOL _CWebViewPlugin_CanGoForward(void *instance); + void _CWebViewPlugin_GoBack(void *instance); + void _CWebViewPlugin_GoForward(void *instance); + void _CWebViewPlugin_Reload(void *instance); + void _CWebViewPlugin_AddCustomHeader(void *instance, const char *headerKey, const char *headerValue); + void _CWebViewPlugin_RemoveCustomHeader(void *instance, const char *headerKey); + void _CWebViewPlugin_ClearCustomHeader(void *instance); + void _CWebViewPlugin_ClearCookies(); + void _CWebViewPlugin_SaveCookies(); + void _CWebViewPlugin_GetCookies(void *instance, const char *url); + const char *_CWebViewPlugin_GetCustomHeaderValue(void *instance, const char *headerKey); + void _CWebViewPlugin_SetBasicAuthInfo(void *instance, const char *userName, const char *password); + void _CWebViewPlugin_ClearCache(void *instance, BOOL includeDiskFiles); + void _CWebViewPlugin_SetSuspended(void *instance, BOOL suspended); +} + +void *_CWebViewPlugin_Init(const char *gameObjectName, BOOL transparent, BOOL zoom, const char *ua, BOOL enableWKWebView, int contentMode, BOOL allowsLinkPreview, BOOL allowsBackForwardNavigationGestures, int radius) +{ + WKContentMode wkContentMode = WKContentModeRecommended; + switch (contentMode) { + case 1: + wkContentMode = WKContentModeMobile; + break; + case 2: + wkContentMode = WKContentModeDesktop; + break; + default: + wkContentMode = WKContentModeRecommended; + break; + } + CWebViewPlugin *webViewPlugin = [[CWebViewPlugin alloc] initWithGameObjectName:gameObjectName transparent:transparent zoom:zoom ua:ua enableWKWebView:enableWKWebView contentMode:wkContentMode allowsLinkPreview:allowsLinkPreview allowsBackForwardNavigationGestures:allowsBackForwardNavigationGestures radius:radius]; + [_instances addObject:webViewPlugin]; + return (__bridge_retained void *)webViewPlugin; +} + +void _CWebViewPlugin_Destroy(void *instance) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge_transfer CWebViewPlugin *)instance; + [_instances removeObject:webViewPlugin]; + [webViewPlugin dispose]; + webViewPlugin = nil; +} + +void _CWebViewPlugin_SetMargins( + void *instance, float left, float top, float right, float bottom, BOOL relative) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setMargins:left top:top right:right bottom:bottom relative:relative]; +} + +void _CWebViewPlugin_SetVisibility(void *instance, BOOL visibility) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setVisibility:visibility]; +} + +void _CWebViewPlugin_SetInteractionEnabled(void *instance, BOOL enabled) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setInteractionEnabled:enabled]; +} + +void _CWebViewPlugin_SetAlertDialogEnabled(void *instance, BOOL enabled) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setAlertDialogEnabled:enabled]; +} + +void _CWebViewPlugin_SetScrollbarsVisibility(void *instance, BOOL visibility) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setScrollbarsVisibility:visibility]; +} + +void _CWebViewPlugin_SetScrollBounceEnabled(void *instance, BOOL enabled) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setScrollBounceEnabled:enabled]; +} + +BOOL _CWebViewPlugin_SetURLPattern(void *instance, const char *allowPattern, const char *denyPattern, const char *hookPattern) +{ + if (instance == NULL) + return NO; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + return [webViewPlugin setURLPattern:allowPattern and:denyPattern and:hookPattern]; +} + +void _CWebViewPlugin_LoadURL(void *instance, const char *url) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin loadURL:url]; +} + +void _CWebViewPlugin_LoadHTML(void *instance, const char *html, const char *baseUrl) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin loadHTML:html baseURL:baseUrl]; +} + +void _CWebViewPlugin_EvaluateJS(void *instance, const char *js) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin evaluateJS:js]; +} + +int _CWebViewPlugin_Progress(void *instance) +{ + if (instance == NULL) + return 0; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + return [webViewPlugin progress]; +} + +BOOL _CWebViewPlugin_CanGoBack(void *instance) +{ + if (instance == NULL) + return false; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + return [webViewPlugin canGoBack]; +} + +BOOL _CWebViewPlugin_CanGoForward(void *instance) +{ + if (instance == NULL) + return false; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + return [webViewPlugin canGoForward]; +} + +void _CWebViewPlugin_GoBack(void *instance) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin goBack]; +} + +void _CWebViewPlugin_GoForward(void *instance) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin goForward]; +} + +void _CWebViewPlugin_Reload(void *instance) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin reload]; +} + +void _CWebViewPlugin_AddCustomHeader(void *instance, const char *headerKey, const char *headerValue) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin addCustomRequestHeader:headerKey value:headerValue]; +} + +void _CWebViewPlugin_RemoveCustomHeader(void *instance, const char *headerKey) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin removeCustomRequestHeader:headerKey]; +} + +void _CWebViewPlugin_ClearCustomHeader(void *instance) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin clearCustomRequestHeader]; +} + +void _CWebViewPlugin_ClearCookies() +{ + [CWebViewPlugin clearCookies]; +} + +void _CWebViewPlugin_SaveCookies() +{ + [CWebViewPlugin saveCookies]; +} + +void _CWebViewPlugin_GetCookies(void *instance, const char *url) +{ + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin getCookies:url]; +} + +const char *_CWebViewPlugin_GetCustomHeaderValue(void *instance, const char *headerKey) +{ + if (instance == NULL) + return NULL; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + return [webViewPlugin getCustomRequestHeaderValue:headerKey]; +} + +void _CWebViewPlugin_SetBasicAuthInfo(void *instance, const char *userName, const char *password) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin setBasicAuthInfo:userName password:password]; +} + +void _CWebViewPlugin_ClearCache(void *instance, BOOL includeDiskFiles) +{ + // no op +} + +void _CWebViewPlugin_SetSuspended(void *instance, BOOL suspended) +{ + // no op +} +#endif // __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0 diff --git a/Assets/AirConsole/unity-webview/Plugins/iOS/WebViewWithUIWebView.mm.meta b/Assets/AirConsole/unity-webview/Plugins/iOS/WebViewWithUIWebView.mm.meta new file mode 100644 index 00000000..f5e0fb85 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/iOS/WebViewWithUIWebView.mm.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: fc99cbfa2b53248b18d60e327b478581 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/unity-webview-webgl-plugin.jslib b/Assets/AirConsole/unity-webview/Plugins/unity-webview-webgl-plugin.jslib new file mode 100644 index 00000000..95777383 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/unity-webview-webgl-plugin.jslib @@ -0,0 +1,31 @@ +mergeInto(LibraryManager.library, { + _gree_unity_webview_init: function(name) { + var stringify = (UTF8ToString === undefined) ? Pointer_stringify : UTF8ToString; + unityWebView.init(stringify(name)); + }, + + _gree_unity_webview_setMargins: function (name, left, top, right, bottom) { + var stringify = (UTF8ToString === undefined) ? Pointer_stringify : UTF8ToString; + unityWebView.setMargins(stringify(name), left, top, right, bottom); + }, + + _gree_unity_webview_setVisibility: function(name, visible) { + var stringify = (UTF8ToString === undefined) ? Pointer_stringify : UTF8ToString; + unityWebView.setVisibility(stringify(name), visible); + }, + + _gree_unity_webview_loadURL: function(name, url) { + var stringify = (UTF8ToString === undefined) ? Pointer_stringify : UTF8ToString; + unityWebView.loadURL(stringify(name), stringify(url)); + }, + + _gree_unity_webview_evaluateJS: function(name, js) { + var stringify = (UTF8ToString === undefined) ? Pointer_stringify : UTF8ToString; + unityWebView.evaluateJS(stringify(name), stringify(js)); + }, + + _gree_unity_webview_destroy: function(name) { + var stringify = (UTF8ToString === undefined) ? Pointer_stringify : UTF8ToString; + unityWebView.destroy(stringify(name)); + }, +}); diff --git a/Assets/AirConsole/unity-webview/Plugins/unity-webview-webgl-plugin.jslib.meta b/Assets/AirConsole/unity-webview/Plugins/unity-webview-webgl-plugin.jslib.meta new file mode 100644 index 00000000..2e24b029 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/unity-webview-webgl-plugin.jslib.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 1353be0798ab043d992cd72e4d92970b +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 0 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + WebGL: WebGL + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Runtime.meta b/Assets/AirConsole/unity-webview/Runtime.meta new file mode 100644 index 00000000..95fd8772 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2a81893663cd042c99b7c90d0b3a12d6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Runtime/VersionInfo.cs b/Assets/AirConsole/unity-webview/Runtime/VersionInfo.cs new file mode 100644 index 00000000..c8dbb3a8 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Runtime/VersionInfo.cs @@ -0,0 +1,4 @@ +public class VersionInfo +{ + public const string VERSION = "1.1.6"; +} \ No newline at end of file diff --git a/Assets/AirConsole/unity-webview/Runtime/VersionInfo.cs.meta b/Assets/AirConsole/unity-webview/Runtime/VersionInfo.cs.meta new file mode 100644 index 00000000..be27fe66 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Runtime/VersionInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 95e821749d58e446aba7fcb97de03a2a +timeCreated: 1696508229 \ No newline at end of file diff --git a/Assets/AirConsole/unity-webview/Runtime/WebViewObject.cs b/Assets/AirConsole/unity-webview/Runtime/WebViewObject.cs new file mode 100644 index 00000000..93235ad2 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Runtime/WebViewObject.cs @@ -0,0 +1,1934 @@ +/* + * Copyright (C) 2011 Keijiro Takahashi + * Copyright (C) 2012 GREE, Inc. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +using UnityEngine; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.InteropServices; +#if UNITY_2018_4_OR_NEWER +using UnityEngine.Networking; +#endif +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX +using System.IO; +using System.Text.RegularExpressions; +using UnityEngine.EventSystems; +using UnityEngine.Rendering; +using UnityEngine.UI; +#endif +#if UNITY_ANDROID +using UnityEngine.Android; +#endif + +using Callback = System.Action; + +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX +///

+/// Provides a Unity-compatible dispatcher that mimics UnitySendMessage behaviour in edit-time player builds. +/// +public class UnitySendMessageDispatcher +{ + /// + /// Invokes on the scene object named and forwards . + /// + /// Target GameObject name. + /// Method to call on the GameObject. + /// Message payload forwarded to the receiver. + public static void Dispatch(string name, string method, string message) + { + GameObject obj = GameObject.Find(name); + if (obj != null) + obj.SendMessage(method, message); + } +} +#endif + +/// +/// High-level wrapper around the native unity-webview plugin, exposing platform-specific WebView features to Unity. +/// +public class WebViewObject : MonoBehaviour +{ + Callback onJS; + Callback onError; + Callback onHttpError; + Callback onStarted; + Callback onLoaded; + Callback onHooked; + Callback onCookies; + Callback onAudioFocusChanged; + bool paused; + bool visibility; + bool alertDialogEnabled; + bool scrollBounceEnabled; + int mMarginLeft; + int mMarginTop; + int mMarginRight; + int mMarginBottom; + bool mMarginRelative; + float mMarginLeftComputed; + float mMarginTopComputed; + float mMarginRightComputed; + float mMarginBottomComputed; + bool mMarginRelativeComputed; + /// + /// Optional canvas used by the macOS editor/player implementation to host background visuals behind the WebView. + /// +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + public GameObject canvas; + Image bg; + IntPtr webView; + Rect rect; + Texture2D texture; + byte[] textureDataBuffer; + string inputString = ""; + bool hasFocus; +#elif UNITY_IPHONE + IntPtr webView; +#elif UNITY_ANDROID + AndroidJavaObject webView; + + bool mVisibility; + int mKeyboardVisibleHeight; + float mResumedTimestamp; + int mLastScreenHeight; +#if UNITYWEBVIEW_ANDROID_ENABLE_NAVIGATOR_ONLINE + float androidNetworkReachabilityCheckT0 = -1.0f; + NetworkReachability? androidNetworkReachability0 = null; +#endif + + void OnApplicationPause(bool paused) + { + this.paused = paused; + if (webView == null) + return; + // if (!paused && mKeyboardVisibleHeight > 0) + // { + // webView.Call("SetVisibility", false); + // mResumedTimestamp = Time.realtimeSinceStartup; + // } + webView.Call("OnApplicationPause", paused); + } + + void Update() + { + // NOTE: + // + // When OnApplicationPause(true) is called and the app is in closing, webView.Call(...) + // after that could cause crashes because underlying java instances were closed. + // + // This has not been cleary confirmed yet. However, as Update() is called once after + // OnApplicationPause(true), it is likely correct. + // + // Base on this assumption, we do nothing here if the app is paused. + // + // cf. https://github.com/gree/unity-webview/issues/991#issuecomment-1776628648 + // cf. https://docs.unity3d.com/2020.3/Documentation/Manual/ExecutionOrder.html + // + // In between frames + // + // * OnApplicationPause: This is called at the end of the frame where the pause is detected, + // effectively between the normal frame updates. One extra frame will be issued after + // OnApplicationPause is called to allow the game to show graphics that indicate the + // paused state. + // + if (paused) + return; + if (webView == null) + return; +#if UNITYWEBVIEW_ANDROID_ENABLE_NAVIGATOR_ONLINE + var t = Time.time; + if (t - 1.0f >= androidNetworkReachabilityCheckT0) + { + androidNetworkReachabilityCheckT0 = t; + var androidNetworkReachability = Application.internetReachability; + if (androidNetworkReachability0 != androidNetworkReachability) + { + androidNetworkReachability0 = androidNetworkReachability; + webView.Call("SetNetworkAvailable", androidNetworkReachability != NetworkReachability.NotReachable); + } + } +#endif + if (mResumedTimestamp != 0.0f && Time.realtimeSinceStartup - mResumedTimestamp > 0.5f) + { + mResumedTimestamp = 0.0f; + webView.Call("SetVisibility", mVisibility); + } + if (Screen.height != mLastScreenHeight) + { + mLastScreenHeight = Screen.height; + webView.Call("EvaluateJS", "(function() {var e = document.activeElement; if (e != null && e.tagName.toLowerCase() != 'body') {e.blur(); e.focus();}})()"); + } + for (;;) { + if (webView == null) + break; + var s = webView.Call("GetMessage"); + if (s == null) + break; + var i = s.IndexOf(':', 0); + if (i == -1) + continue; + switch (s.Substring(0, i)) { + case "CallFromJS": + CallFromJS(s.Substring(i + 1)); + break; + case "CallOnError": + CallOnError(s.Substring(i + 1)); + break; + case "CallOnHttpError": + CallOnHttpError(s.Substring(i + 1)); + break; + case "CallOnLoaded": + CallOnLoaded(s.Substring(i + 1)); + break; + case "CallOnStarted": + CallOnStarted(s.Substring(i + 1)); + break; + case "CallOnHooked": + CallOnHooked(s.Substring(i + 1)); + break; + case "CallOnAudioFocusChanged": + CallOnAudioFocusChanged(s.Substring(i + 1)); + break; + case "SetKeyboardVisible": + SetKeyboardVisible(s.Substring(i + 1)); + break; + case "RequestFileChooserPermissions": + RequestFileChooserPermissions(); + break; + } + } + } + + /// + /// Updates the tracked keyboard height when the native plugin reports visibility changes. + /// + /// Keyboard height, in pixels, supplied by the Android plugin. + public void SetKeyboardVisible(string keyboardVisibleHeight) + { + if (BottomAdjustmentDisabled()) + { + return; + } + var keyboardVisibleHeight0 = mKeyboardVisibleHeight; + var keyboardVisibleHeight1 = Int32.Parse(keyboardVisibleHeight); + if (keyboardVisibleHeight0 != keyboardVisibleHeight1) + { + mKeyboardVisibleHeight = keyboardVisibleHeight1; + SetMargins(mMarginLeft, mMarginTop, mMarginRight, mMarginBottom, mMarginRelative); + } + } + + /// + /// Requests runtime storage permissions required by the Android file chooser implementation. + /// + public void RequestFileChooserPermissions() + { + var permissions = new List(); + using (var version = new AndroidJavaClass("android.os.Build$VERSION")) + { + if (version.GetStatic("SDK_INT") >= 33) + { + if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_IMAGES")) + { + permissions.Add("android.permission.READ_MEDIA_IMAGES"); + } + if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_VIDEO")) + { + permissions.Add("android.permission.READ_MEDIA_VIDEO"); + } + if (!Permission.HasUserAuthorizedPermission("android.permission.READ_MEDIA_AUDIO")) + { + permissions.Add("android.permission.READ_MEDIA_AUDIO"); + } + } + else + { + if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageRead)) + { + permissions.Add(Permission.ExternalStorageRead); + } + if (!Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite)) + { + permissions.Add(Permission.ExternalStorageWrite); + } + } + } + if (!Permission.HasUserAuthorizedPermission(Permission.Camera)) + { + permissions.Add(Permission.Camera); + } + if (permissions.Count > 0) + { +#if UNITY_2020_2_OR_NEWER + var grantedCount = 0; + var deniedCount = 0; + var callbacks = new PermissionCallbacks(); + callbacks.PermissionGranted += (permission) => + { + grantedCount++; + if (grantedCount + deniedCount == permissions.Count) + { + StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count)); + } + }; + callbacks.PermissionDenied += (permission) => + { + deniedCount++; + if (grantedCount + deniedCount == permissions.Count) + { + StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count)); + } + }; + callbacks.PermissionDeniedAndDontAskAgain += (permission) => + { + deniedCount++; + if (grantedCount + deniedCount == permissions.Count) + { + StartCoroutine(CallOnRequestFileChooserPermissionsResult(grantedCount == permissions.Count)); + } + }; + Permission.RequestUserPermissions(permissions.ToArray(), callbacks); +#else + StartCoroutine(RequestFileChooserPermissionsCoroutine(permissions.ToArray())); +#endif + } + else + { + StartCoroutine(CallOnRequestFileChooserPermissionsResult(true)); + } + } + +#if UNITY_2020_2_OR_NEWER +#else + int mRequestPermissionPhase; + + IEnumerator RequestFileChooserPermissionsCoroutine(string[] permissions) + { + foreach (var permission in permissions) + { + mRequestPermissionPhase = 0; + Permission.RequestUserPermission(permission); + // waiting permission dialog that may not be opened. + for (var i = 0; i < 8 && mRequestPermissionPhase == 0; i++) + { + yield return new WaitForSeconds(0.25f); + } + if (mRequestPermissionPhase == 0) + { + // permission dialog was not opened. + continue; + } + while (mRequestPermissionPhase == 1) + { + yield return new WaitForSeconds(0.3f); + } + } + yield return new WaitForSeconds(0.3f); + var granted = 0; + foreach (var permission in permissions) + { + if (Permission.HasUserAuthorizedPermission(permission)) + { + granted++; + } + } + StartCoroutine(CallOnRequestFileChooserPermissionsResult(granted == permissions.Length)); + } + + void OnApplicationFocus(bool hasFocus) + { + if (hasFocus) + { + if (mRequestPermissionPhase == 1) + { + mRequestPermissionPhase = 2; + } + } + else + { + if (mRequestPermissionPhase == 0) + { + mRequestPermissionPhase = 1; + } + } + } +#endif + + private IEnumerator CallOnRequestFileChooserPermissionsResult(bool granted) + { + for (var i = 0; i < 3; i++) + { + yield return null; + } + webView.Call("OnRequestFileChooserPermissionsResult", granted); + } + + /// + /// Computes the bottom margin that keeps the WebView visible above the soft keyboard. + /// + /// Original bottom margin in pixels. + /// The adjusted bottom margin accounting for keyboard height. + public int AdjustBottomMargin(int bottom) + { + if (BottomAdjustmentDisabled()) + { + return bottom; + } + else if (mKeyboardVisibleHeight <= 0) + { + return bottom; + } + else + { + int keyboardHeight = 0; + using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (var activity = unityClass.GetStatic("currentActivity")) + using (var player = activity.Get("mUnityPlayer")) + using (var view = player.Call("getView")) + using (var rect = new AndroidJavaObject("android.graphics.Rect")) + { + if (view.Call("getGlobalVisibleRect", rect)) + { + int h0 = rect.Get("bottom"); + view.Call("getWindowVisibleDisplayFrame", rect); + int h1 = rect.Get("bottom"); + keyboardHeight = h0 - h1; + } + } + return (bottom > keyboardHeight) ? bottom : keyboardHeight; + } + } + + private bool BottomAdjustmentDisabled() + { +#if UNITYWEBVIEW_ANDROID_FORCE_MARGIN_ADJUSTMENT_FOR_KEYBOARD + return false; +#else + return + !Screen.fullScreen + || ((Screen.autorotateToLandscapeLeft || Screen.autorotateToLandscapeRight) + && (Screen.autorotateToPortrait || Screen.autorotateToPortraitUpsideDown)); +#endif + } +#else + IntPtr webView; +#endif + + void Awake() + { + Debug.Log($"Initializing WebViewObject v{VersionInfo.VERSION}"); + alertDialogEnabled = true; + scrollBounceEnabled = true; + mMarginLeftComputed = -9999; + mMarginTopComputed = -9999; + mMarginRightComputed = -9999; + mMarginBottomComputed = -9999; + } + + /// + /// Gets a value indicating whether the soft keyboard is currently visible. + /// + public bool IsKeyboardVisible + { + get + { +#if !UNITY_EDITOR && UNITY_ANDROID + return mKeyboardVisibleHeight > 0; +#elif !UNITY_EDITOR && UNITY_IPHONE + return TouchScreenKeyboard.visible; +#else + return false; +#endif + } + } + +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + [DllImport("WebView")] + private static extern string _CWebViewPlugin_GetAppPath(); + [DllImport("WebView")] + private static extern IntPtr _CWebViewPlugin_InitStatic( + bool inEditor, bool useMetal); + [DllImport("WebView")] + private static extern IntPtr _CWebViewPlugin_Init( + string gameObject, bool transparent, bool zoom, int width, int height, string ua, bool separated); + [DllImport("WebView")] + private static extern int _CWebViewPlugin_Destroy(IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_SetRect( + IntPtr instance, int width, int height); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_SetVisibility( + IntPtr instance, bool visibility); + [DllImport("WebView")] + private static extern bool _CWebViewPlugin_SetURLPattern( + IntPtr instance, string allowPattern, string denyPattern, string hookPattern); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_LoadURL( + IntPtr instance, string url); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_LoadHTML( + IntPtr instance, string html, string baseUrl); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_EvaluateJS( + IntPtr instance, string url); + [DllImport("WebView")] + private static extern int _CWebViewPlugin_Progress( + IntPtr instance); + [DllImport("WebView")] + private static extern bool _CWebViewPlugin_CanGoBack( + IntPtr instance); + [DllImport("WebView")] + private static extern bool _CWebViewPlugin_CanGoForward( + IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_GoBack( + IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_GoForward( + IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_Reload( + IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_SendMouseEvent(IntPtr instance, int x, int y, float deltaY, int mouseState); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_SendKeyEvent(IntPtr instance, int x, int y, string keyChars, ushort keyCode, int keyState); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_Update(IntPtr instance, bool refreshBitmap, int devicePixelRatio); + [DllImport("WebView")] + private static extern int _CWebViewPlugin_BitmapWidth(IntPtr instance); + [DllImport("WebView")] + private static extern int _CWebViewPlugin_BitmapHeight(IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_Render(IntPtr instance, IntPtr textureBuffer); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_AddCustomHeader(IntPtr instance, string headerKey, string headerValue); + [DllImport("WebView")] + private static extern string _CWebViewPlugin_GetCustomHeaderValue(IntPtr instance, string headerKey); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_RemoveCustomHeader(IntPtr instance, string headerKey); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_ClearCustomHeader(IntPtr instance); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_ClearCookies(); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_SaveCookies(); + [DllImport("WebView")] + private static extern void _CWebViewPlugin_GetCookies(IntPtr instance, string url); + [DllImport("WebView")] + private static extern string _CWebViewPlugin_GetMessage(IntPtr instance); +#elif UNITY_IPHONE + [DllImport("__Internal")] + private static extern IntPtr _CWebViewPlugin_Init(string gameObject, bool transparent, bool zoom, string ua, bool enableWKWebView, int wkContentMode, bool wkAllowsLinkPreview, bool wkAllowsBackForwardNavigationGestures, int radius); + [DllImport("__Internal")] + private static extern int _CWebViewPlugin_Destroy(IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetMargins( + IntPtr instance, float left, float top, float right, float bottom, bool relative); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetVisibility( + IntPtr instance, bool visibility); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetScrollbarsVisibility( + IntPtr instance, bool visibility); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetAlertDialogEnabled( + IntPtr instance, bool enabled); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetScrollBounceEnabled( + IntPtr instance, bool enabled); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetInteractionEnabled( + IntPtr instance, bool enabled); + [DllImport("__Internal")] + private static extern bool _CWebViewPlugin_SetURLPattern( + IntPtr instance, string allowPattern, string denyPattern, string hookPattern); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_LoadURL( + IntPtr instance, string url); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_LoadHTML( + IntPtr instance, string html, string baseUrl); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_EvaluateJS( + IntPtr instance, string url); + [DllImport("__Internal")] + private static extern int _CWebViewPlugin_Progress( + IntPtr instance); + [DllImport("__Internal")] + private static extern bool _CWebViewPlugin_CanGoBack( + IntPtr instance); + [DllImport("__Internal")] + private static extern bool _CWebViewPlugin_CanGoForward( + IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_GoBack( + IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_GoForward( + IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_Reload( + IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_AddCustomHeader(IntPtr instance, string headerKey, string headerValue); + [DllImport("__Internal")] + private static extern string _CWebViewPlugin_GetCustomHeaderValue(IntPtr instance, string headerKey); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_RemoveCustomHeader(IntPtr instance, string headerKey); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_ClearCustomHeader(IntPtr instance); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_ClearCookies(); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SaveCookies(); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_GetCookies(IntPtr instance, string url); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetBasicAuthInfo(IntPtr instance, string userName, string password); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_ClearCache(IntPtr instance, bool includeDiskFiles); + [DllImport("__Internal")] + private static extern void _CWebViewPlugin_SetSuspended(IntPtr instance, bool suspended); +#elif UNITY_WEBGL + [DllImport("__Internal")] + private static extern void _gree_unity_webview_init(string name); + [DllImport("__Internal")] + private static extern void _gree_unity_webview_setMargins(string name, int left, int top, int right, int bottom); + [DllImport("__Internal")] + private static extern void _gree_unity_webview_setVisibility(string name, bool visible); + [DllImport("__Internal")] + private static extern void _gree_unity_webview_loadURL(string name, string url); + [DllImport("__Internal")] + private static extern void _gree_unity_webview_evaluateJS(string name, string js); + [DllImport("__Internal")] + private static extern void _gree_unity_webview_destroy(string name); +#endif + + /// + /// Determines whether the current platform exposes a compatible WebView implementation. + /// + /// true when the underlying native plugin can be instantiated; otherwise false. + public static bool IsWebViewAvailable() + { +#if !UNITY_EDITOR && UNITY_ANDROID + using (var plugin = new AndroidJavaObject("net.gree.unitywebview.CWebViewPlugin")) + { + return plugin.CallStatic("IsWebViewAvailable"); + } +#else + return true; +#endif + } + + /// + /// Initialises the platform WebView instance and binds callbacks for native-to-Unity messaging. + /// + /// Invoked when JavaScript calls Unity.call. + /// Invoked when the WebView reports a navigation error. + /// Invoked when the WebView receives an HTTP error status. + /// Invoked after the WebView finishes loading a page. + /// Invoked when the WebView starts navigating to a page. + /// Invoked when a hooked URL pattern is hit. + /// Invoked when cookies are requested from the WebView. + /// Whether the WebView background should be transparent. + /// Whether native zoom controls are enabled. + /// Optional custom user agent string. + /// Rounded corner radius (Android only). + /// Android dark-mode override (0 = system, 1 = off, 2 = on). + /// Switches between WKWebView and UIWebView on iOS. + /// iOS content mode (0 = recommended, 1 = mobile, 2 = desktop). + /// Enables iOS link preview gestures. + /// Enables iOS swipe navigation gestures. + /// Creates a separate native window in the Unity editor. + /// Receives Android audio focus transition events. + public void Init( + Callback cb = null, + Callback err = null, + Callback httpErr = null, + Callback ld = null, + Callback started = null, + Callback hooked = null, + Callback cookies = null, + bool transparent = false, + bool zoom = true, + string ua = "", + int radius = 0, + // android + int androidForceDarkMode = 0, // 0: follow system setting, 1: force dark off, 2: force dark on + // ios + bool enableWKWebView = true, + int wkContentMode = 0, // 0: recommended, 1: mobile, 2: desktop + bool wkAllowsLinkPreview = true, + bool wkAllowsBackForwardNavigationGestures = true, + // editor + bool separated = false, + Callback audioFocusChanged = null) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + _CWebViewPlugin_InitStatic( + Application.platform == RuntimePlatform.OSXEditor, + SystemInfo.graphicsDeviceType == GraphicsDeviceType.Metal); +#endif + onJS = cb; + onError = err; + onHttpError = httpErr; + onStarted = started; + onLoaded = ld; + onHooked = hooked; + onCookies = cookies; + onAudioFocusChanged = audioFocusChanged; +#if UNITY_WEBGL +#if !UNITY_EDITOR + _gree_unity_webview_init(name); +#endif +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.init", name); +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + Debug.LogError("Webview is not supported on this platform."); +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + { + var uri = new Uri(_CWebViewPlugin_GetAppPath()); + var info = File.ReadAllText(uri.LocalPath + "Contents/Info.plist"); + if (Regex.IsMatch(info, @"CFBundleGetInfoString\s*Unity version [5-9]\.[3-9]") + && !Regex.IsMatch(info, @"NSAppTransportSecurity\s*\s*NSAllowsArbitraryLoads\s*\s*")) { + Debug.LogWarning("WebViewObject: NSAppTransportSecurity isn't configured to allow HTTP. If you need to allow any HTTP access, please shutdown Unity and invoke:\n/usr/libexec/PlistBuddy -c \"Add NSAppTransportSecurity:NSAllowsArbitraryLoads bool true\" /Applications/Unity/Unity.app/Contents/Info.plist"); + } + } +#if UNITY_EDITOR_OSX + // if (string.IsNullOrEmpty(ua)) { + // ua = @"Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53"; + // } +#endif + webView = _CWebViewPlugin_Init( + name, + transparent, + zoom, + Screen.width, + Screen.height, + ua +#if UNITY_EDITOR + , separated +#else + , false +#endif + ); + rect = new Rect(0, 0, Screen.width, Screen.height); +#elif UNITY_IPHONE + webView = _CWebViewPlugin_Init(name, transparent, zoom, ua, enableWKWebView, wkContentMode, wkAllowsLinkPreview, wkAllowsBackForwardNavigationGestures, radius); +#elif UNITY_ANDROID + webView = new AndroidJavaObject("net.gree.unitywebview.CWebViewPlugin"); +#if UNITY_2021_1_OR_NEWER + webView.SetStatic("forceBringToFront", true); +#endif + webView.Call("Init", name, transparent, zoom, androidForceDarkMode, ua, radius); +#else + Debug.LogError("Webview is not supported on this platform."); +#endif + } + + public void Destroy() + { +#if UNITY_WEBGL +#if !UNITY_EDITOR + _gree_unity_webview_destroy(name); +#endif +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.destroy", name); +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + if (bg != null) { + Destroy(bg.gameObject); + } + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_Destroy(webView); + webView = IntPtr.Zero; + Destroy(texture); +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_Destroy(webView); + webView = IntPtr.Zero; +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("Destroy"); + webView.Dispose(); + webView = null; +#endif + } + + /// + /// Pauses WebView timers and rendering to match Unity's lifecycle. + /// + public void Pause() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + //TODO: UNSUPPORTED +#elif UNITY_IPHONE + // NOTE: this suspends media playback only. + if (webView == null) + return; + _CWebViewPlugin_SetSuspended(webView, true); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("Pause"); +#endif + } + + /// + /// Resumes WebView timers previously paused via . + /// + public void Resume() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + //TODO: UNSUPPORTED +#elif UNITY_IPHONE + // NOTE: this resumes media playback only. + _CWebViewPlugin_SetSuspended(webView, false); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("Resume"); +#endif + } + + /// + /// Convenience helper that positions the WebView using a center point and size instead of raw margins. + /// + /// Desired centre position in screen pixels (historically anchored to lower-left). + /// Desired width and height of the WebView in pixels. + public void SetCenterPositionWithScale(Vector2 center, Vector2 scale) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#else + float left = (Screen.width - scale.x) / 2.0f + center.x; + float right = Screen.width - (left + scale.x); + float bottom = (Screen.height - scale.y) / 2.0f + center.y; + float top = Screen.height - (bottom + scale.y); + SetMargins((int)left, (int)top, (int)right, (int)bottom); +#endif + } + + /// + /// Applies absolute or relative margins to the WebView rectangle. + /// + /// Left margin in pixels or percentage. + /// Top margin in pixels or percentage. + /// Right margin in pixels or percentage. + /// Bottom margin in pixels or percentage. + /// When true, margins are interpreted as percentages of the screen size. + public void SetMargins(int left, int top, int right, int bottom, bool relative = false) + { +#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return; +#elif UNITY_WEBPLAYER || UNITY_WEBGL +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + if (webView == IntPtr.Zero) + return; +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; +#elif UNITY_ANDROID + if (webView == null) + return; +#endif + + mMarginLeft = left; + mMarginTop = top; + mMarginRight = right; + mMarginBottom = bottom; + mMarginRelative = relative; + float ml, mt, mr, mb; +#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_WEBPLAYER || UNITY_WEBGL + ml = left; + mt = top; + mr = right; + mb = bottom; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + ml = left; + mt = top; + mr = right; + mb = bottom; +#elif UNITY_IPHONE + if (relative) + { + float w = (float)Screen.width; + float h = (float)Screen.height; + ml = left / w; + mt = top / h; + mr = right / w; + mb = bottom / h; + } + else + { + ml = left; + mt = top; + mr = right; + mb = bottom; + } +#elif UNITY_ANDROID + if (relative) + { + float w = (float)Screen.width; + float h = (float)Screen.height; + int iw = Display.main.systemWidth; + int ih = Display.main.systemHeight; + if (!Screen.fullScreen) + { + using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) + using (var activity = unityClass.GetStatic("currentActivity")) + using (var player = activity.Get("mUnityPlayer")) + using (var view = player.Call("getView")) + using (var rect = new AndroidJavaObject("android.graphics.Rect")) + { + view.Call("getDrawingRect", rect); + iw = rect.Call("width"); + ih = rect.Call("height"); + } + } + ml = left / w * iw; + mt = top / h * ih; + mr = right / w * iw; + mb = AdjustBottomMargin((int)(bottom / h * ih)); + } + else + { + ml = left; + mt = top; + mr = right; + mb = AdjustBottomMargin(bottom); + } +#endif + bool r = relative; + + if (ml == mMarginLeftComputed + && mt == mMarginTopComputed + && mr == mMarginRightComputed + && mb == mMarginBottomComputed + && r == mMarginRelativeComputed) + { + return; + } + mMarginLeftComputed = ml; + mMarginTopComputed = mt; + mMarginRightComputed = mr; + mMarginBottomComputed = mb; + mMarginRelativeComputed = r; + +#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.setMargins", name, (int)ml, (int)mt, (int)mr, (int)mb); +#elif UNITY_WEBGL && !UNITY_EDITOR + _gree_unity_webview_setMargins(name, (int)ml, (int)mt, (int)mr, (int)mb); +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + int width = (int)(Screen.width - (ml + mr)); + int height = (int)(Screen.height - (mb + mt)); + _CWebViewPlugin_SetRect(webView, width, height); + rect = new Rect(left, bottom, width, height); + UpdateBGTransform(); +#elif UNITY_IPHONE + _CWebViewPlugin_SetMargins(webView, ml, mt, mr, mb, r); +#elif UNITY_ANDROID + webView.Call("SetMargins", (int)ml, (int)mt, (int)mr, (int)mb); +#endif + } + + /// + /// Shows or hides the WebView while keeping its state intact. + /// + /// true to make the WebView visible; otherwise false. + public void SetVisibility(bool v) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + if (bg != null) + { + bg.gameObject.active = v; + } +#endif + if (GetVisibility() && !v) + { + EvaluateJS("if (document && document.activeElement) document.activeElement.blur();"); + } +#if UNITY_WEBGL +#if !UNITY_EDITOR + _gree_unity_webview_setVisibility(name, v); +#endif +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.setVisibility", name, v); +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetVisibility(webView, v); +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetVisibility(webView, v); +#elif UNITY_ANDROID + if (webView == null) + return; + mVisibility = v; + webView.Call("SetVisibility", v); +#endif + visibility = v; + } + + /// + /// Gets the last visibility flag applied to the WebView. + /// + public bool GetVisibility() + { + return visibility; + } + + /// + /// Toggles native scroll bar rendering where supported. + /// + /// true to show scroll bars; otherwise false. + public void SetScrollbarsVisibility(bool v) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetScrollbarsVisibility(webView, v); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetScrollbarsVisibility", v); +#else + // TODO: UNSUPPORTED +#endif + } + + /// + /// Enables the platform's WebView remote debugging facilities when available (Android only). + /// + /// Whether debugging should be enabled. + public void EnableWebviewDebugging(bool enabled) { +#if UNITY_ANDROID && !(UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX) + if (webView == null) { + return; + } + + webView.Call("enableWebViewDebugging", enabled); +#else + Debug.Log($"EnableWebviewDebugging({enabled}) not implemented on {Application.platform}"); +#endif + } + + /// + /// Enables or disables user interaction with the WebView surface. + /// + /// Whether touch input is forwarded to the WebView. + public void SetInteractionEnabled(bool enabled) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetInteractionEnabled(webView, enabled); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetInteractionEnabled", enabled); +#else + // TODO: UNSUPPORTED +#endif + } + + /// + /// Controls whether JavaScript alert/confirm/prompt dialogs are permitted. + /// + /// true to allow dialogs; otherwise false. + public void SetAlertDialogEnabled(bool e) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetAlertDialogEnabled(webView, e); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetAlertDialogEnabled", e); +#else + // TODO: UNSUPPORTED +#endif + alertDialogEnabled = e; + } + + /// + /// Gets the cached alert dialog enable flag. + /// + public bool GetAlertDialogEnabled() + { + return alertDialogEnabled; + } + + /// + /// Toggles bouncing/elastic scrolling on supported platforms. + /// + /// true to enable bouncing, otherwise false. + public void SetScrollBounceEnabled(bool e) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetScrollBounceEnabled(webView, e); +#elif UNITY_ANDROID + // TODO: UNSUPPORTED +#else + // TODO: UNSUPPORTED +#endif + scrollBounceEnabled = e; + } + + /// + /// Gets the cached bounce/elastic scrolling flag. + /// + public bool GetScrollBounceEnabled() + { + return scrollBounceEnabled; + } + + /// + /// Grants or revokes camera access for WebRTC and file input elements. + /// + /// Whether the WebView should expose camera capture to web content. + public void SetCameraAccess(bool allowed) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + // TODO: UNSUPPORTED +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetCameraAccess", allowed); +#else + // TODO: UNSUPPORTED +#endif + } + + /// + /// Grants or revokes microphone access for WebRTC and audio capture flows. + /// + /// Whether the WebView should expose microphone capture to web content. + public void SetMicrophoneAccess(bool allowed) + { +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + // TODO: UNSUPPORTED +#elif UNITY_IPHONE + // TODO: UNSUPPORTED +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetMicrophoneAccess", allowed); +#else + // TODO: UNSUPPORTED +#endif + } + + /// + /// Forces the Android plugin to request audio focus back for Unity's audio subsystem. + /// + public void RequestUnityAudioFocus() + { +#if UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("RequestUnityAudioFocus"); +#endif + } + + /// + /// Relinquishes Unity's audio focus so WebView media can take control. + /// + public void AbandonUnityAudioFocus() + { +#if UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("AbandonUnityAudioFocus"); +#endif + } + + /// + /// Sets allow/deny/hook regular expressions to control navigation handling. + /// + /// Regex pattern for URLs that are allowed. + /// Regex pattern for URLs that should be blocked. + /// Regex pattern that triggers hook callbacks. + /// true if the operation is supported on the current platform. + public bool SetURLPattern(string allowPattern, string denyPattern, string hookPattern) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return false; + return _CWebViewPlugin_SetURLPattern(webView, allowPattern, denyPattern, hookPattern); +#elif UNITY_ANDROID + if (webView == null) + return false; + return webView.Call("SetURLPattern", allowPattern, denyPattern, hookPattern); +#endif + } + + /// + /// Navigates the WebView to the specified URL. + /// + /// Absolute or relative URL to load. + public void LoadURL(string url) + { + if (string.IsNullOrEmpty(url)) + return; +#if UNITY_WEBGL +#if !UNITY_EDITOR + _gree_unity_webview_loadURL(name, url); +#endif +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.loadURL", name, url); +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_LoadURL(webView, url); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("LoadURL", url); +#endif + } + + /// + /// Loads raw HTML content into the WebView. + /// + /// HTML markup to display. + /// Base URL used for resolving relative paths. + public void LoadHTML(string html, string baseUrl) + { + if (string.IsNullOrEmpty(html)) + return; + if (string.IsNullOrEmpty(baseUrl)) + baseUrl = ""; +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_LoadHTML(webView, html, baseUrl); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("LoadHTML", html, baseUrl); +#endif + } + + /// + /// Evaluates JavaScript inside the current WebView context. + /// + /// Script source to execute. + public void EvaluateJS(string js) + { +#if UNITY_WEBGL +#if !UNITY_EDITOR + _gree_unity_webview_evaluateJS(name, js); +#endif +#elif UNITY_WEBPLAYER + Application.ExternalCall("unityWebView.evaluateJS", name, js); +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_EvaluateJS(webView, js); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("EvaluateJS", js); +#endif + } + + /// + /// Returns the current navigation progress percentage where supported. + /// + public int Progress() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED + return 0; +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return 0; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return 0; + return _CWebViewPlugin_Progress(webView); +#elif UNITY_ANDROID + if (webView == null) + return 0; + return webView.Get("progress"); +#endif + } + + /// + /// Returns whether the WebView has a previous page in its navigation history. + /// + public bool CanGoBack() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return false; + return _CWebViewPlugin_CanGoBack(webView); +#elif UNITY_ANDROID + if (webView == null) + return false; + return webView.Get("canGoBack"); +#endif + } + + /// + /// Returns whether the WebView can navigate forward in its history stack. + /// + public bool CanGoForward() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return false; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return false; + return _CWebViewPlugin_CanGoForward(webView); +#elif UNITY_ANDROID + if (webView == null) + return false; + return webView.Get("canGoForward"); +#endif + } + + /// + /// Navigates to the previous entry in the WebView history if available. + /// + public void GoBack() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_GoBack(webView); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("GoBack"); +#endif + } + + /// + /// Navigates to the next entry in the WebView history if available. + /// + public void GoForward() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_GoForward(webView); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("GoForward"); +#endif + } + + /// + /// Reloads the current WebView page. + /// + public void Reload() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_Reload(webView); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("Reload"); +#endif + } + + /// + /// Invokes the registered error callback with the supplied message. + /// + /// Descriptive error message. + public void CallOnError(string error) + { + if (onError != null) + { + onError(error); + } + } + + /// + /// Invokes the registered HTTP error callback for the given status information. + /// + /// HTTP error payload (status code or detail string). + public void CallOnHttpError(string error) + { + if (onHttpError != null) + { + onHttpError(error); + } + } + + /// + /// Forwards navigation-start notifications to the Unity listener. + /// + /// URL that began loading. + public void CallOnStarted(string url) + { + if (onStarted != null) + { + onStarted(url); + } + } + + /// + /// Forwards navigation-complete notifications to the Unity listener. + /// + /// URL that finished loading. + public void CallOnLoaded(string url) + { + if (onLoaded != null) + { + onLoaded(url); + } + } + + /// + /// Dispatches JavaScript messages received from the native bridge to managed listeners. + /// + /// Message payload supplied by the page. + public void CallFromJS(string message) + { + if (onJS != null) + { +#if !UNITY_ANDROID +#if UNITY_2018_4_OR_NEWER + message = UnityWebRequest.UnEscapeURL(message); +#else // UNITY_2018_4_OR_NEWER + message = WWW.UnEscapeURL(message); +#endif // UNITY_2018_4_OR_NEWER +#endif // !UNITY_ANDROID + onJS(message); + } + } + + /// + /// Dispatches URL-hook notifications to managed listeners. + /// + /// Hooked URL reported by the native layer. + public void CallOnHooked(string message) + { + if (onHooked != null) + { +#if !UNITY_ANDROID +#if UNITY_2018_4_OR_NEWER + message = UnityWebRequest.UnEscapeURL(message); +#else // UNITY_2018_4_OR_NEWER + message = WWW.UnEscapeURL(message); +#endif // UNITY_2018_4_OR_NEWER +#endif // !UNITY_ANDROID + onHooked(message); + } + } + + /// + /// Delivers cookie information retrieved from the WebView. + /// + /// Cookie string in standard HTTP header format. + public void CallOnCookies(string cookies) + { + if (onCookies != null) + { + onCookies(cookies); + } + } + + /// + /// Dispatches audio focus state transitions emitted by the Android plugin. + /// + /// State identifier such as webview-start or unity-gain. + public void CallOnAudioFocusChanged(string state) + { + if (onAudioFocusChanged != null) + { + onAudioFocusChanged(state); + } + } + + /// + /// Overrides the audio focus change callback at runtime. + /// + /// Callback invoked for audio focus transitions. + public void SetOnAudioFocusChanged(Callback cb) + { + onAudioFocusChanged = cb; + } + + /// + /// Adds or replaces a custom HTTP request header for subsequent WebView navigations. + /// + /// HTTP header key. + /// Header value. + public void AddCustomHeader(string headerKey, string headerValue) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_AddCustomHeader(webView, headerKey, headerValue); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("AddCustomHeader", headerKey, headerValue); +#endif + } + + /// + /// Retrieves a previously registered custom header value, if present. + /// + /// HTTP header key to query. + /// The stored header value or null if none is found. + public string GetCustomHeaderValue(string headerKey) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED + return null; +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED + return null; +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return null; + return _CWebViewPlugin_GetCustomHeaderValue(webView, headerKey); +#elif UNITY_ANDROID + if (webView == null) + return null; + return webView.Call("GetCustomHeaderValue", headerKey); +#endif + } + + /// + /// Removes a custom header so it is no longer appended to web requests. + /// + /// HTTP header key to remove. + public void RemoveCustomHeader(string headerKey) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_RemoveCustomHeader(webView, headerKey); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("RemoveCustomHeader", headerKey); +#endif + } + + /// + /// Clears all previously added custom headers. + /// + public void ClearCustomHeader() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_ClearCustomHeader(webView); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("ClearCustomHeader"); +#endif + } + + /// + /// Deletes persistent WebView cookies where supported. + /// + public void ClearCookies() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_ClearCookies(); +#elif UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("ClearCookies"); +#endif + } + + + /// + /// Flushes the in-memory cookie store to disk. + /// + public void SaveCookies() + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SaveCookies(); +#elif UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("SaveCookies"); +#endif + } + + + /// + /// Requests the cookie string for a given URL. Result is returned via . + /// + /// URL whose cookies should be retrieved. + public void GetCookies(string url) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_GetCookies(webView, url); +#elif UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("GetCookies", url); +#else + //TODO: UNSUPPORTED +#endif + } + + /// + /// Supplies basic authentication credentials for upcoming requests. + /// + /// HTTP basic auth user name. + /// HTTP basic auth password. + public void SetBasicAuthInfo(string userName, string password) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + //TODO: UNSUPPORTED +#elif UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_SetBasicAuthInfo(webView, userName, password); +#elif UNITY_ANDROID + if (webView == null) + return; + webView.Call("SetBasicAuthInfo", userName, password); +#endif + } + + /// + /// Clears the WebView cache, optionally including disk-backed resources. + /// + /// When true, disk cache entries are also removed. + public void ClearCache(bool includeDiskFiles) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_IPHONE && !UNITY_EDITOR + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_ClearCache(webView, includeDiskFiles); +#elif UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("ClearCache", includeDiskFiles); +#endif + } + + + /// + /// Adjusts the Android text zoom scaling factor (100 is default size). + /// + /// Text zoom percentage. + public void SetTextZoom(int textZoom) + { +#if UNITY_WEBPLAYER || UNITY_WEBGL + //TODO: UNSUPPORTED +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX + //TODO: UNSUPPORTED +#elif UNITY_IPHONE && !UNITY_EDITOR + //TODO: UNSUPPORTED +#elif UNITY_ANDROID && !UNITY_EDITOR + if (webView == null) + return; + webView.Call("SetTextZoom", textZoom); +#endif + } + +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + void OnApplicationFocus(bool focus) + { + if (!focus) + { + hasFocus = false; + } + } + + void Start() + { + if (canvas != null) + { + var g = new GameObject(gameObject.name + "BG"); + g.transform.parent = canvas.transform; + bg = g.AddComponent(); + UpdateBGTransform(); + } + } + + void Update() + { + if (bg != null) { + bg.transform.SetAsLastSibling(); + } + if (hasFocus) { + inputString += Input.inputString; + } + for (;;) { + if (webView == IntPtr.Zero) + break; + string s = _CWebViewPlugin_GetMessage(webView); + if (s == null) + break; + var i = s.IndexOf(':', 0); + if (i == -1) + continue; + switch (s.Substring(0, i)) { + case "CallFromJS": + CallFromJS(s.Substring(i + 1)); + break; + case "CallOnError": + CallOnError(s.Substring(i + 1)); + break; + case "CallOnHttpError": + CallOnHttpError(s.Substring(i + 1)); + break; + case "CallOnLoaded": + CallOnLoaded(s.Substring(i + 1)); + break; + case "CallOnStarted": + CallOnStarted(s.Substring(i + 1)); + break; + case "CallOnHooked": + CallOnHooked(s.Substring(i + 1)); + break; + case "CallOnCookies": + CallOnCookies(s.Substring(i + 1)); + break; + } + } + if (webView == IntPtr.Zero || !visibility) + return; + bool refreshBitmap = (Time.frameCount % bitmapRefreshCycle == 0); + _CWebViewPlugin_Update(webView, refreshBitmap, devicePixelRatio); + if (refreshBitmap) { + { + var w = _CWebViewPlugin_BitmapWidth(webView); + var h = _CWebViewPlugin_BitmapHeight(webView); + if (texture == null || texture.width != w || texture.height != h) { + bool isLinearSpace = QualitySettings.activeColorSpace == ColorSpace.Linear; + texture = new Texture2D(w, h, TextureFormat.RGBA32, false, !isLinearSpace); + texture.filterMode = FilterMode.Bilinear; + texture.wrapMode = TextureWrapMode.Clamp; + textureDataBuffer = new byte[w * h * 4]; + } + } + if (textureDataBuffer.Length > 0) { + var gch = GCHandle.Alloc(textureDataBuffer, GCHandleType.Pinned); + _CWebViewPlugin_Render(webView, gch.AddrOfPinnedObject()); + gch.Free(); + texture.LoadRawTextureData(textureDataBuffer); + texture.Apply(); + } + } + } + + void UpdateBGTransform() + { + if (bg != null) { + bg.rectTransform.anchorMin = Vector2.zero; + bg.rectTransform.anchorMax = Vector2.zero; + bg.rectTransform.pivot = Vector2.zero; + bg.rectTransform.position = rect.min; + bg.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rect.size.x); + bg.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rect.size.y); + } + } + + /// + /// Frame interval between offscreen bitmap refreshes for the macOS implementation. + /// + public int bitmapRefreshCycle = 1; + + /// + /// Device pixel ratio passed to the native renderer when updating the texture. + /// + public int devicePixelRatio = 1; + + void OnGUI() + { + if (webView == IntPtr.Zero || !visibility) + return; + switch (Event.current.type) { + case EventType.MouseDown: + case EventType.MouseUp: + hasFocus = rect.Contains(Input.mousePosition); + break; + } + switch (Event.current.type) { + case EventType.MouseMove: + case EventType.MouseDown: + case EventType.MouseDrag: + case EventType.MouseUp: + case EventType.ScrollWheel: + if (hasFocus) { + Vector3 p; + p.x = Input.mousePosition.x - rect.x; + p.y = Input.mousePosition.y - rect.y; + { + int mouseState = 0; + if (Input.GetButtonDown("Fire1")) { + mouseState = 1; + } else if (Input.GetButton("Fire1")) { + mouseState = 2; + } else if (Input.GetButtonUp("Fire1")) { + mouseState = 3; + } + //_CWebViewPlugin_SendMouseEvent(webView, (int)p.x, (int)p.y, Input.GetAxis("Mouse ScrollWheel"), mouseState); + _CWebViewPlugin_SendMouseEvent(webView, (int)p.x, (int)p.y, Input.mouseScrollDelta.y, mouseState); + } + } + break; + case EventType.Repaint: + while (!string.IsNullOrEmpty(inputString)) { + var keyChars = inputString.Substring(0, 1); + var keyCode = (ushort)inputString[0]; + inputString = inputString.Substring(1); + if (!string.IsNullOrEmpty(keyChars) || keyCode != 0) { + Vector3 p; + p.x = Input.mousePosition.x - rect.x; + p.y = Input.mousePosition.y - rect.y; + _CWebViewPlugin_SendKeyEvent(webView, (int)p.x, (int)p.y, keyChars, keyCode, 1); + } + } + if (texture != null) { + Matrix4x4 m = GUI.matrix; + GUI.matrix + = Matrix4x4.TRS( + new Vector3(0, Screen.height, 0), + Quaternion.identity, + new Vector3(1, -1, 1)); + Graphics.DrawTexture(rect, texture); + GUI.matrix = m; + } + break; + } + } +#endif +} diff --git a/Assets/AirConsole/unity-webview/Runtime/WebViewObject.cs.meta b/Assets/AirConsole/unity-webview/Runtime/WebViewObject.cs.meta new file mode 100644 index 00000000..ed5382a6 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Runtime/WebViewObject.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 565449e947b864de19939762a9843936 +timeCreated: 1535029316 +licenseType: Pro +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Runtime/Webview.Runtime.asmdef b/Assets/AirConsole/unity-webview/Runtime/Webview.Runtime.asmdef new file mode 100644 index 00000000..29005c84 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Runtime/Webview.Runtime.asmdef @@ -0,0 +1,20 @@ +{ + "name": "com.airconsole.unity-webview.runtime", + "rootNamespace": "", + "references": [ + "UnityEngine.UI" + ], + "includePlatforms": [ + "Android", + "Editor", + "WebGL" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/AirConsole/unity-webview/Runtime/Webview.Runtime.asmdef.meta b/Assets/AirConsole/unity-webview/Runtime/Webview.Runtime.asmdef.meta new file mode 100644 index 00000000..969c67f0 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Runtime/Webview.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 95ab967bd04e34e8e9a6376b44fd5669 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json index d54de470..01a6bc9b 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,10 +1,8 @@ { "dependencies": { - "com.airconsole.unity-webview": "https://github.com/airconsole/airconsole-unity-webview.git?path=/dist/package-nofragment#v1.1.5", "com.unity.ext.nunit": "2.0.5", - "com.unity.ide.rider": "3.0.36", + "com.unity.ide.rider": "3.0.38", "com.unity.ide.vscode": "1.2.5", - "com.unity.mobile.android-logcat": "1.4.5", "com.unity.test-framework": "1.4.4", "com.unity.textmeshpro": "3.0.9", "com.unity.ugui": "2.0.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 97bdb1f5..883d5fe3 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,12 +1,5 @@ { "dependencies": { - "com.airconsole.unity-webview": { - "version": "https://github.com/airconsole/airconsole-unity-webview.git?path=/dist/package-nofragment#v1.1.5", - "depth": 0, - "source": "git", - "dependencies": {}, - "hash": "2879295c80ba9dba67bd271482ba91aafde27c03" - }, "com.unity.ext.nunit": { "version": "2.0.5", "depth": 0, @@ -15,7 +8,7 @@ "url": "https://packages.unity.com" }, "com.unity.ide.rider": { - "version": "3.0.36", + "version": "3.0.38", "depth": 0, "source": "registry", "dependencies": { @@ -30,13 +23,6 @@ "dependencies": {}, "url": "https://packages.unity.com" }, - "com.unity.mobile.android-logcat": { - "version": "1.4.5", - "depth": 0, - "source": "registry", - "dependencies": {}, - "url": "https://packages.unity.com" - }, "com.unity.test-framework": { "version": "1.4.4", "depth": 0, From f4a3303bfd77a217ca05be66a4c51e505fcdd7e9 Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Sun, 12 Oct 2025 20:48:02 +0200 Subject: [PATCH 06/11] . R: Improve web socket lifecycle management during reset --- .../AirConsole/scripts/Runtime/AirConsole.cs | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/Assets/AirConsole/scripts/Runtime/AirConsole.cs b/Assets/AirConsole/scripts/Runtime/AirConsole.cs index c4a40337..241800d2 100644 --- a/Assets/AirConsole/scripts/Runtime/AirConsole.cs +++ b/Assets/AirConsole/scripts/Runtime/AirConsole.cs @@ -1117,7 +1117,6 @@ protected void Awake() { if (IsAndroidRuntime) { AirConsoleLogger.Log(() => $"Launching build {Application.version} in Unity v{Application.unityVersion}"); - defaultScreenHeight = Screen.height; _pluginManager = new PluginManager(this); } } @@ -1561,10 +1560,6 @@ private void ResetCaches() { } private void UnsubscribeWebSocketEvents() { - if (wsListener == null) { - return; - } - // Unsubscribe all event handlers to prevent stale events wsListener.OnSetSafeArea -= OnSetSafeArea; wsListener.onReady -= OnReady; @@ -1598,12 +1593,14 @@ private void UnsubscribeWebSocketEvents() { private void CleanupWebSocketListener() { AirConsoleLogger.LogDevelopment(() => "Cleaning up WebSocket listener"); - // Unsubscribe all event handlers to prevent stale events - UnsubscribeWebSocketEvents(); + if (wsListener != null) { + // Unsubscribe all event handlers to prevent stale events + UnsubscribeWebSocketEvents(); + wsListener = null; + } // Stop websocket server if in editor StopWebsocketServer(); - wsListener = null; AirConsoleLogger.LogDevelopment(() => "WebSocket listener cleanup complete"); } @@ -1629,15 +1626,22 @@ private void RecreateWebView() { // Cleanup websocket listener first to prevent stale events CleanupWebSocketListener(); + // Reset webview manager + _webViewManager = null; + // Destroy the old webview - if (webViewObject != null) { + if (webViewObject) { + if (_pluginManager != null && _reloadWebviewHandler != null) { + _pluginManager.OnReloadWebview -= _reloadWebviewHandler; + _reloadWebviewHandler = null; + } + + webViewObject.Destroy(); + Destroy(webViewObject.gameObject); webViewObject = null; } - // Reset webview manager - _webViewManager = null; - // Recreate the webview with stored connection URL CreateAndroidWebview(_webViewConnectionUrl); } @@ -1877,6 +1881,7 @@ internal static bool IsAndroidOrEditor { private int defaultScreenHeight; private List fixedCanvasScalers = new(); + private Action _reloadWebviewHandler; private PluginManager _pluginManager; private List _devices = new(); @@ -2106,6 +2111,7 @@ private void CreateAndroidWebview(string connectionUrl) { url += "&game-version=" + androidGameVersion; url += "&unity-version=" + Application.unityVersion; + defaultScreenHeight = Screen.height; _webViewOriginalUrl = url; _webViewManager = new WebViewManager(webViewObject, defaultScreenHeight); @@ -2113,13 +2119,16 @@ private void CreateAndroidWebview(string connectionUrl) { AirConsoleLogger.LogDevelopment(() => $"Initial URL: {url}"); webViewObject.LoadURL(url); - if (IsAndroidRuntime && _pluginManager != null) { - _pluginManager.OnReloadWebview += () => webViewObject.LoadURL(url); - _pluginManager.InitializeOfflineCheck(); - } + if (IsAndroidRuntime) { + if (_pluginManager != null) { + _reloadWebviewHandler = () => webViewObject.LoadURL(url); + _pluginManager.OnReloadWebview += _reloadWebviewHandler; + _pluginManager.InitializeOfflineCheck(); + } - bool isWebviewDebuggable = AndroidIntentUtils.GetIntentExtraBool("webview_debuggable", false); - webViewObject.EnableWebviewDebugging(isWebviewDebuggable); + bool isWebviewDebuggable = AndroidIntentUtils.GetIntentExtraBool("webview_debuggable", false); + webViewObject.EnableWebviewDebugging(isWebviewDebuggable); + } _logPlatformMessages = AndroidIntentUtils.GetIntentExtraBool("log_platform_messages", false); InitWebSockets(); From 199c7d2d601030d74bfb72779e16ba8e86361472 Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Sun, 12 Oct 2025 21:32:06 +0200 Subject: [PATCH 07/11] . r: Add native game lifecycle logging to basic logic example --- .../examples/basic/ExampleBasicLogic.cs | 3 +- Assets/AudioPlayer.cs | 38 +++++++++++++++++++ Assets/AudioPlayer.cs.meta | 11 ++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 Assets/AudioPlayer.cs create mode 100644 Assets/AudioPlayer.cs.meta diff --git a/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs b/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs index e74e56a6..460bc60d 100644 --- a/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs +++ b/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs @@ -38,6 +38,7 @@ private void Awake() { } private void OnReady(string code) { + AirConsoleLogger.Log(() => "OnReady: " + code); //Log to on-screen Console logWindow.text = "ExampleBasic: AirConsole is ready! \n \n"; @@ -114,7 +115,7 @@ private void OnAdComplete(bool adWasShown) { } private void OnGameEnd() { - Debug.Log("OnGameEnd is called"); + AirConsoleLogger.Log(() => "OnGameEnd is called"); Camera.main.enabled = false; Time.timeScale = 0.0f; } diff --git a/Assets/AudioPlayer.cs b/Assets/AudioPlayer.cs new file mode 100644 index 00000000..499a9672 --- /dev/null +++ b/Assets/AudioPlayer.cs @@ -0,0 +1,38 @@ +using NDream.AirConsole; +using UnityEngine; + +public class AudioPlayer : MonoBehaviour { + private AudioSource _audioSource; + + private void Awake() { + _audioSource = GetComponent(); + + // AirConsole.instance.onPause += () => _audioSource.Pause(); + // AirConsole.instance.onResume += () => _audioSource.UnPause(); + AirConsole.instance.onReady += HandleOnReady; + + AirConsole.instance.OnMaximumVolumeChanged += HandleAudioVolume; + } + + private void OnDestroy() { + if (AirConsole.instance) { + AirConsole.instance.OnMaximumVolumeChanged -= HandleAudioVolume; + } + } + + private void HandleOnReady(string code) { + AirConsoleLogger.Log(() => $"OnReady for {code}"); + _audioSource.Play(); + } + + private void HandleAudioVolume(float volume) { + AirConsoleLogger.Log(() => $"Setting volume to {volume}"); + if (volume > 0) { + AudioListener.pause = false; + AudioListener.volume = volume; + _audioSource.Play(); + } else if (Mathf.Approximately(0, volume)) { + AudioListener.pause = true; + } + } +} diff --git a/Assets/AudioPlayer.cs.meta b/Assets/AudioPlayer.cs.meta new file mode 100644 index 00000000..a2241e10 --- /dev/null +++ b/Assets/AudioPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d33481c3f96324d4bba3e5db8826fd6e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From f177de3e66b1d9e4e937e1f6d47860d996b3ccf4 Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Sun, 12 Oct 2025 21:38:53 +0200 Subject: [PATCH 08/11] d: Update changelog --- CHANGELOG.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4f841f..9b3bcb51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,25 +6,27 @@ Release notes follow the [keep a changelog](https://keepachangelog.com/en/1.1.0/ ## [Unreleased] +With version 2.6.2, we are targeting Android TV and Android Automotive related issues impacting or blocking game releases. +This includes security related updates like requiring fixed Unity versions and increasing Android Target SDK version. + ### Fixed -- Android: Platform overlay resizes correctly on Android TV -- Editor: No longer update index.html directly for API usage. The index.html should no longer be cleared out during Application Domain reloads. +- **Android:** Platform overlay resizes correctly on Android TV +- **Editor:** Project configuration checks no update index.html directly when validating API version usage. This prevents the index.html from becoming empty. ### Changed -- Android Target SDK: Increased to 35 to meet Google Play requirements per Nov 1, 2025. -- Minimum Versions: The Unity minimum versions have been updated to match `CVE-2025-59489` fix versions. -- Android: After the last device disconnects, the webview is reset along the game state. +- **Android Target SDK:** Increased to 35 to meet Google Play requirements per Nov 1, 2025. +- **Unity Minimum Versions:** The Unity minimum versions have been updated to match `CVE-2025-59489` fix versions. ### Added -- Unity Editor: Update to minimum versions to match `CVE-2025-59489` fix versions. -- **Webview Reset**: Added functionality to reset the webview, allowing users to clear its state and reload content as needed. +- **Unity API:** `OnMaximumVolumeChanged` event to notify when the games maximum volume must be changed. +- **Android:** After the last device disconnects, the webview is reset along the game state. ### Removed -- **Android**: The android library no longer manages AudioFocus or overriding the usage from USAGE_GAME. +- **Android**: The android library no longer manages Audio Focus or overriding the usage from USAGE_GAME. ## [2.6.1] - 2025-09-02 From eebfcabd54de909f485c86e477aa64b6d583c588 Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Sun, 12 Oct 2025 22:30:24 +0200 Subject: [PATCH 09/11] . r: Improve ExampleBasicLogic.cs for repeated native lifecycles --- Assets/AirConsole/examples/basic/ExampleBasicLogic.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs b/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs index 460bc60d..452166d3 100644 --- a/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs +++ b/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs @@ -42,6 +42,8 @@ private void OnReady(string code) { //Log to on-screen Console logWindow.text = "ExampleBasic: AirConsole is ready! \n \n"; + Time.timeScale = 1.0f; + //Mark Buttons as Interactable as soon as AirConsole is ready Button[] allButtons = (Button[])FindObjectsOfType(typeof(Button)); foreach (Button button in allButtons) { @@ -116,7 +118,6 @@ private void OnAdComplete(bool adWasShown) { private void OnGameEnd() { AirConsoleLogger.Log(() => "OnGameEnd is called"); - Camera.main.enabled = false; Time.timeScale = 0.0f; } From 92252e1b6e2a7c625febe581404c09c36173b457 Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Sun, 12 Oct 2025 22:33:53 +0200 Subject: [PATCH 10/11] . R: Improve task queuing with cache reset This avoids the situation where RecreateWebView gets executed out of sheer luck because it is in the currently running task, despite that task being cleared from the queue in ResetCaches. The previous implementation would have been hard to maintain without a lot of documentation. --- Assets/AirConsole/scripts/Runtime/AirConsole.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Assets/AirConsole/scripts/Runtime/AirConsole.cs b/Assets/AirConsole/scripts/Runtime/AirConsole.cs index 241800d2..3cf459b4 100644 --- a/Assets/AirConsole/scripts/Runtime/AirConsole.cs +++ b/Assets/AirConsole/scripts/Runtime/AirConsole.cs @@ -1535,7 +1535,11 @@ private void OnAdComplete(JObject msg) { } } - private void ResetCaches() { + /// + /// Resets the caches. + /// + /// Optional task to queue after the eventQueue gets cleared. + private void ResetCaches(Action taskToQueueAfterClear) { AirConsoleLogger.LogDevelopment(() => "Resetting AirConsole caches"); // Clear device and player data @@ -1555,6 +1559,9 @@ private void ResetCaches() { // Clear event queue eventQueue.Clear(); + if (taskToQueueAfterClear != null) { + eventQueue.Enqueue(taskToQueueAfterClear); + } AirConsoleLogger.LogDevelopment(() => "AirConsole caches reset complete"); } @@ -1663,9 +1670,10 @@ private void OnGameEnd(JObject msg) { } // Reset all caches and recreate webview on the main thread - eventQueue.Enqueue(delegate() { - ResetCaches(); - RecreateWebView(); + eventQueue.Enqueue(delegate { + // We want to chain RecreateWebView to ensure it happens independent of + // the eventQueue getting cleared and related side effects. + ResetCaches(RecreateWebView); }); } catch (Exception e) { if (Settings.debug.error) { From af2ede1a0e0fa81f6210cca8fc94ab6ef14fd23b Mon Sep 17 00:00:00 2001 From: marc-n-dream Date: Sun, 12 Oct 2025 23:43:31 +0200 Subject: [PATCH 11/11] . r: Update Basic Example to include Audio Player --- .../AirConsole/examples/basic/AudioPlayer.cs | 63 ++++++++ .../examples/basic}/AudioPlayer.cs.meta | 0 Assets/AirConsole/examples/basic/basic.unity | 142 ++++++++++++++++++ Assets/AudioPlayer.cs | 38 ----- 4 files changed, 205 insertions(+), 38 deletions(-) create mode 100644 Assets/AirConsole/examples/basic/AudioPlayer.cs rename Assets/{ => AirConsole/examples/basic}/AudioPlayer.cs.meta (100%) delete mode 100644 Assets/AudioPlayer.cs diff --git a/Assets/AirConsole/examples/basic/AudioPlayer.cs b/Assets/AirConsole/examples/basic/AudioPlayer.cs new file mode 100644 index 00000000..f4170267 --- /dev/null +++ b/Assets/AirConsole/examples/basic/AudioPlayer.cs @@ -0,0 +1,63 @@ +#if !DISABLE_AIRCONSOLE +namespace NDream.AirConsole.Examples { + using NDream.AirConsole; + using UnityEngine; + + /// + /// Example Audio Player that starts playing when the game is ready and stops when the game ends. + /// It also listens to volume changes and adjusts the audio accordingly. + /// + [RequireComponent(typeof(AudioSource))] + public class AudioPlayer : MonoBehaviour { + private AudioSource _audioSource; + + private void Awake() { + _audioSource = GetComponent(); + if (!_audioSource) { + AirConsoleLogger.LogError(() => "AudioPlayer requires an AudioSource component.", this); + enabled = false; + return; + } + + SetupAudioSource(); + AirConsole.instance.onReady += HandleOnReady; + AirConsole.instance.onGameEnd += HandleOnGameEnd; + + // Until OnReady is called, we don't want any audio from Unity playing as the Player Lobby overlay will be shown. + AudioListener.pause = true; + } + + private void SetupAudioSource() { + _audioSource.playOnAwake = false; + _audioSource.loop = true; + _audioSource.volume = 1.0f; + if (!_audioSource.clip) { + _audioSource.clip = Resources.Load("Audio/Music/Happy_1"); + } + } + + private void HandleOnGameEnd() { + // After OnGameEnd is called, we must not play any audio until OnReady is called again. During this time the Player Lobby + // overlay will be shown. + AudioListener.pause = true; + } + + private void HandleOnReady(string code) { + AirConsoleLogger.Log(() => $"OnReady for {code}"); + AudioListener.pause = false; + _audioSource.Play(); + } + + private void HandleAudioVolumeChange(float volume) { + AirConsoleLogger.Log(() => $"Setting volume to {volume}"); + if (volume > 0) { + AudioListener.pause = false; + AudioListener.volume = volume; + _audioSource.Play(); + } else if (Mathf.Approximately(0, volume)) { + AudioListener.pause = true; + } + } + } +} +#endif diff --git a/Assets/AudioPlayer.cs.meta b/Assets/AirConsole/examples/basic/AudioPlayer.cs.meta similarity index 100% rename from Assets/AudioPlayer.cs.meta rename to Assets/AirConsole/examples/basic/AudioPlayer.cs.meta diff --git a/Assets/AirConsole/examples/basic/basic.unity b/Assets/AirConsole/examples/basic/basic.unity index aff15d10..5a6e07ed 100644 --- a/Assets/AirConsole/examples/basic/basic.unity +++ b/Assets/AirConsole/examples/basic/basic.unity @@ -4467,6 +4467,147 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1064712596} m_CullTransparentMesh: 0 +--- !u!1 &1070081529 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1070081532} + - component: {fileID: 1070081531} + - component: {fileID: 1070081530} + m_Layer: 0 + m_Name: AudioPlayer + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1070081530 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1070081529} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d33481c3f96324d4bba3e5db8826fd6e, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!82 &1070081531 +AudioSource: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1070081529} + m_Enabled: 1 + serializedVersion: 4 + OutputAudioMixerGroup: {fileID: 0} + m_audioClip: {fileID: 8300000, guid: 804632e1e46684476b4ea5f752e8df15, type: 3} + m_PlayOnAwake: 0 + m_Volume: 1 + m_Pitch: 1 + Loop: 1 + Mute: 0 + Spatialize: 0 + SpatializePostEffects: 0 + Priority: 128 + DopplerLevel: 1 + MinDistance: 1 + MaxDistance: 500 + Pan2D: 0 + rolloffMode: 0 + BypassEffects: 0 + BypassListenerEffects: 0 + BypassReverbZones: 0 + rolloffCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + panLevelCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + spreadCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + reverbZoneMixCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 +--- !u!4 &1070081532 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1070081529} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0.36577117, y: -0.17775297, z: 0.030780792} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1087830224 GameObject: m_ObjectHideFlags: 0 @@ -7557,3 +7698,4 @@ SceneRoots: - {fileID: 607279077} - {fileID: 1179297379} - {fileID: 535473225} + - {fileID: 1070081532} diff --git a/Assets/AudioPlayer.cs b/Assets/AudioPlayer.cs deleted file mode 100644 index 499a9672..00000000 --- a/Assets/AudioPlayer.cs +++ /dev/null @@ -1,38 +0,0 @@ -using NDream.AirConsole; -using UnityEngine; - -public class AudioPlayer : MonoBehaviour { - private AudioSource _audioSource; - - private void Awake() { - _audioSource = GetComponent(); - - // AirConsole.instance.onPause += () => _audioSource.Pause(); - // AirConsole.instance.onResume += () => _audioSource.UnPause(); - AirConsole.instance.onReady += HandleOnReady; - - AirConsole.instance.OnMaximumVolumeChanged += HandleAudioVolume; - } - - private void OnDestroy() { - if (AirConsole.instance) { - AirConsole.instance.OnMaximumVolumeChanged -= HandleAudioVolume; - } - } - - private void HandleOnReady(string code) { - AirConsoleLogger.Log(() => $"OnReady for {code}"); - _audioSource.Play(); - } - - private void HandleAudioVolume(float volume) { - AirConsoleLogger.Log(() => $"Setting volume to {volume}"); - if (volume > 0) { - AudioListener.pause = false; - AudioListener.volume = volume; - _audioSource.Play(); - } else if (Mathf.Approximately(0, volume)) { - AudioListener.pause = true; - } - } -}