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/AirConsole/examples/basic/AudioPlayer.cs.meta b/Assets/AirConsole/examples/basic/AudioPlayer.cs.meta new file mode 100644 index 00000000..a2241e10 --- /dev/null +++ b/Assets/AirConsole/examples/basic/AudioPlayer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d33481c3f96324d4bba3e5db8826fd6e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs b/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs index e74e56a6..452166d3 100644 --- a/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs +++ b/Assets/AirConsole/examples/basic/ExampleBasicLogic.cs @@ -38,9 +38,12 @@ 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"; + 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) { @@ -114,8 +117,7 @@ private void OnAdComplete(bool adWasShown) { } private void OnGameEnd() { - Debug.Log("OnGameEnd is called"); - Camera.main.enabled = false; + AirConsoleLogger.Log(() => "OnGameEnd is called"); Time.timeScale = 0.0f; } 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/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/scripts/Runtime/AirConsole.cs b/Assets/AirConsole/scripts/Runtime/AirConsole.cs index ad73084a..3cf459b4 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 /// @@ -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); } } @@ -1255,7 +1254,7 @@ protected void Update() { } } } - + private void ProcessEvents() { // dispatch event queue on main unity thread while (eventQueue.Count > 0) { @@ -1302,25 +1301,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; - } - if (onDeviceStateChange != null) { - eventQueue.Enqueue(delegate() { - if (onDeviceStateChange != null) { - onDeviceStateChange(deviceId, GetDevice(_device_id)); - } - }); - } + // 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 (Settings.debug.info) { - AirConsoleLogger.Log(() => $"AirConsole: saved devicestate of {deviceId}"); - } + if (onDeviceStateChange != null) { + onDeviceStateChange(deviceId, GetDevice(_device_id)); + } + + if (Settings.debug.info) { + AirConsoleLogger.Log(() => $"AirConsole: saved devicestate of {deviceId}"); + } + }); } catch (Exception e) { if (Settings.debug.error) { AirConsoleLogger.LogError(() => e.Message); @@ -1417,52 +1416,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 +1535,124 @@ private void OnAdComplete(JObject msg) { } } + /// + /// 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 + _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(); + if (taskToQueueAfterClear != null) { + eventQueue.Enqueue(taskToQueueAfterClear); + } + + AirConsoleLogger.LogDevelopment(() => "AirConsole caches reset complete"); + } + + private void UnsubscribeWebSocketEvents() { + // Unsubscribe all event handlers to prevent stale events + 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; + } + } + + private void CleanupWebSocketListener() { + AirConsoleLogger.LogDevelopment(() => "Cleaning up WebSocket listener"); + + if (wsListener != null) { + // Unsubscribe all event handlers to prevent stale events + UnsubscribeWebSocketEvents(); + wsListener = null; + } + + // Stop websocket server if in editor + StopWebsocketServer(); + + AirConsoleLogger.LogDevelopment(() => "WebSocket listener cleanup complete"); + } + + private void RecreateWebView() { + if (string.IsNullOrEmpty(_webViewOriginalUrl) || string.IsNullOrEmpty(_webViewConnectionUrl)) { + 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; + } + + AirConsoleLogger.LogDevelopment(() => $"Recreating webview with URL: {_webViewOriginalUrl}"); + + // Cleanup websocket listener first to prevent stale events + CleanupWebSocketListener(); + + // Reset webview manager + _webViewManager = null; + + // Destroy the old webview + if (webViewObject) { + if (_pluginManager != null && _reloadWebviewHandler != null) { + _pluginManager.OnReloadWebview -= _reloadWebviewHandler; + _reloadWebviewHandler = null; + } + + webViewObject.Destroy(); + + Destroy(webViewObject.gameObject); + webViewObject = null; + } + + // Recreate the webview with stored connection URL + CreateAndroidWebview(_webViewConnectionUrl); + } + private void OnGameEnd(JObject msg) { _webViewManager.RequestStateTransition(WebViewManager.WebViewState.FullScreen); @@ -1552,6 +1668,13 @@ private void OnGameEnd(JObject msg) { if (Settings.debug.info) { AirConsoleLogger.Log(() => "AirConsole: onGameEnd"); } + + // Reset all caches and recreate webview on the main thread + 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) { AirConsoleLogger.LogError(() => e.Message); @@ -1766,6 +1889,7 @@ internal static bool IsAndroidOrEditor { private int defaultScreenHeight; private List fixedCanvasScalers = new(); + private Action _reloadWebviewHandler; private PluginManager _pluginManager; private List _devices = new(); @@ -1779,6 +1903,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 +1915,18 @@ private void StopWebsocketServer() { return; } + // Unregister event handlers before stopping to prevent race conditions + UnsubscribeWebSocketEvents(); + 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) { @@ -1928,7 +2060,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); @@ -1953,6 +2085,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,23 +2095,20 @@ 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(); + 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; } -#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 url += connectionUrl; if (IsAndroidRuntime) { @@ -1988,25 +2119,30 @@ private void CreateAndroidWebview(string connectionUrl) { url += "&game-version=" + androidGameVersion; url += "&unity-version=" + Application.unityVersion; + defaultScreenHeight = Screen.height; + _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(); - } - bool isWebviewDebuggable = AndroidIntentUtils.GetIntentExtraBool("webview_debuggable", false); - webViewObject.EnableWebviewDebugging(isWebviewDebuggable); + if (IsAndroidRuntime) { + if (_pluginManager != null) { + _reloadWebviewHandler = () => webViewObject.LoadURL(url); + _pluginManager.OnReloadWebview += _reloadWebviewHandler; + _pluginManager.InitializeOfflineCheck(); + } + + bool isWebviewDebuggable = AndroidIntentUtils.GetIntentExtraBool("webview_debuggable", false); + webViewObject.EnableWebviewDebugging(isWebviewDebuggable); + } _logPlatformMessages = AndroidIntentUtils.GetIntentExtraBool("log_platform_messages", false); InitWebSockets(); } } - + private static int GetAndroidBundleVersionCode() { AndroidJavaObject ca = UnityAndroidObjectProvider.GetUnityActivity(); AndroidJavaObject packageManager = ca.Call("getPackageManager"); @@ -2021,38 +2157,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/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 00000000..23bdad7d Binary files /dev/null and b/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-development.aar.tmpl differ diff --git a/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-development.aar.tmpl.meta b/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-development.aar.tmpl.meta new file mode 100644 index 00000000..32a588c1 --- /dev/null +++ b/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-development.aar.tmpl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4cfd7b6baeab943089b567032c2b3ee6 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-release.aar.tmpl b/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-release.aar.tmpl new file mode 100644 index 00000000..f3181c27 Binary files /dev/null and b/Assets/AirConsole/unity-webview/Plugins/Android/WebViewPlugin-release.aar.tmpl differ 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 00000000..19f2a22e Binary files /dev/null and b/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/MacOS/WebView differ diff --git a/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/Resources/InfoPlist.strings b/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/Resources/InfoPlist.strings new file mode 100644 index 00000000..5e45963c Binary files /dev/null and b/Assets/AirConsole/unity-webview/Plugins/WebView.bundle/Contents/Resources/InfoPlist.strings differ 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/CHANGELOG.md b/CHANGELOG.md index 38238c52..9b3bcb51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,19 +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 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 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 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,