|
@@ -0,0 +1,487 @@
|
|
|
+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
|
|
|
+ /// <summary>
|
|
|
+ /// * 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.
|
|
|
+ /// </summary>
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Determines if can install the specified packageAsset.
|
|
|
+ /// </summary>
|
|
|
+ /// <returns><c>true</c> if can install the specified packageAsset; otherwise, <c>false</c>.</returns>
|
|
|
+ /// <param name="packageAsset">Package asset.</param>
|
|
|
+ static bool CanInstall(string packageAsset)
|
|
|
+ {
|
|
|
+ return k_IsIAPSupported && AssetExists(packageAsset) &&
|
|
|
+ (k_Purchasing != null || EnableServices()) &&
|
|
|
+ DeleteObsoleteAssets(k_ObsoleteFilesOrDirectories, k_ObsoleteGUIDs);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Detects and fixes the interrupted install.
|
|
|
+ /// </summary>
|
|
|
+ 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<string>();
|
|
|
+ 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<string>();
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 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.
|
|
|
+ /// </summary>
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|