AppLovinPostProcessAndroid.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. //
  2. // MaxPostProcessBuildAndroid.cs
  3. // AppLovin MAX Unity Plugin
  4. //
  5. // Created by Santosh Bagadi on 4/10/20.
  6. // Copyright © 2020 AppLovin. All rights reserved.
  7. //
  8. #if UNITY_ANDROID
  9. using System;
  10. using System.Collections.Generic;
  11. using System.IO;
  12. using System.Linq;
  13. using System.Text.RegularExpressions;
  14. using System.Xml.Linq;
  15. using AppLovinMax.ThirdParty.MiniJson;
  16. using UnityEditor;
  17. using UnityEditor.Android;
  18. namespace AppLovinMax.Scripts.IntegrationManager.Editor
  19. {
  20. /// <summary>
  21. /// A post processor used to update the Android project once it is generated.
  22. /// </summary>
  23. public class AppLovinPostProcessAndroid : IPostGenerateGradleAndroidProject
  24. {
  25. #if UNITY_2019_3_OR_NEWER
  26. private const string PropertyAndroidX = "android.useAndroidX";
  27. private const string PropertyJetifier = "android.enableJetifier";
  28. private const string EnableProperty = "=true";
  29. #endif
  30. private const string PropertyDexingArtifactTransform = "android.enableDexingArtifactTransform";
  31. private const string DisableProperty = "=false";
  32. private const string KeyMetaDataAppLovinVerboseLoggingOn = "applovin.sdk.verbose_logging";
  33. private const string KeyMetaDataGoogleApplicationId = "com.google.android.gms.ads.APPLICATION_ID";
  34. private const string KeyMetaDataGoogleOptimizeInitialization = "com.google.android.gms.ads.flag.OPTIMIZE_INITIALIZATION";
  35. private const string KeyMetaDataGoogleOptimizeAdLoading = "com.google.android.gms.ads.flag.OPTIMIZE_AD_LOADING";
  36. private const string KeyMetaDataMobileFuseAutoInit = "com.mobilefuse.sdk.disable_auto_init";
  37. private const string KeyMetaDataMyTargetAutoInit = "com.my.target.autoInitMode";
  38. private const string KeyMetaDataAppLovinSdkKey = "applovin.sdk.key";
  39. private const string AppLovinSettingsFileName = "applovin_settings.json";
  40. private const string KeySdkKey = "sdk_key";
  41. private const string KeyConsentFlowSettings = "consent_flow_settings";
  42. private const string KeyConsentFlowEnabled = "consent_flow_enabled";
  43. private const string KeyConsentFlowTermsOfService = "consent_flow_terms_of_service";
  44. private const string KeyConsentFlowPrivacyPolicy = "consent_flow_privacy_policy";
  45. private const string KeyConsentFlowShowTermsAndPrivacyPolicyAlertInGDPR = "consent_flow_show_terms_and_privacy_policy_alert_in_gdpr";
  46. private const string KeyConsentFlowDebugUserGeography = "consent_flow_debug_user_geography";
  47. private const string KeyRenderOutsideSafeArea = "render_outside_safe_area";
  48. #if UNITY_2022_3_OR_NEWER
  49. // To match "'com.android.library' version '7.3.1'" line in build.gradle
  50. private static readonly Regex TokenGradleVersionLibrary = new Regex(".*id ['\"]com\\.android\\.library['\"] version");
  51. private static readonly Regex TokenGradleVersion = new Regex(".*id ['\"]com\\.android\\.application['\"] version");
  52. #else
  53. // To match "classpath 'com.android.tools.build:gradle:4.0.1'" line in build.gradle
  54. private static readonly Regex TokenGradleVersion = new Regex(".*classpath ['\"]com\\.android\\.tools\\.build:gradle:.*");
  55. #endif
  56. // To match "distributionUrl=..." in gradle-wrapper.properties file
  57. private static readonly Regex TokenDistributionUrl = new Regex(".*distributionUrl.*");
  58. private static readonly XNamespace AndroidNamespace = "http://schemas.android.com/apk/res/android";
  59. public void OnPostGenerateGradleAndroidProject(string path)
  60. {
  61. #if UNITY_2019_3_OR_NEWER
  62. var rootGradleBuildFilePath = Path.Combine(path, "../build.gradle");
  63. var gradlePropertiesPath = Path.Combine(path, "../gradle.properties");
  64. var gradleWrapperPropertiesPath = Path.Combine(path, "../gradle/wrapper/gradle-wrapper.properties");
  65. #else
  66. var rootGradleBuildFilePath = Path.Combine(path, "build.gradle");
  67. var gradlePropertiesPath = Path.Combine(path, "gradle.properties");
  68. var gradleWrapperPropertiesPath = Path.Combine(path, "gradle/wrapper/gradle-wrapper.properties");
  69. #endif
  70. UpdateGradleVersionsIfNeeded(gradleWrapperPropertiesPath, rootGradleBuildFilePath);
  71. var gradlePropertiesUpdated = new List<string>();
  72. // If the gradle properties file already exists, make sure to add any previous properties.
  73. if (File.Exists(gradlePropertiesPath))
  74. {
  75. var lines = File.ReadAllLines(gradlePropertiesPath);
  76. #if UNITY_2019_3_OR_NEWER
  77. // Add all properties except AndroidX, Jetifier, and DexingArtifactTransform since they may already exist. We will re-add them below.
  78. gradlePropertiesUpdated.AddRange(lines.Where(line => !line.Contains(PropertyAndroidX) && !line.Contains(PropertyJetifier) && !line.Contains(PropertyDexingArtifactTransform)));
  79. #else
  80. // Add all properties except DexingArtifactTransform since it may already exist. We will re-add it below.
  81. gradlePropertiesUpdated.AddRange(lines.Where(line => !line.Contains(PropertyDexingArtifactTransform)));
  82. #endif
  83. }
  84. #if UNITY_2019_3_OR_NEWER
  85. // Enable AndroidX and Jetifier properties
  86. gradlePropertiesUpdated.Add(PropertyAndroidX + EnableProperty);
  87. gradlePropertiesUpdated.Add(PropertyJetifier + EnableProperty);
  88. #endif
  89. // `DexingArtifactTransform` has been removed in Gradle 8+ which is the default Gradle version for Unity 6.
  90. #if !UNITY_6000_0_OR_NEWER
  91. // Disable dexing using artifact transform (it causes issues for ExoPlayer with Gradle plugin 3.5.0+)
  92. gradlePropertiesUpdated.Add(PropertyDexingArtifactTransform + DisableProperty);
  93. #endif
  94. try
  95. {
  96. File.WriteAllText(gradlePropertiesPath, string.Join("\n", gradlePropertiesUpdated.ToArray()) + "\n");
  97. }
  98. catch (Exception exception)
  99. {
  100. MaxSdkLogger.UserError("Failed to enable AndroidX and Jetifier. gradle.properties file write failed.");
  101. Console.WriteLine(exception);
  102. }
  103. ProcessAndroidManifest(path);
  104. AddSdkSettings(path);
  105. }
  106. public int callbackOrder
  107. {
  108. get { return AppLovinPreProcess.CallbackOrder; }
  109. }
  110. private static void ProcessAndroidManifest(string path)
  111. {
  112. var manifestPath = Path.Combine(path, "src/main/AndroidManifest.xml");
  113. XDocument manifest;
  114. try
  115. {
  116. manifest = XDocument.Load(manifestPath);
  117. }
  118. #pragma warning disable 0168
  119. catch (IOException exception)
  120. #pragma warning restore 0168
  121. {
  122. MaxSdkLogger.UserWarning("[AppLovin MAX] AndroidManifest.xml is missing.");
  123. return;
  124. }
  125. // Get the `manifest` element.
  126. var elementManifest = manifest.Element("manifest");
  127. if (elementManifest == null)
  128. {
  129. MaxSdkLogger.UserWarning("[AppLovin MAX] AndroidManifest.xml is invalid.");
  130. return;
  131. }
  132. var elementApplication = elementManifest.Element("application");
  133. if (elementApplication == null)
  134. {
  135. MaxSdkLogger.UserWarning("[AppLovin MAX] AndroidManifest.xml is invalid.");
  136. return;
  137. }
  138. var metaDataElements = elementApplication.Descendants().Where(element => element.Name.LocalName.Equals("meta-data"));
  139. EnableVerboseLoggingIfNeeded(elementApplication);
  140. AddGoogleApplicationIdIfNeeded(elementApplication, metaDataElements);
  141. AddGoogleOptimizationFlagsIfNeeded(elementApplication, metaDataElements);
  142. DisableAutoInitIfNeeded(elementApplication, metaDataElements);
  143. RemoveSdkKeyIfNeeded(metaDataElements);
  144. // Save the updated manifest file.
  145. manifest.Save(manifestPath);
  146. }
  147. private static void EnableVerboseLoggingIfNeeded(XElement elementApplication)
  148. {
  149. var enabled = EditorPrefs.GetBool(MaxSdkLogger.KeyVerboseLoggingEnabled, false);
  150. var descendants = elementApplication.Descendants();
  151. var verboseLoggingMetaData = descendants.FirstOrDefault(descendant => descendant.FirstAttribute != null &&
  152. descendant.FirstAttribute.Name.LocalName.Equals("name") &&
  153. descendant.FirstAttribute.Value.Equals(KeyMetaDataAppLovinVerboseLoggingOn) &&
  154. descendant.LastAttribute != null &&
  155. descendant.LastAttribute.Name.LocalName.Equals("value"));
  156. // check if applovin.sdk.verbose_logging meta data exists.
  157. if (verboseLoggingMetaData != null)
  158. {
  159. if (enabled)
  160. {
  161. // update applovin.sdk.verbose_logging meta data value.
  162. verboseLoggingMetaData.LastAttribute.Value = enabled.ToString();
  163. }
  164. else
  165. {
  166. // remove applovin.sdk.verbose_logging meta data.
  167. verboseLoggingMetaData.Remove();
  168. }
  169. }
  170. else
  171. {
  172. if (enabled)
  173. {
  174. // add applovin.sdk.verbose_logging meta data if it does not exist.
  175. var metaData = CreateMetaDataElement(KeyMetaDataAppLovinVerboseLoggingOn, enabled.ToString());
  176. elementApplication.Add(metaData);
  177. }
  178. }
  179. }
  180. private static void AddGoogleApplicationIdIfNeeded(XElement elementApplication, IEnumerable<XElement> metaDataElements)
  181. {
  182. if (!AppLovinPackageManager.IsAdapterInstalled("Google") && !AppLovinPackageManager.IsAdapterInstalled("GoogleAdManager")) return;
  183. var googleApplicationIdMetaData = GetMetaDataElement(metaDataElements, KeyMetaDataGoogleApplicationId);
  184. var appId = AppLovinSettings.Instance.AdMobAndroidAppId;
  185. // Log error if the App ID is not set.
  186. if (string.IsNullOrEmpty(appId) || !appId.StartsWith("ca-app-pub-"))
  187. {
  188. MaxSdkLogger.UserError("Google App ID is not set. Please enter a valid app ID within the AppLovin Integration Manager window.");
  189. return;
  190. }
  191. // Check if the Google App ID meta data already exists. Update if it already exists.
  192. if (googleApplicationIdMetaData != null)
  193. {
  194. googleApplicationIdMetaData.SetAttributeValue(AndroidNamespace + "value", appId);
  195. }
  196. // Meta data doesn't exist, add it.
  197. else
  198. {
  199. elementApplication.Add(CreateMetaDataElement(KeyMetaDataGoogleApplicationId, appId));
  200. }
  201. }
  202. private static void AddGoogleOptimizationFlagsIfNeeded(XElement elementApplication, IEnumerable<XElement> metaDataElements)
  203. {
  204. if (!AppLovinPackageManager.IsAdapterInstalled("Google") && !AppLovinPackageManager.IsAdapterInstalled("GoogleAdManager")) return;
  205. var googleOptimizeInitializationMetaData = GetMetaDataElement(metaDataElements, KeyMetaDataGoogleOptimizeInitialization);
  206. // If meta data doesn't exist, add it
  207. if (googleOptimizeInitializationMetaData == null)
  208. {
  209. elementApplication.Add(CreateMetaDataElement(KeyMetaDataGoogleOptimizeInitialization, true));
  210. }
  211. var googleOptimizeAdLoadingMetaData = GetMetaDataElement(metaDataElements, KeyMetaDataGoogleOptimizeAdLoading);
  212. // If meta data doesn't exist, add it
  213. if (googleOptimizeAdLoadingMetaData == null)
  214. {
  215. elementApplication.Add(CreateMetaDataElement(KeyMetaDataGoogleOptimizeAdLoading, true));
  216. }
  217. }
  218. private static void DisableAutoInitIfNeeded(XElement elementApplication, IEnumerable<XElement> metaDataElements)
  219. {
  220. if (AppLovinPackageManager.IsAdapterInstalled("MobileFuse"))
  221. {
  222. var mobileFuseMetaData = GetMetaDataElement(metaDataElements, KeyMetaDataMobileFuseAutoInit);
  223. // If MobileFuse meta data doesn't exist, add it
  224. if (mobileFuseMetaData == null)
  225. {
  226. elementApplication.Add(CreateMetaDataElement(KeyMetaDataMobileFuseAutoInit, true));
  227. }
  228. }
  229. if (AppLovinPackageManager.IsAdapterInstalled("MyTarget"))
  230. {
  231. var myTargetMetaData = GetMetaDataElement(metaDataElements, KeyMetaDataMyTargetAutoInit);
  232. // If MyTarget meta data doesn't exist, add it
  233. if (myTargetMetaData == null)
  234. {
  235. elementApplication.Add(CreateMetaDataElement(KeyMetaDataMyTargetAutoInit, 0));
  236. }
  237. }
  238. }
  239. private static void RemoveSdkKeyIfNeeded(IEnumerable<XElement> metaDataElements)
  240. {
  241. var sdkKeyMetaData = GetMetaDataElement(metaDataElements, KeyMetaDataAppLovinSdkKey);
  242. if (sdkKeyMetaData == null) return;
  243. sdkKeyMetaData.Remove();
  244. }
  245. private static void UpdateGradleVersionsIfNeeded(string gradleWrapperPropertiesPath, string rootGradleBuildFilePath)
  246. {
  247. var customGradleVersionUrl = AppLovinSettings.Instance.CustomGradleVersionUrl;
  248. var customGradleToolsVersion = AppLovinSettings.Instance.CustomGradleToolsVersion;
  249. if (MaxSdkUtils.IsValidString(customGradleVersionUrl))
  250. {
  251. var newDistributionUrl = string.Format("distributionUrl={0}", customGradleVersionUrl);
  252. if (ReplaceStringInFile(gradleWrapperPropertiesPath, TokenDistributionUrl, newDistributionUrl))
  253. {
  254. MaxSdkLogger.D("Distribution url set to " + newDistributionUrl);
  255. }
  256. else
  257. {
  258. MaxSdkLogger.E("Failed to set distribution URL");
  259. }
  260. }
  261. if (MaxSdkUtils.IsValidString(customGradleToolsVersion))
  262. {
  263. #if UNITY_2022_3_OR_NEWER
  264. // Unity 2022.3+ requires Gradle Plugin version 7.1.2+.
  265. if (MaxSdkUtils.CompareVersions(customGradleToolsVersion, "7.1.2") == MaxSdkUtils.VersionComparisonResult.Lesser)
  266. {
  267. MaxSdkLogger.E("Failed to set gradle plugin version. Unity 2022.3+ requires gradle plugin version 7.1.2+");
  268. return;
  269. }
  270. var newGradleVersionLibraryLine = AppLovinProcessGradleBuildFile.GetFormattedBuildScriptLine(string.Format("id 'com.android.library' version '{0}' apply false", customGradleToolsVersion));
  271. if (ReplaceStringInFile(rootGradleBuildFilePath, TokenGradleVersionLibrary, newGradleVersionLibraryLine))
  272. {
  273. MaxSdkLogger.D("Gradle library version set to " + newGradleVersionLibraryLine);
  274. }
  275. else
  276. {
  277. MaxSdkLogger.E("Failed to set gradle library version");
  278. }
  279. var newGradleVersionLine = AppLovinProcessGradleBuildFile.GetFormattedBuildScriptLine(string.Format("id 'com.android.application' version '{0}' apply false", customGradleToolsVersion));
  280. #else
  281. var newGradleVersionLine = AppLovinProcessGradleBuildFile.GetFormattedBuildScriptLine(string.Format("classpath 'com.android.tools.build:gradle:{0}'", customGradleToolsVersion));
  282. #endif
  283. if (ReplaceStringInFile(rootGradleBuildFilePath, TokenGradleVersion, newGradleVersionLine))
  284. {
  285. MaxSdkLogger.D("Gradle version set to " + newGradleVersionLine);
  286. }
  287. else
  288. {
  289. MaxSdkLogger.E("Failed to set gradle plugin version");
  290. }
  291. }
  292. }
  293. private static void AddSdkSettings(string path)
  294. {
  295. var appLovinSdkSettings = new Dictionary<string, object>();
  296. var rawResourceDirectory = Path.Combine(path, "src/main/res/raw");
  297. // Add the SDK key to the SDK settings.
  298. appLovinSdkSettings[KeySdkKey] = AppLovinSettings.Instance.SdkKey;
  299. appLovinSdkSettings[KeyRenderOutsideSafeArea] = PlayerSettings.Android.renderOutsideSafeArea;
  300. // Add the Terms and Privacy Policy flow settings if needed.
  301. EnableConsentFlowIfNeeded(rawResourceDirectory, appLovinSdkSettings);
  302. WriteAppLovinSettings(rawResourceDirectory, appLovinSdkSettings);
  303. }
  304. private static void EnableConsentFlowIfNeeded(string rawResourceDirectory, Dictionary<string, object> applovinSdkSettings)
  305. {
  306. // Check if consent flow is enabled. No need to create the applovin_consent_flow_settings.json if consent flow is disabled.
  307. var consentFlowEnabled = AppLovinInternalSettings.Instance.ConsentFlowEnabled;
  308. if (!consentFlowEnabled)
  309. {
  310. RemoveAppLovinSettingsRawResourceFileIfNeeded(rawResourceDirectory);
  311. return;
  312. }
  313. var privacyPolicyUrl = AppLovinInternalSettings.Instance.ConsentFlowPrivacyPolicyUrl;
  314. if (string.IsNullOrEmpty(privacyPolicyUrl))
  315. {
  316. AppLovinIntegrationManager.ShowBuildFailureDialog("You cannot use the AppLovin SDK's consent flow without defining a Privacy Policy URL in the AppLovin Integration Manager.");
  317. // No need to update the applovin_consent_flow_settings.json here. Default consent flow state will be determined on the SDK side.
  318. return;
  319. }
  320. var consentFlowSettings = new Dictionary<string, object>();
  321. consentFlowSettings[KeyConsentFlowEnabled] = consentFlowEnabled;
  322. consentFlowSettings[KeyConsentFlowPrivacyPolicy] = privacyPolicyUrl;
  323. var termsOfServiceUrl = AppLovinInternalSettings.Instance.ConsentFlowTermsOfServiceUrl;
  324. if (MaxSdkUtils.IsValidString(termsOfServiceUrl))
  325. {
  326. consentFlowSettings[KeyConsentFlowTermsOfService] = termsOfServiceUrl;
  327. }
  328. consentFlowSettings[KeyConsentFlowShowTermsAndPrivacyPolicyAlertInGDPR] = AppLovinInternalSettings.Instance.ShouldShowTermsAndPrivacyPolicyAlertInGDPR;
  329. var debugUserGeography = AppLovinInternalSettings.Instance.DebugUserGeography;
  330. if (debugUserGeography == MaxSdkBase.ConsentFlowUserGeography.Gdpr)
  331. {
  332. consentFlowSettings[KeyConsentFlowDebugUserGeography] = "gdpr";
  333. }
  334. applovinSdkSettings[KeyConsentFlowSettings] = consentFlowSettings;
  335. }
  336. private static void WriteAppLovinSettingsRawResourceFile(string applovinSdkSettingsJson, string rawResourceDirectory)
  337. {
  338. if (!Directory.Exists(rawResourceDirectory))
  339. {
  340. Directory.CreateDirectory(rawResourceDirectory);
  341. }
  342. var consentFlowSettingsFilePath = Path.Combine(rawResourceDirectory, AppLovinSettingsFileName);
  343. try
  344. {
  345. File.WriteAllText(consentFlowSettingsFilePath, applovinSdkSettingsJson + "\n");
  346. }
  347. catch (Exception exception)
  348. {
  349. MaxSdkLogger.UserError("applovin_settings.json file write failed due to: " + exception.Message);
  350. Console.WriteLine(exception);
  351. }
  352. }
  353. /// <summary>
  354. /// Removes the applovin_settings json file from the build if it exists.
  355. /// </summary>
  356. /// <param name="rawResourceDirectory">The raw resource directory that holds the json file</param>
  357. private static void RemoveAppLovinSettingsRawResourceFileIfNeeded(string rawResourceDirectory)
  358. {
  359. var consentFlowSettingsFilePath = Path.Combine(rawResourceDirectory, AppLovinSettingsFileName);
  360. if (!File.Exists(consentFlowSettingsFilePath)) return;
  361. try
  362. {
  363. File.Delete(consentFlowSettingsFilePath);
  364. }
  365. catch (Exception exception)
  366. {
  367. MaxSdkLogger.UserError("Deleting applovin_settings.json failed due to: " + exception.Message);
  368. Console.WriteLine(exception);
  369. }
  370. }
  371. private static void WriteAppLovinSettings(string rawResourceDirectory, Dictionary<string, object> applovinSdkSettings)
  372. {
  373. var applovinSdkSettingsJson = Json.Serialize(applovinSdkSettings);
  374. WriteAppLovinSettingsRawResourceFile(applovinSdkSettingsJson, rawResourceDirectory);
  375. }
  376. /// <summary>
  377. /// Creates and returns a <c>meta-data</c> element with the given name and value.
  378. /// </summary>
  379. private static XElement CreateMetaDataElement(string name, object value)
  380. {
  381. var metaData = new XElement("meta-data");
  382. metaData.Add(new XAttribute(AndroidNamespace + "name", name));
  383. metaData.Add(new XAttribute(AndroidNamespace + "value", value));
  384. return metaData;
  385. }
  386. /// <summary>
  387. /// Looks through all the given meta-data elements to check if the required one exists. Returns <c>null</c> if it doesn't exist.
  388. /// </summary>
  389. private static XElement GetMetaDataElement(IEnumerable<XElement> metaDataElements, string metaDataName)
  390. {
  391. foreach (var metaDataElement in metaDataElements)
  392. {
  393. var attributes = metaDataElement.Attributes();
  394. if (attributes.Any(attribute => attribute.Name.Namespace.Equals(AndroidNamespace)
  395. && attribute.Name.LocalName.Equals("name")
  396. && attribute.Value.Equals(metaDataName)))
  397. {
  398. return metaDataElement;
  399. }
  400. }
  401. return null;
  402. }
  403. /// <summary>
  404. /// Finds the first line that contains regexToMatch and replaces the whole line with replacement
  405. /// </summary>
  406. /// <param name="path">Path to the file you want to replace a line in</param>
  407. /// <param name="regexToMatch">Regex to search for in the line you want to replace</param>
  408. /// <param name="replacement">String that you want as the new line</param>
  409. /// <returns>Returns whether the string was successfully replaced or not</returns>
  410. private static bool ReplaceStringInFile(string path, Regex regexToMatch, string replacement)
  411. {
  412. if (!File.Exists(path)) return false;
  413. var lines = File.ReadAllLines(path);
  414. for (var i = 0; i < lines.Length; i++)
  415. {
  416. if (regexToMatch.IsMatch(lines[i]))
  417. {
  418. lines[i] = replacement;
  419. File.WriteAllLines(path, lines);
  420. return true;
  421. }
  422. }
  423. return false;
  424. }
  425. }
  426. }
  427. #endif