Inheritance: UnityEditor.AssetPostprocessor
        internal static string ValidatePackageDir(string directory)
        {
            // Make sure the package directory starts with the same name.
            // This is case insensitive to handle cases where developers rename Unity
            // project directories on Windows (which has a case insensitive file system by
            // default) then they use the project on OSX / Linux.
            if (!directory.ToLowerInvariant().StartsWith(AndroidPluginsDir.ToLower()))
            {
                directory = AndroidPluginsDir;
            }
            directory = FileUtils.NormalizePathSeparators(directory);
            var searchDirectory = FileUtils.FindDirectoryByCaseInsensitivePath(directory);

            if (String.IsNullOrEmpty(searchDirectory))
            {
                searchDirectory = directory;
            }
            if (directory != searchDirectory &&
                (previouslyValidatedPackageDir == null ||
                 searchDirectory != previouslyValidatedPackageDir))
            {
                PlayServicesResolver.Log(
                    String.Format("Resolving to Android package directory {0} instead of the " +
                                  "requested target directory {1}\n" +
                                  "\n" +
                                  "Is {0} in a different case to {1} ?\n",
                                  searchDirectory, directory), level: LogLevel.Warning);
                directory = searchDirectory;
            }
            else if ((previouslyValidatedPackageDir == null ||
                      searchDirectory != previouslyValidatedPackageDir) &&
                     searchDirectory == null)
            {
                PlayServicesResolver.Log(
                    String.Format("Android package directory {0} not found.",
                                  directory), level: LogLevel.Warning);
            }
            previouslyValidatedPackageDir = searchDirectory;

            return(directory);
        }
        /// <summary>
        /// Find paths to repositories that are included in the project.
        /// </summary>
        /// <param name="dependencies">Dependencies to search for local repositories.</param>
        /// <returns>Set of repository paths in the project.</returns>
        public static HashSet <string> FindLocalRepos(ICollection <Dependency> dependencies)
        {
            // Find all repositories embedded in the project.
            var repos       = new HashSet <string>();
            var projectPath = FileUtils.PosixPathSeparators(Path.GetFullPath("."));

            foreach (var reposAndSources in
                     PlayServicesResolver.GetRepos(dependencies: dependencies))
            {
                var repoUri = reposAndSources.Key;
                if (repoUri.StartsWith(PlayServicesResolver.FILE_SCHEME))
                {
                    var fullPath = repoUri.Substring(PlayServicesResolver.FILE_SCHEME.Length);
                    if (fullPath.StartsWith(projectPath))
                    {
                        repos.Add(fullPath.Substring(projectPath.Length + 1));
                    }
                }
            }
            return(repos);
        }
Example #3
0
        internal static string ValidateLocalMavenRepoDir(string directory) {
            if (!directory.ToLowerInvariant().StartsWith(FileUtils.ASSETS_FOLDER.ToLower())) {
                directory = DefaultLocalMavenRepoDir;
            }
            directory = FileUtils.NormalizePathSeparators(directory);

            // TODO: Remove these restrictions
            // Cannot set to be under "Assets/Plugins/Android".  Seems like all .aar and .pom
            // genereted under this folder will be removed in gradle template mode after
            // being generated.  Need to investigate why.
            if (directory.StartsWith(AndroidPluginsDir)) {
                directory = DefaultLocalMavenRepoDir;
                PlayServicesResolver.Log(String.Format(
                        "Currently LocalMavenRepoDir does not work at any folder " +
                        "under \"Assets/Plugins/Android\""), level: LogLevel.Warning);
            }
            if (directory.EndsWith(Path.DirectorySeparatorChar.ToString())) {
                directory = directory.Substring(0, directory.Length - 1);
            }

            return directory;
        }
        /// <summary>
        /// Inject properties in the gradle properties template file.
        /// Because of a change in structure of android projects built with
        /// Unity 2019.3 and above, the correct way to enable jetifier and
        /// Android X is by updating the gradle properties template.
        /// </summary>
        /// <returns>true if successful, false otherwise.</returns>
        public static bool InjectProperties()
        {
            var resolutionMeasurementParameters =
                PlayServicesResolver.GetResolutionMeasurementParameters(null);

            PlayServicesResolver.analytics.Report(
                "/resolve/gradleproperties", resolutionMeasurementParameters,
                "Gradle Properties Resolve");
            var propertiesLines = new List <string>();

            // Lines to add Custom Gradle properties template to enable
            // jetifier and androidx
            propertiesLines.AddRange(new [] {
                "android.useAndroidX=true",
                "android.enableJetifier=true",
            });
            var propertiesFileDescription = String.Format(
                "gradle properties template" + GradlePropertiesTemplatePath);

            TextFileLineInjector[] propertiesInjectors = new [] {
                new TextFileLineInjector(PropertiesInjectionLine,
                                         PropertiesStartLine, PropertiesEndLine,
                                         propertiesLines,
                                         "Properties",
                                         propertiesFileDescription)
            };
            if (!PatchFile(GradlePropertiesTemplatePath, propertiesFileDescription,
                           "Gradle Properties", "gradleproperties",
                           propertiesInjectors,
                           resolutionMeasurementParameters))
            {
                PlayServicesResolver.Log(
                    String.Format("Unable to patch " + propertiesFileDescription),
                    level: LogLevel.Error);
                return(false);
            }
            return(true);
        }
 /// <summary>
 /// Write new contents to a file on disk
 /// </summary>
 /// <param name="filePath">Path to file to modify</param>
 /// <param name="fileDescription">Used in logs for describing the file</param>
 /// <param name="outputText">Updated contents to write to the file</param>
 /// <param name="analyticsReportName">Name used in analytics logs</param>
 /// <param name="analyticsReportUrlToken">Token used in forming analytics path</param>
 /// <param name="resolutionMeasurementProperties">used in analytics reporting</param>
 /// <returns>true if successful, false otherwise.</returns>
 private static bool WriteToFile(string filePath,
                                 string fileDescription,
                                 string outputText,
                                 string analyticsReportName,
                                 string analyticsReportUrlToken,
                                 ICollection <KeyValuePair <string, string> >
                                 resolutionMeasurementParameters)
 {
     if (!FileUtils.CheckoutFile(filePath, PlayServicesResolver.logger))
     {
         PlayServicesResolver.Log(
             String.Format("Failed to checkout '{0}', unable to patch the file.",
                           filePath), level: LogLevel.Error);
         PlayServicesResolver.analytics.Report(
             "/resolve/" + analyticsReportUrlToken + "/failed/checkout",
             analyticsReportName + " Resolve: Failed to checkout");
         return(false);
     }
     PlayServicesResolver.Log(
         String.Format("Writing updated {0}", fileDescription),
         level: LogLevel.Verbose);
     try {
         File.WriteAllText(filePath, outputText);
         PlayServicesResolver.analytics.Report(
             "/resolve/" + analyticsReportUrlToken + "/success",
             resolutionMeasurementParameters,
             analyticsReportName + " Resolve Success");
     } catch (Exception ex) {
         PlayServicesResolver.analytics.Report(
             "/resolve/" + analyticsReportUrlToken + "/failed/write",
             analyticsReportName + " Resolve: Failed to write");
         PlayServicesResolver.Log(
             String.Format("Unable to patch {0} ({1})", fileDescription,
                           ex.ToString()), level: LogLevel.Error);
         return(false);
     }
     return(true);
 }
Example #6
0
 /// <summary>
 /// Asynchronously execute a command line tool in this window, showing progress
 /// and finally calling the specified delegate on completion from the main / UI thread.
 /// </summary>
 /// <param name="toolPath">Tool to execute.</param>
 /// <param name="arguments">String to pass to the tools' command line.</param>
 /// <param name="completionDelegate">Called when the tool completes.</param>
 /// <param name="workingDirectory">Directory to execute the tool from.</param>
 /// <param name="ioHandler">Allows a caller to provide interactive input and also handle
 /// both output and error streams from a single delegate.</param>
 /// <param name="maxProgressLines">Specifies the number of lines output by the
 /// command line that results in a 100% value on a progress bar.</param>
 /// <returns>Reference to the new window.</returns>
 public void RunAsync(
     string toolPath, string arguments,
     CommandLine.CompletionHandler completionDelegate,
     string workingDirectory         = null, Dictionary <string, string> envVars = null,
     CommandLine.IOHandler ioHandler = null, int maxProgressLines                = 0)
 {
     CommandLineDialog.ProgressReporter reporter = new CommandLineDialog.ProgressReporter();
     reporter.maxProgressLines = maxProgressLines;
     // Call the reporter from the UI thread from this window.
     UpdateEvent += reporter.Update;
     // Connect the user's delegate to the reporter's completion method.
     reporter.Complete += completionDelegate;
     // Connect the caller's IoHandler delegate to the reporter.
     reporter.DataHandler += ioHandler;
     // Disconnect the reporter when the command completes.
     CommandLine.CompletionHandler reporterUpdateDisable =
         (CommandLine.Result unusedResult) => { this.UpdateEvent -= reporter.Update; };
     reporter.Complete += reporterUpdateDisable;
     PlayServicesResolver.Log(String.Format(
                                  "Executing command: {0} {1}", toolPath, arguments), level: LogLevel.Verbose);
     CommandLine.RunAsync(toolPath, arguments, reporter.CommandLineToolCompletion,
                          workingDirectory: workingDirectory, envVars: envVars,
                          ioHandler: reporter.AggregateLine);
 }
Example #7
0
        /// <summary>
        /// Find a tool in the Android SDK.
        /// </summary>
        /// <param name="toolName">Name of the tool to search for.</param>
        /// <param name="sdkPath">SDK path to search for the tool.  If this is null or empty, the
        // system path is searched instead.</param>
        /// <returns>String with the path to the tool if found, null otherwise.</returns>
        private static string FindAndroidSdkTool(string toolName, string sdkPath = null)
        {
            if (String.IsNullOrEmpty(sdkPath))
            {
                PlayServicesResolver.Log(String.Format(
                                             "{0}\n" +
                                             "Falling back to searching for the Android SDK tool {1} in the system path.",
                                             PlayServicesSupport.AndroidSdkConfigurationError, toolName));
            }
            else
            {
                var extensions = new List <string> {
                    CommandLine.GetExecutableExtension()
                };
                if (UnityEngine.RuntimePlatform.WindowsEditor ==
                    UnityEngine.Application.platform)
                {
                    extensions.AddRange(new [] { ".bat", ".cmd" });
                }
                foreach (var dir in new [] { "tools", Path.Combine("tools", "bin") })
                {
                    foreach (var extension in extensions)
                    {
                        var currentPath = Path.Combine(sdkPath,
                                                       Path.Combine(dir, toolName + extension));
                        if (File.Exists(currentPath))
                        {
                            return(currentPath);
                        }
                    }
                }
            }
            var toolPath = CommandLine.FindExecutable(toolName);

            return(toolPath != null && File.Exists(toolPath) ? toolPath : null);
        }
Example #8
0
        /// <summary>
        /// Install a set of packages.
        /// </summary>
        /// <param name="packages">List of package versions to install / upgrade.</param>
        /// <param name="complete">Called when installation is complete.</param>
        public void InstallPackages(HashSet <AndroidSdkPackageNameVersion> packages,
                                    Action <bool> complete)
        {
            var packagesString = AndroidSdkPackageNameVersion.ListToString(packages);
            // TODO: Remove this dialog when the package manager provides feedback while
            // downloading.
            bool installPackage = UnityEditor.EditorUtility.DisplayDialog(
                "Missing Android SDK packages",
                String.Format(
                    "Android SDK packages need to be installed:\n" +
                    "{0}\n" +
                    "\n" +
                    "The install process can be *slow* and does not provide any feedback " +
                    "which may lead you to think Unity has hung / crashed.  Would you like " +
                    "to wait for these package to be installed?",
                    packagesString),
                "Yes", cancel: "No");

            if (!installPackage)
            {
                PlayServicesResolver.Log(
                    "User cancelled installation of Android SDK tools package.",
                    level: LogLevel.Warning);
                complete(false);
                return;
            }
            var packageNames = new List <string>();

            foreach (var pkg in packages)
            {
                packageNames.Add(pkg.Name);
            }
            SdkManagerUtil.InstallPackages(toolPath, String.Join(" ", packageNames.ToArray()),
                                           packages, "Accept? (y/N):", "y", "N",
                                           new Regex("^License\\W+[^ ]+:"), complete);
        }
        // Private method to avoid too deeply nested code in "DoResolution".
        private void GradleResolve(AndroidSdkPackageCollection packages,
                                   PlayServicesSupport svcSupport, string destinationDirectory,
                                   System.Action resolutionComplete)
        {
            string errorOutro = "make sure you have the latest version of this plugin and if you " +
                                "still get this error, report it in a a bug here:\n" +
                                "https://github.com/googlesamples/unity-jar-resolver/issues\n";
            string errorIntro = null;

            int    targetSdkVersion  = UnityCompat.GetAndroidTargetSDKVersion();
            string buildToolsVersion = null;

            if (targetSdkVersion < 0)
            {
                // A value of -1 means the targetSDK Version enum returned "Auto"
                // instead of an actual version, so it's up to us to actually figure it out.
                targetSdkVersion = GetLatestInstalledAndroidPlatformVersion(packages);
                PlayServicesResolver.Log(
                    String.Format("TargetSDK is set to Auto-detect, and the latest Platform has " +
                                  "been detected as: android-{0}", targetSdkVersion),
                    level: LogLevel.Verbose);

                errorIntro = String.Format("The Target SDK is set to automatically pick the highest " +
                                           "installed platform in the Android Player Settings, which appears to be " +
                                           "\"android-{0}\". This requires build-tools with at least the same version, " +
                                           "however ", targetSdkVersion);
            }
            else
            {
                errorIntro = String.Format("The target SDK version is set in the Android Player " +
                                           "Settings to \"android-{0}\" which requires build tools with " +
                                           "at least the same version, however ", targetSdkVersion);
            }

            // You can use a higher version of the build-tools than your compileSdkVersion, in order
            // to pick up new/better compiler while not changing what you build your app against. --Xav
            // https://stackoverflow.com/a/24523113
            // Implicitly Xav is also saying, you can't use a build tool version less than the
            // platform level you're building. This is backed up from testing.
            if (targetSdkVersion > TESTED_BUILD_TOOLS_VERSION_MAJOR)
            {
                buildToolsVersion = GetLatestMinorBuildToolsVersion(packages, targetSdkVersion);

                if (buildToolsVersion == null)
                {
                    PlayServicesResolver.Log(
                        errorIntro + String.Format("no build-tools are available " +
                                                   "at this level in the sdk manager. This plugin has been tested with " +
                                                   "platforms up to android-{0} using build-tools {0}.{1}.{2}. You can try " +
                                                   "selecting a lower targetSdkVersion in the Android Player Settings.  Please ",
                                                   TESTED_BUILD_TOOLS_VERSION_MAJOR, TESTED_BUILD_TOOLS_VERSION_MINOR,
                                                   TESTED_BUILD_TOOLS_VERSION_REV) + errorOutro,
                        level: LogLevel.Error);
                    return;
                }
                else
                {
                    PlayServicesResolver.Log(
                        errorIntro + String.Format("this plugin has only been " +
                                                   "tested with build-tools up to version {0}.{1}.{2}. Corresponding " +
                                                   "build-tools version {3} will be used, however this is untested with this " +
                                                   "plugin and MAY NOT WORK! If you have trouble, please select a target SDK " +
                                                   "version at or below \"android-{0}\". If you need to get this working with " +
                                                   "the latest platform, please ",
                                                   TESTED_BUILD_TOOLS_VERSION_MAJOR,
                                                   TESTED_BUILD_TOOLS_VERSION_MINOR,
                                                   TESTED_BUILD_TOOLS_VERSION_REV,
                                                   buildToolsVersion) + errorOutro, level: LogLevel.Warning);
                }
            }

            if (buildToolsVersion == null)
            {
                // Use the tested build tools version, which we know will be able to handle
                // this targetSDK version.
                buildToolsVersion = String.Format("{0}.{1}.{2}", TESTED_BUILD_TOOLS_VERSION_MAJOR,
                                                  TESTED_BUILD_TOOLS_VERSION_MINOR,
                                                  TESTED_BUILD_TOOLS_VERSION_REV);
                // We don't have to bother with checking if it's installed because gradle actually
                // does that for us.
            }

            string minSdkVersion = UnityCompat.GetAndroidMinSDKVersion().ToString();

            var config = new Dictionary <string, string>()
            {
                { "app_id", UnityCompat.ApplicationId },
                { "sdk_version", targetSdkVersion.ToString() },
                { "min_sdk_version", minSdkVersion },
                { "build_tools_version", buildToolsVersion },
                { "android_sdk_dir", svcSupport.SDK }
            };

            // This creates an enumerable of strings with the json lines for each dep like this:
            // "[\"namespace\", \"package\", \"version\"]"
            var dependencies = PlayServicesSupport.GetAllDependencies();
            var depLines     = from d in dependencies
                               select "[" + ToJSONList(DepsVersionAsArray(d.Value)) + "]";
            // Get a flattened list of dependencies, excluding any with the "$SDK" path variable,
            // since those will automatically be included in the gradle build.
            var repoLines = new HashSet <string>(
                dependencies.SelectMany(d => d.Value.Repositories)
                .Where(s => !s.Contains(PlayServicesSupport.SdkVariable)));

            var proguard_config_paths = new List <string>()
            {
                Path.Combine(GRADLE_SCRIPT_LOCATION, PROGUARD_UNITY_CONFIG),
                Path.Combine(GRADLE_SCRIPT_LOCATION, PROGUARD_MSG_FIX_CONFIG)
            };

            // Build the full json config as a string.
            string json_config = @"{{
""config"": {{
{0}
}},
""project_deps"": [
{1}
],
""extra_m2repositories"": [
{2}
],
""extra_proguard_configs"": [
{3}
]
}}";

            json_config = String.Format(json_config, ToJSONDictionary(config),
                                        ToJSONList(depLines, ",\n", 4, true),
                                        ToJSONList(repoLines, ",\n", 4),
                                        ToJSONList(proguard_config_paths, ",\n", 4));

            // Escape any literal backslashes (such as those from paths on windows), since we want to
            // preserve them when reading the config as backslashes and not interpret them
            // as escape characters.
            json_config = json_config.Replace(@"\", @"\\");

            System.IO.File.WriteAllText(GENERATE_CONFIG_PATH, json_config);
            var outDir = Path.Combine(destinationDirectory, GENERATE_GRADLE_OUTPUT_DIR);

            RunGenGradleScript(
                " -c \"" + GENERATE_CONFIG_PATH + "\"" +
                " -b \"" + GENERATE_GRADLE_BUILD_PATH + "\"" +
                " -o \"" + outDir + "\"",
                (result) => {
                if (result.exitCode == 0)
                {
                    var currentAbi = PlayServicesResolver.AndroidTargetDeviceAbi;
                    var activeAbis = GetSelectedABIDirs(currentAbi);
                    var libsDir    = Path.Combine(outDir, "libs");
                    if (Directory.Exists(libsDir))
                    {
                        foreach (var directory in Directory.GetDirectories(libsDir))
                        {
                            var abiDir = Path.GetFileName(directory).ToLower();
                            if (!activeAbis.Contains(abiDir))
                            {
                                FileUtils.DeleteExistingFileOrDirectory(directory);
                            }
                        }
                    }
                    if (Directory.Exists(outDir))
                    {
                        PlayServicesResolver.LabelAssets(new [] { outDir }, true, true);
                    }
                    AssetDatabase.Refresh();
                    resolutionComplete();
                }
            });
        }
        /// <summary>
        /// Copy srcaar files to aar files that are excluded from Unity's build process.
        /// </summary>
        /// <param name="dependencies">Dependencies to inject.</param>
        /// <returns>true if successful, false otherwise.</returns>
        private static bool CopySrcAars(ICollection <Dependency> dependencies)
        {
            bool succeeded = true;
            var  aarFiles  = new List <string>();

            // Copy each .srcaar file to .aar while configuring the plugin importer to ignore the
            // file.
            foreach (var aar in LocalMavenRepository.FindAarsInLocalRepos(dependencies))
            {
                var  dir            = Path.GetDirectoryName(aar);
                var  filename       = Path.GetFileNameWithoutExtension(aar);
                var  targetFilename = Path.Combine(dir, filename + ".aar");
                bool configuredAar  = File.Exists(targetFilename);
                if (!configuredAar)
                {
                    bool copiedAndLabeledAar = AssetDatabase.CopyAsset(aar, targetFilename);
                    if (copiedAndLabeledAar)
                    {
                        var unlabeledAssets = new HashSet <string>();
                        PlayServicesResolver.LabelAssets(
                            new [] { targetFilename },
                            complete: (unlabeled) => { unlabeledAssets.UnionWith(unlabeled); });
                        copiedAndLabeledAar = unlabeledAssets.Count == 0;
                    }
                    if (copiedAndLabeledAar)
                    {
                        try {
                            PluginImporter importer = (PluginImporter)AssetImporter.GetAtPath(
                                targetFilename);
                            importer.SetCompatibleWithAnyPlatform(false);
                            importer.SetCompatibleWithPlatform(BuildTarget.Android, false);
                            configuredAar = true;
                        } catch (Exception ex) {
                            PlayServicesResolver.Log(String.Format(
                                                         "Failed to disable {0} from being included by Unity's " +
                                                         "internal build.  {0} has been deleted and will not be " +
                                                         "included in Gradle builds. ({1})", aar, ex),
                                                     level: LogLevel.Error);
                        }
                    }
                    else
                    {
                        PlayServicesResolver.Log(String.Format(
                                                     "Unable to copy {0} to {1}.  {1} will not be included in Gradle " +
                                                     "builds.", aar, targetFilename), level: LogLevel.Error);
                    }
                    if (configuredAar)
                    {
                        aarFiles.Add(targetFilename);
                        // Some versions of Unity do not mark the asset database as dirty when
                        // plugin importer settings change so reimport the asset to synchronize
                        // the state.
                        AssetDatabase.ImportAsset(targetFilename, ImportAssetOptions.ForceUpdate);
                    }
                    else
                    {
                        if (File.Exists(targetFilename))
                        {
                            AssetDatabase.DeleteAsset(targetFilename);
                        }
                        succeeded = false;
                    }
                }
            }
            foreach (var aar in aarFiles)
            {
                if (!LocalMavenRepository.PatchPomFile(aar))
                {
                    succeeded = false;
                }
            }
            return(succeeded);
        }
        /// <summary>
        /// Inject / update dependencies in the gradle template file.
        /// </summary>
        /// <param name="dependencies">Dependencies to inject.</param>
        /// <returns>true if successful, false otherwise.</returns>
        public static bool InjectDependencies(ICollection <Dependency> dependencies)
        {
            var resolutionMeasurementParameters =
                PlayServicesResolver.GetResolutionMeasurementParameters(null);

            if (dependencies.Count > 0)
            {
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate", resolutionMeasurementParameters,
                    "Gradle Template Resolve");
            }

            var fileDescription = String.Format("gradle template {0}", GradleTemplatePath);

            PlayServicesResolver.Log(String.Format("Reading {0}", fileDescription),
                                     level: LogLevel.Verbose);
            IEnumerable <string> lines;

            try {
                lines = File.ReadAllLines(GradleTemplatePath);
            } catch (Exception ex) {
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate/failed/templateunreadable",
                    "Gradle Template Resolve: Failed Template Unreadable");
                PlayServicesResolver.Log(
                    String.Format("Unable to patch {0} ({1})", fileDescription, ex.ToString()),
                    level: LogLevel.Error);
                return(false);
            }

            PlayServicesResolver.Log(String.Format("Searching for {0} in {1}", DependenciesToken,
                                                   fileDescription),
                                     level: LogLevel.Verbose);
            // Determine whether dependencies should be injected.
            var  dependenciesToken = new Regex(DependenciesToken);
            bool containsDeps      = false;

            foreach (var line in lines)
            {
                if (dependenciesToken.IsMatch(line))
                {
                    containsDeps = true;
                    break;
                }
            }

            // If a dependencies token isn't present report a warning and abort.
            if (!containsDeps)
            {
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate/failed/noinjectionpoint",
                    "Gradle Template Resolve: Failed No Injection Point");
                PlayServicesResolver.Log(
                    String.Format("No {0} token found in {1}, Android Resolver libraries will " +
                                  "not be added to the file.", DependenciesToken, fileDescription),
                    level: LogLevel.Warning);
                return(true);
            }

            // Copy all srcaar files in the project to aar filenames so that they'll be included in
            // the Gradle build.
            if (!CopySrcAars(dependencies))
            {
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate/failed/srcaarcopy",
                    "Gradle Template Resolve: Failed srcaar I/O");
                return(false);
            }

            var repoLines = new List <string>();

            // Optionally enable the jetifier.
            if (SettingsDialog.UseJetifier && dependencies.Count > 0)
            {
                repoLines.AddRange(new [] {
                    "([rootProject] + (rootProject.subprojects as List)).each {",
                    "    ext {",
                    "        it.setProperty(\"android.useAndroidX\", true)",
                    "        it.setProperty(\"android.enableJetifier\", true)",
                    "    }",
                    "}"
                });
            }
            repoLines.AddRange(PlayServicesResolver.GradleMavenReposLines(dependencies));

            TextFileLineInjector[] injectors = new [] {
                new TextFileLineInjector(ReposInjectionLine, ReposStartLine, ReposEndLine,
                                         repoLines, "Repos", fileDescription),
                new TextFileLineInjector(DependenciesToken, DependenciesStartLine,
                                         DependenciesEndLine,
                                         PlayServicesResolver.GradleDependenciesLines(
                                             dependencies, includeDependenciesBlock: false),
                                         "Dependencies", fileDescription),
                new TextFileLineInjector(PackagingOptionsToken, PackagingOptionsStartLine,
                                         PackagingOptionsEndLine,
                                         PlayServicesResolver.PackagingOptionsLines(dependencies),
                                         "Packaging Options", fileDescription),
            };
            // Lines that will be written to the output file.
            var outputLines = new List <string>();

            foreach (var line in lines)
            {
                var currentOutputLines = new List <string>();
                foreach (var injector in injectors)
                {
                    bool injectionApplied = false;
                    currentOutputLines = injector.ProcessLine(line, out injectionApplied);
                    if (injectionApplied || currentOutputLines.Count == 0)
                    {
                        break;
                    }
                }
                outputLines.AddRange(currentOutputLines);
            }
            var inputText  = String.Join("\n", (new List <string>(lines)).ToArray()) + "\n";
            var outputText = String.Join("\n", outputLines.ToArray()) + "\n";

            if (inputText == outputText)
            {
                PlayServicesResolver.Log(String.Format("No changes to {0}", fileDescription),
                                         level: LogLevel.Verbose);
                return(true);
            }
            if (!FileUtils.CheckoutFile(GradleTemplatePath, PlayServicesResolver.logger))
            {
                PlayServicesResolver.Log(
                    String.Format("Failed to checkout '{0}', unable to patch the file.",
                                  GradleTemplatePath), level: LogLevel.Error);
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate/failed/checkout",
                    "Gradle Template Resolve: Failed to checkout");
                return(false);
            }
            PlayServicesResolver.Log(
                String.Format("Writing updated {0}", fileDescription),
                level: LogLevel.Verbose);
            try {
                File.WriteAllText(GradleTemplatePath, outputText);
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate/success", resolutionMeasurementParameters,
                    "Gradle Template Resolve Success");
            } catch (Exception ex) {
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate/failed/write",
                    "Gradle Template Resolve: Failed to write");
                PlayServicesResolver.Log(
                    String.Format("Unable to patch {0} ({1})", fileDescription,
                                  ex.ToString()), level: LogLevel.Error);
                return(false);
            }
            return(true);
        }
        /// <summary>
        /// Explodes a single aar file.  This is done by calling the
        /// JDK "jar" command, then moving the classes.jar file.
        /// </summary>
        /// <param name="dir">the parent directory of the plugin.</param>
        /// <param name="aarFile">Aar file to explode.</param>
        /// <param name="antProject">true to explode into an Ant style project or false
        /// to repack the processed AAR as a new AAR.</param>
        /// <param name="abi">ABI of the AAR or null if it's universal.</param>
        /// <returns>true if successful, false otherwise.</returns>
        internal virtual bool ProcessAar(string dir, string aarFile, bool antProject,
                                         out string abi)
        {
            abi = null;
            string workingDir = Path.Combine(dir, Path.GetFileNameWithoutExtension(aarFile));

            PlayServicesSupport.DeleteExistingFileOrDirectory(workingDir, includeMetaFiles: true);
            Directory.CreateDirectory(workingDir);
            if (!ExtractAar(aarFile, null, workingDir))
            {
                return(false);
            }
            ReplaceVariables(workingDir);

            string nativeLibsDir = null;

            if (antProject)
            {
                // Create the libs directory to store the classes.jar and non-Java shared
                // libraries.
                string libDir = Path.Combine(workingDir, "libs");
                nativeLibsDir = libDir;
                Directory.CreateDirectory(libDir);

                // Move the classes.jar file to libs.
                string classesFile = Path.Combine(workingDir, "classes.jar");
                if (File.Exists(classesFile))
                {
                    string targetClassesFile = Path.Combine(libDir, Path.GetFileName(classesFile));
                    if (File.Exists(targetClassesFile))
                    {
                        File.Delete(targetClassesFile);
                    }
                    File.Move(classesFile, targetClassesFile);
                }
            }

            // Copy non-Java shared libraries (.so) files from the "jni" directory into the
            // lib directory so that Unity's legacy (Ant-like) build system includes them in the
            // built APK.
            string jniLibDir = Path.Combine(workingDir, "jni");

            nativeLibsDir = nativeLibsDir ?? jniLibDir;
            if (Directory.Exists(jniLibDir))
            {
                if (jniLibDir != nativeLibsDir)
                {
                    PlayServicesSupport.CopyDirectory(jniLibDir, nativeLibsDir);
                    PlayServicesSupport.DeleteExistingFileOrDirectory(jniLibDir,
                                                                      includeMetaFiles: true);
                }
                // Remove shared libraries for all ABIs that are not required for the selected
                // target ABI.
                var activeAbis = new HashSet <string>();
                var currentAbi = PlayServicesResolver.AndroidTargetDeviceAbi.ToLower();
                foreach (var kv in UNITY_ABI_TO_NATIVE_LIBRARY_ABI_DIRECTORY)
                {
                    if (currentAbi == kv.Key)
                    {
                        activeAbis.Add(kv.Value);
                    }
                }
                if (activeAbis.Count == 0)
                {
                    activeAbis.UnionWith(UNITY_ABI_TO_NATIVE_LIBRARY_ABI_DIRECTORY.Values);
                }
                foreach (var directory in Directory.GetDirectories(nativeLibsDir))
                {
                    var abiDir = Path.GetFileName(directory);
                    if (!activeAbis.Contains(abiDir))
                    {
                        PlayServicesSupport.DeleteExistingFileOrDirectory(
                            directory, includeMetaFiles: true);
                    }
                }
                abi = currentAbi;
            }

            if (antProject)
            {
                // Create the project.properties file which indicates to
                // Unity that this directory is a plugin.
                string projectProperties = Path.Combine(workingDir, "project.properties");
                if (!File.Exists(projectProperties))
                {
                    File.WriteAllLines(projectProperties, new [] {
                        "# Project target.",
                        "target=android-9",
                        "android.library=true"
                    });
                }
                // Clean up the aar file.
                PlayServicesSupport.DeleteExistingFileOrDirectory(Path.GetFullPath(aarFile),
                                                                  includeMetaFiles: true);
                // Add a tracking label to the exploded files.
                PlayServicesResolver.LabelAssets(new [] { workingDir });
            }
            else
            {
                // Add a tracking label to the exploded files just in-case packaging fails.
                PlayServicesResolver.LabelAssets(new [] { workingDir });
                // Create a new AAR file.
                PlayServicesSupport.DeleteExistingFileOrDirectory(Path.GetFullPath(aarFile),
                                                                  includeMetaFiles: true);
                if (!ArchiveAar(aarFile, workingDir))
                {
                    return(false);
                }
                // Clean up the exploded directory.
                PlayServicesSupport.DeleteExistingFileOrDirectory(workingDir,
                                                                  includeMetaFiles: true);
            }
            return(true);
        }
        /// <summary>
        /// Search for the POM file associated with the specified maven artifact and patch the
        /// packaging reference if the POM doesn't reference the artifact.
        /// file.
        /// </summary>
        /// <param name="artifactFilename">artifactFilename</param>
        /// <returns>true if successful, false otherwise.</returns>
        public static bool PatchPomFile(string artifactFilename)
        {
            var failureImpact = String.Format("{0} may not be included in your project",
                                              Path.GetFileName(artifactFilename));
            var pomFilename = PathWithoutExtension(artifactFilename) + ".pom";

            if (!File.Exists(pomFilename))
            {
                PlayServicesResolver.Log(
                    String.Format("Maven POM {0} for {1} does not exist. " + failureImpact,
                                  pomFilename, artifactFilename), level: LogLevel.Warning);
                return(false);
            }
            var artifactPackaging = Path.GetExtension(artifactFilename).ToLower().Substring(1);
            var pom = new XmlDocument();

            try {
                using (var stream = new StreamReader(pomFilename)) {
                    pom.Load(stream);
                }
            } catch (Exception ex) {
                PlayServicesResolver.Log(
                    String.Format("Unable to read maven POM {0} for {1} ({2}). " + failureImpact,
                                  pom, artifactFilename, ex), level: LogLevel.Error);
                return(false);
            }
            bool        updatedPackaging = false;
            XmlNodeList packagingNode    = pom.GetElementsByTagName("packaging");

            foreach (XmlNode node in packagingNode)
            {
                if (node.InnerText != artifactPackaging)
                {
                    PlayServicesResolver.Log(String.Format(
                                                 "Replacing packaging of maven POM {0} {1} --> {2}",
                                                 pomFilename, node.InnerText, artifactPackaging), level: LogLevel.Verbose);
                    node.InnerText   = artifactPackaging;
                    updatedPackaging = true;
                }
            }
            if (updatedPackaging)
            {
                try {
                    using (var xmlWriter =
                               XmlWriter.Create(pomFilename,
                                                new XmlWriterSettings {
                        Indent = true,
                        IndentChars = "  ",
                        NewLineChars = "\n",
                        NewLineHandling = NewLineHandling.Replace
                    })) {
                        pom.Save(xmlWriter);
                    }
                } catch (Exception ex) {
                    PlayServicesResolver.Log(
                        String.Format("Unable to write patch maven POM {0} for {1} with " +
                                      "packaging {2} ({3}). " + failureImpact,
                                      pom, artifactFilename, artifactPackaging, ex));
                    return(false);
                }
            }
            return(true);
        }
        /// <summary>
        /// Called when the GUI should be rendered.
        /// </summary>
        public void OnGUI()
        {
            GUI.skin.label.wordWrap = true;
            GUILayout.BeginVertical();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Prebuild With Gradle (Experimental)", EditorStyles.boldLabel);
            prebuildWithGradle = EditorGUILayout.Toggle(prebuildWithGradle);
            GUILayout.EndHorizontal();

            EditorGUI.BeginDisabledGroup(prebuildWithGradle == true);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Fetch Dependencies with Gradle", EditorStyles.boldLabel);
            fetchDependenciesWithGradle = EditorGUILayout.Toggle(fetchDependenciesWithGradle);
            GUILayout.EndHorizontal();
            if (fetchDependenciesWithGradle)
            {
                GUILayout.Label("AARs are fetched using Gradle which enables assets to be " +
                                "fetched from remote Maven repositories and Gradle version " +
                                "expressions.");
            }
            else
            {
                GUILayout.Label("Legacy AAR fetching method that only queries the Android SDK " +
                                "manager's local maven repository and user specified local " +
                                "maven repositories for dependencies.");
            }
            EditorGUI.EndDisabledGroup();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Enable Auto-Resolution", EditorStyles.boldLabel);
            enableAutoResolution = EditorGUILayout.Toggle(enableAutoResolution);
            GUILayout.EndHorizontal();

            EditorGUI.BeginDisabledGroup(prebuildWithGradle == true);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Install Android Packages", EditorStyles.boldLabel);
            installAndroidPackages = EditorGUILayout.Toggle(installAndroidPackages);
            GUILayout.EndHorizontal();

            if (ConfigurablePackageDir)
            {
                GUILayout.BeginHorizontal();
                string previousPackageDir = packageDir;
                GUILayout.Label("Package Directory", EditorStyles.boldLabel);
                if (GUILayout.Button("Browse"))
                {
                    string path = EditorUtility.OpenFolderPanel("Set Package Directory",
                                                                PackageDir, "");
                    int startOfPath = path.IndexOf(AndroidPluginsDir);
                    if (startOfPath < 0)
                    {
                        packageDir = "";
                    }
                    else
                    {
                        packageDir = path.Substring(startOfPath, path.Length - startOfPath);
                    }
                }
                if (!previousPackageDir.Equals(packageDir))
                {
                    packageDir = ValidatePackageDir(packageDir);
                }
                GUILayout.EndHorizontal();
                packageDir = EditorGUILayout.TextField(packageDir);
            }

            GUILayout.BeginHorizontal();
            GUILayout.Label("Explode AARs", EditorStyles.boldLabel);
            explodeAars = EditorGUILayout.Toggle(explodeAars);
            GUILayout.EndHorizontal();
            if (explodeAars)
            {
                GUILayout.Label("AARs will be exploded (unpacked) when ${applicationId} " +
                                "variable replacement is required in an AAR's " +
                                "AndroidManifest.xml or a single target ABI is selected " +
                                "without a compatible build system.");
            }
            else
            {
                GUILayout.Label("AAR explosion will be disabled in exported Gradle builds " +
                                "(Unity 5.5 and above). You will need to set " +
                                "android.defaultConfig.applicationId to your bundle ID in your " +
                                "build.gradle to generate a functional APK.");
            }
            EditorGUI.EndDisabledGroup();

            EditorGUI.BeginDisabledGroup(enableAutoResolution);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Auto-Resolution Disabled Warning", EditorStyles.boldLabel);
            autoResolutionDisabledWarning = EditorGUILayout.Toggle(autoResolutionDisabledWarning);
            GUILayout.EndHorizontal();
            EditorGUI.EndDisabledGroup();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Verbose Logging", EditorStyles.boldLabel);
            verboseLogging = EditorGUILayout.Toggle(verboseLogging);
            GUILayout.EndHorizontal();

            GUILayout.Space(10);
            GUILayout.BeginHorizontal();
            bool closeWindow = GUILayout.Button("Cancel");
            bool ok          = GUILayout.Button("OK");

            closeWindow |= ok;
            if (ok)
            {
                PrebuildWithGradle          = prebuildWithGradle;
                FetchDependenciesWithGradle = fetchDependenciesWithGradle;
                EnableAutoResolution        = enableAutoResolution;
                InstallAndroidPackages      = installAndroidPackages;
                if (ConfigurablePackageDir)
                {
                    PackageDir = packageDir;
                }
                ExplodeAars    = explodeAars;
                VerboseLogging = verboseLogging;
                AutoResolutionDisabledWarning = autoResolutionDisabledWarning;
                PlayServicesResolver.OnSettingsChanged();
            }
            if (closeWindow)
            {
                Close();
            }
            GUILayout.EndHorizontal();
            GUILayout.EndVertical();
        }
        /// <summary>
        /// Explodes a single aar file.  This is done by calling the
        /// JDK "jar" command, then moving the classes.jar file.
        /// </summary>
        /// <param name="dir">The directory to unpack / explode the AAR to.  If antProject is true
        /// the ant project will be located in Path.Combine(dir, Path.GetFileName(aarFile)).</param>
        /// <param name="aarFile">Aar file to explode.</param>
        /// <param name="antProject">true to explode into an Ant style project or false
        /// to repack the processed AAR as a new AAR.</param>
        /// <param name="abis">ABIs in the AAR or null if it's universal.</param>
        /// <returns>true if successful, false otherwise.</returns>
        internal virtual bool ProcessAar(string dir, string aarFile, bool antProject,
                                         out AndroidAbis abis)
        {
            PlayServicesResolver.Log(String.Format("ProcessAar {0} {1} antProject={2}",
                                                   dir, aarFile, antProject),
                                     level: LogLevel.Verbose);
            abis = null;
            string aarDirName = Path.GetFileNameWithoutExtension(aarFile);
            // Output directory for the contents of the AAR / JAR.
            string outputDir  = Path.Combine(dir, aarDirName);
            string stagingDir = FileUtils.CreateTemporaryDirectory();

            if (stagingDir == null)
            {
                PlayServicesResolver.Log(String.Format(
                                             "Unable to create temporary directory to process AAR {0}", aarFile),
                                         level: LogLevel.Error);
                return(false);
            }
            try {
                string workingDir = Path.Combine(stagingDir, aarDirName);
                FileUtils.DeleteExistingFileOrDirectory(workingDir);
                Directory.CreateDirectory(workingDir);
                if (!PlayServicesResolver.ExtractZip(aarFile, null, workingDir))
                {
                    return(false);
                }
                PlayServicesResolver.ReplaceVariablesInAndroidManifest(
                    Path.Combine(workingDir, "AndroidManifest.xml"),
                    PlayServicesResolver.GetAndroidApplicationId(),
                    new Dictionary <string, string>());

                string nativeLibsDir = null;
                if (antProject)
                {
                    // Create the libs directory to store the classes.jar and non-Java shared
                    // libraries.
                    string libDir = Path.Combine(workingDir, "libs");
                    nativeLibsDir = libDir;
                    Directory.CreateDirectory(libDir);

                    // Move the classes.jar file to libs.
                    string classesFile       = Path.Combine(workingDir, "classes.jar");
                    string targetClassesFile = Path.Combine(libDir, Path.GetFileName(classesFile));
                    if (File.Exists(targetClassesFile))
                    {
                        File.Delete(targetClassesFile);
                    }
                    if (File.Exists(classesFile))
                    {
                        FileUtils.MoveFile(classesFile, targetClassesFile);
                    }
                    else
                    {
                        // Some libraries publish AARs that are poorly formatted (e.g missing
                        // a classes.jar file).  Firebase's license AARs at certain versions are
                        // examples of this.  When Unity's internal build system detects an Ant
                        // project or AAR without a classes.jar, the build is aborted.  This
                        // generates an empty classes.jar file to workaround the issue.
                        string emptyClassesDir = Path.Combine(stagingDir, "empty_classes_jar");
                        if (!ArchiveAar(targetClassesFile, emptyClassesDir))
                        {
                            return(false);
                        }
                    }
                }

                // Copy non-Java shared libraries (.so) files from the "jni" directory into the
                // lib directory so that Unity's legacy (Ant-like) build system includes them in the
                // built APK.
                string jniLibDir = Path.Combine(workingDir, "jni");
                nativeLibsDir = nativeLibsDir ?? jniLibDir;
                if (Directory.Exists(jniLibDir))
                {
                    var abisInArchive = AarDirectoryFindAbis(workingDir);
                    if (jniLibDir != nativeLibsDir)
                    {
                        FileUtils.CopyDirectory(jniLibDir, nativeLibsDir);
                        FileUtils.DeleteExistingFileOrDirectory(jniLibDir);
                    }
                    if (abisInArchive != null)
                    {
                        // Remove shared libraries for all ABIs that are not required for the
                        // selected ABIs.
                        var activeAbisSet            = AndroidAbis.Current.ToSet();
                        var abisInArchiveSet         = abisInArchive.ToSet();
                        var abisInArchiveToRemoveSet = new HashSet <string>(abisInArchiveSet);
                        abisInArchiveToRemoveSet.ExceptWith(activeAbisSet);

                        Func <IEnumerable <string>, string> setToString = (setToConvert) => {
                            return(String.Join(", ", (new List <string>(setToConvert)).ToArray()));
                        };
                        PlayServicesResolver.Log(
                            String.Format(
                                "Target ABIs [{0}], ABIs [{1}] in {2}, will remove [{3}] ABIs",
                                setToString(activeAbisSet),
                                setToString(abisInArchiveSet),
                                aarFile,
                                setToString(abisInArchiveToRemoveSet)),
                            level: LogLevel.Verbose);

                        foreach (var abiToRemove in abisInArchiveToRemoveSet)
                        {
                            abisInArchiveSet.Remove(abiToRemove);
                            FileUtils.DeleteExistingFileOrDirectory(Path.Combine(nativeLibsDir,
                                                                                 abiToRemove));
                        }
                        abis = new AndroidAbis(abisInArchiveSet);
                    }
                }

                if (antProject)
                {
                    // Create the project.properties file which indicates to Unity that this
                    // directory is a plugin.
                    string projectProperties = Path.Combine(workingDir, "project.properties");
                    if (!File.Exists(projectProperties))
                    {
                        File.WriteAllLines(projectProperties, new [] {
                            "# Project target.",
                            "target=android-9",
                            "android.library=true"
                        });
                    }
                    PlayServicesResolver.Log(
                        String.Format("Creating Ant project: Replacing {0} with {1}", aarFile,
                                      outputDir), level: LogLevel.Verbose);
                    // Clean up the aar file.
                    FileUtils.DeleteExistingFileOrDirectory(Path.GetFullPath(aarFile));
                    // Create the output directory.
                    FileUtils.MoveDirectory(workingDir, outputDir);
                    // Add a tracking label to the exploded files.
                    PlayServicesResolver.LabelAssets(new [] { outputDir });
                }
                else
                {
                    // Add a tracking label to the exploded files just in-case packaging fails.
                    PlayServicesResolver.Log(String.Format("Repacking {0} from {1}",
                                                           aarFile, workingDir),
                                             level: LogLevel.Verbose);
                    // Create a new AAR file.
                    FileUtils.DeleteExistingFileOrDirectory(Path.GetFullPath(aarFile));
                    if (!ArchiveAar(aarFile, workingDir))
                    {
                        return(false);
                    }
                    PlayServicesResolver.LabelAssets(new [] { aarFile });
                }
            } catch (Exception e) {
                PlayServicesResolver.Log(String.Format("Failed to process AAR {0} ({1}",
                                                       aarFile, e),
                                         level: LogLevel.Error);
            } finally {
                // Clean up the temporary directory.
                FileUtils.DeleteExistingFileOrDirectory(stagingDir);
            }
            return(true);
        }
Example #16
0
        /// <summary>
        /// Called when the GUI should be rendered.
        /// </summary>
        public void OnGUI()
        {
            GUILayout.BeginVertical();
            GUILayout.Label(String.Format("Android Resolver (version {0}.{1}.{2})",
                                          AndroidResolverVersionNumber.Value.Major,
                                          AndroidResolverVersionNumber.Value.Minor,
                                          AndroidResolverVersionNumber.Value.Build));
            scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);

            GUI.skin.label.wordWrap = true;
            GUILayout.BeginVertical();
            GUILayout.BeginHorizontal();
            GUILayout.Label("Use Gradle Daemon", EditorStyles.boldLabel);
            settings.useGradleDaemon = EditorGUILayout.Toggle(settings.useGradleDaemon);
            GUILayout.EndHorizontal();
            GUILayout.Label(
                settings.useGradleDaemon ?
                ("Gradle Daemon will be used to fetch dependencies.  " +
                 "This is faster but can be flakey in some environments.") :
                ("Gradle Daemon will not be used.  This is slow but reliable."));

            GUILayout.BeginHorizontal();
            GUILayout.Label("Enable Auto-Resolution", EditorStyles.boldLabel);
            settings.enableAutoResolution = EditorGUILayout.Toggle(settings.enableAutoResolution);
            GUILayout.EndHorizontal();
            GUILayout.Label(
                settings.enableAutoResolution ?
                ("Android libraries will be downloaded and processed in the editor.") :
                ("Android libraries will *not* be downloaded or processed in the editor."));

            GUILayout.BeginHorizontal();
            GUILayout.Label("Enable Resolution On Build", EditorStyles.boldLabel);
            settings.autoResolveOnBuild = EditorGUILayout.Toggle(settings.autoResolveOnBuild);
            GUILayout.EndHorizontal();
            GUILayout.Label(
                settings.autoResolveOnBuild ?
                ("Android libraries will be downloaded and processed in a pre-build step.") :
                ("Android libraries will *not* be downloaded or processed in a pre-build step."));

            GUILayout.BeginHorizontal();
            GUILayout.Label("Install Android Packages", EditorStyles.boldLabel);
            settings.installAndroidPackages =
                EditorGUILayout.Toggle(settings.installAndroidPackages);
            GUILayout.EndHorizontal();

            if (ConfigurablePackageDir)
            {
                GUILayout.BeginHorizontal();
                string previousPackageDir = settings.packageDir;
                GUILayout.Label("Package Directory", EditorStyles.boldLabel);
                if (GUILayout.Button("Browse"))
                {
                    string path = EditorUtility.OpenFolderPanel("Set Package Directory",
                                                                PackageDir, "");
                    int startOfPath = path.IndexOf(AndroidPluginsDir);
                    settings.packageDir = startOfPath < 0 ? "" :
                                          path.Substring(startOfPath, path.Length - startOfPath);;
                }
                if (!previousPackageDir.Equals(settings.packageDir))
                {
                    settings.packageDir = ValidatePackageDir(settings.packageDir);
                }
                GUILayout.EndHorizontal();
                settings.packageDir = EditorGUILayout.TextField(settings.packageDir);
            }

            GUILayout.BeginHorizontal();
            GUILayout.Label("Explode AARs", EditorStyles.boldLabel);
            settings.explodeAars = EditorGUILayout.Toggle(settings.explodeAars);
            GUILayout.EndHorizontal();
            if (settings.explodeAars)
            {
                GUILayout.Label("AARs will be exploded (unpacked) when ${applicationId} " +
                                "variable replacement is required in an AAR's " +
                                "AndroidManifest.xml or a single target ABI is selected " +
                                "without a compatible build system.");
            }
            else
            {
                GUILayout.Label("AAR explosion will be disabled in exported Gradle builds " +
                                "(Unity 5.5 and above). You will need to set " +
                                "android.defaultConfig.applicationId to your bundle ID in your " +
                                "build.gradle to generate a functional APK.");
            }

            // Disable the ability to toggle the auto-resolution disabled warning
            // when auto resolution is enabled.
            EditorGUI.BeginDisabledGroup(settings.enableAutoResolution ||
                                         settings.autoResolveOnBuild);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Auto-Resolution Disabled Warning", EditorStyles.boldLabel);
            settings.autoResolutionDisabledWarning =
                EditorGUILayout.Toggle(settings.autoResolutionDisabledWarning);
            GUILayout.EndHorizontal();
            EditorGUI.EndDisabledGroup();

            // Disable the ability to toggle the auto-resolution disabled warning
            // when auto resolution is enabled.
            EditorGUI.BeginDisabledGroup(!settings.enableAutoResolution);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Prompt Before Auto-Resolution", EditorStyles.boldLabel);
            settings.promptBeforeAutoResolution =
                EditorGUILayout.Toggle(settings.promptBeforeAutoResolution);
            GUILayout.EndHorizontal();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Auto Resolution Delay", EditorStyles.boldLabel);
            settings.autoResolutionDelay = ClampAutoResolutionDelay(
                EditorGUILayout.IntField(settings.autoResolutionDelay));
            GUILayout.EndHorizontal();
            GUILayout.Label("Time, in seconds, to wait before auto-resolution.");
            EditorGUI.EndDisabledGroup();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Patch AndroidManifest.xml", EditorStyles.boldLabel);
            settings.patchAndroidManifest = EditorGUILayout.Toggle(settings.patchAndroidManifest);
            GUILayout.EndHorizontal();
            if (settings.patchAndroidManifest)
            {
                GUILayout.Label(String.Format(
                                    "Instances of \"applicationId\" variable references will be replaced in " +
                                    "{0} with the bundle ID.  If the bundle ID " +
                                    "is changed the previous bundle ID will be replaced with the new " +
                                    "bundle ID by the plugin.\n\n" +
                                    "This works around a bug in Unity 2018.x where the " +
                                    "\"applicationId\" variable is not replaced correctly.",
                                    AndroidManifestPath));
            }
            else
            {
                GUILayout.Label(String.Format(
                                    "{0} is not modified.\n\n" +
                                    "If you're using Unity 2018.x and have an AndroidManifest.xml " +
                                    "that uses the \"applicationId\" variable, your build may fail.",
                                    AndroidManifestPath));
            }

            GUILayout.BeginHorizontal();
            GUILayout.Label("Patch mainTemplate.gradle", EditorStyles.boldLabel);
            settings.patchMainTemplateGradle =
                EditorGUILayout.Toggle(settings.patchMainTemplateGradle);
            GUILayout.EndHorizontal();
            if (settings.patchMainTemplateGradle)
            {
                GUILayout.Label(
                    "If Gradle builds are enabled and a mainTemplate.gradle file is present, " +
                    "the mainTemplate.gradle file will be patched with dependencies managed " +
                    "by the Android Resolver.");
            }
            else
            {
                GUILayout.Label(String.Format(
                                    "If Gradle builds are enabled and a mainTemplate.gradle file is present, " +
                                    "the mainTemplate.gradle file will not be modified.  Instead dependencies " +
                                    "managed by the Android Resolver will be added to the project under {0}",
                                    settings.packageDir));
            }

            GUILayout.BeginHorizontal();
            GUILayout.Label("Verbose Logging", EditorStyles.boldLabel);
            settings.verboseLogging = EditorGUILayout.Toggle(settings.verboseLogging);
            GUILayout.EndHorizontal();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Use project settings", EditorStyles.boldLabel);
            settings.useProjectSettings = EditorGUILayout.Toggle(settings.useProjectSettings);
            GUILayout.EndHorizontal();

            GUILayout.Space(10);

            if (GUILayout.Button("Reset to Defaults"))
            {
                // Load default settings into the dialog but preserve the state in the user's
                // saved preferences.
                var backupSettings = new Settings();
                RestoreDefaultSettings();
                LoadSettings();
                backupSettings.Save();
            }

            GUILayout.BeginHorizontal();
            bool closeWindow = GUILayout.Button("Cancel");
            bool ok          = GUILayout.Button("OK");

            closeWindow |= ok;
            if (ok)
            {
                settings.Save();
                PlayServicesResolver.OnSettingsChanged();
            }
            if (closeWindow)
            {
                Close();
            }
            GUILayout.EndHorizontal();

            GUILayout.EndVertical();
            EditorGUILayout.EndScrollView();
            GUILayout.EndVertical();
        }
        /// <summary>
        /// Copy srcaar files to aar files that are excluded from Unity's build process.
        /// </summary>
        /// <param name="dependencies">Dependencies to inject.</param>
        /// <returns>true if successful, false otherwise.</returns>
        private static bool CopySrcAars(ICollection <Dependency> dependencies)
        {
            bool succeeded = true;
            var  aarFiles  = new List <KeyValuePair <string, string> >();

            // Copy each .srcaar file to .aar while configuring the plugin importer to ignore the
            // file.
            foreach (var aar in LocalMavenRepository.FindAarsInLocalRepos(dependencies))
            {
                // Only need to copy for .srcaar
                if (Path.GetExtension(aar).CompareTo(".srcaar") != 0)
                {
                    continue;
                }

                var aarPath = aar;
                if (FileUtils.IsUnderPackageDirectory(aar))
                {
                    // Physical paths work better for copy operations than
                    // logical Unity paths.
                    var physicalPackagePath = FileUtils.GetPackageDirectory(aar,
                                                                            FileUtils.PackageDirectoryType.PhysicalPath);
                    aarPath = FileUtils.ReplaceBaseAssetsOrPackagesFolder(
                        aar, physicalPackagePath);
                }
                var dir = FileUtils.ReplaceBaseAssetsOrPackagesFolder(
                    Path.GetDirectoryName(aar),
                    GooglePlayServices.SettingsDialog.LocalMavenRepoDir);
                var filename       = Path.GetFileNameWithoutExtension(aarPath);
                var targetFilename = Path.Combine(dir, filename + ".aar");

                // Avoid situations where we can have a mix of file path
                // separators based on platform.
                aarPath        = FileUtils.NormalizePathSeparators(aarPath);
                targetFilename = FileUtils.NormalizePathSeparators(
                    targetFilename);

                bool configuredAar = File.Exists(targetFilename);
                if (!configuredAar)
                {
                    var error = PlayServicesResolver.CopyAssetAndLabel(
                        aarPath, targetFilename);
                    if (String.IsNullOrEmpty(error))
                    {
                        try {
                            PluginImporter importer = (PluginImporter)AssetImporter.GetAtPath(
                                targetFilename);
                            importer.SetCompatibleWithAnyPlatform(false);
                            importer.SetCompatibleWithPlatform(BuildTarget.Android, false);
                            configuredAar = true;
                        } catch (Exception ex) {
                            PlayServicesResolver.Log(String.Format(
                                                         "Failed to disable {0} from being included by Unity's " +
                                                         "internal build.  {0} has been deleted and will not be " +
                                                         "included in Gradle builds. ({1})", aar, ex),
                                                     level: LogLevel.Error);
                        }
                    }
                    else
                    {
                        PlayServicesResolver.Log(String.Format(
                                                     "Unable to copy {0} to {1}.  {1} will not be included in Gradle " +
                                                     "builds. Reason: {2}", aarPath, targetFilename, error),
                                                 level: LogLevel.Error);
                    }
                }
                if (configuredAar)
                {
                    aarFiles.Add(new KeyValuePair <string, string>(aarPath, targetFilename));
                    // Some versions of Unity do not mark the asset database as dirty when
                    // plugin importer settings change so reimport the asset to synchronize
                    // the state.
                    AssetDatabase.ImportAsset(targetFilename, ImportAssetOptions.ForceUpdate);
                }
                else
                {
                    if (File.Exists(targetFilename))
                    {
                        AssetDatabase.DeleteAsset(targetFilename);
                    }
                    succeeded = false;
                }
            }
            foreach (var keyValue in aarFiles)
            {
                succeeded &= LocalMavenRepository.PatchPomFile(keyValue.Value, keyValue.Key);
            }
            return(succeeded);
        }
Example #18
0
 static ResolverVer1_1()
 {
     PlayServicesResolver.RegisterResolver(new ResolverVer1_1());
 }
        /// <summary>
        /// Inject / update dependencies in the gradle template file.
        /// </summary>
        /// <param name="dependencies">Dependencies to inject.</param>
        /// <returns>true if successful, false otherwise.</returns>
        public static bool InjectDependencies(ICollection <Dependency> dependencies)
        {
            var resolutionMeasurementParameters =
                PlayServicesResolver.GetResolutionMeasurementParameters(null);

            if (dependencies.Count > 0)
            {
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate", resolutionMeasurementParameters,
                    "Gradle Template Resolve");
            }

            var fileDescription = String.Format("gradle template {0}", GradleTemplatePath);

            PlayServicesResolver.Log(String.Format("Reading {0}", fileDescription),
                                     level: LogLevel.Verbose);
            IEnumerable <string> lines;

            try {
                lines = File.ReadAllLines(GradleTemplatePath);
            } catch (Exception ex) {
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate/failed/templateunreadable",
                    "Gradle Template Resolve: Failed Template Unreadable");
                PlayServicesResolver.Log(
                    String.Format("Unable to patch {0} ({1})", fileDescription, ex.ToString()),
                    level: LogLevel.Error);
                return(false);
            }

            PlayServicesResolver.Log(String.Format("Searching for {0} in {1}", DependenciesToken,
                                                   fileDescription),
                                     level: LogLevel.Verbose);
            // Determine whether dependencies should be injected.
            var  dependenciesToken = new Regex(DependenciesToken);
            bool containsDeps      = false;

            foreach (var line in lines)
            {
                if (dependenciesToken.IsMatch(line))
                {
                    containsDeps = true;
                    break;
                }
            }

            // If a dependencies token isn't present report a warning and abort.
            if (!containsDeps)
            {
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate/failed/noinjectionpoint",
                    "Gradle Template Resolve: Failed No Injection Point");
                PlayServicesResolver.Log(
                    String.Format("No {0} token found in {1}, Android Resolver libraries will " +
                                  "not be added to the file.", DependenciesToken, fileDescription),
                    level: LogLevel.Warning);
                return(true);
            }

            // Copy all srcaar files in the project to aar filenames so that they'll be included in
            // the Gradle build.
            if (!CopySrcAars(dependencies))
            {
                PlayServicesResolver.analytics.Report(
                    "/resolve/gradletemplate/failed/srcaarcopy",
                    "Gradle Template Resolve: Failed srcaar I/O");
                return(false);
            }

            var repoLines = new List <string>();

            // Optionally enable the jetifier.
            if (SettingsDialog.UseJetifier && dependencies.Count > 0)
            {
                // For Unity versions lower than 2019.3 add jetifier and AndroidX
                // properties to custom main gradle template
                if (VersionHandler.GetUnityVersionMajorMinor() < 2019.3f)
                {
                    repoLines.AddRange(new [] {
                        "([rootProject] + (rootProject.subprojects as List)).each {",
                        "    ext {",
                        "        it.setProperty(\"android.useAndroidX\", true)",
                        "        it.setProperty(\"android.enableJetifier\", true)",
                        "    }",
                        "}"
                    });
                }
            }
            repoLines.AddRange(PlayServicesResolver.GradleMavenReposLines(dependencies));

            TextFileLineInjector[] injectors = new [] {
                new TextFileLineInjector(ReposInjectionLine, ReposStartLine, ReposEndLine,
                                         repoLines, "Repos", fileDescription),
                new TextFileLineInjector(DependenciesToken, DependenciesStartLine,
                                         DependenciesEndLine,
                                         PlayServicesResolver.GradleDependenciesLines(
                                             dependencies, includeDependenciesBlock: false),
                                         "Dependencies", fileDescription),
                new TextFileLineInjector(PackagingOptionsToken, PackagingOptionsStartLine,
                                         PackagingOptionsEndLine,
                                         PlayServicesResolver.PackagingOptionsLines(dependencies),
                                         "Packaging Options", fileDescription),
            };
            return(PatchFile(GradleTemplatePath, fileDescription,
                             "Gradle Template", "gradletemplate",
                             injectors, resolutionMeasurementParameters));
        }
        /// <summary>
        /// Called when the GUI should be rendered.
        /// </summary>
        public void OnGUI()
        {
            GUILayout.BeginVertical();
            GUILayout.Label(String.Format("Android Resolver (version {0}.{1}.{2})",
                                          AndroidResolverVersionNumber.Value.Major,
                                          AndroidResolverVersionNumber.Value.Minor,
                                          AndroidResolverVersionNumber.Value.Build));
            scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);

            GUI.skin.label.wordWrap = true;
            GUILayout.BeginVertical();
            GUILayout.BeginHorizontal();
            GUILayout.Label("Use Gradle Daemon", EditorStyles.boldLabel);
            settings.useGradleDaemon = EditorGUILayout.Toggle(settings.useGradleDaemon);
            GUILayout.EndHorizontal();
            GUILayout.Label(
                settings.useGradleDaemon ?
                ("Gradle Daemon will be used to fetch dependencies.  " +
                 "This is faster but can be flakey in some environments.") :
                ("Gradle Daemon will not be used.  This is slow but reliable."));

            GUILayout.BeginHorizontal();
            GUILayout.Label("Enable Auto-Resolution", EditorStyles.boldLabel);
            settings.enableAutoResolution = EditorGUILayout.Toggle(settings.enableAutoResolution);
            GUILayout.EndHorizontal();
            GUILayout.Label(
                settings.enableAutoResolution ?
                ("Android libraries will be downloaded and processed in the editor.") :
                ("Android libraries will *not* be downloaded or processed in the editor."));

            GUILayout.BeginHorizontal();
            GUILayout.Label("Enable Resolution On Build", EditorStyles.boldLabel);
            settings.autoResolveOnBuild = EditorGUILayout.Toggle(settings.autoResolveOnBuild);
            GUILayout.EndHorizontal();
            GUILayout.Label(
                settings.autoResolveOnBuild ?
                ("Android libraries will be downloaded and processed in a pre-build step.") :
                ("Android libraries will *not* be downloaded or processed in a pre-build step."));

            GUILayout.BeginHorizontal();
            GUILayout.Label("Install Android Packages", EditorStyles.boldLabel);
            settings.installAndroidPackages =
                EditorGUILayout.Toggle(settings.installAndroidPackages);
            GUILayout.EndHorizontal();

            if (ConfigurablePackageDir)
            {
                GUILayout.BeginHorizontal();
                string previousPackageDir = settings.packageDir;
                GUILayout.Label("Package Directory", EditorStyles.boldLabel);
                if (GUILayout.Button("Browse"))
                {
                    string path = EditorUtility.OpenFolderPanel("Set Package Directory",
                                                                PackageDir, "");
                    int startOfPath = path.IndexOf(AndroidPluginsDir);
                    settings.packageDir = FileUtils.PosixPathSeparators(
                        startOfPath < 0 ? "" : path.Substring(startOfPath,
                                                              path.Length - startOfPath));
                }
                if (!previousPackageDir.Equals(settings.packageDir))
                {
                    settings.packageDir = ValidatePackageDir(settings.packageDir);
                }
                GUILayout.EndHorizontal();
                settings.packageDir = FileUtils.PosixPathSeparators(
                    EditorGUILayout.TextField(settings.packageDir));
            }

            GUILayout.BeginHorizontal();
            GUILayout.Label("Explode AARs", EditorStyles.boldLabel);
            settings.explodeAars = EditorGUILayout.Toggle(settings.explodeAars);
            GUILayout.EndHorizontal();
            if (settings.explodeAars)
            {
                GUILayout.Label("AARs will be exploded (unpacked) when ${applicationId} " +
                                "variable replacement is required in an AAR's " +
                                "AndroidManifest.xml or a single target ABI is selected " +
                                "without a compatible build system.");
            }
            else
            {
                GUILayout.Label("AAR explosion will be disabled in exported Gradle builds " +
                                "(Unity 5.5 and above). You will need to set " +
                                "android.defaultConfig.applicationId to your bundle ID in your " +
                                "build.gradle to generate a functional APK.");
            }

            // Disable the ability to toggle the auto-resolution disabled warning
            // when auto resolution is enabled.
            EditorGUI.BeginDisabledGroup(settings.enableAutoResolution ||
                                         settings.autoResolveOnBuild);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Auto-Resolution Disabled Warning", EditorStyles.boldLabel);
            settings.autoResolutionDisabledWarning =
                EditorGUILayout.Toggle(settings.autoResolutionDisabledWarning);
            GUILayout.EndHorizontal();
            EditorGUI.EndDisabledGroup();

            // Disable the ability to toggle the auto-resolution disabled warning
            // when auto resolution is enabled.
            EditorGUI.BeginDisabledGroup(!settings.enableAutoResolution);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Prompt Before Auto-Resolution", EditorStyles.boldLabel);
            settings.promptBeforeAutoResolution =
                EditorGUILayout.Toggle(settings.promptBeforeAutoResolution);
            GUILayout.EndHorizontal();
            EditorGUI.EndDisabledGroup();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Patch AndroidManifest.xml", EditorStyles.boldLabel);
            settings.patchAndroidManifest = EditorGUILayout.Toggle(settings.patchAndroidManifest);
            GUILayout.EndHorizontal();
            if (settings.patchAndroidManifest)
            {
                GUILayout.Label(String.Format(
                                    "Instances of \"applicationId\" variable references will be replaced in " +
                                    "{0} with the bundle ID.  If the bundle ID " +
                                    "is changed the previous bundle ID will be replaced with the new " +
                                    "bundle ID by the plugin.\n\n" +
                                    "This works around a bug in Unity 2018.x where the " +
                                    "\"applicationId\" variable is not replaced correctly.",
                                    AndroidManifestPath));
            }
            else
            {
                GUILayout.Label(String.Format(
                                    "{0} is not modified.\n\n" +
                                    "If you're using Unity 2018.x and have an AndroidManifest.xml " +
                                    "that uses the \"applicationId\" variable, your build may fail.",
                                    AndroidManifestPath));
            }

            GUILayout.BeginHorizontal();
            GUILayout.Label("Patch mainTemplate.gradle", EditorStyles.boldLabel);
            settings.patchMainTemplateGradle =
                EditorGUILayout.Toggle(settings.patchMainTemplateGradle);
            GUILayout.EndHorizontal();
            if (settings.patchMainTemplateGradle)
            {
                GUILayout.Label(
                    "If Gradle builds are enabled and a mainTemplate.gradle file is present, " +
                    "the mainTemplate.gradle file will be patched with dependencies managed " +
                    "by the Android Resolver.");
            }
            else
            {
                GUILayout.Label(String.Format(
                                    "If Gradle builds are enabled and a mainTemplate.gradle file is present, " +
                                    "the mainTemplate.gradle file will not be modified.  Instead dependencies " +
                                    "managed by the Android Resolver will be added to the project under {0}",
                                    settings.packageDir));
            }

            if (settings.patchMainTemplateGradle)
            {
                GUILayout.BeginHorizontal();
                string previousDir = settings.localMavenRepoDir;
                GUILayout.Label("Local Maven Repo Directory", EditorStyles.boldLabel);
                if (GUILayout.Button("Browse"))
                {
                    string path = EditorUtility.OpenFolderPanel("Set Local Maven Repo Directory",
                                                                settings.localMavenRepoDir, "");
                    int startOfPath = path.IndexOf(
                        FileUtils.ASSETS_FOLDER + Path.DirectorySeparatorChar);
                    settings.localMavenRepoDir = FileUtils.PosixPathSeparators(
                        startOfPath < 0 ? DefaultLocalMavenRepoDir :
                        path.Substring(startOfPath, path.Length - startOfPath));
                }
                if (!previousDir.Equals(settings.localMavenRepoDir))
                {
                    settings.localMavenRepoDir =
                        ValidateLocalMavenRepoDir(settings.localMavenRepoDir);
                }
                GUILayout.EndHorizontal();
                GUILayout.Label(
                    "Please pick a folder under Assets folder.  Currently it won't work at " +
                    "any folder under \"Assets/Plugins/Android\"");
                settings.localMavenRepoDir = FileUtils.PosixPathSeparators(
                    ValidateLocalMavenRepoDir(EditorGUILayout.TextField(
                                                  settings.localMavenRepoDir)));
            }

            if (settings.patchMainTemplateGradle)
            {
                GUILayout.BeginHorizontal();
                GUILayout.Label("Use relative repository path", EditorStyles.boldLabel);
                settings.useRelativeRepoPath =
                    EditorGUILayout.Toggle(settings.useRelativeRepoPath);
                GUILayout.EndHorizontal();

                if (settings.useRelativeRepoPath)
                {
                    GUILayout.Label(
                        "The mainTemplate.gradle file will be patched with a relative path to " +
                        "the Local Maven Repository for an exported Android project. This can be used " +
                        "to limit file changes when the template is versioned, but the exported " +
                        "project will not work if relocated after building.");
                }
                else
                {
                    GUILayout.Label(
                        "The mainTemplate.gradle file will be patched with an absolute path to " +
                        "the Local Maven Repository. This ensures that the exported project can be " +
                        "relocated anywhere after building.");
                }
            }

            GUILayout.BeginHorizontal();
            GUILayout.Label("Use Jetifier.", EditorStyles.boldLabel);
            settings.useJetifier = EditorGUILayout.Toggle(settings.useJetifier);
            GUILayout.EndHorizontal();
            if (settings.useJetifier)
            {
                GUILayout.Label(
                    "Legacy Android support libraries and references to them from other " +
                    "libraries will be rewritten to use Jetpack using the Jetifier tool. " +
                    "Enabling option allows an application to use Android Jetpack " +
                    "when other libraries in the project use the Android support libraries.");
            }
            else
            {
                GUILayout.Label(
                    "Class References to legacy Android support libraries (pre-Jetpack) will be " +
                    "left unmodified in the project. This will possibly result in broken Android " +
                    "builds when mixing legacy Android support libraries and Jetpack libraries.");
            }

            if (settings.useJetifier)
            {
                GUILayout.BeginHorizontal();
                GUILayout.Label("Patch gradleTemplate.properties", EditorStyles.boldLabel);
                settings.patchPropertiesTemplateGradle = EditorGUILayout.Toggle(settings.patchPropertiesTemplateGradle);
                GUILayout.EndHorizontal();
                GUILayout.Label(
                    "For Unity 2019.3 and above, it is recommended to enable Jetifier " +
                    "and AndroidX via gradleTemplate.properties. Please enable " +
                    "Custom Gradle Properties Template' found under 'Player Settings > " +
                    "Settings for Android > Publishing Settings' menu item. " +
                    "This has no effect in older versions of Unity.");
            }
            settings.analyticsSettings.RenderGui();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Verbose Logging", EditorStyles.boldLabel);
            settings.verboseLogging = EditorGUILayout.Toggle(settings.verboseLogging);
            GUILayout.EndHorizontal();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Use project settings", EditorStyles.boldLabel);
            settings.useProjectSettings = EditorGUILayout.Toggle(settings.useProjectSettings);
            GUILayout.EndHorizontal();

            GUILayout.Space(10);

            if (GUILayout.Button("Reset to Defaults"))
            {
                // Load default settings into the dialog but preserve the state in the user's
                // saved preferences.
                var backupSettings = new Settings();
                RestoreDefaultSettings();
                PlayServicesResolver.analytics.Report("settings/reset", "Settings Reset");
                LoadSettings();
                backupSettings.Save();
            }

            GUILayout.BeginHorizontal();
            bool closeWindow = GUILayout.Button("Cancel");

            if (closeWindow)
            {
                PlayServicesResolver.analytics.Report("settings/cancel", "Settings Cancel");
            }
            bool ok = GUILayout.Button("OK");

            closeWindow |= ok;
            if (ok)
            {
                PlayServicesResolver.analytics.Report(
                    "settings/save",
                    new KeyValuePair <string, string>[] {
                    new KeyValuePair <string, string>(
                        "useGradleDaemon",
                        SettingsDialog.UseGradleDaemon.ToString()),
                    new KeyValuePair <string, string>(
                        "enableAutoResolution",
                        SettingsDialog.EnableAutoResolution.ToString()),
                    new KeyValuePair <string, string>(
                        "installAndroidPackages",
                        SettingsDialog.InstallAndroidPackages.ToString()),
                    new KeyValuePair <string, string>(
                        "explodeAars",
                        SettingsDialog.ExplodeAars.ToString()),
                    new KeyValuePair <string, string>(
                        "patchAndroidManifest",
                        SettingsDialog.PatchAndroidManifest.ToString()),
                    new KeyValuePair <string, string>(
                        "localMavenRepoDir",
                        SettingsDialog.LocalMavenRepoDir.ToString()),
                    new KeyValuePair <string, string>(
                        "useJetifier",
                        SettingsDialog.UseJetifier.ToString()),
                    new KeyValuePair <string, string>(
                        "verboseLogging",
                        SettingsDialog.VerboseLogging.ToString()),
                    new KeyValuePair <string, string>(
                        "autoResolutionDisabledWarning",
                        SettingsDialog.AutoResolutionDisabledWarning.ToString()),
                    new KeyValuePair <string, string>(
                        "promptBeforeAutoResolution",
                        SettingsDialog.PromptBeforeAutoResolution.ToString()),
                },
                    "Settings Save");

                settings.Save();
                PlayServicesResolver.OnSettingsChanged();
            }
            if (closeWindow)
            {
                Close();
            }
            GUILayout.EndHorizontal();

            GUILayout.EndVertical();
            EditorGUILayout.EndScrollView();
            GUILayout.EndVertical();
        }
Example #21
0
        /// <summary>
        /// Search for the POM file associated with the specified maven artifact and patch the
        /// packaging reference if the POM doesn't reference the artifact.
        /// file.
        /// </summary>
        /// <param name="artifactFilename">artifactFilename</param>
        /// <param name="sourceFilename">If artifactFilename is copied from a different location,
        /// pass the original location where POM file lives.</param>
        /// <returns>true if successful, false otherwise.</returns>
        public static bool PatchPomFile(string artifactFilename, string sourceFilename)
        {
            if (sourceFilename == null)
            {
                sourceFilename = artifactFilename;
            }
            if (FileUtils.IsUnderPackageDirectory(artifactFilename))
            {
                // File under Packages folder is immutable.
                PlayServicesResolver.Log(
                    String.Format("Cannot patch POM from Packages directory since it is immutable" +
                                  " ({0})", artifactFilename), level: LogLevel.Error);
                return(false);
            }

            var failureImpact = String.Format("{0} may not be included in your project",
                                              Path.GetFileName(artifactFilename));
            var pomFilename = PathWithoutExtension(artifactFilename) + ".pom";

            // Copy POM file if artifact has been copied from a different location as well.
            if (String.Compare(sourceFilename, artifactFilename) != 0 &&
                !File.Exists(pomFilename))
            {
                var sourcePomFilename = PathWithoutExtension(sourceFilename) + ".pom";
                var error             = PlayServicesResolver.CopyAssetAndLabel(
                    sourcePomFilename, pomFilename);
                if (!String.IsNullOrEmpty(error))
                {
                    PlayServicesResolver.Log(
                        String.Format("Failed to copy POM from {0} to {1} due to:\n{2}",
                                      sourcePomFilename, pomFilename, error),
                        level: LogLevel.Error);
                    return(false);
                }
            }
            var artifactPackaging = Path.GetExtension(artifactFilename).ToLower().Substring(1);
            var pom = new XmlDocument();

            try {
                using (var stream = new StreamReader(pomFilename)) {
                    pom.Load(stream);
                }
            } catch (Exception ex) {
                PlayServicesResolver.Log(
                    String.Format("Unable to read maven POM {0} for {1} ({2}). " + failureImpact,
                                  pom, artifactFilename, ex), level: LogLevel.Error);
                return(false);
            }
            bool        updatedPackaging = false;
            XmlNodeList packagingNode    = pom.GetElementsByTagName("packaging");

            foreach (XmlNode node in packagingNode)
            {
                if (node.InnerText != artifactPackaging)
                {
                    PlayServicesResolver.Log(String.Format(
                                                 "Replacing packaging of maven POM {0} {1} --> {2}",
                                                 pomFilename, node.InnerText, artifactPackaging), level: LogLevel.Verbose);
                    node.InnerText   = artifactPackaging;
                    updatedPackaging = true;
                }
            }
            if (!FileUtils.CheckoutFile(pomFilename, PlayServicesResolver.logger))
            {
                PlayServicesResolver.Log(
                    String.Format("Unable to checkout '{0}' to patch the file for inclusion in a " +
                                  "Gradle project.", pomFilename), LogLevel.Error);
                return(false);
            }
            if (updatedPackaging)
            {
                try {
                    using (var xmlWriter =
                               XmlWriter.Create(pomFilename,
                                                new XmlWriterSettings {
                        Indent = true,
                        IndentChars = "  ",
                        NewLineChars = "\n",
                        NewLineHandling = NewLineHandling.Replace
                    })) {
                        pom.Save(xmlWriter);
                    }
                } catch (Exception ex) {
                    PlayServicesResolver.Log(
                        String.Format("Unable to write patch maven POM {0} for {1} with " +
                                      "packaging {2} ({3}). " + failureImpact,
                                      pom, artifactFilename, artifactPackaging, ex));
                    return(false);
                }
            }
            return(true);
        }
        /// <summary>
        /// Explodes a single aar file.  This is done by calling the
        /// JDK "jar" command, then moving the classes.jar file.
        /// </summary>
        /// <param name="dir">The directory to unpack / explode the AAR to.  If antProject is true
        /// the ant project will be located in Path.Combine(dir, Path.GetFileName(aarFile)).</param>
        /// <param name="aarFile">Aar file to explode.</param>
        /// <param name="antProject">true to explode into an Ant style project or false
        /// to repack the processed AAR as a new AAR.</param>
        /// <param name="abis">ABIs in the AAR or null if it's universal.</param>
        /// <returns>true if successful, false otherwise.</returns>
        internal virtual bool ProcessAar(string dir, string aarFile, bool antProject,
                                         out AndroidAbis abis)
        {
            PlayServicesResolver.Log(String.Format("ProcessAar {0} {1} antProject={2}",
                                                   dir, aarFile, antProject),
                                     level: LogLevel.Verbose);
            abis = null;
            string workingDir = Path.Combine(dir, Path.GetFileNameWithoutExtension(aarFile));

            FileUtils.DeleteExistingFileOrDirectory(workingDir);
            Directory.CreateDirectory(workingDir);
            if (!PlayServicesResolver.ExtractZip(aarFile, null, workingDir))
            {
                return(false);
            }
            PlayServicesResolver.ReplaceVariablesInAndroidManifest(
                Path.Combine(workingDir, "AndroidManifest.xml"),
                UnityCompat.ApplicationId, new Dictionary <string, string>());

            string nativeLibsDir = null;

            if (antProject)
            {
                // Create the libs directory to store the classes.jar and non-Java shared
                // libraries.
                string libDir = Path.Combine(workingDir, "libs");
                nativeLibsDir = libDir;
                Directory.CreateDirectory(libDir);

                // Move the classes.jar file to libs.
                string classesFile       = Path.Combine(workingDir, "classes.jar");
                string targetClassesFile = Path.Combine(libDir, Path.GetFileName(classesFile));
                if (File.Exists(targetClassesFile))
                {
                    File.Delete(targetClassesFile);
                }
                if (File.Exists(classesFile))
                {
                    File.Move(classesFile, targetClassesFile);
                }
                else
                {
                    // Generate an empty classes.jar file.
                    string temporaryDirectory = CreateTemporaryDirectory();
                    if (temporaryDirectory == null)
                    {
                        return(false);
                    }
                    if (!ArchiveAar(targetClassesFile, temporaryDirectory))
                    {
                        return(false);
                    }
                }
            }

            // Copy non-Java shared libraries (.so) files from the "jni" directory into the
            // lib directory so that Unity's legacy (Ant-like) build system includes them in the
            // built APK.
            string jniLibDir = Path.Combine(workingDir, "jni");

            nativeLibsDir = nativeLibsDir ?? jniLibDir;
            if (Directory.Exists(jniLibDir))
            {
                var abisInArchive = AarDirectoryFindAbis(workingDir);
                if (jniLibDir != nativeLibsDir)
                {
                    FileUtils.CopyDirectory(jniLibDir, nativeLibsDir);
                    FileUtils.DeleteExistingFileOrDirectory(jniLibDir);
                }
                if (abisInArchive != null)
                {
                    // Remove shared libraries for all ABIs that are not required for the selected
                    // ABIs.
                    var activeAbisSet            = AndroidAbis.Current.ToSet();
                    var abisInArchiveSet         = abisInArchive.ToSet();
                    var abisInArchiveToRemoveSet = new HashSet <string>(abisInArchiveSet);
                    abisInArchiveToRemoveSet.ExceptWith(activeAbisSet);

                    Func <IEnumerable <string>, string> setToString = (setToConvert) => {
                        return(String.Join(", ", (new List <string>(setToConvert)).ToArray()));
                    };
                    PlayServicesResolver.Log(
                        String.Format(
                            "Target ABIs [{0}], ABIs [{1}] in {2}, will remove [{3}] ABIs",
                            setToString(activeAbisSet),
                            setToString(abisInArchiveSet),
                            aarFile,
                            setToString(abisInArchiveToRemoveSet)),
                        level: LogLevel.Verbose);

                    foreach (var abiToRemove in abisInArchiveToRemoveSet)
                    {
                        abisInArchiveSet.Remove(abiToRemove);
                        FileUtils.DeleteExistingFileOrDirectory(Path.Combine(nativeLibsDir,
                                                                             abiToRemove));
                    }
                    abis = new AndroidAbis(abisInArchiveSet);
                }
            }

            if (antProject)
            {
                // Create the project.properties file which indicates to
                // Unity that this directory is a plugin.
                string projectProperties = Path.Combine(workingDir, "project.properties");
                if (!File.Exists(projectProperties))
                {
                    File.WriteAllLines(projectProperties, new [] {
                        "# Project target.",
                        "target=android-9",
                        "android.library=true"
                    });
                }
                PlayServicesResolver.Log(String.Format("Replacing {0} with {1}",
                                                       aarFile, workingDir),
                                         level: LogLevel.Verbose);
                // Clean up the aar file.
                FileUtils.DeleteExistingFileOrDirectory(Path.GetFullPath(aarFile));
                // Add a tracking label to the exploded files.
                PlayServicesResolver.LabelAssets(new [] { workingDir });
            }
            else
            {
                // Add a tracking label to the exploded files just in-case packaging fails.
                PlayServicesResolver.LabelAssets(new [] { workingDir });
                PlayServicesResolver.Log(String.Format("Repacking {0} from {1}",
                                                       aarFile, workingDir),
                                         level: LogLevel.Verbose);
                // Create a new AAR file.
                FileUtils.DeleteExistingFileOrDirectory(Path.GetFullPath(aarFile));
                if (!ArchiveAar(aarFile, workingDir))
                {
                    return(false);
                }
                // Clean up the exploded directory.
                FileUtils.DeleteExistingFileOrDirectory(workingDir);
            }
            return(true);
        }
Example #23
0
        /// <summary>
        /// Called when the GUI should be rendered.
        /// </summary>
        public void OnGUI()
        {
            GUI.skin.label.wordWrap = true;
            GUILayout.BeginVertical();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Prebuild With Gradle (Experimental)", EditorStyles.boldLabel);
            settings.prebuildWithGradle = EditorGUILayout.Toggle(settings.prebuildWithGradle);
            GUILayout.EndHorizontal();

            EditorGUI.BeginDisabledGroup(settings.prebuildWithGradle == true);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Fetch Dependencies with Gradle", EditorStyles.boldLabel);
            settings.fetchDependenciesWithGradle =
                EditorGUILayout.Toggle(settings.fetchDependenciesWithGradle);
            GUILayout.EndHorizontal();
            if (settings.fetchDependenciesWithGradle)
            {
                GUILayout.Label("AARs are fetched using Gradle which enables assets to be " +
                                "fetched from remote Maven repositories and Gradle version " +
                                "expressions.");
            }
            else
            {
                GUILayout.Label("Legacy AAR fetching method that only queries the Android SDK " +
                                "manager's local maven repository and user specified local " +
                                "maven repositories for dependencies.");
            }
            EditorGUI.BeginDisabledGroup(settings.fetchDependenciesWithGradle == false);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Use Gradle Daemon", EditorStyles.boldLabel);
            settings.useGradleDaemon = EditorGUILayout.Toggle(settings.useGradleDaemon);
            GUILayout.EndHorizontal();
            GUILayout.Label(
                settings.useGradleDaemon ?
                ("Gradle Daemon will be used to fetch dependencies.  " +
                 "This is faster but can be flakey in some environments.") :
                ("Gradle Daemon will not be used.  This is slow but reliable."));
            EditorGUI.EndDisabledGroup();
            EditorGUI.EndDisabledGroup();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Enable Auto-Resolution", EditorStyles.boldLabel);
            settings.enableAutoResolution = EditorGUILayout.Toggle(settings.enableAutoResolution);
            GUILayout.EndHorizontal();

            EditorGUI.BeginDisabledGroup(settings.prebuildWithGradle == true);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Install Android Packages", EditorStyles.boldLabel);
            settings.installAndroidPackages =
                EditorGUILayout.Toggle(settings.installAndroidPackages);
            GUILayout.EndHorizontal();

            if (ConfigurablePackageDir)
            {
                GUILayout.BeginHorizontal();
                string previousPackageDir = settings.packageDir;
                GUILayout.Label("Package Directory", EditorStyles.boldLabel);
                if (GUILayout.Button("Browse"))
                {
                    string path = EditorUtility.OpenFolderPanel("Set Package Directory",
                                                                PackageDir, "");
                    int startOfPath = path.IndexOf(AndroidPluginsDir);
                    settings.packageDir = startOfPath < 0 ? "" :
                                          path.Substring(startOfPath, path.Length - startOfPath);;
                }
                if (!previousPackageDir.Equals(settings.packageDir))
                {
                    settings.packageDir = ValidatePackageDir(settings.packageDir);
                }
                GUILayout.EndHorizontal();
                settings.packageDir = EditorGUILayout.TextField(settings.packageDir);
            }

            GUILayout.BeginHorizontal();
            GUILayout.Label("Explode AARs", EditorStyles.boldLabel);
            settings.explodeAars = EditorGUILayout.Toggle(settings.explodeAars);
            GUILayout.EndHorizontal();
            if (settings.explodeAars)
            {
                GUILayout.Label("AARs will be exploded (unpacked) when ${applicationId} " +
                                "variable replacement is required in an AAR's " +
                                "AndroidManifest.xml or a single target ABI is selected " +
                                "without a compatible build system.");
            }
            else
            {
                GUILayout.Label("AAR explosion will be disabled in exported Gradle builds " +
                                "(Unity 5.5 and above). You will need to set " +
                                "android.defaultConfig.applicationId to your bundle ID in your " +
                                "build.gradle to generate a functional APK.");
            }
            EditorGUI.EndDisabledGroup();

            EditorGUI.BeginDisabledGroup(settings.enableAutoResolution);
            GUILayout.BeginHorizontal();
            GUILayout.Label("Auto-Resolution Disabled Warning", EditorStyles.boldLabel);
            settings.autoResolutionDisabledWarning =
                EditorGUILayout.Toggle(settings.autoResolutionDisabledWarning);
            GUILayout.EndHorizontal();
            EditorGUI.EndDisabledGroup();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Verbose Logging", EditorStyles.boldLabel);
            settings.verboseLogging = EditorGUILayout.Toggle(settings.verboseLogging);
            GUILayout.EndHorizontal();

            GUILayout.BeginHorizontal();
            GUILayout.Label("Use project settings", EditorStyles.boldLabel);
            settings.useProjectSettings = EditorGUILayout.Toggle(settings.useProjectSettings);
            GUILayout.EndHorizontal();

            GUILayout.Space(10);

            if (GUILayout.Button("Reset to Defaults"))
            {
                // Load default settings into the dialog but preserve the state in the user's
                // saved preferences.
                var backupSettings = new Settings();
                RestoreDefaultSettings();
                LoadSettings();
                backupSettings.Save();
            }

            GUILayout.BeginHorizontal();
            bool closeWindow = GUILayout.Button("Cancel");
            bool ok          = GUILayout.Button("OK");

            closeWindow |= ok;
            if (ok)
            {
                settings.Save();
                PlayServicesResolver.OnSettingsChanged();
            }
            if (closeWindow)
            {
                Close();
            }
            GUILayout.EndHorizontal();
            GUILayout.EndVertical();
        }
Example #24
0
        /// <summary>
        /// Explodes a single aar file.  This is done by calling the
        /// JDK "jar" command, then moving the classes.jar file.
        /// </summary>
        /// <param name="dir">the parent directory of the plugin.</param>
        /// <param name="aarFile">Aar file to explode.</param>
        /// <param name="antProject">true to explode into an Ant style project or false
        /// to repack the processed AAR as a new AAR.</param>
        /// <param name="abi">ABI of the AAR or null if it's universal.</param>
        /// <returns>true if successful, false otherwise.</returns>
        internal virtual bool ProcessAar(string dir, string aarFile, bool antProject,
                                         out string abi)
        {
            PlayServicesResolver.Log(String.Format("ProcessAar {0} {1} antProject={2}",
                                                   dir, aarFile, antProject),
                                     level: LogLevel.Verbose);
            abi = null;
            string workingDir = Path.Combine(dir, Path.GetFileNameWithoutExtension(aarFile));

            FileUtils.DeleteExistingFileOrDirectory(workingDir);
            Directory.CreateDirectory(workingDir);
            if (!ExtractAar(aarFile, null, workingDir))
            {
                return(false);
            }
            PlayServicesResolver.ReplaceVariablesInAndroidManifest(
                Path.Combine(workingDir, "AndroidManifest.xml"),
                UnityCompat.ApplicationId, new Dictionary <string, string>());

            string nativeLibsDir = null;

            if (antProject)
            {
                // Create the libs directory to store the classes.jar and non-Java shared
                // libraries.
                string libDir = Path.Combine(workingDir, "libs");
                nativeLibsDir = libDir;
                Directory.CreateDirectory(libDir);

                // Move the classes.jar file to libs.
                string classesFile       = Path.Combine(workingDir, "classes.jar");
                string targetClassesFile = Path.Combine(libDir, Path.GetFileName(classesFile));
                if (File.Exists(targetClassesFile))
                {
                    File.Delete(targetClassesFile);
                }
                if (File.Exists(classesFile))
                {
                    File.Move(classesFile, targetClassesFile);
                }
                else
                {
                    // Generate an empty classes.jar file.
                    string temporaryDirectory = CreateTemporaryDirectory();
                    if (temporaryDirectory == null)
                    {
                        return(false);
                    }
                    ArchiveAar(targetClassesFile, temporaryDirectory);
                }
            }

            // Copy non-Java shared libraries (.so) files from the "jni" directory into the
            // lib directory so that Unity's legacy (Ant-like) build system includes them in the
            // built APK.
            string jniLibDir = Path.Combine(workingDir, "jni");

            nativeLibsDir = nativeLibsDir ?? jniLibDir;
            if (Directory.Exists(jniLibDir))
            {
                if (jniLibDir != nativeLibsDir)
                {
                    FileUtils.CopyDirectory(jniLibDir, nativeLibsDir);
                    FileUtils.DeleteExistingFileOrDirectory(jniLibDir);
                }
                // Remove shared libraries for all ABIs that are not required for the selected
                // target ABI.
                var currentAbi = PlayServicesResolver.AndroidTargetDeviceAbi;
                var activeAbis = GetSelectedABIDirs(currentAbi);
                foreach (var directory in Directory.GetDirectories(nativeLibsDir))
                {
                    var abiDir = Path.GetFileName(directory);
                    if (!activeAbis.Contains(abiDir))
                    {
                        FileUtils.DeleteExistingFileOrDirectory(directory);
                    }
                }
                abi = currentAbi;
            }

            if (antProject)
            {
                // Create the project.properties file which indicates to
                // Unity that this directory is a plugin.
                string projectProperties = Path.Combine(workingDir, "project.properties");
                if (!File.Exists(projectProperties))
                {
                    File.WriteAllLines(projectProperties, new [] {
                        "# Project target.",
                        "target=android-9",
                        "android.library=true"
                    });
                }
                // Clean up the aar file.
                FileUtils.DeleteExistingFileOrDirectory(Path.GetFullPath(aarFile));
                // Add a tracking label to the exploded files.
                PlayServicesResolver.LabelAssets(new [] { workingDir });
            }
            else
            {
                // Add a tracking label to the exploded files just in-case packaging fails.
                PlayServicesResolver.LabelAssets(new [] { workingDir });
                // Create a new AAR file.
                FileUtils.DeleteExistingFileOrDirectory(Path.GetFullPath(aarFile));
                if (!ArchiveAar(aarFile, workingDir))
                {
                    return(false);
                }
                // Clean up the exploded directory.
                FileUtils.DeleteExistingFileOrDirectory(workingDir);
            }
            return(true);
        }
Example #25
0
        /// <summary>
        /// Inject / update dependencies in the gradle template file.
        /// </summary>
        /// <param name="dependencies">Dependencies to inject.</param>
        /// <returns>true if successful, false otherwise.</returns>
        public static bool InjectDependencies(ICollection <Dependency> dependencies)
        {
            var fileDescription = String.Format("gradle template {0}", GradleTemplatePath);

            PlayServicesResolver.Log(String.Format("Reading {0}", fileDescription),
                                     level: LogLevel.Verbose);
            IEnumerable <string> lines;

            try {
                lines = File.ReadAllLines(GradleTemplatePath);
            } catch (Exception ex) {
                PlayServicesResolver.Log(
                    String.Format("Unable to patch {0} ({1})", fileDescription, ex.ToString()),
                    level: LogLevel.Error);
                return(false);
            }

            PlayServicesResolver.Log(String.Format("Searching for {0} in {1}", DependenciesToken,
                                                   fileDescription),
                                     level: LogLevel.Verbose);
            // Determine whether dependencies should be injected.
            bool containsDeps = false;

            foreach (var line in lines)
            {
                if (line.Contains(DependenciesToken))
                {
                    containsDeps = true;
                    break;
                }
            }

            // If a dependencies token isn't present report a warning and abort.
            if (!containsDeps)
            {
                PlayServicesResolver.Log(
                    String.Format("No {0} token found in {1}, Android Resolver libraries will " +
                                  "not be added to the file.", DependenciesToken, fileDescription),
                    level: LogLevel.Warning);
                return(true);
            }

            // Copy all srcaar files in the project to aar filenames so that they'll be included in
            // the Gradle build.
            if (!CopySrcAars(dependencies))
            {
                return(false);
            }

            TextFileLineInjector[] injectors = new [] {
                new TextFileLineInjector(ReposInjectionLine, ReposStartLine, ReposEndLine,
                                         PlayServicesResolver.GradleMavenReposLines(dependencies),
                                         "Repos", fileDescription),
                new TextFileLineInjector(DependenciesToken, DependenciesStartLine,
                                         DependenciesEndLine,
                                         PlayServicesResolver.GradleDependenciesLines(
                                             dependencies, includeDependenciesBlock: false),
                                         "Dependencies", fileDescription)
            };
            // Lines that will be written to the output file.
            var outputLines = new List <string>();

            foreach (var line in lines)
            {
                var currentOutputLines = new List <string>();
                foreach (var injector in injectors)
                {
                    bool injectionApplied = false;
                    currentOutputLines = injector.ProcessLine(line, out injectionApplied);
                    if (injectionApplied || currentOutputLines.Count == 0)
                    {
                        break;
                    }
                }
                outputLines.AddRange(currentOutputLines);
            }
            PlayServicesResolver.Log(
                String.Format("Writing updated {0}", fileDescription),
                level: LogLevel.Verbose);
            try {
                File.WriteAllText(GradleTemplatePath,
                                  String.Join("\n", outputLines.ToArray()) + "\n");
            } catch (Exception ex) {
                PlayServicesResolver.Log(
                    String.Format("Unable to patch {0} ({1})", fileDescription,
                                  ex.ToString()), level: LogLevel.Error);
                return(false);
            }
            return(true);
        }