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: