using System; using System.Collections.Generic; using System.IO; using System.Reflection; using UnityEngine; namespace Ubisoft.Hotel.Package.Editor { public class FirebaseBuildProperty : PackageProperty { [Serializable] public class AndroidDependency { private string m_name; private Version m_version; public AndroidDependency(string name, Version version) { Name = name; Version = version; } public string Name { get { return m_name; } set { m_name = value; } } public Version Version { get { return m_version; } set { m_version = value; } } } private static readonly string PACKAGE_NAME = "com.ubisoft.hotel.core"; private const string FIREBASE_PREFIX = "Firebase"; private const string DEFAULT_SRC_MANIFEST_UNITY_PATH = ""; private static readonly string DST_MANIFEST_ROOT_UNITY_PATH = $"{AssetManager.HOTEL_GIT_IGNORED_PATH}/Firebase"; private const string MANIFEST_DESKTOP_UNITY_PATH = "Assets/StreamingAssets/google-services-desktop.json"; private const string ANDROID_RESOLVER_DEPENDENCIES_UNITY_PATH = "ProjectSettings/AndroidResolverDependencies.xml"; private const string PLUGINS_ANDROID_ROOT_UNITY_PATH = "Assets/Plugins/Android"; private const string EDITOR_CRASHLYTICS_SETTINGS_PATH = "Assets/Editor Default Resources/CrashlyticsSettings.asset"; private const string FIREBASE_GENERATE_XML_CLASSNAME = "Firebase.Editor.GenerateXmlFromGoogleServicesJson"; private const string FIREBASE_GENERATE_XML_METHODNAME = "ForceJsonUpdate"; private const string FIREBASE_M2REPOSITORY_PATH = "Assets/Hotel/Editor/Verbatim/com.ubisoft.hotel.firebasecore/Firebase/m2repository"; private const string FILE_PREFIX = "file:/" + "/" + "/"; private static readonly List UNITY_PATHS_TO_GIT_IGNORE = new List() { MANIFEST_DESKTOP_UNITY_PATH, ANDROID_RESOLVER_DEPENDENCIES_UNITY_PATH, $"{PLUGINS_ANDROID_ROOT_UNITY_PATH}/{FIREBASE_PREFIX}*", EDITOR_CRASHLYTICS_SETTINGS_PATH }; public static void AddPathsToGitIgnore(HashSet pathsToIgnore) { if (pathsToIgnore != null) { _ = pathsToIgnore.AddRange(UNITY_PATHS_TO_GIT_IGNORE); } } private static readonly List UNITY_PATHS_TO_DELETE = new List() { MANIFEST_DESKTOP_UNITY_PATH }; /// /// Create a FirebaseBuildProperty that consumers can use to get Firebase configured via code. /// /// Name to use for the property.Relative path inside '/Hotel/Assets' of the Firebase manifest to use. Example: 'Firebase/google-services.json' in case this file is stored in '/Hotel/Assets/Firebase/google-services.json' /// true to allow Firebase Analytics, in case Hotel FirebaseAnalytics package is installed, report all events. false to allow Firebase Analytics report ONLY the install event (called 'first-open' in Firebase documentation) which is Ubisoft Legal's recommended approach. /// A build property to configure Firebase via code. public static FirebaseBuildProperty CreateInstanceWithParams(string name, string srcManifestUnityPath = DEFAULT_SRC_MANIFEST_UNITY_PATH, bool enableAnalyticsFullMode = false) { FirebaseBuildProperty returnValue = CreateInstance(); returnValue.name = name; returnValue.SrcManifestUnityPath = srcManifestUnityPath; returnValue.EnableAnalyticsFullMode = enableAnalyticsFullMode; return returnValue; } /// /// Whether or not FirebaseAnalytics full mode (all events are allowed) is enabled. /// When disabled only 'install' event (first_open) is sent when Hotel FirebaseAnalytics package is installed. /// Please, get Legal's approval before enabling this mode. /// [SerializeField] private bool m_enableAnalyticsFullMode; /// /// Unity path where the Firebase Manifest is placed at, typically inside 'Hotel/Assets' folder. /// [SerializeField] private string m_srcManifestUnityPath; /// /// Android dependencies. /// [SerializeField] private List m_androidDependencies; private bool EnableAnalyticsFullMode { get { return m_enableAnalyticsFullMode; } set { m_enableAnalyticsFullMode = value; } } private string SrcManifestUnityPath { get { return m_srcManifestUnityPath; } set { m_srcManifestUnityPath = value; } } public List AndroidDependencies { get { return m_androidDependencies; } set { m_androidDependencies = value; } } public void Clear() { EnableAnalyticsFullMode = false; SrcManifestUnityPath = DEFAULT_SRC_MANIFEST_UNITY_PATH; if (m_androidDependencies != null) { m_androidDependencies.Clear(); } } public override void Apply(BuildSuite buildSuite) { if (buildSuite != null) { buildSuite.OrderProperty(this); if (EnableAnalyticsFullMode) { UnityBuildProperty unityProperty = UnityBuildProperty.CreateInstanceWithParams($"{name}_unity"); unityProperty.AddDefineSymbol("HT_FIREBASEANALYTICS_FULL"); buildSuite.OrderProperty(unityProperty); } // Android property to handle firebase m2repository PlatformBuildProperty_Android androidProperty = PlatformBuildProperty_Android.CreateInstanceWithParams($"{name}_android"); string rootDirectory = Path.GetDirectoryName(Application.dataPath).Replace("\\", "/"); string m2RepositoryUrl = $"{FILE_PREFIX}{rootDirectory}/{FIREBASE_M2REPOSITORY_PATH}"; androidProperty.AddMavenRepository(m2RepositoryUrl); buildSuite.OrderProperty(androidProperty); } } public void Join(FirebaseBuildProperty p) { if (p != null) { // EnableAnalyticsFullMode if (p.EnableAnalyticsFullMode) { EnableAnalyticsFullMode = p.EnableAnalyticsFullMode; } // Manifest path if (!p.IsSrcManifestUnityPathEmpty()) { if (IsSrcManifestUnityPathEmpty()) { SrcManifestUnityPath = p.SrcManifestUnityPath; } else if (!SrcManifestUnityPath.Equals(p.SrcManifestUnityPath)) { string valuesStr = $"Values: {Debug.FormatTextInUserContext(p.SrcManifestUnityPath)} and {Debug.FormatTextInUserContext(SrcManifestUnityPath)}."; Debug.EditorLogError($"Two {Debug.FormatTextInCodeContext("FirebaseBuildProperty")} properties have been defined but Hotel only supports one. {valuesStr}"); } } // Android dependencies if (p.AndroidDependencies != null) { int count = p.AndroidDependencies.Count; for (int i = 0; i < count; i++) { BuildGradle_AddDependency(p.AndroidDependencies[i].Name, p.AndroidDependencies[i].Version.ToString()); } } } } public void OrderAsset(AssetBuildProperty assetBuildProperty) { if (assetBuildProperty != null) { // If firebase manifest is not required then we need to delete all files automatically generated by Firebase if (!IsSrcManifestUnityPathEmpty()) { // Copy firebase Manifest over Unity in a folder that is added to .gitignore AssetBuildProperty assetProperty = AssetBuildProperty.CreateInstanceWithParams($"{name}_assets"); string fileName = HtAssetDatabase.UnityPathGetFileName(SrcManifestUnityPath); assetProperty.AddOrderItem(SrcManifestUnityPath, $"{DST_MANIFEST_ROOT_UNITY_PATH}/{fileName}"); assetBuildProperty.Join(assetProperty); } } } public void PreBuild() { GenerateManifest(); BuildGradle_PatchFile(); DeleteGeneratedFiles(); } private static void GenerateManifest() { Debug.EditorLog($"Generating Firebase manifest..."); // The installation process of HT FirebaseCore package copied Firebase.Editor.dll over this location string unityPath = "Assets/Hotel/Editor/Verbatim/com.ubisoft.hotel.firebasecore/Firebase/Firebase.Editor.dll"; string platformPath = HtAssetDatabase.UnityPathToPlatformPath(unityPath, true); string error = null; if (File.Exists(platformPath)) { Assembly assembly = Assembly.LoadFile(platformPath); if (assembly == null) { error = $"Assembly {Debug.FormatTextInCodeContext(platformPath)} not found."; } else { Type type = assembly.GetType(FIREBASE_GENERATE_XML_CLASSNAME); if (type == null) { error = $"Class {Debug.FormatTextInCodeContext(FIREBASE_GENERATE_XML_CLASSNAME)} not found."; } else { MethodInfo methodInfo = type.GetMethod(FIREBASE_GENERATE_XML_METHODNAME); if (methodInfo == null) { error = $"Method {Debug.FormatTextInCodeContext(FIREBASE_GENERATE_XML_METHODNAME)} not found."; } else { // Load edm4u dlls explicitly to guarantee that Firebase manifest will be generated successfully LoadEdm4uDlls(); object classInstance = Activator.CreateInstance(type, null); if (classInstance == null) { error = $"Couldn't instantiate {Debug.FormatTextInCodeContext(FIREBASE_GENERATE_XML_CLASSNAME)} class."; } else { ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length == 1) { // Do not trigger popup for bundle id mismatch _ = methodInfo.Invoke(classInstance, new object[] { false }); } else { error = $"The amount of parameters of the method {Debug.FormatTextInCodeContext(FIREBASE_GENERATE_XML_METHODNAME)} doesn't match with the one expected by code."; } } } } } } if (!string.IsNullOrEmpty(error)) { Debug.EditorLogError($"{Debug.FormatTextInCodeContext("FirebaseBuildProperty")} error: {error}"); } } private static void LoadEdm4uDlls() { Debug.EditorLog($"Loading EDM4U dlls..."); string unityPath = HtAssetUtility.GetAssetInPackagePath("com.ubisoft.hotel.edm4u", "Source/ExternalDependencyManager/Editor"); string platformPath = HtAssetDatabase.UnityPathToPlatformPath(unityPath, true); string[] dllFiles = Directory.GetFiles(platformPath, "*.dll", SearchOption.AllDirectories); int count = dllFiles.Length; for (int i = 0; i < count; ++i) { _ = Assembly.LoadFile(dllFiles[i]); } } private void DeleteGeneratedFiles() { DeleteResolvedLibraries(); // If firebase manifest is not required then we need to delete all files automatically generated by Firebase if (IsSrcManifestUnityPathEmpty()) { foreach (string path in UNITY_PATHS_TO_DELETE) { HtAssetDatabase.DeleteAsset(path); } // Delete Firebase*.androidlib folders in Assets/Plugins/Android string[] directories = HtAssetDatabase.DirectoryGetDirectories(PLUGINS_ANDROID_ROOT_UNITY_PATH); if (directories != null) { foreach (string directory in directories) { if (HtAssetDatabase.GetUnityPathLastEntry(directory).StartsWith(FIREBASE_PREFIX)) { HtAssetDatabase.DeleteFolder(directory); } } } } } public void PostBuild() { // Delete libraries resolved by EDM4U before building so they're not pushed to the repository by consumers DeleteResolvedLibraries(); } private bool IsSrcManifestUnityPathEmpty() { return string.IsNullOrEmpty(SrcManifestUnityPath); } private void DeleteResolvedLibraries() { #if HT_PACKAGE_EDM4U_PROD GooglePlayServices.PlayServicesResolver.DeleteResolvedLibraries(); #endif } #region build_gradle private static readonly string BUILD_GRADLE_FILE_NAME = "build.gradle"; private static readonly string BUILD_GRADLE_DST_PATH = $"Assets/Plugins/Android/FirebaseApp.androidlib/{BUILD_GRADLE_FILE_NAME}"; private const string BUILD_GRADLE_DEPENDENCIES_TOKEN = "** HOTEL DEPENDENCIES **"; private const string BUILD_GRADLE_ERROR_MSG = "Error when defining a Firebase Android dependency."; public void BuildGradle_AddDependency(string name, string rawVersion) { if (AndroidDependencies == null) { AndroidDependencies = new List(); } if (string.IsNullOrEmpty(name)) { Debug.EditorLogError($"{BUILD_GRADLE_ERROR_MSG} Name can not be empty."); } else if (string.IsNullOrEmpty(rawVersion)) { Debug.EditorLogError($"{BUILD_GRADLE_ERROR_MSG} Version can not be empty."); } else { Version version = new Version(rawVersion); AndroidDependency dependency = BuildGradle_GetDependency(name); if (dependency == null) { dependency = new AndroidDependency(name, version); AndroidDependencies.Add(dependency); } else { // Keep the highest one if (version.CompareTo(dependency.Version) == 1) { dependency.Version = version; } } } } private AndroidDependency BuildGradle_GetDependency(string name) { AndroidDependency returnValue = null; if (AndroidDependencies != null) { for (int i = AndroidDependencies.Count - 1; i > -1 && returnValue == null; --i) { if (AndroidDependencies[i].Name.Equals(name)) { returnValue = AndroidDependencies[i]; } } } return returnValue; } private string BuildGradles_GetPackageUnityPath() { string assetsDirectoryUnityPath = "Package/Editor/Assets"; #if HT_PACKAGE_DEV return $"Assets/Hotel/Core/{assetsDirectoryUnityPath}"; #else return HtAssetUtility.GetAssetInPackagePath(PACKAGE_NAME, assetsDirectoryUnityPath); #endif } private void BuildGradle_PatchFile() { string dependencies = ""; if (AndroidDependencies != null && AndroidDependencies.Count > 0) { foreach (AndroidDependency dependency in AndroidDependencies) { if (!string.IsNullOrEmpty(dependencies)) { dependencies += "\n\t"; } dependencies += $"implementation '{dependency.Name}:{dependency.Version}'"; } } // Copy the result over App space string srcUnityPath = HtAssetDatabase.UnityPathCombine(BuildGradles_GetPackageUnityPath(), BUILD_GRADLE_FILE_NAME); string srcPlatformPath = HtAssetDatabase.UnityPathToPlatformPath(srcUnityPath); string output = File.ReadAllText(srcPlatformPath); // Add dependencies output = output.Replace(BUILD_GRADLE_DEPENDENCIES_TOKEN, dependencies); // Create dst folder if it doesn't exist string fullDstPath = HtAssetDatabase.UnityPathToPlatformPath(BUILD_GRADLE_DST_PATH); FileInfo fileInfo = new FileInfo(fullDstPath); fileInfo.Directory.Create(); // Save build.gradle into App space File.WriteAllText(fullDstPath, output); } #endregion } }