using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEngine; namespace UnityEditor.Purchasing { static class UnityIAPInstaller { static readonly string k_ServiceName = "IAP"; static readonly string k_PackageName = "Unity IAP"; static readonly string k_PackageFile = "Plugins/UnityPurchasing/UnityIAP.unitypackage"; static readonly string k_InstallerFile = "Plugins/UnityPurchasing/Editor/UnityIAPInstaller.cs"; static readonly string k_ObsoleteFilesCSVFile = "Plugins/UnityPurchasing/Editor/ObsoleteFilesOrDir.csv"; static readonly string k_ObsoleteGUIDsCSVFile = "Plugins/UnityPurchasing/Editor/ObsoleteGUIDs.csv"; static readonly string k_IAPHelpURL = "https://docs.unity3d.com/Manual/UnityIAPSettingUp.html"; static readonly string k_ProjectHelpURL = "https://docs.unity3d.com/Manual/SettingUpProjectServices.html"; static readonly string k_PrefsKey_ImportingAssetPackage = "UnityIAPInstaller_ImportingAssetPackage"; // Prevent multiple simultaneous installs static readonly string k_PrefsKey_LastAssetPackageImport = "UnityIAPInstaller_LastAssetPackageImportDateTimeBinary"; static readonly double k_MaxLastImportReasonableTicks = 30 * 10000000; // Installs started n seconds from 'now' are not considered 'simultaneous' static readonly string[] k_ObsoleteFilesOrDirectories = GetFromCSV(GetAbsoluteFilePath(k_ObsoleteFilesCSVFile)); static readonly string[] k_ObsoleteGUIDs = GetFromCSV(GetAbsoluteFilePath(k_ObsoleteGUIDsCSVFile)); static readonly bool k_RunningInBatchMode = Environment.CommandLine.ToLower().Contains(" -batchmode"); static readonly Type k_Purchasing = ( from assembly in AppDomain.CurrentDomain.GetAssemblies() from type in assembly.GetTypes() where type.Name == "UnityPurchasing" && type.GetMethods().Any(m => m.Name == "Initialize") select type).FirstOrDefault(); #if UNITY_5_3 || UNITY_5_3_OR_NEWER static readonly bool k_IsIAPSupported = true; #else static readonly bool k_IsIAPSupported = false; #endif #if UNITY_5_5_OR_NEWER && false // Service window prevents this from working properly. Disabling for now. static readonly bool k_IsEditorSettingsSupported = true; #else static readonly bool k_IsEditorSettingsSupported = false; #endif #if !DISABLE_UNITY_IAP_INSTALLER [Callbacks.DidReloadScripts] #endif /// /// * Install may be called multiple times during the AssetDatabase.ImportPackage /// process. Detect this and avoid restarting installation. /// * Install may fail unexpectedly in the middle due to crash. Detect /// this heuristically with a timestamp, deleting mutex for multiple /// install detector. /// static void Install () { // Detect and fix interrupted installation FixInterruptedInstall(); // Detect multiple calls to this method and ignore if (PlayerPrefs.HasKey(k_PrefsKey_ImportingAssetPackage)) { // Resubscribe to "I'm done installing" callback as it's lost // on each Reload. EditorApplication.delayCall += OnComplete; return; } if (!DisplayInstallerDialog()) { DisplayCanceledInstallerDialog(); OnComplete(); return; } string packageAsset = GetAssetPath(k_PackageFile); if (k_RunningInBatchMode) { Debug.LogFormat("Preparing to install the {0} asset package...", k_PackageName); } if (CanInstall(packageAsset)) { // Record fact installation has started PlayerPrefs.SetInt(k_PrefsKey_ImportingAssetPackage, 1); // Record time installation started PlayerPrefs.SetString(k_PrefsKey_LastAssetPackageImport, DateTime.UtcNow.ToBinary().ToString()); // Start async ImportPackage operation, causing one or more // Domain Reloads as a side-effect AssetDatabase.ImportPackage(packageAsset, false); // All in-memory values hereafter may be cleared due to Domain // Reloads by async ImportPackage operation EditorApplication.delayCall += OnComplete; } else { OnComplete(); } } /// /// Determines if can install the specified packageAsset. /// /// true if can install the specified packageAsset; otherwise, false. /// Package asset. static bool CanInstall(string packageAsset) { return k_IsIAPSupported && AssetExists(packageAsset) && (k_Purchasing != null || EnableServices()) && DeleteObsoleteAssets(k_ObsoleteFilesOrDirectories, k_ObsoleteGUIDs); } /// /// Detects and fixes the interrupted install. /// static void FixInterruptedInstall() { if (PlayerPrefs.HasKey(k_PrefsKey_LastAssetPackageImport)) { string lastImportDateTimeBinary = PlayerPrefs.GetString(k_PrefsKey_LastAssetPackageImport); long lastImportLong = 0; try { lastImportLong = Convert.ToInt64(lastImportDateTimeBinary); } catch (SystemException e) { // Ignoring exception converting long // By default '0' value will trigger install-cleanup } DateTime lastImport = DateTime.FromBinary(lastImportLong); double dt = Math.Abs(DateTime.UtcNow.Ticks - lastImport.Ticks); if (dt > k_MaxLastImportReasonableTicks) { Debug.Log("Detected interrupted installation, " + dt / 10000000 + " seconds ago. Reenabling install."); // Fix it! PlayerPrefs.DeleteKey(k_PrefsKey_ImportingAssetPackage); PlayerPrefs.DeleteKey(k_PrefsKey_LastAssetPackageImport); } else { // dt is not too large, installation okay to proceed } } } static void OnComplete () { if (PlayerPrefs.HasKey(k_PrefsKey_ImportingAssetPackage)) { // Cleanup mutexes for next install PlayerPrefs.DeleteKey(k_PrefsKey_ImportingAssetPackage); PlayerPrefs.DeleteKey(k_PrefsKey_LastAssetPackageImport); if (k_RunningInBatchMode) { Debug.LogFormat("Successfully imported the {0} asset package.", k_PackageName); } } if (k_RunningInBatchMode) { Debug.LogFormat("Deleting {0} package installer files...", k_PackageName); } AssetDatabase.DeleteAsset(GetAssetPath(k_PackageFile)); AssetDatabase.DeleteAsset(GetAssetPath(k_InstallerFile)); AssetDatabase.DeleteAsset(GetAssetPath(k_ObsoleteFilesCSVFile)); AssetDatabase.DeleteAsset(GetAssetPath(k_ObsoleteGUIDsCSVFile)); AssetDatabase.Refresh(); SaveAssets(); if (k_RunningInBatchMode) { Debug.LogFormat("{0} asset package install complete.", k_PackageName); EditorApplication.Exit(0); } } static bool EnableServices () { if (!k_IsEditorSettingsSupported) { if (!DisplayEnableServiceManuallyDialog()) { Application.OpenURL(k_IAPHelpURL); } return false; } if (string.IsNullOrEmpty(PlayerSettings.cloudProjectId)) { if (!DisplayProjectConfigDialog()) { Application.OpenURL(k_ProjectHelpURL); } return false; } if (DisplayEnableServiceDialog()) { #if UNITY_5_5_OR_NEWER Analytics.AnalyticsSettings.enabled = true; PurchasingSettings.enabled = true; #endif SaveAssets(); return true; } if (!DisplayCanceledEnableServiceDialog()) { Application.OpenURL(k_IAPHelpURL); } return false; } static bool DisplayInstallerDialog () { if (k_RunningInBatchMode) return true; return EditorUtility.DisplayDialog( k_PackageName + " Installer", "The " + k_PackageName + " installer will determine if your project is configured properly " + "before importing the " + k_PackageName + " asset package.\n\n" + "Would you like to run the " + k_PackageName + " installer now?", "Install Now", "Cancel" ); } static bool DisplayCanceledInstallerDialog () { if (k_RunningInBatchMode) { Debug.LogFormat("User declined to run the {0} installer. Canceling installer process now...", k_PackageName); return true; } return EditorUtility.DisplayDialog( k_PackageName + " Installer", "The " + k_PackageName + " installer has been canceled. " + "Please import the " + k_PackageName + " asset package again to continue the install.", "OK" ); } static bool DisplayProjectConfigDialog () { if (k_RunningInBatchMode) { Debug.Log("Unity Project ID is not currently set. Canceling installer process now..."); return true; } return EditorUtility.DisplayDialog( k_PackageName + " Installer", "A Unity Project ID is not currently configured for this project.\n\n" + "Before the " + k_ServiceName + " service can be enabled, a Unity Project ID must first be " + "linked to this project. Once linked, please import the " + k_PackageName + " asset package again" + "to continue the install.\n\n" + "Select 'Help...' to see further instructions.", "OK", "Help..." ); } static bool DisplayEnableServiceDialog () { if (k_RunningInBatchMode) { Debug.LogFormat("The {0} service is currently disabled. Enabling the {0} Service now...", k_ServiceName); return true; } return EditorUtility.DisplayDialog( k_PackageName + " Installer", "The " + k_ServiceName + " service is currently disabled.\n\n" + "To avoid encountering errors when importing the " + k_PackageName + " asset package, " + "the " + k_ServiceName + " service must be enabled first before importing the latest " + k_PackageName + " asset package.\n\n" + "Would you like to enable the " + k_ServiceName + " service now?", "Enable Now", "Cancel" ); } static bool DisplayEnableServiceManuallyDialog () { if (k_RunningInBatchMode) { Debug.LogFormat("The {0} service is currently disabled. Canceling installer process now...", k_ServiceName); return true; } return EditorUtility.DisplayDialog( k_PackageName + " Installer", "The " + k_ServiceName + " service is currently disabled.\n\n" + "Canceling the install process now to avoid encountering errors when importing the " + k_PackageName + " asset package. The " + k_ServiceName + " service must be enabled first " + "before importing the latest " + k_PackageName + " asset package.\n\n" + "Please enable the " + k_ServiceName + " service through the Services window. " + "Then import the " + k_PackageName + " asset package again to continue the install.\n\n" + "Select 'Help...' to see further instructions.", "OK", "Help..." ); } static bool DisplayCanceledEnableServiceDialog () { if (k_RunningInBatchMode) { Debug.LogFormat("User declined to enable the {0} service. Canceling installer process now...", k_ServiceName); return true; } return EditorUtility.DisplayDialog( k_PackageName + " Installer", "The " + k_PackageName + " installer has been canceled.\n\n" + "Please enable the " + k_ServiceName + " service through the Services window. " + "Then import the " + k_PackageName + " asset package again to continue the install.\n\n" + "Select 'Help...' to see further instructions.", "OK", "Help..." ); } static bool DisplayDeleteAssetsDialog () { if (k_RunningInBatchMode) { Debug.LogFormat("Found obsolete {0} assets. Deleting obsolete assets now...", k_PackageName); return true; } return EditorUtility.DisplayDialog( k_PackageName + " Installer", "Found obsolete assets from an older version of the " + k_PackageName + " asset package.\n\n" + "Would you like to remove these obsolete " + k_PackageName + " assets now?", "Delete Now", "Cancel" ); } static bool DisplayCanceledDeleteAssetsDialog () { if (k_RunningInBatchMode) { Debug.LogFormat("User declined to remove obsolete {0} assets. Canceling installer process now...", k_PackageName); return true; } return EditorUtility.DisplayDialog( k_PackageName + " Installer", "The " + k_PackageName + " installer has been canceled.\n\n" + "Please delete any previously imported " + k_PackageName + " assets from your project. " + "Then import the " + k_PackageName + " asset package again to continue the install.", "OK" ); } static string GetAssetPath (string path) { return string.Concat("Assets/", path); } static string GetAbsoluteFilePath (string path) { return Path.Combine(Application.dataPath, path.Replace('/', Path.DirectorySeparatorChar)); } static string[] GetFromCSV (string filePath) { var lines = new List(); int row = 0; if (File.Exists(filePath)) { try { using (var reader = new StreamReader(filePath)) { while (!reader.EndOfStream) { string[] line = reader.ReadLine().Split(','); lines.Add(line[0].Trim().Trim('"')); row++; } } } catch (Exception e) { Debug.LogException(e); } } return lines.ToArray(); } static bool AssetExists (string path) { if (path.Length > 7) path = path.Substring(7); else return false; if (Application.platform == RuntimePlatform.WindowsEditor) { path = path.Replace("/", @"\"); } path = Path.Combine(Application.dataPath, path); return File.Exists(path) || Directory.Exists(path); } static bool AssetsExist (string[] legacyAssetPaths, string[] legacyAssetGUIDs, out string[] existingAssetPaths) { var paths = new List(); for (int i = 0; i < legacyAssetPaths.Length; i++) { if (AssetExists(legacyAssetPaths[i])) { paths.Add(legacyAssetPaths[i]); } } for (int i = 0; i < legacyAssetGUIDs.Length; i++) { string path = AssetDatabase.GUIDToAssetPath(legacyAssetGUIDs[i]); if (AssetExists(path) && !paths.Contains(path)) { paths.Add(path); } } existingAssetPaths = paths.ToArray(); return paths.Count > 0; } static bool DeleteObsoleteAssets (string[] paths, string[] guids) { var assets = new string[0]; if (!AssetsExist(paths, guids, out assets)) return true; if (DisplayDeleteAssetsDialog()) { for (int i = 0; i < assets.Length; i++) { FileUtil.DeleteFileOrDirectory(assets[i]); } AssetDatabase.Refresh(); SaveAssets(); return true; } DisplayCanceledDeleteAssetsDialog(); return false; } /// /// Solves issues seen in projects when deleting other files in projects /// after installation but before project is closed and reopened. /// Script continue to live as compiled entities but are not stored in /// the AssetDatabase. /// static void SaveAssets () { #if UNITY_5_5_OR_NEWER AssetDatabase.SaveAssets(); // Not reliable prior to major refactoring in Unity 5.5. #else EditorApplication.SaveAssets(); // Reliable, but removed in Unity 5.5. #endif } } }