UnityIAPInstaller.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using UnityEngine;
  6. namespace UnityEditor.Purchasing
  7. {
  8. static class UnityIAPInstaller
  9. {
  10. static readonly string k_ServiceName = "IAP";
  11. static readonly string k_PackageName = "Unity IAP";
  12. static readonly string k_PackageFile = "Plugins/UnityPurchasing/UnityIAP.unitypackage";
  13. static readonly string k_InstallerFile = "Plugins/UnityPurchasing/Editor/UnityIAPInstaller.cs";
  14. static readonly string k_ObsoleteFilesCSVFile = "Plugins/UnityPurchasing/Editor/ObsoleteFilesOrDir.csv";
  15. static readonly string k_ObsoleteGUIDsCSVFile = "Plugins/UnityPurchasing/Editor/ObsoleteGUIDs.csv";
  16. static readonly string k_IAPHelpURL = "https://docs.unity3d.com/Manual/UnityIAPSettingUp.html";
  17. static readonly string k_ProjectHelpURL = "https://docs.unity3d.com/Manual/SettingUpProjectServices.html";
  18. static readonly string k_PrefsKey_ImportingAssetPackage = "UnityIAPInstaller_ImportingAssetPackage"; // Prevent multiple simultaneous installs
  19. static readonly string k_PrefsKey_LastAssetPackageImport = "UnityIAPInstaller_LastAssetPackageImportDateTimeBinary";
  20. static readonly double k_MaxLastImportReasonableTicks = 30 * 10000000; // Installs started n seconds from 'now' are not considered 'simultaneous'
  21. static readonly string[] k_ObsoleteFilesOrDirectories = GetFromCSV(GetAbsoluteFilePath(k_ObsoleteFilesCSVFile));
  22. static readonly string[] k_ObsoleteGUIDs = GetFromCSV(GetAbsoluteFilePath(k_ObsoleteGUIDsCSVFile));
  23. static readonly bool k_RunningInBatchMode = Environment.CommandLine.ToLower().Contains(" -batchmode");
  24. static readonly Type k_Purchasing = (
  25. from assembly in AppDomain.CurrentDomain.GetAssemblies()
  26. from type in assembly.GetTypes()
  27. where type.Name == "UnityPurchasing" && type.GetMethods().Any(m => m.Name == "Initialize")
  28. select type).FirstOrDefault();
  29. #if UNITY_5_3 || UNITY_5_3_OR_NEWER
  30. static readonly bool k_IsIAPSupported = true;
  31. #else
  32. static readonly bool k_IsIAPSupported = false;
  33. #endif
  34. #if UNITY_5_5_OR_NEWER && false // Service window prevents this from working properly. Disabling for now.
  35. static readonly bool k_IsEditorSettingsSupported = true;
  36. #else
  37. static readonly bool k_IsEditorSettingsSupported = false;
  38. #endif
  39. #if !DISABLE_UNITY_IAP_INSTALLER
  40. [Callbacks.DidReloadScripts]
  41. #endif
  42. /// <summary>
  43. /// * Install may be called multiple times during the AssetDatabase.ImportPackage
  44. /// process. Detect this and avoid restarting installation.
  45. /// * Install may fail unexpectedly in the middle due to crash. Detect
  46. /// this heuristically with a timestamp, deleting mutex for multiple
  47. /// install detector.
  48. /// </summary>
  49. static void Install ()
  50. {
  51. // Detect and fix interrupted installation
  52. FixInterruptedInstall();
  53. // Detect multiple calls to this method and ignore
  54. if (PlayerPrefs.HasKey(k_PrefsKey_ImportingAssetPackage))
  55. {
  56. // Resubscribe to "I'm done installing" callback as it's lost
  57. // on each Reload.
  58. EditorApplication.delayCall += OnComplete;
  59. return;
  60. }
  61. if (!DisplayInstallerDialog())
  62. {
  63. DisplayCanceledInstallerDialog();
  64. OnComplete();
  65. return;
  66. }
  67. string packageAsset = GetAssetPath(k_PackageFile);
  68. if (k_RunningInBatchMode)
  69. {
  70. Debug.LogFormat("Preparing to install the {0} asset package...", k_PackageName);
  71. }
  72. if (CanInstall(packageAsset))
  73. {
  74. // Record fact installation has started
  75. PlayerPrefs.SetInt(k_PrefsKey_ImportingAssetPackage, 1);
  76. // Record time installation started
  77. PlayerPrefs.SetString(k_PrefsKey_LastAssetPackageImport, DateTime.UtcNow.ToBinary().ToString());
  78. // Start async ImportPackage operation, causing one or more
  79. // Domain Reloads as a side-effect
  80. AssetDatabase.ImportPackage(packageAsset, false);
  81. // All in-memory values hereafter may be cleared due to Domain
  82. // Reloads by async ImportPackage operation
  83. EditorApplication.delayCall += OnComplete;
  84. }
  85. else
  86. {
  87. OnComplete();
  88. }
  89. }
  90. /// <summary>
  91. /// Determines if can install the specified packageAsset.
  92. /// </summary>
  93. /// <returns><c>true</c> if can install the specified packageAsset; otherwise, <c>false</c>.</returns>
  94. /// <param name="packageAsset">Package asset.</param>
  95. static bool CanInstall(string packageAsset)
  96. {
  97. return k_IsIAPSupported && AssetExists(packageAsset) &&
  98. (k_Purchasing != null || EnableServices()) &&
  99. DeleteObsoleteAssets(k_ObsoleteFilesOrDirectories, k_ObsoleteGUIDs);
  100. }
  101. /// <summary>
  102. /// Detects and fixes the interrupted install.
  103. /// </summary>
  104. static void FixInterruptedInstall()
  105. {
  106. if (PlayerPrefs.HasKey(k_PrefsKey_LastAssetPackageImport))
  107. {
  108. string lastImportDateTimeBinary = PlayerPrefs.GetString(k_PrefsKey_LastAssetPackageImport);
  109. long lastImportLong = 0;
  110. try {
  111. lastImportLong = Convert.ToInt64(lastImportDateTimeBinary);
  112. } catch (SystemException e) {
  113. // Ignoring exception converting long
  114. // By default '0' value will trigger install-cleanup
  115. }
  116. DateTime lastImport = DateTime.FromBinary(lastImportLong);
  117. double dt = Math.Abs(DateTime.UtcNow.Ticks - lastImport.Ticks);
  118. if (dt > k_MaxLastImportReasonableTicks)
  119. {
  120. Debug.Log("Detected interrupted installation, " + dt / 10000000 + " seconds ago. Reenabling install.");
  121. // Fix it!
  122. PlayerPrefs.DeleteKey(k_PrefsKey_ImportingAssetPackage);
  123. PlayerPrefs.DeleteKey(k_PrefsKey_LastAssetPackageImport);
  124. }
  125. else
  126. {
  127. // dt is not too large, installation okay to proceed
  128. }
  129. }
  130. }
  131. static void OnComplete ()
  132. {
  133. if (PlayerPrefs.HasKey(k_PrefsKey_ImportingAssetPackage))
  134. {
  135. // Cleanup mutexes for next install
  136. PlayerPrefs.DeleteKey(k_PrefsKey_ImportingAssetPackage);
  137. PlayerPrefs.DeleteKey(k_PrefsKey_LastAssetPackageImport);
  138. if (k_RunningInBatchMode)
  139. {
  140. Debug.LogFormat("Successfully imported the {0} asset package.", k_PackageName);
  141. }
  142. }
  143. if (k_RunningInBatchMode)
  144. {
  145. Debug.LogFormat("Deleting {0} package installer files...", k_PackageName);
  146. }
  147. AssetDatabase.DeleteAsset(GetAssetPath(k_PackageFile));
  148. AssetDatabase.DeleteAsset(GetAssetPath(k_InstallerFile));
  149. AssetDatabase.DeleteAsset(GetAssetPath(k_ObsoleteFilesCSVFile));
  150. AssetDatabase.DeleteAsset(GetAssetPath(k_ObsoleteGUIDsCSVFile));
  151. AssetDatabase.Refresh();
  152. SaveAssets();
  153. if (k_RunningInBatchMode)
  154. {
  155. Debug.LogFormat("{0} asset package install complete.", k_PackageName);
  156. EditorApplication.Exit(0);
  157. }
  158. }
  159. static bool EnableServices ()
  160. {
  161. if (!k_IsEditorSettingsSupported)
  162. {
  163. if (!DisplayEnableServiceManuallyDialog())
  164. {
  165. Application.OpenURL(k_IAPHelpURL);
  166. }
  167. return false;
  168. }
  169. if (string.IsNullOrEmpty(PlayerSettings.cloudProjectId))
  170. {
  171. if (!DisplayProjectConfigDialog())
  172. {
  173. Application.OpenURL(k_ProjectHelpURL);
  174. }
  175. return false;
  176. }
  177. if (DisplayEnableServiceDialog())
  178. {
  179. #if UNITY_5_5_OR_NEWER
  180. Analytics.AnalyticsSettings.enabled = true;
  181. PurchasingSettings.enabled = true;
  182. #endif
  183. SaveAssets();
  184. return true;
  185. }
  186. if (!DisplayCanceledEnableServiceDialog())
  187. {
  188. Application.OpenURL(k_IAPHelpURL);
  189. }
  190. return false;
  191. }
  192. static bool DisplayInstallerDialog ()
  193. {
  194. if (k_RunningInBatchMode) return true;
  195. return EditorUtility.DisplayDialog(
  196. k_PackageName + " Installer",
  197. "The " + k_PackageName + " installer will determine if your project is configured properly " +
  198. "before importing the " + k_PackageName + " asset package.\n\n" +
  199. "Would you like to run the " + k_PackageName + " installer now?",
  200. "Install Now",
  201. "Cancel"
  202. );
  203. }
  204. static bool DisplayCanceledInstallerDialog ()
  205. {
  206. if (k_RunningInBatchMode)
  207. {
  208. Debug.LogFormat("User declined to run the {0} installer. Canceling installer process now...", k_PackageName);
  209. return true;
  210. }
  211. return EditorUtility.DisplayDialog(
  212. k_PackageName + " Installer",
  213. "The " + k_PackageName + " installer has been canceled. " +
  214. "Please import the " + k_PackageName + " asset package again to continue the install.",
  215. "OK"
  216. );
  217. }
  218. static bool DisplayProjectConfigDialog ()
  219. {
  220. if (k_RunningInBatchMode)
  221. {
  222. Debug.Log("Unity Project ID is not currently set. Canceling installer process now...");
  223. return true;
  224. }
  225. return EditorUtility.DisplayDialog(
  226. k_PackageName + " Installer",
  227. "A Unity Project ID is not currently configured for this project.\n\n" +
  228. "Before the " + k_ServiceName + " service can be enabled, a Unity Project ID must first be " +
  229. "linked to this project. Once linked, please import the " + k_PackageName + " asset package again" +
  230. "to continue the install.\n\n" +
  231. "Select 'Help...' to see further instructions.",
  232. "OK",
  233. "Help..."
  234. );
  235. }
  236. static bool DisplayEnableServiceDialog ()
  237. {
  238. if (k_RunningInBatchMode)
  239. {
  240. Debug.LogFormat("The {0} service is currently disabled. Enabling the {0} Service now...", k_ServiceName);
  241. return true;
  242. }
  243. return EditorUtility.DisplayDialog(
  244. k_PackageName + " Installer",
  245. "The " + k_ServiceName + " service is currently disabled.\n\n" +
  246. "To avoid encountering errors when importing the " + k_PackageName + " asset package, " +
  247. "the " + k_ServiceName + " service must be enabled first before importing the latest " +
  248. k_PackageName + " asset package.\n\n" +
  249. "Would you like to enable the " + k_ServiceName + " service now?",
  250. "Enable Now",
  251. "Cancel"
  252. );
  253. }
  254. static bool DisplayEnableServiceManuallyDialog ()
  255. {
  256. if (k_RunningInBatchMode)
  257. {
  258. Debug.LogFormat("The {0} service is currently disabled. Canceling installer process now...", k_ServiceName);
  259. return true;
  260. }
  261. return EditorUtility.DisplayDialog(
  262. k_PackageName + " Installer",
  263. "The " + k_ServiceName + " service is currently disabled.\n\n" +
  264. "Canceling the install process now to avoid encountering errors when importing the " +
  265. k_PackageName + " asset package. The " + k_ServiceName + " service must be enabled first " +
  266. "before importing the latest " + k_PackageName + " asset package.\n\n" +
  267. "Please enable the " + k_ServiceName + " service through the Services window. " +
  268. "Then import the " + k_PackageName + " asset package again to continue the install.\n\n" +
  269. "Select 'Help...' to see further instructions.",
  270. "OK",
  271. "Help..."
  272. );
  273. }
  274. static bool DisplayCanceledEnableServiceDialog ()
  275. {
  276. if (k_RunningInBatchMode)
  277. {
  278. Debug.LogFormat("User declined to enable the {0} service. Canceling installer process now...", k_ServiceName);
  279. return true;
  280. }
  281. return EditorUtility.DisplayDialog(
  282. k_PackageName + " Installer",
  283. "The " + k_PackageName + " installer has been canceled.\n\n" +
  284. "Please enable the " + k_ServiceName + " service through the Services window. " +
  285. "Then import the " + k_PackageName + " asset package again to continue the install.\n\n" +
  286. "Select 'Help...' to see further instructions.",
  287. "OK",
  288. "Help..."
  289. );
  290. }
  291. static bool DisplayDeleteAssetsDialog ()
  292. {
  293. if (k_RunningInBatchMode)
  294. {
  295. Debug.LogFormat("Found obsolete {0} assets. Deleting obsolete assets now...", k_PackageName);
  296. return true;
  297. }
  298. return EditorUtility.DisplayDialog(
  299. k_PackageName + " Installer",
  300. "Found obsolete assets from an older version of the " + k_PackageName + " asset package.\n\n" +
  301. "Would you like to remove these obsolete " + k_PackageName + " assets now?",
  302. "Delete Now",
  303. "Cancel"
  304. );
  305. }
  306. static bool DisplayCanceledDeleteAssetsDialog ()
  307. {
  308. if (k_RunningInBatchMode)
  309. {
  310. Debug.LogFormat("User declined to remove obsolete {0} assets. Canceling installer process now...", k_PackageName);
  311. return true;
  312. }
  313. return EditorUtility.DisplayDialog(
  314. k_PackageName + " Installer",
  315. "The " + k_PackageName + " installer has been canceled.\n\n" +
  316. "Please delete any previously imported " + k_PackageName + " assets from your project. " +
  317. "Then import the " + k_PackageName + " asset package again to continue the install.",
  318. "OK"
  319. );
  320. }
  321. static string GetAssetPath (string path)
  322. {
  323. return string.Concat("Assets/", path);
  324. }
  325. static string GetAbsoluteFilePath (string path)
  326. {
  327. return Path.Combine(Application.dataPath, path.Replace('/', Path.DirectorySeparatorChar));
  328. }
  329. static string[] GetFromCSV (string filePath)
  330. {
  331. var lines = new List<string>();
  332. int row = 0;
  333. if (File.Exists(filePath))
  334. {
  335. try
  336. {
  337. using (var reader = new StreamReader(filePath))
  338. {
  339. while (!reader.EndOfStream)
  340. {
  341. string[] line = reader.ReadLine().Split(',');
  342. lines.Add(line[0].Trim().Trim('"'));
  343. row++;
  344. }
  345. }
  346. }
  347. catch (Exception e)
  348. {
  349. Debug.LogException(e);
  350. }
  351. }
  352. return lines.ToArray();
  353. }
  354. static bool AssetExists (string path)
  355. {
  356. if (path.Length > 7)
  357. path = path.Substring(7);
  358. else return false;
  359. if (Application.platform == RuntimePlatform.WindowsEditor)
  360. {
  361. path = path.Replace("/", @"\");
  362. }
  363. path = Path.Combine(Application.dataPath, path);
  364. return File.Exists(path) || Directory.Exists(path);
  365. }
  366. static bool AssetsExist (string[] legacyAssetPaths, string[] legacyAssetGUIDs, out string[] existingAssetPaths)
  367. {
  368. var paths = new List<string>();
  369. for (int i = 0; i < legacyAssetPaths.Length; i++)
  370. {
  371. if (AssetExists(legacyAssetPaths[i]))
  372. {
  373. paths.Add(legacyAssetPaths[i]);
  374. }
  375. }
  376. for (int i = 0; i < legacyAssetGUIDs.Length; i++)
  377. {
  378. string path = AssetDatabase.GUIDToAssetPath(legacyAssetGUIDs[i]);
  379. if (AssetExists(path) && !paths.Contains(path))
  380. {
  381. paths.Add(path);
  382. }
  383. }
  384. existingAssetPaths = paths.ToArray();
  385. return paths.Count > 0;
  386. }
  387. static bool DeleteObsoleteAssets (string[] paths, string[] guids)
  388. {
  389. var assets = new string[0];
  390. if (!AssetsExist(paths, guids, out assets)) return true;
  391. if (DisplayDeleteAssetsDialog())
  392. {
  393. for (int i = 0; i < assets.Length; i++)
  394. {
  395. FileUtil.DeleteFileOrDirectory(assets[i]);
  396. }
  397. AssetDatabase.Refresh();
  398. SaveAssets();
  399. return true;
  400. }
  401. DisplayCanceledDeleteAssetsDialog();
  402. return false;
  403. }
  404. /// <summary>
  405. /// Solves issues seen in projects when deleting other files in projects
  406. /// after installation but before project is closed and reopened.
  407. /// Script continue to live as compiled entities but are not stored in
  408. /// the AssetDatabase.
  409. /// </summary>
  410. static void SaveAssets ()
  411. {
  412. #if UNITY_5_5_OR_NEWER
  413. AssetDatabase.SaveAssets(); // Not reliable prior to major refactoring in Unity 5.5.
  414. #else
  415. EditorApplication.SaveAssets(); // Reliable, but removed in Unity 5.5.
  416. #endif
  417. }
  418. }
  419. }