diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9591d7..7e41d01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,14 @@ All notable changes to this package will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+## [4.4.0] - 2023-03-30
+
+### Added
+- Created `AndroidManifestProcessor` as an Android manifest management system for XR packages and providers that target platforms based on Android.
+
+### Removed
+- AndroidManifest.xml is no longer deleted after each Android build.
+
## [4.3.3] - 2023-02-14
### Fixed
diff --git a/Editor/AndroidManifest.meta b/Editor/AndroidManifest.meta
new file mode 100644
index 0000000..9a2ee73
--- /dev/null
+++ b/Editor/AndroidManifest.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7ab74fff4fadf1841820ded42d6ecd4c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/AndroidManifest/AndroidManifestBuildEventReceiver.cs b/Editor/AndroidManifest/AndroidManifestBuildEventReceiver.cs
new file mode 100644
index 0000000..40a508a
--- /dev/null
+++ b/Editor/AndroidManifest/AndroidManifestBuildEventReceiver.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEditor;
+using UnityEditor.Android;
+using UnityEditor.XR.Management;
+using UnityEngine.XR.Management;
+
+namespace Unity.XR.Management.AndroidManifest.Editor
+{
+ ///
+ /// Class that receives the build event when building an Android Gradle project,
+ /// so the manifest element processing can be executed.
+ ///
+ internal class AndroidManifestBuildEventReceiver : IPostGenerateGradleAndroidProject
+ {
+
+ public int callbackOrder => 1;
+
+ public void OnPostGenerateGradleAndroidProject(string gradleProjectPath)
+ {
+ var processor = CreateManifestProcessor(gradleProjectPath);
+
+ var manifestProviders = GetManifestProviders();
+ processor.ProcessManifestRequirements(manifestProviders);
+ }
+
+ private AndroidManifestProcessor CreateManifestProcessor(string gradleProjectPath)
+ {
+#if UNITY_2021_1_OR_NEWER
+ var xrManagementPackagePath = EditorUtilities.GetPackagePath("com.unity.xr.management");
+ return new AndroidManifestProcessor(gradleProjectPath, xrManagementPackagePath, GetXRManagerSettings());
+#else
+ return new AndroidManifestProcessor(gradleProjectPath, GetXRManagerSettings());
+#endif
+ }
+
+ ///
+ /// Finds all implementation of in the assemblies,
+ /// and creates instances for each type into a single collection.
+ ///
+ /// collection of instances. All contained objects are unique.
+ private List GetManifestProviders()
+ {
+ return TypeCache
+ .GetTypesDerivedFrom()
+ .Where(type => !type.IsInterface && !type.IsAbstract && !type.IsNestedPrivate)
+ .Select(providerType => Activator.CreateInstance(providerType)) // Instantiate providers
+ .OfType()
+ .Distinct()
+ .ToList();
+ }
+
+ private XRManagerSettings GetXRManagerSettings()
+ {
+ if (XRGeneralSettingsPerBuildTarget.XRGeneralSettingsForBuildTarget(BuildTargetGroup.Android))
+ {
+ return XRGeneralSettingsPerBuildTarget.XRGeneralSettingsForBuildTarget(BuildTargetGroup.Android).AssignedSettings;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/Editor/AndroidManifest/AndroidManifestBuildEventReceiver.cs.meta b/Editor/AndroidManifest/AndroidManifestBuildEventReceiver.cs.meta
new file mode 100644
index 0000000..c0a7689
--- /dev/null
+++ b/Editor/AndroidManifest/AndroidManifestBuildEventReceiver.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d19006cf3476fe8418e1018605a8ef99
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/AndroidManifest/AndroidManifestDocument.cs b/Editor/AndroidManifest/AndroidManifestDocument.cs
new file mode 100644
index 0000000..1b5c3d9
--- /dev/null
+++ b/Editor/AndroidManifest/AndroidManifestDocument.cs
@@ -0,0 +1,202 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text;
+using System.Xml;
+
+namespace Unity.XR.Management.AndroidManifest.Editor
+{
+ ///
+ /// This class holds information that should be displayed in an Editor tooltip for a given package.
+ ///
+ internal class AndroidManifestDocument : XmlDocument
+ {
+ internal static readonly string k_androidXmlNamespace = "http://schemas.android.com/apk/res/android";
+
+ private readonly string m_Path;
+ private readonly XmlNamespaceManager m_nsMgr;
+
+ internal AndroidManifestDocument()
+ {
+ m_nsMgr = new XmlNamespaceManager(NameTable);
+ m_nsMgr.AddNamespace("android", k_androidXmlNamespace);
+ }
+
+ internal AndroidManifestDocument(string path)
+ {
+ m_Path = path;
+
+ using (var reader = new XmlTextReader(m_Path))
+ {
+ reader.Read();
+ Load(reader);
+ }
+
+ m_nsMgr = new XmlNamespaceManager(NameTable);
+ m_nsMgr.AddNamespace("android", k_androidXmlNamespace);
+ }
+
+ internal string Save()
+ {
+ return SaveAs(m_Path);
+ }
+
+ internal string SaveAs(string path)
+ {
+ // ensure the folder exists so that the XmlTextWriter doesn't fail
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+ using (var writer = new XmlTextWriter(path, new UTF8Encoding(false)))
+ {
+ writer.Formatting = Formatting.Indented;
+ Save(writer);
+ }
+
+ return path;
+ }
+
+ internal void CreateNewElement(List path, Dictionary attributes)
+ {
+ // Look up for closest parent node to new leaf node
+ XmlElement parentNode, node = null;
+ int nextNodeIndex = -1;
+ do
+ {
+ nextNodeIndex++;
+ parentNode = node;
+ node = (XmlElement)(parentNode == null ?
+ SelectSingleNode(path[nextNodeIndex]) :
+ parentNode.SelectSingleNode(path[nextNodeIndex]));
+ } while (node != null && nextNodeIndex < path.Count - 1);
+
+ // If nodes are missing between root and leaf, fill out hierarchy including leaf node
+ for (int i = nextNodeIndex; i < path.Count; i++)
+ {
+ node = CreateElement(path[i]);
+ parentNode.AppendChild(node);
+ parentNode = node;
+ }
+
+ // Apply attributes to leaf node
+ foreach (var attributePair in attributes)
+ {
+ node.SetAttribute(attributePair.Key, k_androidXmlNamespace, attributePair.Value);
+ }
+ }
+
+ internal void CreateNewElementIfDoesntExist(List path, Dictionary attributes)
+ {
+ var existingNodeElements = SelectNodes(string.Join("/", path));
+ foreach(XmlElement element in existingNodeElements)
+ {
+ if (CheckNodeAttributesMatch(element, attributes))
+ {
+ return;
+ }
+ }
+
+ CreateNewElement(path, attributes);
+ }
+
+ internal void CreateOrOverrideElement(List path, Dictionary attributes)
+ {
+ // Look up for leaf node or closest
+ XmlElement parentNode, node = null;
+ int nextNodeIndex = -1;
+ do
+ {
+ nextNodeIndex++;
+ parentNode = node;
+ node = (XmlElement)(parentNode == null ?
+ SelectSingleNode(path[nextNodeIndex]) :
+ parentNode.SelectSingleNode(path[nextNodeIndex]));
+ } while (node != null && nextNodeIndex < path.Count - 1);
+
+ // If nodes are missing between root and leaf, fill out hierarchy including leaf node
+ if (node == null)
+ {
+ for (int i = nextNodeIndex; i < path.Count; i++)
+ {
+ node = CreateElement(path[i]);
+ parentNode.AppendChild(node);
+ parentNode = node;
+ }
+ }
+
+ // Apply attributes to leaf node
+ foreach (var attributePair in attributes)
+ {
+ node.SetAttribute(attributePair.Key, k_androidXmlNamespace, attributePair.Value);
+ }
+ }
+
+ internal void RemoveMatchingElement(List elementPath, Dictionary attributes)
+ {
+ var xmlNodeList = SelectNodes(string.Join("/", elementPath));
+
+ foreach (XmlElement node in xmlNodeList)
+ {
+ if (CheckNodeAttributesMatch(node, attributes))
+ {
+ node.ParentNode?.RemoveChild(node);
+ }
+ }
+ }
+
+ private bool CheckNodeAttributesMatch(XmlNode node, Dictionary attributes)
+ {
+ var nodeAttributes = node.Attributes;
+ foreach (XmlAttribute attribute in nodeAttributes)
+ {
+ var rawAttributeName = attribute.Name.Split(':').Last();
+ if (!attributes.Contains(new KeyValuePair(rawAttributeName, attribute.Value)))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ internal void CreateElements(IEnumerable newElements, bool allowDuplicates = true)
+ {
+ if(allowDuplicates)
+ {
+ foreach (var requirement in newElements)
+ {
+ this
+ .CreateNewElement(
+ requirement.ElementPath, requirement.Attributes);
+ }
+ }
+ else
+ {
+ foreach (var requirement in newElements)
+ {
+ this
+ .CreateNewElementIfDoesntExist(
+ requirement.ElementPath, requirement.Attributes);
+ }
+ }
+ }
+
+ internal void OverrideElements(IEnumerable overrideElements)
+ {
+ foreach (var requirement in overrideElements)
+ {
+ this
+ .CreateOrOverrideElement(
+ requirement.ElementPath, requirement.Attributes);
+ }
+ }
+
+ internal void RemoveElements(IEnumerable removableElements)
+ {
+ foreach (var requirement in removableElements)
+ {
+ this
+ .RemoveMatchingElement(
+ requirement.ElementPath, requirement.Attributes);
+ }
+ }
+ }
+}
diff --git a/Editor/AndroidManifest/AndroidManifestDocument.cs.meta b/Editor/AndroidManifest/AndroidManifestDocument.cs.meta
new file mode 100644
index 0000000..5f37435
--- /dev/null
+++ b/Editor/AndroidManifest/AndroidManifestDocument.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 155fb79913e46e546926a3cad34ee561
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/AndroidManifest/AndroidManifestProcessor.cs b/Editor/AndroidManifest/AndroidManifestProcessor.cs
new file mode 100644
index 0000000..c57f361
--- /dev/null
+++ b/Editor/AndroidManifest/AndroidManifestProcessor.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using UnityEditor;
+using UnityEditor.Android;
+using UnityEditor.XR.Management;
+using UnityEngine;
+using UnityEngine.XR.Management;
+
+namespace Unity.XR.Management.AndroidManifest.Editor
+{
+ ///
+ /// Class that retrieves Android manifest entries required by classes that implement the IAndroidManifestEntryProvider interface.
+ ///
+ internal class AndroidManifestProcessor
+ {
+ private static readonly string k_androidManifestFileName = "AndroidManifest.xml";
+#if UNITY_2021_1_OR_NEWER
+ private static readonly string k_xrLibraryDirectoryName = "xrmanifest.androidlib";
+ private static readonly string k_xrLibraryManifestRelativePath = string.Join(Path.DirectorySeparatorChar.ToString(), k_xrLibraryDirectoryName, k_androidManifestFileName);
+#endif
+
+ private readonly string m_unityLibraryManifestFilePath;
+#if UNITY_2021_1_OR_NEWER
+ private readonly string m_xrPackageManifestTemplateFilePath;
+ private readonly string m_xrLibraryManifestFilePath;
+#endif
+ private readonly XRManagerSettings m_xrSettings;
+
+ internal AndroidManifestProcessor(string gradleProjectPath, XRManagerSettings settings)
+ {
+ m_unityLibraryManifestFilePath = string.Join(Path.DirectorySeparatorChar.ToString(), gradleProjectPath, "src", "main", k_androidManifestFileName);
+ m_xrSettings = settings;
+ }
+
+
+#if UNITY_2021_1_OR_NEWER
+ internal AndroidManifestProcessor(
+ string gradleProjectPath,
+ string xrManagementPackagePath,
+ XRManagerSettings settings)
+ {
+ m_xrPackageManifestTemplateFilePath = string.Join(Path.DirectorySeparatorChar.ToString(), xrManagementPackagePath, k_xrLibraryManifestRelativePath);
+ m_xrLibraryManifestFilePath = string.Join(Path.DirectorySeparatorChar.ToString(), gradleProjectPath, k_xrLibraryManifestRelativePath);
+ m_unityLibraryManifestFilePath = string.Join(Path.DirectorySeparatorChar.ToString(), gradleProjectPath, "src", "main", k_androidManifestFileName);
+ m_xrSettings = settings;
+ }
+#endif
+
+ internal void ProcessManifestRequirements(List manifestProviders)
+ {
+ var activeLoaders = GetActiveLoaderList();
+
+ // Get manifest entries from providers
+ var manifestRequirements = manifestProviders
+ .Select(provider => provider.ProvideManifestRequirement())
+ .OfType()
+ .Distinct()
+ // Requirements can apply to different platforms, so we filter out those whose loaders aren't currently active
+ .Where(requirement => requirement.SupportedXRLoaders.Any(loaderType => activeLoaders.Contains(loaderType)))
+ .ToList();
+
+ var mergedRequiredElements =
+ MergeElements(
+ manifestRequirements
+ .SelectMany(requirement => requirement.OverrideElements));
+ var elementsToBeRemoved = manifestRequirements
+ .SelectMany(requirement => requirement.RemoveElements)
+ .OfType();
+
+#if UNITY_2021_1_OR_NEWER
+ // The intent-filter elements are not merged by default,
+ // so we separate them from the XR manifest to add them later.
+ // Otherwise, the application won't load correctly.
+ var newRequiredElements = manifestRequirements
+ .SelectMany(requirement => requirement.NewElements)
+ .Where(element => !element.ElementPath.Contains("intent-filter"));
+ var newIntentElements = manifestRequirements
+ .SelectMany(requirement => requirement.NewElements)
+ .Where(element => element.ElementPath.Contains("intent-filter"));
+
+ {
+ var xrLibraryManifest = new AndroidManifestDocument(m_xrPackageManifestTemplateFilePath);
+ var unityLibraryManifest = new AndroidManifestDocument(m_unityLibraryManifestFilePath);
+
+ xrLibraryManifest.CreateElements(newRequiredElements);
+ xrLibraryManifest.OverrideElements(mergedRequiredElements);
+ unityLibraryManifest.CreateElements(newIntentElements, false); // Add the intents in the unity library manifest
+ unityLibraryManifest.RemoveElements(elementsToBeRemoved);
+
+ // Write updated manifests
+ xrLibraryManifest.SaveAs(m_xrLibraryManifestFilePath);
+ unityLibraryManifest.Save();
+ }
+#else
+ var newRequiredElements = manifestRequirements
+ .SelectMany(requirement => requirement.NewElements);
+
+ {
+ var manifest = new AndroidManifestDocument(m_unityLibraryManifestFilePath);
+
+ manifest.CreateElements(newRequiredElements);
+ manifest.OverrideElements(mergedRequiredElements);
+ manifest.RemoveElements(elementsToBeRemoved);
+
+ // Write manifest into project's library path
+ manifest.Save();
+ }
+#endif
+ }
+
+ ///
+ /// Merges the elements of given of type based on their .
+ /// Their key-value pair attributes are deduped and merged into a single element.
+ ///
+ /// of type containing all elements to be merged.
+ /// Filtered of type with unique elements.
+ private IEnumerable MergeElements(IEnumerable source)
+ {
+ return source
+ .GroupBy(
+ (requirement) => requirement.ElementPath,
+ (requirement) => requirement,
+ (elementPath, groupedRequirements) => {
+ var mergedAttributes = groupedRequirements
+ .SelectMany(requirement => requirement.Attributes)
+ .Distinct()
+ .ToDictionary(pair => pair.Key, pair => pair.Value);
+ return new ManifestElement
+ {
+ ElementPath = elementPath,
+ Attributes = mergedAttributes
+ };
+ });
+ }
+
+ private List GetActiveLoaderList()
+ {
+ if (!m_xrSettings)
+ {
+ // No loaders active, don't throw error
+ Debug.LogWarning("No XR Manager settings found, manifest entries will not be updated.");
+ return new List();
+ }
+
+ return m_xrSettings.activeLoaders
+ .Select(loader => loader.GetType())
+ .ToList();
+ }
+ }
+}
diff --git a/Editor/AndroidManifest/AndroidManifestProcessor.cs.meta b/Editor/AndroidManifest/AndroidManifestProcessor.cs.meta
new file mode 100644
index 0000000..d8c28cc
--- /dev/null
+++ b/Editor/AndroidManifest/AndroidManifestProcessor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a1797bfd7905e844ab00f68dca3ed050
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/AndroidManifest/IAndroidManifestRequirementProvider.cs b/Editor/AndroidManifest/IAndroidManifestRequirementProvider.cs
new file mode 100644
index 0000000..17f54b7
--- /dev/null
+++ b/Editor/AndroidManifest/IAndroidManifestRequirementProvider.cs
@@ -0,0 +1,19 @@
+namespace Unity.XR.Management.AndroidManifest.Editor
+{
+ ///
+ /// Interface for classes providing Android manifest entries.
+ ///
+ /// All implementers must be top level classes for the Android manifest processsor to call them
+ /// Nested classes aren't supported.
+ ///
+ public interface IAndroidManifestRequirementProvider
+ {
+ ///
+ /// Provides a object with the required Android manifest elements and its attributes needed to be
+ /// added, overriden or removed.
+ ///
+ ///
+ /// with element requirements data.
+ ManifestRequirement ProvideManifestRequirement();
+ }
+}
diff --git a/Editor/AndroidManifest/IAndroidManifestRequirementProvider.cs.meta b/Editor/AndroidManifest/IAndroidManifestRequirementProvider.cs.meta
new file mode 100644
index 0000000..5d6a5a0
--- /dev/null
+++ b/Editor/AndroidManifest/IAndroidManifestRequirementProvider.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3b45a81d07011564297a01edd9f8bbfb
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/AndroidManifest/ManifestElement.cs b/Editor/AndroidManifest/ManifestElement.cs
new file mode 100644
index 0000000..5596bc4
--- /dev/null
+++ b/Editor/AndroidManifest/ManifestElement.cs
@@ -0,0 +1,43 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Unity.XR.Management.AndroidManifest.Editor
+{
+ ///
+ /// This class holds information for a single Android manifest element, including its path and attributes.
+ ///
+ public class ManifestElement
+ {
+ ///
+ ///
+ /// List of element names representing the full XML path to the element. It must include last the element that this object represents.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// The order in which the elements are added is important,
+ /// as each list member represents an XML element name in the order,
+ /// so specifying a list as { "manifest", "application" } is not the same
+ /// as { "application", "manifest"}.
+ ///
+ ///
+ /// Example for accessing a meta-data element:
+ ///
+ /// new ManifestElement {
+ /// ElementPath = new List() {
+ /// "manifest", "application", "meta-data"
+ /// }
+ /// }
+ ///
+ ///
+ ///
+ public List ElementPath { get; set; }
+
+ ///
+ /// Dictionary of Name-Value pairs of the represented element's attributes.
+ ///
+ public Dictionary Attributes { get; set; }
+ }
+}
diff --git a/Editor/AndroidManifest/ManifestElement.cs.meta b/Editor/AndroidManifest/ManifestElement.cs.meta
new file mode 100644
index 0000000..ed33196
--- /dev/null
+++ b/Editor/AndroidManifest/ManifestElement.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 101cdf2d24ab75f4cb29ac9cb47e8209
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/AndroidManifest/ManifestRequirement.cs b/Editor/AndroidManifest/ManifestRequirement.cs
new file mode 100644
index 0000000..83c3ab0
--- /dev/null
+++ b/Editor/AndroidManifest/ManifestRequirement.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+
+namespace Unity.XR.Management.AndroidManifest.Editor
+{
+ ///
+ /// This class contains lists of Android manifest elements that need to be added, overriden or removed from the application manifest.
+ ///
+ public class ManifestRequirement : IEquatable
+ {
+ ///
+ /// Set of supported types by these requirements.
+ /// If none of the listed loaders is active at the moment of building, the requirements will be ignored.
+ ///
+ public HashSet SupportedXRLoaders { get; set; } = new HashSet();
+
+ ///
+ /// List of elements that will be added to the Android manifest.
+ /// Each entry represents a single element within its specified node path, and it won't overwrite or override any other element to be added.
+ ///
+ public List NewElements { get; set; } = new List();
+
+ ///
+ /// List of elements whose attirbutes will be merged or overriden with existing the Android manifest elements.
+ /// If the manifest element doesn't exist in the file, it will be created.
+ ///
+ public List OverrideElements { get; set; } = new List();
+
+ ///
+ /// List of elements which will be removed from the Android manifest.
+ /// Entries not found will be ignored.
+ /// Only entries that specify the same attributes and its respective values in the manifest will be taken in account for deletion.
+ ///
+ public List RemoveElements { get; set; } = new List();
+
+ ///
+ public override bool Equals(object obj)
+ {
+ if (obj == null)
+ {
+ return false;
+ }
+
+ return obj is ManifestRequirement && Equals(obj as ManifestRequirement);
+ }
+
+ ///
+ public bool Equals(ManifestRequirement other)
+ {
+ return other != null &&
+ ((NewElements == null && other.NewElements == null) || (NewElements != null && NewElements.Equals(other.NewElements))) &&
+ ((OverrideElements == null && other.OverrideElements == null) || (OverrideElements != null && OverrideElements.Equals(other.OverrideElements))) &&
+ ((RemoveElements == null && other.RemoveElements == null) || (RemoveElements != null && RemoveElements.Equals(other.RemoveElements)));
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+#if UNITY_2021_3_OR_NEWER
+ return HashCode.Combine(NewElements, OverrideElements, RemoveElements);
+#else
+ // Unfortunately, 2020.3 compiles against .NET 2.0, which doesn't include the more reasonable HashCode class.
+ // Still, a hash function is needed for comparing these objects, so going on with the option outlined here:
+ // https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations
+ int factor = 31;
+ int hash = 17;
+ hash = hash * factor + (NewElements != null ? NewElements.GetHashCode() : 0);
+ hash = hash * factor + (OverrideElements != null ? OverrideElements.GetHashCode() : 0);
+ hash = hash * factor + (RemoveElements != null ? RemoveElements.GetHashCode() : 0);
+ return hash;
+#endif // UNITY_2021_3_OR_NEWER
+ }
+ }
+}
diff --git a/Editor/AndroidManifest/ManifestRequirement.cs.meta b/Editor/AndroidManifest/ManifestRequirement.cs.meta
new file mode 100644
index 0000000..e2f26e6
--- /dev/null
+++ b/Editor/AndroidManifest/ManifestRequirement.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fac60b1ea386ed14da6c50ad574e0ab0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/EditorUtilities.cs b/Editor/EditorUtilities.cs
index 79403e6..f008b6e 100644
--- a/Editor/EditorUtilities.cs
+++ b/Editor/EditorUtilities.cs
@@ -114,5 +114,21 @@ internal static ScriptableObject CreateScriptableObjectInstance(string typeName,
return null;
}
+ ///
+ /// Retrieves the path of the XR Management package's Android manifest XML template.
+ ///
+ /// Package name in Java convention (eg com.package.module).
+ /// of the XML file path.
+ internal static string GetPackagePath(string packageName)
+ {
+#if UNITY_2021_3_OR_NEWER
+ var xrManagementPackageInfo = UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages()
+ .Where(package => package.name == packageName)
+ .First();
+ return xrManagementPackageInfo.resolvedPath;
+#else
+ throw new System.NotSupportedException("XR Management package couldn't be found, please make sure you have the XR Management package added to your project through the Package Management window.");
+#endif // UNITY_2021_3_OR_NEWER
+ }
}
}
diff --git a/Editor/XRGeneralBuildProcessor.cs b/Editor/XRGeneralBuildProcessor.cs
index 2109e2c..55edd9a 100644
--- a/Editor/XRGeneralBuildProcessor.cs
+++ b/Editor/XRGeneralBuildProcessor.cs
@@ -92,7 +92,7 @@ internal void WriteBootConfig()
}
}
- class XRGeneralBuildProcessor : IPreprocessBuildWithReport, IPostprocessBuildWithReport, IPostGenerateGradleAndroidProject
+ class XRGeneralBuildProcessor : IPreprocessBuildWithReport, IPostprocessBuildWithReport
{
public static readonly string kPreInitLibraryKey = "xrsdk-pre-init-library";
@@ -116,10 +116,6 @@ public int callbackOrder
get { return s_CallbackOrder; }
}
- // This has to be static as the OnPostprocessBuild and OnPostGenerateGradleAndroidProject
- // callbacks are made on different instances.
- internal static string s_ManifestPath;
-
void CleanOldSettings()
{
BuildHelpers.CleanOldSettings();
@@ -157,9 +153,9 @@ internal void OnPreprocessBuildImpl(in GUID buildGuid, in BuildTargetGroup targe
// dirty later builds with assets that may not be needed or are out of date.
CleanOldSettings();
- // Fix for [1378643](https://fogbugz.unity3d.com/f/cases/1378643/)
- if (!TryGetSettingsPerBuildTarget(out var buildTargetSettings))
- return;
+ var buildTargetSettings = XRGeneralSettingsPerBuildTarget.GetOrCreate();
+ if (!buildTargetSettings)
+ return;
XRGeneralSettings settings = buildTargetSettings.SettingsForBuildTarget(targetGroup);
if (settings == null)
@@ -266,34 +262,6 @@ public void OnPostprocessBuild(BuildReport report)
// Always remember to cleanup preloaded assets after build to make sure we don't
// dirty later builds with assets that may not be needed or are out of date.
CleanOldSettings();
-
- // This is done to clean up XR-provider-specific manifest entries from Android builds when
- // the incremental build system fails to rebuild the manifest, as there is currently
- // (2023-09-27) no way to mark the manifest as "dirty."
- CleanupAndroidManifest();
- }
-
- public void CleanupAndroidManifest()
- {
- if (!string.IsNullOrEmpty(s_ManifestPath))
- {
- try
- {
- File.Delete(s_ManifestPath);
- }
- catch (Exception e)
- {
- // This only fails if the file can't be deleted; it is quiet if the file does not exist.
- Debug.LogWarning("Failed to clean up AndroidManifest file located at " + s_ManifestPath + " : " + e.ToString());
- }
- }
- s_ManifestPath = "";
- }
-
- public void OnPostGenerateGradleAndroidProject(string path)
- {
- const string k_ManifestPath = "/src/main/AndroidManifest.xml";
- s_ManifestPath = path + k_ManifestPath;
}
}
}
diff --git a/Editor/XRGeneralSettingsPerBuildTarget.cs b/Editor/XRGeneralSettingsPerBuildTarget.cs
index 622a684..24a61cf 100644
--- a/Editor/XRGeneralSettingsPerBuildTarget.cs
+++ b/Editor/XRGeneralSettingsPerBuildTarget.cs
@@ -3,10 +3,8 @@
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
-using System.Threading;
using UnityEngine;
using UnityEngine.XR.Management;
-using UnityEditor.PackageManager;
using UnityEditor.XR.Management.Metadata;
@@ -61,28 +59,12 @@ static XRGeneralSettingsPerBuildTarget CreateAssetSynchronized()
AssetDatabase.CreateAsset(generalSettings, assetPath);
AssetDatabase.SaveAssets();
}
-
EditorBuildSettings.AddConfigObject(XRGeneralSettings.k_SettingsKey, generalSettings, true);
return generalSettings;
}
internal static XRGeneralSettingsPerBuildTarget GetOrCreate()
- {
- TryFindSettingsAsset(out var generalSettings);
- generalSettings = generalSettings ?? CreateAssetSynchronized();
-
- var buildTargetSettings = generalSettings.SettingsForBuildTarget(
- EditorUserBuildSettings.selectedBuildTargetGroup
- );
- var activeLoaderCount = (buildTargetSettings?.AssignedSettings?.activeLoaders?.Count ?? 0);
- if (IsInURPGraphicsTest() && activeLoaderCount == 0)
- {
- Debug.Log("In URP graphics test and no loaders selected. Disabling 'Initialize XR On Startup'");
- buildTargetSettings.InitManagerOnStart = false;
- }
-
- return generalSettings;
- }
+ => TryFindSettingsAsset(out var generalSettings) ? generalSettings : CreateAssetSynchronized();
// Simple class to give us updates when the asset database changes.
class AssetCallbacks : AssetPostprocessor
@@ -153,43 +135,6 @@ internal static bool ContainsLoaderForAnyBuildTarget(string loaderTypeName)
return false;
}
-
- // Temporary workaround for a graphics issue
- static bool? s_IsInURPGraphicsTest = null;
- internal static bool IsInURPGraphicsTest()
- {
- if (!s_IsInURPGraphicsTest.HasValue)
- {
- s_IsInURPGraphicsTest = false;
-
- const string kGfxFramework = "com.unity.testing.urp";
-
- var request = Client.List(offlineMode: true);
- while (request.IsCompleted == false)
- {
- Thread.Yield();
- }
-
- if (request.Status != StatusCode.Success)
- {
- throw new InvalidOperationException(
- $"Unexpected package query failure searching for {kGfxFramework}"
- );
- }
-
- // if we find the com.unity.testing.urp package then assume we're in the URP
- // graphics test.
- foreach (var result in request.Result)
- {
- if (result.packageId.StartsWith(kGfxFramework))
- {
- s_IsInURPGraphicsTest = true;
- break;
- }
- }
- }
- return s_IsInURPGraphicsTest.Value;
- }
#endif
///
@@ -305,9 +250,7 @@ public void OnAfterDeserialize()
/// The instance of assigned to the key, or null if not.
public static XRGeneralSettings XRGeneralSettingsForBuildTarget(BuildTargetGroup targetGroup)
{
- XRGeneralSettingsPerBuildTarget buildTargetSettings = null;
- EditorBuildSettings.TryGetConfigObject(XRGeneralSettings.k_SettingsKey, out buildTargetSettings);
- if (buildTargetSettings == null)
+ if (!TryFindSettingsAsset(out var buildTargetSettings))
return null;
return buildTargetSettings.SettingsForBuildTarget(targetGroup);
diff --git a/Tests/Editor/AndroidManifestTests.cs b/Tests/Editor/AndroidManifestTests.cs
new file mode 100644
index 0000000..f1194cf
--- /dev/null
+++ b/Tests/Editor/AndroidManifestTests.cs
@@ -0,0 +1,615 @@
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml;
+using Unity.XR.Management.AndroidManifest.Editor;
+using UnityEditor;
+using UnityEditor.XR.Management;
+using UnityEngine;
+using UnityEngine.XR.Management;
+
+public class AndroidManifestTests
+{
+ private string tempProjectPath;
+ private string xrManifestTemplateFilePath;
+ private string xrLibraryManifestFilePath;
+ private string unityLibraryManifestFilePath;
+ private DirectoryInfo dirInfo;
+ private XRManagerSettings mockXrSettings;
+ private Type supportedLoaderType;
+
+ [SetUp]
+ public void SetUp()
+ {
+ tempProjectPath = FileUtil.GetUniqueTempPathInProject();
+ dirInfo = Directory.CreateDirectory(tempProjectPath);
+
+ var xrPackagePath = dirInfo.CreateSubdirectory(string.Join(Path.DirectorySeparatorChar.ToString(), "xrPackage", "xrmanifest.androidlib"));
+ var xrLibraryPath = dirInfo.CreateSubdirectory("xrmanifest.androidlib");
+ var unityLibraryPath = dirInfo.CreateSubdirectory(string.Join(Path.DirectorySeparatorChar.ToString(), "src", "main"));
+
+ xrManifestTemplateFilePath = string.Join(Path.DirectorySeparatorChar.ToString(), xrPackagePath.FullName, "AndroidManifest.xml");
+ xrLibraryManifestFilePath = string.Join(Path.DirectorySeparatorChar.ToString(), xrLibraryPath.FullName, "AndroidManifest.xml");
+ unityLibraryManifestFilePath = string.Join(Path.DirectorySeparatorChar.ToString(), unityLibraryPath.FullName, "AndroidManifest.xml");
+
+ CreateMockManifestDocument(xrManifestTemplateFilePath);
+ CreateMockManifestDocument(xrLibraryManifestFilePath);
+ CreateMockManifestDocument(unityLibraryManifestFilePath);
+
+ mockXrSettings = ScriptableObject.CreateInstance();
+ supportedLoaderType = typeof(MockXrLoader);
+ mockXrSettings.TrySetLoaders(new List
+ {
+ ScriptableObject.CreateInstance()
+ });
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ dirInfo.Delete(true);
+ ScriptableObject.DestroyImmediate(mockXrSettings);
+ }
+
+ [Test]
+ public void AndroidManifestProcessor_AddOneNewManifestElement()
+ {
+ var processor = CreateProcessor();
+
+ // Initialize data
+ var newElementPath = new List { "manifest", "application", "meta-data" };
+ var newElementAttributes = new Dictionary()
+ {
+ { "name", "custom-data" },
+ { "value", "test-data" },
+ };
+ var providers = new List()
+ {
+ new MockManifestRequirementProvider(new ManifestRequirement
+ {
+ SupportedXRLoaders = new HashSet
+ {
+ supportedLoaderType
+ },
+ NewElements = new List()
+ {
+ new ManifestElement()
+ {
+ ElementPath = newElementPath,
+ Attributes = newElementAttributes
+ }
+ }
+ })
+ };
+
+ // Execute
+ processor.ProcessManifestRequirements(providers);
+
+ // Validate
+ var updatedLibraryManifest = GetXrLibraryManifest();
+ var nodes = updatedLibraryManifest.SelectNodes(string.Join("/", newElementPath));
+ Assert.AreEqual(
+ 1,
+ nodes.Count,
+ "Additional elements exist in the Manifest when expecting 1");
+
+ var attributeList = nodes[0].Attributes;
+ Assert.AreEqual(
+ newElementAttributes.Count,
+ attributeList.Count,
+ "Attribute count in element doesn't match expected count");
+
+ AssertAttributesAreEqual(nodes[0].Name, newElementAttributes, attributeList);
+ }
+
+ [Test]
+ public void AndroidManifestProcessor_AddTwoNewManifestElements()
+ {
+ var processor = CreateProcessor();
+
+ // Initialize data
+ var newElementPath = new List { "manifest", "application", "meta-data" };
+ var newElementAttributes = new Dictionary()
+ {
+ { "name", "custom-data" },
+ { "value", "test-data" },
+ };
+ var providers = new List()
+ {
+ new MockManifestRequirementProvider(new ManifestRequirement
+ {
+ SupportedXRLoaders = new HashSet
+ {
+ supportedLoaderType
+ },
+ NewElements = new List()
+ {
+ new ManifestElement()
+ {
+ ElementPath = newElementPath,
+ Attributes = newElementAttributes
+ },
+ new ManifestElement()
+ {
+ ElementPath = newElementPath,
+ Attributes = newElementAttributes
+ }
+ }
+ })
+ };
+
+ // Execute
+ processor.ProcessManifestRequirements(providers);
+
+ // Validate
+ var updatedLibraryManifest = GetXrLibraryManifest();
+ var nodes = updatedLibraryManifest.SelectNodes(string.Join("/", newElementPath));
+ Assert.AreEqual(
+ 2,
+ nodes.Count,
+ "Additional elements exist in the Manifest when expecting 2");
+
+ foreach(XmlElement node in nodes)
+ {
+ var attributeList = node.Attributes;
+ Assert.AreEqual(
+ newElementAttributes.Count,
+ attributeList.Count,
+ "Attribute count in element doesn't match expected count");
+
+ AssertAttributesAreEqual(node.Name, newElementAttributes, attributeList);
+ }
+ }
+
+ [Test]
+ public void AndroidManifestProcessor_CreateSingleNewManifestElementFromTwoOverridenElements()
+ {
+ // Use the Assert class to test conditions
+ var processor = CreateProcessor();
+
+ // Initialize data
+ var overrideElementPath = new List { "manifest", "application", "activity" };
+ var overrideElement1Attributes = new Dictionary();
+ var overrideElement2Attributes = new Dictionary();
+ var providers = new List()
+ {
+ new MockManifestRequirementProvider(new ManifestRequirement
+ {
+ SupportedXRLoaders = new HashSet
+ {
+ supportedLoaderType
+ },
+ OverrideElements = new List()
+ {
+ new ManifestElement()
+ {
+ ElementPath = overrideElementPath,
+ Attributes = overrideElement1Attributes
+ },
+ new ManifestElement()
+ {
+ ElementPath = overrideElementPath,
+ Attributes = overrideElement2Attributes
+ }
+ }
+ })
+ };
+
+ // Execute
+ processor.ProcessManifestRequirements(providers);
+
+ // Validate
+ var updatedLibraryManifest = GetXrLibraryManifest();
+ var nodes = updatedLibraryManifest.SelectNodes(string.Join("/", overrideElementPath));
+ Assert.AreEqual(
+ 1,
+ nodes.Count,
+ "Additional elements exist in the Manifest when expecting 1");
+
+ var attributeList = nodes[0].Attributes;
+ var expectedElementAttrributes = MergeDictionaries(overrideElement1Attributes, overrideElement2Attributes);
+ Assert.AreEqual(
+ expectedElementAttrributes.Count,
+ attributeList.Count,
+ $"Attribute count in element doesn't match expected {expectedElementAttrributes.Count}");
+
+ AssertAttributesAreEqual(nodes[0].Name, expectedElementAttrributes, attributeList);
+ }
+
+
+ [Test]
+ public void AndroidManifestProcessor_UpdateExistingElementWithOverridenElement()
+ {
+ // Use the Assert class to test conditions
+ var processor = CreateProcessor();
+
+ // Initialize data
+ var overrideElementPath = new List { "manifest", "application", "activity" };
+ var existingElementAttributes = new Dictionary()
+ {
+ { "name", "com.test.app" }
+ };
+ var overrideElementAttributes = new Dictionary()
+ {
+ { "isGame", "true" },
+ { "testOnly", "true" },
+ };
+ var providers = new List()
+ {
+ new MockManifestRequirementProvider(new ManifestRequirement
+ {
+ SupportedXRLoaders = new HashSet
+ {
+ supportedLoaderType
+ },
+ OverrideElements = new List()
+ {
+ new ManifestElement()
+ {
+ ElementPath = overrideElementPath,
+ Attributes = overrideElementAttributes
+ }
+ }
+ })
+ };
+
+ // Prepare test document
+ var libManifest = GetXrLibraryManifest();
+ libManifest.CreateNewElement(overrideElementPath, existingElementAttributes);
+ libManifest.Save();
+
+ // Execute
+ processor.ProcessManifestRequirements(providers);
+
+ // Validate
+ var updatedLibraryManifest = GetXrLibraryManifest();
+ var nodes = updatedLibraryManifest.SelectNodes(string.Join("/", overrideElementPath));
+ Assert.AreEqual(
+ 1,
+ nodes.Count,
+ "Additional elements exist in the Manifest when expecting 1");
+
+ var attributeList = nodes[0].Attributes;
+ var expectedElementAttrributes = MergeDictionaries(existingElementAttributes, overrideElementAttributes);
+ Assert.AreEqual(
+ expectedElementAttrributes.Count,
+ attributeList.Count,
+ $"Attribute count {attributeList.Count} in element doesn't match expected {expectedElementAttrributes.Count}");
+
+ AssertAttributesAreEqual(nodes[0].Name, expectedElementAttrributes, attributeList);
+ }
+
+ [Test]
+ public void AndroidManifestProcessor_DeleteExistingManifestElement()
+ {
+ var processor = CreateProcessor();
+
+ // Initialize data
+ var deletedElementPath = new List { "manifest", "uses-permission" };
+ var deletedElementAttributes = new Dictionary()
+ {
+ { "name", "BLUETOOTH" }
+ };
+ var providers = new List()
+ {
+ new MockManifestRequirementProvider(new ManifestRequirement
+ {
+ SupportedXRLoaders = new HashSet
+ {
+ supportedLoaderType
+ },
+ RemoveElements = new List()
+ {
+ new ManifestElement()
+ {
+ ElementPath = deletedElementPath,
+ Attributes = deletedElementAttributes
+ }
+ }
+ })
+ };
+
+ // Prepare test document
+ var appManifest = GetUnityLibraryManifest();
+ appManifest.CreateNewElement(deletedElementPath, deletedElementAttributes);
+ appManifest.Save();
+
+ // Execute
+ processor.ProcessManifestRequirements(providers);
+
+ // Validate
+ var updatedAppManifest = GetXrLibraryManifest();
+ var removedElementPath = string.Join("/", deletedElementPath);
+ var removedNodes = updatedAppManifest.SelectNodes(removedElementPath);
+ Assert.AreEqual(
+ 0,
+ removedNodes.Count,
+ $"Expected element in path \"{removedElementPath}\" wasn't deleted");
+ }
+
+ [Test]
+ public void AndroidManifestProcessor_DontModifyManifestIfNoSupportedLoadersAdded()
+ {
+ var processor = CreateProcessor();
+
+ // Initialize data
+ var newElementPath = new List { "manifest", "application", "meta-data" };
+ var newElementAttributes = new Dictionary()
+ {
+ { "name", "custom-data" },
+ { "value", "test-data" },
+ };
+ var providers = new List()
+ {
+ new MockManifestRequirementProvider(new ManifestRequirement
+ {
+ SupportedXRLoaders = new HashSet
+ {
+ typeof(object) // Dummy object representing an inactive loader
+ },
+ NewElements = new List()
+ {
+ new ManifestElement()
+ {
+ ElementPath = newElementPath,
+ Attributes = newElementAttributes
+ }
+ }
+ })
+ };
+
+ // Execute
+ processor.ProcessManifestRequirements(providers);
+
+ // Validate
+ var updatedLibraryManifest = GetXrLibraryManifest();
+ var nodes = updatedLibraryManifest.SelectNodes(string.Join("/", newElementPath));
+ Assert.AreEqual(
+ 0,
+ nodes.Count,
+ "Elements exist in the Manifest when expecting 0");
+ }
+
+#if UNITY_2021_1_OR_NEWER
+ [Test]
+ public void AndroidManifestProcessor_AddNewIntentsOnlyInUnityLibraryManifest()
+ {
+ var processor = CreateProcessor();
+
+ // Initialize data
+ var newElementPath =
+ new List { "manifest", "application", "activity", "intent-filter", "category" };
+ var newElementAttributes = new Dictionary()
+ {
+ { "name", "com.oculus.intent.category.VR" }
+ };
+ var providers = new List()
+ {
+ new MockManifestRequirementProvider(new ManifestRequirement
+ {
+ SupportedXRLoaders = new HashSet
+ {
+ supportedLoaderType
+ },
+ NewElements = new List()
+ {
+ new ManifestElement()
+ {
+ ElementPath = newElementPath,
+ Attributes = newElementAttributes
+ }
+ }
+ })
+ };
+
+ // Execute
+ processor.ProcessManifestRequirements(providers);
+
+ var elementPath = string.Join("/", newElementPath);
+
+ // Validate that the intent is created in Unity library manifest
+ var unityLibManifest = GetUnityLibraryManifest();
+ var addedNodes = unityLibManifest.SelectNodes(elementPath);
+ Assert.AreEqual(
+ 1,
+ addedNodes.Count,
+ $"Expected new element in path \"{elementPath}\" in Unity Library manifest");
+
+ // Validate that the intent isn't created in XR Library manifest
+ var xrLibManifest = GetXrLibraryManifest();
+ var emptyNodes = xrLibManifest.SelectNodes(elementPath);
+ Assert.AreEqual(
+ 0,
+ emptyNodes.Count,
+ $"Expected no new element in path \"{elementPath}\" in XR Library manifest");
+ }
+
+ [Test]
+ public void AndroidManifestProcessor_KeepOnlyOneIntentOfTheSameType()
+ {
+ var processor = CreateProcessor();
+
+ // Initialize data
+ var newElementPath =
+ new List { "manifest", "application", "activity", "intent-filter", "category" };
+ var newElementAttributes = new Dictionary()
+ {
+ { "name", "com.oculus.intent.category.VR" }
+ };
+ var providers = new List()
+ {
+ new MockManifestRequirementProvider(new ManifestRequirement
+ {
+ SupportedXRLoaders = new HashSet
+ {
+ supportedLoaderType
+ },
+ NewElements = new List()
+ {
+ new ManifestElement()
+ {
+ ElementPath = newElementPath,
+ Attributes = newElementAttributes
+ }
+ }
+ })
+ };
+
+ // Prepare test document
+ var appManifest = GetUnityLibraryManifest();
+ appManifest.CreateNewElement(newElementPath, newElementAttributes);
+ appManifest.Save();
+
+ // Execute
+ processor.ProcessManifestRequirements(providers);
+
+ var elementPath = string.Join("/", newElementPath);
+
+ // Validate that only one intent of the same kind is in the manifest
+ var unityLibManifest = GetUnityLibraryManifest();
+ var addedNodes = unityLibManifest.SelectNodes(elementPath);
+ Assert.AreEqual(
+ 1,
+ addedNodes.Count,
+ $"Expected only 1 element in path \"{elementPath}\" in Unity Library manifest");
+
+ }
+
+ [Test]
+ public void AndroidManifestProcessor_AddManyIntentsOfTheSameTypeButKeepOnlyOne()
+ {
+ var processor = CreateProcessor();
+
+ // Initialize data
+ var newElementPath =
+ new List { "manifest", "application", "activity", "intent-filter", "category" };
+ var newElementAttributes = new Dictionary()
+ {
+ { "name", "com.oculus.intent.category.VR" }
+ };
+ var providers = new List()
+ {
+ new MockManifestRequirementProvider(new ManifestRequirement
+ {
+ SupportedXRLoaders = new HashSet
+ {
+ supportedLoaderType
+ },
+ NewElements = new List()
+ {
+ new ManifestElement()
+ {
+ ElementPath = newElementPath,
+ Attributes = newElementAttributes
+ },
+ new ManifestElement()
+ {
+ ElementPath = newElementPath,
+ Attributes = newElementAttributes
+ }
+ }
+ })
+ };
+
+ // Execute
+ processor.ProcessManifestRequirements(providers);
+
+ var elementPath = string.Join("/", newElementPath);
+
+ // Validate that only one intent of the same kind is in the manifest
+ var unityLibManifest = GetUnityLibraryManifest();
+ var addedNodes = unityLibManifest.SelectNodes(elementPath);
+ Assert.AreEqual(
+ 1,
+ addedNodes.Count,
+ $"Expected only 1 element in path \"{elementPath}\" in Unity Library manifest");
+
+ }
+#endif
+
+ private AndroidManifestDocument GetXrLibraryManifest()
+ {
+#if UNITY_2021_1_OR_NEWER
+ return new AndroidManifestDocument(xrLibraryManifestFilePath);
+#else
+ // Unity 2020 and lower use the same manifest for XR entries as the rest of the app
+ return GetUnityLibraryManifest();
+#endif
+ }
+
+ private AndroidManifestDocument GetUnityLibraryManifest()
+ {
+ return new AndroidManifestDocument(unityLibraryManifestFilePath);
+ }
+
+ private AndroidManifestProcessor CreateProcessor()
+ {
+#if UNITY_2021_1_OR_NEWER
+ return new AndroidManifestProcessor(
+ tempProjectPath,
+ tempProjectPath,
+ mockXrSettings);
+#else
+ return new AndroidManifestProcessor(tempProjectPath, mockXrSettings);
+#endif
+ }
+
+ private void CreateMockManifestDocument(string filePath)
+ {
+ var manifestDocument = new AndroidManifestDocument();
+ var manifestNode = manifestDocument.CreateElement("manifest");
+ manifestNode.SetAttribute("xmlns:android", "http://schemas.android.com/apk/res/android");
+ manifestDocument.AppendChild(manifestNode);
+ var applicationNode = manifestDocument.CreateElement("application");
+ manifestNode.AppendChild(applicationNode);
+ manifestDocument.SaveAs(filePath);
+ }
+
+ private void AssertAttributesAreEqual(
+ string elementName,
+ Dictionary expectedAttributes,
+ XmlAttributeCollection attributes)
+ {
+ foreach (XmlAttribute attrib in attributes)
+ {
+ var attributeName = attrib.Name.Split(':').Last(); // Values are returned with preffixed namespace name, pick only the attribute name
+ if (!expectedAttributes.Contains(new KeyValuePair(attributeName, attrib.Value)))
+ {
+ Assert.Fail($"Unexpected attribute \"{attrib.Name}\" " +
+ $"with value \"{attrib.Value}\" found in element {elementName}");
+ }
+ }
+ }
+
+ private Dictionary MergeDictionaries(Dictionary dict1, Dictionary dict2)
+ {
+ return new List> { dict1, dict2 }
+ .SelectMany(dict => dict)
+ .ToDictionary(pair => pair.Key, pair => pair.Value);
+ }
+
+ private class MockManifestRequirementProvider : IAndroidManifestRequirementProvider
+ {
+ private readonly ManifestRequirement requirement;
+
+ public MockManifestRequirementProvider(ManifestRequirement mockRequirments)
+ {
+ requirement = mockRequirments;
+ }
+
+ public ManifestRequirement ProvideManifestRequirement()
+ {
+ return requirement;
+ }
+ }
+
+ private class MockXrLoader : XRLoader
+ {
+ public override T GetLoadedSubsystem()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Tests/Editor/AndroidManifestTests.cs.meta b/Tests/Editor/AndroidManifestTests.cs.meta
new file mode 100644
index 0000000..7b0bb2f
--- /dev/null
+++ b/Tests/Editor/AndroidManifestTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: fe69636be56b48041abf9fd83457a282
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Tests/Editor/BuildTests.cs b/Tests/Editor/BuildTests.cs
index e2af711..574ebb7 100644
--- a/Tests/Editor/BuildTests.cs
+++ b/Tests/Editor/BuildTests.cs
@@ -440,94 +440,5 @@ private XRManagerSettings GetXRManagerSettings()
.AssignedSettings;
}
}
-
- [TestFixture(BuildTargetGroup.Android, BuildTarget.Android)]
- class TestAndroidManifestReset
- {
- public class PostGradleCallback : IPostGenerateGradleAndroidProject
- {
- public int callbackOrder
- {
- get { return XRGeneralBuildProcessor.s_CallbackOrder + 1; }
- }
-
- public void OnPostGenerateGradleAndroidProject(string path)
- {
- s_onPostGenerateGradleAndroidProjectEvent?.Invoke(this, path);
- }
- }
-
- private readonly BuildTargetGroup m_BuildTargetGroup;
- private readonly BuildTarget m_BuildTarget;
-
- private XRGeneralSettingsPerBuildTarget m_OldBuildTargetSettings;
-
- private static event EventHandler s_onPostGenerateGradleAndroidProjectEvent;
-
- public TestAndroidManifestReset(BuildTargetGroup group, BuildTarget target)
- {
- m_BuildTargetGroup = group;
- m_BuildTarget = target;
- }
-
- [Test]
- public void DoesCleanUpAfterBuild()
- {
- const string k_ScenePath = "TestScene.unity";
- const string k_BuildFolder = "./build";
- const string k_BuildFile = k_BuildFolder + "/build.apk";
-
- if (!BuildPipeline.IsBuildTargetSupported(m_BuildTargetGroup, m_BuildTarget))
- {
- Debug.LogWarning(
- string.Format(
- "Test platform lacks {0}/{1} build target support",
- m_BuildTargetGroup,
- m_BuildTarget
- )
- );
- return;
- }
-
- Scene currentScene = SceneManager.GetActiveScene();
- currentScene.name = "TestScene";
- EditorSceneManager.SaveScene(currentScene, k_ScenePath);
-
- EventHandler gradleCallback = (object sender, string s) =>
- {
- Assert.True(File.Exists(XRGeneralBuildProcessor.s_ManifestPath));
- };
- s_onPostGenerateGradleAndroidProjectEvent += gradleCallback;
-
- // if the project is blank and no package name was set, this prevents the test from
- // failing
- var packageID = PlayerSettings.GetApplicationIdentifier(m_BuildTargetGroup);
- bool shouldChange = packageID?.StartsWith("com.DefaultCompany") ?? false;
- if (shouldChange) {
- PlayerSettings.SetApplicationIdentifier(m_BuildTargetGroup, "com.unity.test");
- }
-
- Directory.CreateDirectory(k_BuildFolder);
- BuildPipeline.BuildPlayer(
- new BuildPlayerOptions{
- locationPathName = k_BuildFile,
- options = BuildOptions.None,
- scenes = new string[]{k_ScenePath},
- target = m_BuildTarget,
- targetGroup = m_BuildTargetGroup
- }
- );
-
- Assert.False(File.Exists(XRGeneralBuildProcessor.s_ManifestPath));
-
- File.Delete(k_ScenePath);
- Directory.Delete(k_BuildFolder, true);
- if (shouldChange) {
- PlayerSettings.SetApplicationIdentifier(m_BuildTargetGroup, packageID);
- }
-
- s_onPostGenerateGradleAndroidProjectEvent -= gradleCallback;
- }
- }
}
#endif //UNITY_EDITOR_WIN || UNITY_EDITOR_OSX
diff --git a/package.json b/package.json
index 2b4d391..06d49fa 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "com.unity.xr.management",
"displayName": "XR Plugin Management",
- "version": "4.3.3",
+ "version": "4.4.0",
"unity": "2019.4",
"unityRelease": "15f1",
"description": "Package that provides simple management of XR plug-ins. Manages and offers help with loading, initialization, settings, and build support for XR plug-ins.",
@@ -23,15 +23,15 @@
"repository": {
"url": "https://github.cds.internal.unity3d.com/unity/xr.sdk.management.git",
"type": "git",
- "revision": "11953d1846a70bfe37146dc4655a8b2d93d32b95"
+ "revision": "6fefbe0e3deca93f6eb53e66ca554b58f2acf0d8"
},
"_upm": {
- "changelog": "### Fixed\n- Settings and Loader names with numbers will now keep the numbers for the generated asset names."
+ "changelog": "### Added\n- Created `AndroidManifestProcessor` as an Android manifest management system for XR packages and providers that target platforms based on Android.\n\n### Removed\n- AndroidManifest.xml is no longer deleted after each Android build."
},
"upmCi": {
- "footprint": "e47471cb7ebbf7c240bc0eccde6e675b16d3c6a9"
+ "footprint": "eda98e50f28e9cbade3f5ad2433dc3b55cbe5eb5"
},
- "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.xr.management@4.3/manual/index.html",
+ "documentationUrl": "https://docs.unity3d.com/Packages/com.unity.xr.management@4.4/manual/index.html",
"samples": [
{
"displayName": "Example XR Management implementation",
diff --git a/xrmanifest.androidlib.meta b/xrmanifest.androidlib.meta
new file mode 100644
index 0000000..68f6d6f
--- /dev/null
+++ b/xrmanifest.androidlib.meta
@@ -0,0 +1,71 @@
+fileFormatVersion: 2
+guid: 8443d3621b17161459d890b196695db3
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 1
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ : Any
+ second:
+ enabled: 0
+ settings:
+ Exclude Android: 0
+ Exclude Editor: 1
+ Exclude Linux64: 1
+ Exclude OSXUniversal: 1
+ Exclude Win: 1
+ Exclude Win64: 1
+ - first:
+ Android: Android
+ second:
+ enabled: 1
+ settings:
+ AndroidSharedLibraryType: Executable
+ CPU: ARMv7
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ DefaultValueInitialized: true
+ OS: AnyOS
+ - first:
+ Standalone: Linux64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: OSXUniversal
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/xrmanifest.androidlib/AndroidManifest.xml b/xrmanifest.androidlib/AndroidManifest.xml
new file mode 100644
index 0000000..80eaab9
--- /dev/null
+++ b/xrmanifest.androidlib/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/xrmanifest.androidlib/AndroidManifest.xml.meta b/xrmanifest.androidlib/AndroidManifest.xml.meta
new file mode 100644
index 0000000..2bb4217
--- /dev/null
+++ b/xrmanifest.androidlib/AndroidManifest.xml.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 2223823ee8acbf942995707617e70ce1
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant: