diff --git a/Editor.meta b/Editor.meta
new file mode 100644
index 0000000..088d0ea
--- /dev/null
+++ b/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 26c29dd5c4e5b44da83fe6d7f7501b48
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/WebViewBuildPostprocessor.cs b/Editor/WebViewBuildPostprocessor.cs
new file mode 100644
index 0000000..eb16960
--- /dev/null
+++ b/Editor/WebViewBuildPostprocessor.cs
@@ -0,0 +1,239 @@
+#if UNITY_EDITOR
+using System.IO;
+using System.Xml;
+using UnityEditor;
+using System.Text;
+using UnityEditor.Android;
+using UnityEditor.Callbacks;
+
+#if UNITY_IOS
+using UnityEditor.iOS.Xcode;
+#endif
+
+namespace ReadyPlayerMe.WebView
+{
+ ///
+ /// Receives a callback after the Android Gradle project is generated,
+ /// and the callback is used for generating a manifest file with required permissions.
+ ///
+ public class WebViewBuildPostprocessor : IPostGenerateGradleAndroidProject
+ {
+ public int callbackOrder => 1;
+
+ public void OnPostGenerateGradleAndroidProject(string basePath)
+ {
+ var manifestPath = GetManifestPath(basePath);
+ var androidManifest = new AndroidManifest(manifestPath);
+
+ androidManifest
+ .SetHardwareAccelerated(true)
+ .SetUsesCleartextTraffic(true)
+ .UseCamera()
+ .UseMicrophone()
+ .UseGallery()
+ .AllowBackup();
+
+ androidManifest.Save();
+ }
+
+ 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();
+ }
+
+ [PostProcessBuild(100)]
+ public static void OnPostprocessBuild(BuildTarget buildTarget, string path)
+ {
+#if UNITY_IOS
+ if (buildTarget == BuildTarget.iOS) {
+ string projPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
+ PBXProject proj = new PBXProject();
+ proj.ReadFromString(File.ReadAllText(projPath));
+ proj.AddFrameworkToProject(proj.GetUnityFrameworkTargetGuid(), "WebKit.framework", false);
+ File.WriteAllText(projPath, proj.WriteToString());
+ }
+#endif
+ }
+ }
+
+ ///
+ /// AndroidManifest.xml file that is created with necessary permissions.
+ ///
+ internal class AndroidXmlDocument : XmlDocument
+ {
+ private string manifestPath;
+ protected XmlNamespaceManager namespaceManager;
+ public readonly string AndroidXmlNamespace = "http://schemas.android.com/apk/res/android";
+ public readonly string ToolsXmlNamespace = "http://schemas.android.com/tools";
+
+ public AndroidXmlDocument(string path)
+ {
+ manifestPath = path;
+ using (var reader = new XmlTextReader(manifestPath))
+ {
+ reader.Read();
+ Load(reader);
+ }
+
+ namespaceManager = new XmlNamespaceManager(NameTable);
+ namespaceManager.AddNamespace("android", AndroidXmlNamespace);
+ namespaceManager.AddNamespace("tools", ToolsXmlNamespace);
+ }
+
+ public string Save()
+ {
+ return SaveAs(manifestPath);
+ }
+
+ 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 const string NodeKey = "name";
+ private const string UsesFeature = "uses-feature";
+ private const string UsesPermission = "uses-permission";
+
+ private const string CameraPermission = "android.permission.CAMERA";
+ private const string CameraFeature = "android.hardware.camera";
+
+ private const string MicrophonePermission = "android.permission.MICROPHONE";
+ private const string MicrophoneFeature = "android.hardware.microphone";
+
+ private const string ReadExternalStoragePermission = "android.permission.READ_EXTERNAL_STORAGE";
+ private const string WriteExternalStoragePermission = "android.permission.Write_EXTERNAL_STORAGE";
+
+ private const string UsesCleartextTrafficAttribute = "usesCleartextTraffic";
+ private const string HardwareAcceleratedAttribute = "hardwareAccelerated";
+
+ private const string XPath = "/manifest/application/activity[intent-filter/action/@android:name='android.intent.action.MAIN' and intent-filter/category/@android:name='android.intent.category.LAUNCHER']";
+
+ private static XmlNode ActivityWithLaunchIntent = null;
+
+ private readonly XmlElement ManifestElement;
+
+ public AndroidManifest(string path) : base(path)
+ {
+ ManifestElement = SelectSingleNode("/manifest") as XmlElement;
+ }
+
+ internal XmlNode GetActivityWithLaunchIntent()
+ {
+ return ActivityWithLaunchIntent ?? SelectSingleNode(XPath, namespaceManager);
+ }
+
+ #region Node Edit Methods
+
+ private XmlAttribute CreateAndroidAttribute(string key, string value)
+ {
+ XmlAttribute attr = CreateAttribute("android", key, AndroidXmlNamespace);
+ attr.Value = value;
+ return attr;
+ }
+
+ private XmlAttribute CreateToolsAttribute(string key, string value)
+ {
+ XmlAttribute attr = CreateAttribute("tools", key, ToolsXmlNamespace);
+ attr.Value = value;
+ return attr;
+ }
+
+ internal void UpdateAttribute(XmlElement activity, string attribute, bool enabled)
+ {
+ var value = enabled.ToString();
+
+ if (activity.GetAttribute(attribute, AndroidXmlNamespace) != value)
+ {
+ activity.SetAttribute(attribute, AndroidXmlNamespace, value);
+ }
+ }
+
+ internal void UpdateNode(string nodeName, string nodeValue)
+ {
+ XmlNodeList node = SelectNodes($"/manifest/{nodeName}[@android:{NodeKey}='{nodeValue}']", namespaceManager);
+ if (node?.Count == 0)
+ {
+ XmlElement elem = CreateElement(nodeName);
+ elem.Attributes.Append(CreateAndroidAttribute(NodeKey, nodeValue));
+ ManifestElement.AppendChild(elem);
+ }
+ }
+
+ internal void UseFeature(string feature)
+ {
+ UpdateNode(UsesFeature, feature);
+ }
+
+ internal void UsePermission(string permission)
+ {
+ UpdateNode(UsesPermission, permission);
+ }
+
+ #endregion
+
+ #region AndroidManifest Options
+
+ internal AndroidManifest SetUsesCleartextTraffic(bool enabled)
+ {
+ var activity = GetActivityWithLaunchIntent() as XmlElement;
+ UpdateAttribute(activity, UsesCleartextTrafficAttribute, enabled);
+ return this;
+ }
+
+ internal AndroidManifest AllowBackup()
+ {
+ XmlNode elem = SelectSingleNode("/manifest/application");
+ if (elem?.Attributes != null)
+ {
+ elem.Attributes.Append(CreateAndroidAttribute("allowBackup", "false"));
+ elem.Attributes.Append(CreateToolsAttribute("replace", "android:allowBackup"));
+ }
+
+ return this;
+ }
+
+ internal AndroidManifest SetHardwareAccelerated(bool enabled)
+ {
+ var activity = GetActivityWithLaunchIntent() as XmlElement;
+ UpdateAttribute(activity, HardwareAcceleratedAttribute, enabled);
+ return this;
+ }
+
+ internal AndroidManifest UseCamera()
+ {
+ UsePermission(CameraPermission);
+ UseFeature(CameraFeature);
+ return this;
+ }
+
+ internal AndroidManifest UseMicrophone()
+ {
+ UsePermission(MicrophonePermission);
+ UseFeature(MicrophoneFeature);
+ return this;
+ }
+
+ internal AndroidManifest UseGallery()
+ {
+ UsePermission(ReadExternalStoragePermission);
+ UsePermission(WriteExternalStoragePermission);
+ return this;
+ }
+
+ #endregion
+ }
+}
+#endif
diff --git a/Editor/WebViewBuildPostprocessor.cs.meta b/Editor/WebViewBuildPostprocessor.cs.meta
new file mode 100644
index 0000000..9eded63
--- /dev/null
+++ b/Editor/WebViewBuildPostprocessor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0856672d812a15e459cee5e879c109d7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/WebViewEditor.cs b/Editor/WebViewEditor.cs
new file mode 100644
index 0000000..339153e
--- /dev/null
+++ b/Editor/WebViewEditor.cs
@@ -0,0 +1,25 @@
+#if UNITY_EDITOR
+using UnityEditor;
+using UnityEngine;
+
+namespace ReadyPlayerMe.WebView
+{
+ public class WebViewEditor : Editor
+ {
+ private const string WEB_VIEW_CANVAS_FILE_NAME = "WebView Canvas";
+
+ ///
+ /// Loads a WebView Canvas prefab to the current scene.
+ ///
+ [MenuItem("GameObject/UI/Ready Player Me/WebView Canvas", false)]
+ private static void LoadWebViewCanvas()
+ {
+ var prefab = Resources.Load(WEB_VIEW_CANVAS_FILE_NAME);
+ GameObject instance = Instantiate(prefab);
+ instance.name = WEB_VIEW_CANVAS_FILE_NAME;
+ Selection.activeGameObject = instance;
+ EditorUtility.SetDirty(instance);
+ }
+ }
+}
+#endif
diff --git a/Editor/WebViewEditor.cs.meta b/Editor/WebViewEditor.cs.meta
new file mode 100644
index 0000000..6784b0b
--- /dev/null
+++ b/Editor/WebViewEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 055a341980f246a47b8227fd1ae60f27
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: