// AppLovin MAX Unity Plugin // // Created by Santosh Bagadi on 9/3/19. // Copyright © 2019 AppLovin. All rights reserved. // #if UNITY_ANDROID using System.Text; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using UnityEditorInternal; using UnityEngine; using UnityEngine.Networking; using Debug = UnityEngine.Debug; namespace AppLovinMax.Scripts.IntegrationManager.Editor { [Serializable] public class AppLovinQualityServiceData { public string api_key; } /// /// Adds or updates the AppLovin Quality Service plugin to the provided build.gradle file. /// If the gradle file already has the plugin, the API key is updated. /// public abstract class AppLovinProcessGradleBuildFile : AppLovinPreProcess { private static readonly Regex TokenBuildScriptRepositories = new Regex(".*repositories.*"); private static readonly Regex TokenBuildScriptDependencies = new Regex(".*classpath \'com.android.tools.build:gradle.*"); private static readonly Regex TokenApplicationPlugin = new Regex(".*apply plugin: \'com.android.application\'.*"); private static readonly Regex TokenApiKey = new Regex(".*apiKey.*"); private static readonly Regex TokenAppLovinPlugin = new Regex(".*apply plugin:.+?(?=applovin-quality-service).*"); #if UNITY_2022_2_OR_NEWER private const string PluginsMatcher = "plugins"; private const string PluginManagementMatcher = "pluginManagement"; private const string QualityServicePluginRoot = " id 'com.applovin.quality' version '+' apply false // NOTE: Requires version 4.8.3+ for Gradle version 7.2+"; #endif private const string BuildScriptMatcher = "buildscript"; private const string QualityServiceMavenRepo = "maven { url 'https://artifacts.applovin.com/android'; content { includeGroupByRegex 'com.applovin.*' } }"; private const string QualityServiceDependencyClassPath = "classpath 'com.applovin.quality:AppLovinQualityServiceGradlePlugin:+'"; private const string QualityServiceApplyPlugin = "apply plugin: 'applovin-quality-service'"; private const string QualityServicePlugin = "applovin {"; private const string QualityServiceApiKey = " apiKey '{0}'"; private const string QualityServiceBintrayMavenRepo = "https://applovin.bintray.com/Quality-Service"; private const string QualityServiceNoRegexMavenRepo = "maven { url 'https://artifacts.applovin.com/android' }"; // Legacy plugin detection variables private const string QualityServiceDependencyClassPathV3 = "classpath 'com.applovin.quality:AppLovinQualityServiceGradlePlugin:3.+'"; private static readonly Regex TokenSafeDkLegacyApplyPlugin = new Regex(".*apply plugin:.+?(?=safedk).*"); private const string SafeDkLegacyPlugin = "safedk {"; private const string SafeDkLegacyMavenRepo = "http://download.safedk.com"; private const string SafeDkLegacyDependencyClassPath = "com.safedk:SafeDKGradlePlugin:"; /// /// Updates the provided Gradle script to add Quality Service plugin. /// /// The gradle file to update. protected static void AddAppLovinQualityServicePlugin(string applicationGradleBuildFilePath) { if (!AppLovinSettings.Instance.QualityServiceEnabled) return; var sdkKey = AppLovinSettings.Instance.SdkKey; if (string.IsNullOrEmpty(sdkKey)) { MaxSdkLogger.UserError("Failed to install AppLovin Quality Service plugin. SDK Key is empty. Please enter the AppLovin SDK Key in the Integration Manager."); return; } // Retrieve the API Key using the SDK Key. var qualityServiceData = RetrieveQualityServiceData(sdkKey); var apiKey = qualityServiceData.api_key; if (string.IsNullOrEmpty(apiKey)) { MaxSdkLogger.UserError("Failed to install AppLovin Quality Service plugin. API Key is empty."); return; } // Generate the updated Gradle file that needs to be written. var lines = File.ReadAllLines(applicationGradleBuildFilePath).ToList(); var sanitizedLines = RemoveLegacySafeDkPlugin(lines); var outputLines = GenerateUpdatedBuildFileLines( sanitizedLines, apiKey, #if UNITY_2019_3_OR_NEWER false // On Unity 2019.3+, the buildscript closure related lines will to be added to the root build.gradle file. #else true #endif ); // outputLines can be null if we couldn't add the plugin. if (outputLines == null) return; try { File.WriteAllText(applicationGradleBuildFilePath, string.Join("\n", outputLines.ToArray()) + "\n"); } catch (Exception exception) { MaxSdkLogger.UserError("Failed to install AppLovin Quality Service plugin. Gradle file write failed."); Console.WriteLine(exception); } } #if UNITY_2022_2_OR_NEWER /// /// Adds AppLovin Quality Service plugin DSL element to the project's root build.gradle file. /// /// The path to project's root build.gradle file. /// true when the plugin was added successfully. protected bool AddPluginToRootGradleBuildFile(string rootGradleBuildFile) { var lines = File.ReadAllLines(rootGradleBuildFile).ToList(); // Check if the plugin is already added to the file. var pluginAdded = lines.Any(line => line.Contains(QualityServicePluginRoot)); if (pluginAdded) return true; var outputLines = new List(); var insidePluginsClosure = false; foreach (var line in lines) { if (line.Contains(PluginsMatcher)) { insidePluginsClosure = true; } if (!pluginAdded && insidePluginsClosure && line.Contains("}")) { outputLines.Add(QualityServicePluginRoot); pluginAdded = true; insidePluginsClosure = false; } outputLines.Add(line); } if (!pluginAdded) { MaxSdkLogger.UserError("Failed to add AppLovin Quality Service plugin to root gradle file."); return false; } try { File.WriteAllText(rootGradleBuildFile, string.Join("\n", outputLines.ToArray()) + "\n"); } catch (Exception exception) { MaxSdkLogger.UserError("Failed to install AppLovin Quality Service plugin. Root Gradle file write failed."); Console.WriteLine(exception); return false; } return true; } /// /// Adds the AppLovin maven repository to the project's settings.gradle file. /// /// The path to the project's settings.gradle file. /// true if the repository was added successfully. protected bool AddAppLovinRepository(string settingsGradleFile) { var lines = File.ReadLines(settingsGradleFile).ToList(); var outputLines = new List(); var mavenRepoAdded = false; var pluginManagementClosureDepth = 0; var insidePluginManagementClosure = false; var pluginManagementMatched = false; foreach (var line in lines) { outputLines.Add(line); if (!pluginManagementMatched && line.Contains(PluginManagementMatcher)) { pluginManagementMatched = true; insidePluginManagementClosure = true; } if (insidePluginManagementClosure) { if (line.Contains("{")) { pluginManagementClosureDepth++; } if (line.Contains("}")) { pluginManagementClosureDepth--; } if (pluginManagementClosureDepth == 0) { insidePluginManagementClosure = false; } } if (insidePluginManagementClosure) { if (!mavenRepoAdded && TokenBuildScriptRepositories.IsMatch(line)) { outputLines.Add(GetFormattedBuildScriptLine(QualityServiceMavenRepo)); mavenRepoAdded = true; } } } if (!mavenRepoAdded) { MaxSdkLogger.UserError("Failed to add AppLovin Quality Service plugin maven repo to settings gradle file."); return false; } try { File.WriteAllText(settingsGradleFile, string.Join("\n", outputLines.ToArray()) + "\n"); } catch (Exception exception) { MaxSdkLogger.UserError("Failed to install AppLovin Quality Service plugin. Setting Gradle file write failed."); Console.WriteLine(exception); return false; } return true; } #endif #if UNITY_2019_3_OR_NEWER /// /// Adds the necessary AppLovin Quality Service dependency and maven repo lines to the provided root build.gradle file. /// /// The root build.gradle file path /// true if the build script lines were applied correctly. protected bool AddQualityServiceBuildScriptLines(string rootGradleBuildFile) { var lines = File.ReadAllLines(rootGradleBuildFile).ToList(); var outputLines = GenerateUpdatedBuildFileLines(lines, null, true); // outputLines will be null if we couldn't add the build script lines. if (outputLines == null) return false; try { File.WriteAllText(rootGradleBuildFile, string.Join("\n", outputLines.ToArray()) + "\n"); } catch (Exception exception) { MaxSdkLogger.UserError("Failed to install AppLovin Quality Service plugin. Root Gradle file write failed."); Console.WriteLine(exception); return false; } return true; } /// /// Removes the AppLovin Quality Service Plugin or Legacy SafeDK plugin from the given gradle template file if either of them are present. /// /// The gradle template file from which to remove the plugin from protected static void RemoveAppLovinQualityServiceOrSafeDkPlugin(string gradleTemplateFile) { var lines = File.ReadAllLines(gradleTemplateFile).ToList(); lines = RemoveLegacySafeDkPlugin(lines); lines = RemoveAppLovinQualityServicePlugin(lines); try { File.WriteAllText(gradleTemplateFile, string.Join("\n", lines.ToArray()) + "\n"); } catch (Exception exception) { MaxSdkLogger.UserError("Failed to remove AppLovin Quality Service Plugin from mainTemplate.gradle. Please remove the Quality Service plugin from the mainTemplate.gradle manually."); Console.WriteLine(exception); } } #endif private static AppLovinQualityServiceData RetrieveQualityServiceData(string sdkKey) { var postJson = string.Format("{{\"sdk_key\" : \"{0}\"}}", sdkKey); var bodyRaw = Encoding.UTF8.GetBytes(postJson); // Upload handler is automatically disposed when UnityWebRequest is disposed var uploadHandler = new UploadHandlerRaw(bodyRaw); uploadHandler.contentType = "application/json"; using (var unityWebRequest = new UnityWebRequest("https://api2.safedk.com/v1/build/cred")) { unityWebRequest.method = UnityWebRequest.kHttpVerbPOST; unityWebRequest.uploadHandler = uploadHandler; unityWebRequest.downloadHandler = new DownloadHandlerBuffer(); var operation = unityWebRequest.SendWebRequest(); // Wait for the download to complete or the request to timeout. while (!operation.isDone) { } #if UNITY_2020_1_OR_NEWER if (unityWebRequest.result != UnityWebRequest.Result.Success) #else if (unityWebRequest.isNetworkError || unityWebRequest.isHttpError) #endif { MaxSdkLogger.UserError("Failed to retrieve API Key for SDK Key: " + sdkKey + "with error: " + unityWebRequest.error); return new AppLovinQualityServiceData(); } try { return JsonUtility.FromJson(unityWebRequest.downloadHandler.text); } catch (Exception exception) { MaxSdkLogger.UserError("Failed to parse API Key." + exception); return new AppLovinQualityServiceData(); } } } private static List RemoveLegacySafeDkPlugin(List lines) { return RemovePlugin(lines, SafeDkLegacyPlugin, SafeDkLegacyMavenRepo, SafeDkLegacyDependencyClassPath, TokenSafeDkLegacyApplyPlugin); } private static List RemoveAppLovinQualityServicePlugin(List lines) { return RemovePlugin(lines, QualityServicePlugin, QualityServiceMavenRepo, QualityServiceDependencyClassPath, TokenAppLovinPlugin); } private static List RemovePlugin(List lines, string pluginLine, string mavenRepo, string dependencyClassPath, Regex applyPluginToken) { var sanitizedLines = new List(); var legacyRepoRemoved = false; var legacyDependencyClassPathRemoved = false; var legacyPluginRemoved = false; var legacyPluginMatched = false; var insideLegacySafeDkClosure = false; foreach (var line in lines) { if (!legacyPluginMatched && line.Contains(pluginLine)) { legacyPluginMatched = true; insideLegacySafeDkClosure = true; } if (insideLegacySafeDkClosure && line.Contains("}")) { insideLegacySafeDkClosure = false; continue; } if (insideLegacySafeDkClosure) { continue; } if (!legacyRepoRemoved && line.Contains(mavenRepo)) { legacyRepoRemoved = true; continue; } if (!legacyDependencyClassPathRemoved && line.Contains(dependencyClassPath)) { legacyDependencyClassPathRemoved = true; continue; } if (!legacyPluginRemoved && applyPluginToken.IsMatch(line)) { legacyPluginRemoved = true; continue; } sanitizedLines.Add(line); } return sanitizedLines; } private static List GenerateUpdatedBuildFileLines(List lines, string apiKey, bool addBuildScriptLines) { var addPlugin = !string.IsNullOrEmpty(apiKey); // A sample of the template file. // ... // allprojects { // repositories {**ARTIFACTORYREPOSITORY** // google() // jcenter() // flatDir { // dirs 'libs' // } // } // } // // apply plugin: 'com.android.application' // **APPLY_PLUGINS** // // dependencies { // implementation fileTree(dir: 'libs', include: ['*.jar']) // **DEPS**} // ... var outputLines = new List(); // Check if the plugin exists, if so, update the SDK Key. var pluginExists = lines.Any(line => TokenAppLovinPlugin.IsMatch(line)); if (pluginExists) { var pluginMatched = false; var insideAppLovinClosure = false; var updatedApiKey = false; var mavenRepoUpdated = false; var dependencyClassPathUpdated = false; foreach (var line in lines) { // Bintray maven repo is no longer being used. Update to s3 maven repo with regex check if (!mavenRepoUpdated && (line.Contains(QualityServiceBintrayMavenRepo) || line.Contains(QualityServiceNoRegexMavenRepo))) { outputLines.Add(GetFormattedBuildScriptLine(QualityServiceMavenRepo)); mavenRepoUpdated = true; continue; } // We no longer use version specific dependency class path. Just use + for version to always pull the latest. if (!dependencyClassPathUpdated && line.Contains(QualityServiceDependencyClassPathV3)) { outputLines.Add(GetFormattedBuildScriptLine(QualityServiceDependencyClassPath)); dependencyClassPathUpdated = true; continue; } if (!pluginMatched && line.Contains(QualityServicePlugin)) { insideAppLovinClosure = true; pluginMatched = true; } if (insideAppLovinClosure && line.Contains("}")) { insideAppLovinClosure = false; } // Update the API key. if (insideAppLovinClosure && !updatedApiKey && TokenApiKey.IsMatch(line)) { outputLines.Add(string.Format(QualityServiceApiKey, apiKey)); updatedApiKey = true; } // Keep adding the line until we find and update the plugin. else { outputLines.Add(line); } } } // Plugin hasn't been added yet, add it. else { var buildScriptClosureDepth = 0; var insideBuildScriptClosure = false; var buildScriptMatched = false; var qualityServiceRepositoryAdded = false; var qualityServiceDependencyClassPathAdded = false; var qualityServicePluginAdded = false; foreach (var line in lines) { // Add the line to the output lines. outputLines.Add(line); // Check if we need to add the build script lines and add them. if (addBuildScriptLines) { if (!buildScriptMatched && line.Contains(BuildScriptMatcher)) { buildScriptMatched = true; insideBuildScriptClosure = true; } // Match the parenthesis to track if we are still inside the buildscript closure. if (insideBuildScriptClosure) { if (line.Contains("{")) { buildScriptClosureDepth++; } if (line.Contains("}")) { buildScriptClosureDepth--; } if (buildScriptClosureDepth == 0) { insideBuildScriptClosure = false; // There may be multiple buildscript closures and we need to keep looking until we added both the repository and classpath. buildScriptMatched = qualityServiceRepositoryAdded && qualityServiceDependencyClassPathAdded; } } if (insideBuildScriptClosure) { // Add the build script dependency repositories. if (!qualityServiceRepositoryAdded && TokenBuildScriptRepositories.IsMatch(line)) { outputLines.Add(GetFormattedBuildScriptLine(QualityServiceMavenRepo)); qualityServiceRepositoryAdded = true; } // Add the build script dependencies. else if (!qualityServiceDependencyClassPathAdded && TokenBuildScriptDependencies.IsMatch(line)) { outputLines.Add(GetFormattedBuildScriptLine(QualityServiceDependencyClassPath)); qualityServiceDependencyClassPathAdded = true; } } } // Check if we need to add the plugin and add it. if (addPlugin) { // Add the plugin. if (!qualityServicePluginAdded && TokenApplicationPlugin.IsMatch(line)) { outputLines.Add(QualityServiceApplyPlugin); outputLines.AddRange(GenerateAppLovinPluginClosure(apiKey)); qualityServicePluginAdded = true; } } } if ((addBuildScriptLines && (!qualityServiceRepositoryAdded || !qualityServiceDependencyClassPathAdded)) || (addPlugin && !qualityServicePluginAdded)) { MaxSdkLogger.UserError("Failed to add AppLovin Quality Service plugin. Quality Service Plugin Added?: " + qualityServicePluginAdded + ", Quality Service Repo added?: " + qualityServiceRepositoryAdded + ", Quality Service dependency added?: " + qualityServiceDependencyClassPathAdded); return null; } } return outputLines; } public static string GetFormattedBuildScriptLine(string buildScriptLine) { #if UNITY_2022_2_OR_NEWER return " " #elif UNITY_2019_3_OR_NEWER return " " #else return " " #endif + buildScriptLine; } private static IEnumerable GenerateAppLovinPluginClosure(string apiKey) { // applovin { // // NOTE: DO NOT CHANGE - this is NOT your AppLovin MAX SDK key - this is a derived key. // apiKey "456...a1b" // } var linesToInject = new List(5); linesToInject.Add(""); linesToInject.Add("applovin {"); linesToInject.Add(" // NOTE: DO NOT CHANGE - this is NOT your AppLovin MAX SDK key - this is a derived key."); linesToInject.Add(string.Format(QualityServiceApiKey, apiKey)); linesToInject.Add("}"); return linesToInject; } } } #endif