/**
* Copyright (c) 2021 Vuplex Inc. All rights reserved.
*
* Licensed under the Vuplex Commercial Software Library License, you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* https://vuplex.com/commercial-library-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#if UNITY_ANDROID && !UNITY_EDITOR
#pragma warning disable CS0108
#pragma warning disable CS0067
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.Rendering;
namespace Vuplex.WebView {
///
/// The IWebView implementation used by 3D WebView for Android.
/// This class also includes extra methods for Android-specific functionality.
///
public class AndroidWebView : BaseWebView,
IWebView,
IWithMovablePointer,
IWithPointerDownAndUp,
IWithPopups {
public WebPluginType PluginType {
get {
return WebPluginType.Android;
}
}
///
public event EventHandler PopupRequested;
///
/// Indicates that the browser's render process terminated, either because it
/// crashed or because the operating system killed it.
///
///
/// 3D WebView for Android internally uses the `android.webkit.WebView` system
/// package as its browser engine. Android's documentation indicates that
/// the browser's render process can terminate in some rare circumstances.
/// This RenderProcessGone event indicates when that occurs so that the application
/// can recover be destroying the existing webviews and creating new webviews.
///
/// Sources:
/// - [`android.webkit.WebViewClient.onRenderProcessGone()`](https://developer.android.com/reference/android/webkit/WebViewClient#onRenderProcessGone(android.webkit.WebView,%20android.webkit.RenderProcessGoneDetail))
/// - [Termination Handling API (Android docs)](https://developer.android.com/guide/webapps/managing-webview#termination-handle)
///
public event EventHandler RenderProcessGone;
[Obsolete("The ScriptAlert event has been renamed to ScriptAlerted. Please use ScriptAlerted instead.", true)]
public event EventHandler ScriptAlert;
///
/// Event raised when a script in the page calls `window.alert()`.
///
///
/// If no handler is attached to this event, then `window.alert()` will return
/// immediately and the script will continue execution. If a handler is attached to
/// this event, then script execution will be paused until `ScriptDialogEventArgs.Continue()`
/// is called.
///
public event EventHandler ScriptAlerted {
add {
if (_scriptAlertHandler != null) {
throw new InvalidOperationException("ScriptAlerted supports only one event handler. Please remove the existing handler before adding a new one.");
}
_scriptAlertHandler = value;
_webView.Call("setScriptAlertHandler", new AndroidStringAndBoolDelegateCallback(_handleScriptAlert));
}
remove {
if (_scriptAlertHandler == value) {
_scriptAlertHandler = null;
_webView.Call("setScriptAlertHandler", null);
}
}
}
///
/// Event raised when a script in the page calls `window.confirm()`.
///
///
/// If no handler is attached to this event, then `window.confirm()` will return
/// `false` immediately and the script will continue execution. If a handler is attached to
/// this event, then script execution will be paused until `ScriptDialogEventArgs.Continue()`
/// is called, and `window.confirm()` will return the value passed to `Continue()`.
///
public event EventHandler> ScriptConfirmRequested {
add {
if (_scriptConfirmHandler != null) {
throw new InvalidOperationException("ScriptConfirmRequested supports only one event handler. Please remove the existing handler before adding a new one.");
}
_scriptConfirmHandler = value;
_webView.Call("setScriptConfirmHandler", new AndroidStringAndBoolDelegateCallback(_handleScriptConfirm));
}
remove {
if (_scriptConfirmHandler == value) {
_scriptConfirmHandler = null;
_webView.Call("setScriptConfirmHandler", null);
}
}
}
public static AndroidWebView Instantiate() {
return (AndroidWebView) new GameObject().AddComponent();
}
public override void Init(Texture2D viewportTexture, float width, float height, Texture2D videoTexture) {
AssertWebViewIsAvailable();
_init(viewportTexture, width, height, videoTexture, null);
}
internal static void AssertWebViewIsAvailable() {
if (!IsWebViewAvailable()) {
throw new WebViewUnavailableException("The Android WebView package is currently unavailable. This is rare but can occur if it's not installed on the system or is currently being updated.");
}
}
public override void Blur() {
_assertValidState();
_webView.Call("blur");
}
public override void CanGoBack(Action callback) {
_assertValidState();
_webView.Call("canGoBack", new AndroidBoolCallback(callback));
}
public override void CanGoForward(Action callback) {
_assertValidState();
_webView.Call("canGoForward", new AndroidBoolCallback(callback));
}
///
/// Overrides `BaseWebView.CaptureScreenshot()` because it doesn't work
/// with Android OES textures.
///
public override void CaptureScreenshot(Action callback) {
_assertValidState();
_webView.Call("captureScreenshot", new AndroidByteArrayCallback(callback));
}
public static void ClearAllData() {
_class.CallStatic("clearAllData");
}
///
/// Clears the webview's back / forward navigation history.
///
public void ClearHistory() {
_assertValidState();
_webView.Call("clearHistory");
}
public override void Click(Vector2 point) {
_assertValidState();
var nativeX = (int)(point.x * _nativeWidth);
var nativeY = (int)(point.y * _nativeHeight);
_webView.Call("click", nativeX, nativeY);
}
public override void DisableViewUpdates() {
_assertValidState();
_webView.Call("disableViewUpdates");
_viewUpdatesAreEnabled = false;
}
public override void Dispose() {
_assertValidState();
// Cancel the render if it has been scheduled via GL.IssuePluginEvent().
WebView_removePointer(_webView.GetRawObject());
IsDisposed = true;
_webView.Call("destroy");
_webView.Dispose();
Destroy(gameObject);
}
public override void EnableViewUpdates() {
_assertValidState();
_webView.Call("enableViewUpdates");
_viewUpdatesAreEnabled = true;
}
public override void ExecuteJavaScript(string javaScript, Action callback) {
_assertValidState();
var nativeCallback = callback == null ? null : new AndroidStringCallback(callback);
_webView.Call("executeJavaScript", javaScript, nativeCallback);
}
public override void Focus() {
_assertValidState();
_webView.Call("focus");
}
public static string GetGraphicsApiErrorMessage(GraphicsDeviceType graphicsDeviceType) {
var isValid = graphicsDeviceType == GraphicsDeviceType.OpenGLES3 || graphicsDeviceType == GraphicsDeviceType.OpenGLES2;
if (isValid) {
return null;
}
return String.Format("Unsupported graphics API: 3D WebView for Android requires OpenGLES3 or OpenGLES2, but the graphics API in use is {0}. Please go to Player Settings and set \"Graphics APIs\" to OpenGLES3 or OpenGLES2.", graphicsDeviceType);
}
///
/// Overrides `BaseWebView.GetRawTextureData()` because it's slow on Android.
///
public override void GetRawTextureData(Action callback) {
_assertValidState();
_webView.Call("getRawTextureData", new AndroidByteArrayCallback(callback));
}
public static void GloballySetUserAgent(bool mobile) {
_class.CallStatic("globallySetUserAgent", mobile);
}
public static void GloballySetUserAgent(string userAgent) {
_class.CallStatic("globallySetUserAgent", userAgent);
}
[Obsolete("AndroidWebView.GloballyUseAlternativeInputEventSystem() has been removed. Please use AndroidWebView.SetAlternativePointerInputSystemEnabled() and/or SetAlternativeKeyboardInputSystemEnabled() instead.", true)]
public static void GloballyUseAlternativeInputEventSystem(bool useAlternativeInputEventSystem) {}
public override void GoBack() {
_assertValidState();
_webView.Call("goBack");
}
public override void GoForward() {
_assertValidState();
_webView.Call("goForward");
}
public override void HandleKeyboardInput(string input) {
_assertValidState();
_webView.Call("handleKeyboardInput", input);
}
///
/// Indicates whether native video rendering is available for the current
/// version of Android and is enabled.
///
///
/// Native video rendering is available in Android API level 23 and above.
/// If native video rendering isn't supported (i.e. the Android version is
/// lower than 23), then the AndroidWebView plugin will use a fallback video
/// implementation to support basic video playback.
///
public static bool IsUsingNativeVideoRendering() {
return _class.CallStatic("isUsingNativeVideoRendering");
}
///
/// Indicates whether the Android WebView package is installed on the system and available.
///
///
/// 3D WebView internally depends on Android's WebView package, which is normally installed
/// as part of the operating system. In rare circumstances, the Android WebView package may be unavailable.
/// For example, this can happen if the user used developer tools to delete the WebView package
/// or if [updates to the WebView package are currently being installed](https://bugs.chromium.org/p/chromium/issues/detail?id=506369) .
///
public static bool IsWebViewAvailable() {
if (_webViewPackageIsAvailable == null) {
_webViewPackageIsAvailable = _class.CallStatic("isWebViewAvailable");
}
return (bool)_webViewPackageIsAvailable;
}
public override void LoadHtml(string html) {
_assertValidState();
_webView.Call("loadHtml", html);
}
///
/// Like `LoadHtml(string html)`, but also allows a virtual base URL
/// to be specified.
///
public void LoadHtml(string html, string baseUrl) {
_assertValidState();
_webView.Call("loadHtml", html, baseUrl);
}
public override void LoadUrl(string url) {
_assertValidState();
_webView.Call("loadUrl", _transformStreamingAssetsUrlIfNeeded(url));
}
public override void LoadUrl(string url, Dictionary additionalHttpHeaders) {
_assertValidState();
if (additionalHttpHeaders == null) {
LoadUrl(url);
} else {
var map = _convertDictionaryToJavaMap(additionalHttpHeaders);
_webView.Call("loadUrl", url, map);
}
}
///
public void MovePointer(Vector2 point) {
_assertValidState();
var nativeX = (int)(point.x * _nativeWidth);
var nativeY = (int)(point.y * _nativeHeight);
_webView.Call("movePointer", nativeX, nativeY);
}
///
/// Pauses processing, media, and rendering for this webview instance
/// until `Resume()` is called.
///
public void Pause() {
_assertValidState();
_webView.Call("pause");
}
///
/// Pauses processing, media, and rendering for all webview instances.
/// This method is automatically called by the plugin when the application
/// is paused.
///
public static void PauseAll() {
_class.CallStatic("pauseAll");
}
///
public void PointerDown(Vector2 point) {
_pointerDown(point, MouseButton.Left, 1);
}
///
public void PointerDown(Vector2 point, PointerOptions options) {
if (options == null) {
options = new PointerOptions();
}
_pointerDown(point, options.Button, options.ClickCount);
}
///
public void PointerUp(Vector2 point) {
_pointerUp(point, MouseButton.Left, 1);
}
///
public void PointerUp(Vector2 point, PointerOptions options) {
if (options == null) {
options = new PointerOptions();
}
_pointerUp(point, options.Button, options.ClickCount);
}
///
/// Loads the given URL using an HTTP POST request and the given
/// application/x-www-form-urlencoded data.
///
///
/// webView.PostUrl("https://postman-echo.com/post", Encoding.Unicode.GetBytes("foo=bar"));
///
public void PostUrl(string url, byte[] data) {
_assertValidState();
_webView.Call("postUrl", url, data);
}
public override void Reload() {
_assertValidState();
_webView.Call("reload");
}
///
/// Resumes processing and rendering for all webview instances
/// after a previous call to `Pause().`
///
public void Resume() {
_assertValidState();
_webView.Call("resume");
}
///
/// Resumes processing and rendering for all webview instances
/// after a previous call to `PauseAll().` This method
/// is automatically called by the plugin when the application resumes after
/// being paused.
///
public static void ResumeAll() {
_class.CallStatic("resumeAll");
}
public override void Scroll(Vector2 scrollDelta) {
_assertValidState();
var deltaX = (int)(scrollDelta.x * _numberOfPixelsPerUnityUnit);
var deltaY = (int)(scrollDelta.y * _numberOfPixelsPerUnityUnit);
_webView.Call("scroll", deltaX, deltaY);
}
public override void Scroll(Vector2 scrollDelta, Vector2 point) {
_assertValidState();
var deltaX = (int)(scrollDelta.x * _numberOfPixelsPerUnityUnit);
var deltaY = (int)(scrollDelta.y * _numberOfPixelsPerUnityUnit);
var pointerX = (int)(point.x * _nativeWidth);
var pointerY = (int)(point.y * _nativeHeight);
_webView.Call("scroll", deltaX, deltaY, pointerX, pointerY);
}
public static void SetAlternativeKeyboardInputSystemEnabled(bool enabled) {
_class.CallStatic("setAlternativeKeyboardInputSystemEnabled", enabled);
}
///
/// By default, 3D WebView dispatches pointer (a.k.a mouse) events to the
/// browser engine in a way that accurately mimics the functionality of
/// a desktop browser. This works great in most cases, but on some systems
/// (i.e. Oculus Quest 2), the system version of Chromium is buggy and out-of-date,
/// which can lead to issues where pointer events aren't dispatched accurately.
/// In those cases, this method can be used to enable an alternative pointer
/// input system that is less flexible but doesn't suffer from the Chromium
/// bugs. This method is called automatically by AndroidWebPlugin.cs when
/// running on Oculus Quest 2. Note that calling this method effectively disables
/// the ability to trigger hover or drag events with `MovePointer()`.
///
public static void SetAlternativePointerInputSystemEnabled(bool enabled) {
_class.CallStatic("setAlternativePointerInputSystemEnabled", enabled);
}
///
/// By default, web pages cannot access the device's
/// camera or microphone via JavaScript, even if the user has granted
/// the app permission to use them. Invoking `SetAudioAndVideoCaptureEnabled(true)` allows
/// **all web pages** to access the camera and microphone if the user has
/// granted the app permission to use them via the standard Android permission dialogs.
///
///
/// This is useful, for example, to enable WebRTC support.
/// In addition to calling this method, the application must include the following Android
/// permissions in its AndroidManifest.xml and also request the permissions at runtime.
/// - android.permission.RECORD_AUDIO
/// - android.permission.MODIFY_AUDIO_SETTINGS
/// - android.permission.CAMERA
///
public static void SetAudioAndVideoCaptureEnabled(bool enabled) {
_class.CallStatic("setAudioAndVideoCaptureEnabled", enabled);
}
public static void SetClickCorrectionEnabled(bool enabled) {
_class.CallStatic("setClickCorrectionEnabled", enabled);
}
[Obsolete("AndroidWebView.SetCustomUriSchemesEnabled() has been removed. Now when a page redirects to a URI with a custom scheme, 3D WebView will automatically emit the UrlChanged and LoadProgressChanged events for the navigation, but a deep link (i.e. to an external application) won't occur.", true)]
public static void SetCustomUriSchemesEnabled(bool enabled) {}
[Obsolete("AndroidWebView.SetForceDrawEnabled() has been removed because it is no longer needed.", true)]
public static void SetForceDrawEnabled(bool enabled) {}
///
/// By default, web pages cannot access the device's
/// geolocation via JavaScript, even if the user has granted
/// the app permission to access location. Invoking `SetGeolocationPermissionEnabled(true)` allows
/// **all web pages** to access the geolocation if the user has
/// granted the app location permissions via the standard Android permission dialogs.
///
///
/// The following Android permissions must be included in the app's AndroidManifest.xml
/// and also requested by the application at runtime:
/// - android.permission.ACCESS_COARSE_LOCATION
/// - android.permission.ACCESS_FINE_LOCATION
///
public static void SetGeolocationPermissionEnabled(bool enabled) {
_class.CallStatic("setGeolocationPermissionEnabled", enabled);
}
public static void SetIgnoreCertificateErrors(bool ignore) {
_class.CallStatic("setIgnoreCertificateErrors", ignore);
}
[Obsolete("AndroidWebView.SetIgnoreSslErrors() is now deprecated. Please use Web.SetIgnoreCertificateErrors() instead.")]
public static void SetIgnoreSslErrors(bool ignore) {
SetIgnoreCertificateErrors(ignore);
}
///
/// Sets the initial scale for web content, where 1.0 is the default scale.
///
public void SetInitialScale(float scale) {
_assertValidState();
_webView.Call("setInitialScale", scale);
}
///
/// By default, AndroidWebView prevents JavaScript from auto-playing sound
/// from most sites unless the user has first interacted with the page.
/// You can call this method to disable or re-enable enforcement of this auto-play policy.
///
public void SetMediaPlaybackRequiresUserGesture(bool mediaPlaybackRequiresUserGesture) {
_assertValidState();
_webView.Call("setMediaPlaybackRequiresUserGesture", mediaPlaybackRequiresUserGesture);
}
[Obsolete("AndroidWebView.SetNativeKeyboardEnabled() is now deprecated. Please use Web.SetTouchScreenKeyboardEnabled() instead.")]
public static void SetNativeKeyboardEnabled(bool enabled) {
SetTouchScreenKeyboardEnabled(enabled);
}
///
/// Enables or disables native video rendering on versions of Android
/// that support native video rendering.
///
///
/// The default is enabled. If disabled, then the `AndroidWebView`
/// plugin will use a fallback video implementation to support basic
/// video playback. This method is automatically called when the
/// Oculus VR SDK is enabled, because the Oculus Go and Quest
/// headsets don't support native video rendering.
///
public static void SetNativeVideoRenderingEnabled(bool enabled) {
_class.CallStatic("setNativeVideoRenderingEnabled", enabled);
}
///
public void SetPopupMode(PopupMode popupMode) {
_assertValidState();
_webView.Call("setPopupMode", (int)popupMode);
}
public static void SetStorageEnabled(bool enabled) {
_class.CallStatic("setStorageEnabled", enabled);
}
///
/// Sets the `android.view.Surface` to which the webview renders.
/// This can be used, for example, to render to an Oculus
/// [OVROverlay](https://developer.oculus.com/reference/unity/1.30/class_o_v_r_overlay).
/// After this method is called, the webview no longer renders
/// to its original texture and instead renders to the given surface.
///
///
/// var surface = ovrOverlay.externalSurfaceObject();
/// // Set the resolution to 1 px / Unity unit
/// // to make it easy to specify the size in pixels.
/// webView.SetResolution(1);
/// // Or if the webview is attached to a prefab, call WebViewPrefab.Resize()
/// webView.WebView.Resize(surface.externalSurfaceWidth(), surface.externalSurfaceHeight());
/// #if UNITY_ANDROID && !UNITY_EDITOR
/// (webView as AndroidWebView).SetSurface(surface);
/// #endif
///
public void SetSurface(IntPtr surface) {
_assertValidState();
var surfaceObject = _convertIntPtrToAndroidJavaObject(surface);
_webView.Call("setSurface", surfaceObject);
}
public static void SetTouchScreenKeyboardEnabled(bool enabled) {
_class.CallStatic("setTouchScreenKeyboardEnabled", enabled);
}
///
/// Like `Web.SetUserAgent(bool mobile)`, except it sets the user-agent
/// for a single webview instance instead of setting it globally.
///
///
/// If you globally set a default user-agent using `Web.SetUserAgent()`,
/// you can still use this method to override the user-agent for a
/// single webview instance.
///
public void SetUserAgent(bool mobile) {
_assertValidState();
_webView.Call("setUserAgent", mobile);
}
///
/// Like `Web.SetUserAgent(string userAgent)`, except it sets the user-agent
/// for a single webview instance instead of setting it globally.
///
///
/// If you globally set a default user-agent using `Web.SetUserAgent()`,
/// you can still use this method to override the user-agent for a
/// single webview instance.
///
public void SetUserAgent(string userAgent) {
_assertValidState();
_webView.Call("setUserAgent", userAgent);
}
[Obsolete("AndroidWebView.UseAlternativeInputEventSystem() has been removed. Please use AndroidWebView.SetAlternativePointerInputSystemEnabled() and/or SetAlternativeKeyboardInputSystemEnabled() instead.", true)]
public void UseAlternativeInputEventSystem(bool useAlternativeInputEventSystem) {}
///
/// Zooms in or out by the given factor, which is multiplied by the current zoom level
/// to reach the new zoom level.
///
///
/// Note that the zoom level gets reset when a new page is loaded.
///
///
/// The zoom factor to apply in the range from 0.01 to 100.0.
///
public void ZoomBy(float zoomFactor) {
_assertValidState();
_webView.Call("zoomBy", zoomFactor);
}
public override void ZoomIn() {
_assertValidState();
_webView.Call("zoomIn");
}
public override void ZoomOut() {
_assertValidState();
_webView.Call("zoomOut");
}
// Get a reference to AndroidJavaObject's hidden constructor that takes
// the IntPtr for a jobject as a parameter.
readonly static ConstructorInfo _androidJavaObjectIntPtrConstructor = typeof(AndroidJavaObject).GetConstructor(
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new []{ typeof(IntPtr) },
null
);
internal static AndroidJavaClass _class = new AndroidJavaClass(FULL_CLASS_NAME);
const string FULL_CLASS_NAME = "com.vuplex.webview.WebView";
EventHandler _scriptAlertHandler;
EventHandler> _scriptConfirmHandler;
readonly WaitForEndOfFrame _waitForEndOfFrame = new WaitForEndOfFrame();
internal AndroidJavaObject _webView;
static bool? _webViewPackageIsAvailable = null;
AndroidJavaObject _convertDictionaryToJavaMap(Dictionary dictionary) {
AndroidJavaObject map = new AndroidJavaObject("java.util.HashMap");
IntPtr putMethod = AndroidJNIHelper.GetMethodID(map.GetRawClass(), "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
foreach (var entry in dictionary) {
AndroidJNI.CallObjectMethod(
map.GetRawObject(),
putMethod,
AndroidJNIHelper.CreateJNIArgArray(new object[] { entry.Key, entry.Value })
);
}
return map;
}
static AndroidJavaObject _convertIntPtrToAndroidJavaObject(IntPtr jobject) {
if (jobject == IntPtr.Zero) {
return null;
}
return (AndroidJavaObject) _androidJavaObjectIntPtrConstructor.Invoke(new object[] { jobject });
}
///
/// The native plugin invokes this method.
///
protected virtual void HandleInitialVideoPlayRequest(string serializedVideo) {
_assertValidState();
var video = JsonUtility.FromJson