暫無描述
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UnityPurchasingEditor.cs 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using UnityEditor.Callbacks;
  7. using UnityEditor.Connect;
  8. using UnityEditor.PackageManager;
  9. using UnityEditor.PackageManager.Requests;
  10. using UnityEngine;
  11. using UnityEngine.Purchasing;
  12. namespace UnityEditor.Purchasing
  13. {
  14. /// <summary>
  15. /// Editor tools to set build-time configurations for app stores.
  16. /// </summary>
  17. [InitializeOnLoad]
  18. public static class UnityPurchasingEditor
  19. {
  20. const string PurchasingPackageName = "com.unity.purchasing";
  21. [Obsolete("Internal API to be removed with UDP deprecation.")]
  22. internal const string UdpPackageName = "com.unity.purchasing.udp";
  23. [Obsolete("Internal API to be removed with UDP deprecation.")]
  24. const string k_UdpErrorText = "In order to use UDP functionality, you must install or update the Unity Distribution Portal Package. Please configure your project's packages before running UDP-related editor commands in batch mode.";
  25. const string ModePath = "Assets/Resources/BillingMode.json";
  26. const string prevModePath = "Assets/Plugins/UnityPurchasing/Resources/BillingMode.json";
  27. [Obsolete("Internal API to be removed with UDP deprecation.")]
  28. static ListRequest m_ListRequestOfDependentPackages;
  29. [Obsolete("Internal API to be removed with UDP deprecation.")]
  30. static SearchRequest m_SearchRequestOfAvailablePackages;
  31. [Obsolete("Internal API to be removed with UDP deprecation.")]
  32. static bool m_UdpUpmPackageInstalled;
  33. [Obsolete("Internal API to be removed with UDP deprecation.")]
  34. static bool m_UdpUpmPackageAvailable;
  35. const string BinPath = "Packages/com.unity.purchasing/Plugins/UnityPurchasing/Android";
  36. [Obsolete("Internal API to be removed with UDP deprecation.")]
  37. const string AssetStoreUdpBinPath = "Assets/Plugins/UDP/Android";
  38. [Obsolete("Internal API to be removed with UDP deprecation.")]
  39. static readonly string PackManUdpBinPath = $"Packages/{UdpPackageName}/Android";
  40. static StoreConfiguration config;
  41. static readonly AppStore defaultAppStore = AppStore.GooglePlay;
  42. internal delegate void AndroidTargetChange(AppStore store);
  43. internal static AndroidTargetChange OnAndroidTargetChange;
  44. [Obsolete("Internal API to be removed with UDP deprecation.")]
  45. static readonly bool s_udpAvailable = UdpSynchronizationApi.CheckUdpAvailability();
  46. [Obsolete("Internal API to be removed with UDP deprecation.")]
  47. internal static bool IsUdpUpmPackageInstalled()
  48. {
  49. return m_UdpUpmPackageInstalled || File.Exists($"Packages/{UdpPackageName}/package.json");
  50. }
  51. [Obsolete("Internal API to be removed with UDP deprecation.")]
  52. static void ListingCurrentPackageProgress()
  53. {
  54. if (m_ListRequestOfDependentPackages.IsCompleted)
  55. {
  56. m_UdpUpmPackageInstalled = false;
  57. EditorApplication.update -= ListingCurrentPackageProgress;
  58. if (m_ListRequestOfDependentPackages.Status == StatusCode.Success)
  59. {
  60. var udpPackage = m_ListRequestOfDependentPackages.Result.FirstOrDefault(package => package.name == UdpPackageName);
  61. m_UdpUpmPackageInstalled = udpPackage != null;
  62. }
  63. else if (m_ListRequestOfDependentPackages.Status >= StatusCode.Failure)
  64. {
  65. Debug.LogError(m_ListRequestOfDependentPackages.Error.message);
  66. }
  67. if (!m_UdpUpmPackageInstalled)
  68. {
  69. CheckUdpUpmPackageAvailableViaPackageManager();
  70. }
  71. }
  72. }
  73. [Obsolete("Internal API to be removed with UDP deprecation.")]
  74. static void SearchingAvailablePackageProgress()
  75. {
  76. if (m_SearchRequestOfAvailablePackages.IsCompleted)
  77. {
  78. m_UdpUpmPackageAvailable = false;
  79. EditorApplication.update -= SearchingAvailablePackageProgress;
  80. if (m_SearchRequestOfAvailablePackages.Status == StatusCode.Success)
  81. {
  82. var udpPackage = m_SearchRequestOfAvailablePackages.Result.FirstOrDefault(package => package.name == UdpPackageName);
  83. m_UdpUpmPackageAvailable = udpPackage != null;
  84. }
  85. else if (m_SearchRequestOfAvailablePackages.Status >= StatusCode.Failure)
  86. {
  87. Debug.LogError(m_SearchRequestOfAvailablePackages.Error.message);
  88. }
  89. }
  90. }
  91. [Obsolete("Internal API to be removed with UDP deprecation.")]
  92. internal static bool IsUdpAssetStorePackageInstalled()
  93. {
  94. return File.Exists("Assets/UDP/UDP.dll") || File.Exists("Assets/Plugins/UDP/UDP.dll");
  95. }
  96. [Obsolete("Internal API to be removed with UDP deprecation.")]
  97. [InitializeOnLoadMethod]
  98. static void CheckUdpUpmPackageInstalled()
  99. {
  100. if (IsInBatchMode())
  101. {
  102. CheckUdpUpmPackageInstalledViaManifest();
  103. }
  104. else
  105. {
  106. CheckUdpUpmPackageInstalledViaPackageManager();
  107. }
  108. }
  109. static bool IsInBatchMode()
  110. {
  111. return UnityEditorInternal.InternalEditorUtility.inBatchMode;
  112. }
  113. [Obsolete("Internal API to be removed with UDP deprecation.")]
  114. static void CheckUdpUpmPackageInstalledViaPackageManager()
  115. {
  116. if (IsInBatchMode())
  117. {
  118. Debug.unityLogger.LogIAPError("CheckUdpUmpPackageInstalledViaPackageManager will always fail in Batch Mode. Call CheckUdpUmpPackageInstalledViaManifest instead");
  119. }
  120. m_ListRequestOfDependentPackages = Client.List();
  121. EditorApplication.update += ListingCurrentPackageProgress;
  122. }
  123. [Obsolete("Internal API to be removed with UDP deprecation.")]
  124. static void CheckUdpUpmPackageInstalledViaManifest()
  125. {
  126. if (!IsInBatchMode())
  127. {
  128. Debug.unityLogger.LogIAPWarning("When not running in batch mode, it's more reliable to check the presence of UDP via CheckUdpUmpPackageInstalledViaPackageManager, in case the manifest file is out of date.");
  129. }
  130. m_UdpUpmPackageInstalled = false;
  131. if (File.Exists("Packages/manifest.json"))
  132. {
  133. var jsonText = File.ReadAllText("Packages/manifest.json");
  134. m_UdpUpmPackageInstalled = jsonText.Contains(UdpPackageName);
  135. }
  136. }
  137. [Obsolete("Internal API to be removed with UDP deprecation.")]
  138. static void CheckUdpUpmPackageAvailableViaPackageManager()
  139. {
  140. if (IsInBatchMode())
  141. {
  142. Debug.unityLogger.LogIAPError("CheckUdpUpmPackageAvailableViaPackageManager will always fail in Batch Mode.");
  143. }
  144. m_SearchRequestOfAvailablePackages = Client.SearchAll();
  145. EditorApplication.update += SearchingAvailablePackageProgress;
  146. }
  147. /// <summary>
  148. /// Since we are changing the billing mode's location, it may be necessary to migrate existing billing
  149. /// mode file to the new location.
  150. /// </summary>
  151. [InitializeOnLoadMethod]
  152. internal static void MigrateBillingMode()
  153. {
  154. try
  155. {
  156. var file = new FileInfo(ModePath);
  157. // This will create the new billing file location, if it already exists, this will not do anything.
  158. file.Directory.Create();
  159. // See if the file already exists in the new location.
  160. if (File.Exists(ModePath))
  161. {
  162. return;
  163. }
  164. // check if the old exists before moving it
  165. if (DoesPrevModePathExist())
  166. {
  167. AssetDatabase.MoveAsset(prevModePath, ModePath);
  168. }
  169. }
  170. catch (Exception ex)
  171. {
  172. Debug.LogException(ex);
  173. }
  174. }
  175. internal static bool DoesPrevModePathExist()
  176. {
  177. return File.Exists(prevModePath);
  178. }
  179. // Notice: Multiple files per target supported. While Key must be unique, Value can be duplicated!
  180. static readonly Dictionary<string, AppStore> StoreSpecificFiles = new Dictionary<string, AppStore>()
  181. {
  182. {"billing-5.2.1.aar", AppStore.GooglePlay},
  183. {"AmazonAppStore.aar", AppStore.AmazonAppStore}
  184. };
  185. [Obsolete("Internal API to be removed with UDP deprecation.")]
  186. static readonly Dictionary<string, AppStore> UdpSpecificFiles = new Dictionary<string, AppStore>() {
  187. { "udp.aar", AppStore.UDP},
  188. { "udpsandbox.aar", AppStore.UDP},
  189. { "utils.aar", AppStore.UDP}
  190. };
  191. // Create or read BillingMode.json at Project Editor load
  192. static UnityPurchasingEditor()
  193. {
  194. EditorApplication.delayCall += () =>
  195. {
  196. if (File.Exists(ModePath))
  197. {
  198. var oldAppStore = GetAppStoreSafe();
  199. config = StoreConfiguration.Deserialize(File.ReadAllText(ModePath));
  200. if (oldAppStore != config.androidStore)
  201. {
  202. OnAndroidTargetChange?.Invoke(config.androidStore);
  203. }
  204. }
  205. else
  206. {
  207. CreateDefaultBillingModeFile();
  208. }
  209. };
  210. }
  211. static void CreateDefaultBillingModeFile()
  212. {
  213. TargetAndroidStore(defaultAppStore);
  214. }
  215. #if !ENABLE_EDITOR_GAME_SERVICES
  216. const string SwitchStoreMenuItem = IapMenuConsts.MenuItemRoot + "/Switch Store...";
  217. [MenuItem(SwitchStoreMenuItem, false, 200)]
  218. static void OnSwitchStoreMenu()
  219. {
  220. var window = EditorWindow.GetWindow(typeof(SwitchStoreEditorWindow));
  221. window.titleContent.text = IapMenuConsts.SwitchStoreTitleText;
  222. window.minSize = new Vector2(340, 180);
  223. window.Show();
  224. GameServicesEventSenderHelpers.SendTopMenuSwitchStoreEvent();
  225. }
  226. #else
  227. const string SwitchStoreMenuItem = IapMenuConsts.MenuItemRoot + "/Configure...";
  228. #endif
  229. private static AppStore GetAppStoreSafe()
  230. {
  231. var store = AppStore.NotSpecified;
  232. if (config != null)
  233. {
  234. store = config.androidStore;
  235. }
  236. return store;
  237. }
  238. /// <summary>
  239. /// Target a specified Android store.
  240. /// This sets the correct plugin importer settings for the store
  241. /// and writes the choice to BillingMode.json so the player
  242. /// can choose the correct store API at runtime.
  243. /// Note: This can fail if preconditions are not met for the AppStore.UDP target.
  244. /// </summary>
  245. /// <param name="target">App store to enable for next build</param>
  246. public static void TargetAndroidStore(AppStore target)
  247. {
  248. TryTargetAndroidStore(target);
  249. }
  250. internal static AppStore TryTargetAndroidStore(AppStore target)
  251. {
  252. if (!target.IsAndroid())
  253. {
  254. throw new ArgumentException(string.Format("AppStore parameter ({0}) must be an Android app store", target));
  255. }
  256. if (target == AppStore.UDP)
  257. {
  258. if (CheckAndHandleUdpUnavailability())
  259. {
  260. return ConfiguredAppStore();
  261. }
  262. }
  263. ConfigureProject(target);
  264. SaveConfig(target);
  265. OnAndroidTargetChange?.Invoke(target);
  266. var targetString = Enum.GetName(typeof(AppStore), target);
  267. GenericEditorDropdownSelectEventSenderHelpers.SendIapMenuSelectTargetStoreEvent(targetString);
  268. return ConfiguredAppStore();
  269. }
  270. [Obsolete("Internal API to be removed with UDP deprecation.")]
  271. static bool CheckAndHandleUdpUnavailability()
  272. {
  273. if (!s_udpAvailable || (!IsUdpUpmPackageInstalled() && !IsUdpAssetStorePackageInstalled()) || !UdpSynchronizationApi.CheckUdpCompatibility())
  274. {
  275. if (IsInBatchMode())
  276. {
  277. Debug.unityLogger.LogIAPError(k_UdpErrorText);
  278. }
  279. else
  280. {
  281. if (m_UdpUpmPackageAvailable)
  282. {
  283. UdpInstaller.PromptUdpInstallation();
  284. }
  285. else
  286. {
  287. UdpInstaller.PromptUdpUnavailability();
  288. }
  289. }
  290. return true;
  291. }
  292. return false;
  293. }
  294. // Unfortunately the UnityEditor API updates only the in-memory list of
  295. // files available to the build when what we want is a persistent modification
  296. // to the .meta files. So we must also rely upon the PostProcessScene attribute
  297. // below to process the
  298. private static void ConfigureProject(AppStore target)
  299. {
  300. foreach (var mapping in StoreSpecificFiles)
  301. {
  302. // All files enabled when store is determined at runtime.
  303. var enabled = target == AppStore.NotSpecified;
  304. // Otherwise this file must be needed on the target.
  305. enabled |= mapping.Value == target;
  306. var path = string.Format("{0}/{1}", BinPath, mapping.Key);
  307. var importer = (PluginImporter)AssetImporter.GetAtPath(path);
  308. if (importer != null)
  309. {
  310. importer.SetCompatibleWithPlatform(BuildTarget.Android, enabled);
  311. }
  312. else
  313. {
  314. // Search for any occurrence of this file
  315. // Only fail if more than one found
  316. var paths = FindPaths(mapping.Key);
  317. if (paths.Length == 1)
  318. {
  319. importer = (PluginImporter)AssetImporter.GetAtPath(paths[0]);
  320. importer.SetCompatibleWithPlatform(BuildTarget.Android, enabled);
  321. }
  322. }
  323. }
  324. ConfigureProjectForUdp(target);
  325. }
  326. [Obsolete("Internal API to be removed with UDP deprecation.")]
  327. static void ConfigureProjectForUdp(AppStore target)
  328. {
  329. var UdpBinPath = IsUdpUpmPackageInstalled() ? PackManUdpBinPath :
  330. IsUdpAssetStorePackageInstalled() ? AssetStoreUdpBinPath :
  331. null;
  332. if (s_udpAvailable && !string.IsNullOrEmpty(UdpBinPath))
  333. {
  334. foreach (var mapping in UdpSpecificFiles)
  335. {
  336. // All files enabled when store is determined at runtime.
  337. var enabled = target == AppStore.NotSpecified;
  338. // Otherwise this file must be needed on the target.
  339. enabled |= mapping.Value == target;
  340. var path = $"{UdpBinPath}/{mapping.Key}";
  341. var importer = (PluginImporter)AssetImporter.GetAtPath(path);
  342. if (importer != null)
  343. {
  344. importer.SetCompatibleWithPlatform(BuildTarget.Android, enabled);
  345. }
  346. else
  347. {
  348. // Search for any occurrence of this file
  349. // Only fail if more than one found
  350. var paths = FindPaths(mapping.Key);
  351. if (paths.Length == 1)
  352. {
  353. importer = (PluginImporter)AssetImporter.GetAtPath(paths[0]);
  354. importer.SetCompatibleWithPlatform(BuildTarget.Android, enabled);
  355. }
  356. }
  357. }
  358. }
  359. }
  360. /// <summary>
  361. /// To enable or disable importation of assets at build-time, collect Project-relative
  362. /// paths matching <paramref name="filename"/>.
  363. /// </summary>
  364. /// <param name="filename">Name of file to search for in this Project</param>
  365. /// <returns>Relative paths matching <paramref name="filename"/></returns>
  366. public static string[] FindPaths(string filename)
  367. {
  368. var paths = new List<string>();
  369. var guids = AssetDatabase.FindAssets(Path.GetFileNameWithoutExtension(filename));
  370. foreach (var guid in guids)
  371. {
  372. var path = AssetDatabase.GUIDToAssetPath(guid);
  373. var foundFilename = Path.GetFileName(path);
  374. if (filename == foundFilename)
  375. {
  376. paths.Add(path);
  377. }
  378. }
  379. return paths.ToArray();
  380. }
  381. private static void SaveConfig(AppStore enabled)
  382. {
  383. var configToSave = new StoreConfiguration(enabled);
  384. File.WriteAllText(ModePath, StoreConfiguration.Serialize(configToSave));
  385. AssetDatabase.ImportAsset(ModePath);
  386. config = configToSave;
  387. }
  388. internal static AppStore ConfiguredAppStore()
  389. {
  390. if (config == null)
  391. {
  392. return defaultAppStore;
  393. }
  394. return config.androidStore;
  395. }
  396. // Run me to configure the project's set of Android stores before build
  397. [PostProcessScene(0)]
  398. internal static void OnPostProcessScene()
  399. {
  400. if (File.Exists(ModePath))
  401. {
  402. try
  403. {
  404. config = StoreConfiguration.Deserialize(File.ReadAllText(ModePath));
  405. ConfigureProject(config.androidStore);
  406. }
  407. catch (Exception e)
  408. {
  409. #if ENABLE_EDITOR_GAME_SERVICES
  410. Debug.LogError("Unity IAP unable to strip undesired Android stores from build, check file: " + ModePath);
  411. #else
  412. Debug.LogError("Unity IAP unable to strip undesired Android stores from build, use menu (e.g. "
  413. + SwitchStoreMenuItem + ") and check file: " + ModePath);
  414. #endif
  415. Debug.LogError(e);
  416. }
  417. }
  418. }
  419. [MenuItem(IapMenuConsts.MenuItemRoot + "/Configure...", false, 0)]
  420. private static void ConfigurePurchasingSettings()
  421. {
  422. #if ENABLE_EDITOR_GAME_SERVICES && SERVICES_SDK_CORE_ENABLED
  423. var path = PurchasingSettingsProvider.GetSettingsPath();
  424. SettingsService.OpenProjectSettings(path);
  425. #elif UNITY_2020_3_OR_NEWER
  426. ServicesUtils.OpenServicesProjectSettings(PurchasingService.instance.projectSettingsPath, PurchasingService.instance.settingsProviderClassName);
  427. #else
  428. EditorApplication.ExecuteMenuItem("Window/General/Services");
  429. #endif
  430. GameServicesEventSenderHelpers.SendTopMenuConfigure();
  431. }
  432. }
  433. }