/// <summary>
        /// Read package metadata from the source.properties file within the specified directory.
        /// </summary>
        /// <param name="sdkDirectory">Android SDK directory to query.</param>
        /// <param name="packageDirectory">Directory containing the package relative to
        /// sdkDirectory.</param>
        public static AndroidSdkPackage ReadFromSourceProperties(string sdkDirectory,
                                                                 string packageDirectory)
        {
            var propertiesPath = System.IO.Path.Combine(
                sdkDirectory, System.IO.Path.Combine(packageDirectory, "source.properties"));
            string propertiesText = null;

            try {
                propertiesText = File.ReadAllText(propertiesPath);
            } catch (Exception e) {
                PlayServicesSupport.Log(String.Format("Unable to read {0}\n{1}\n",
                                                      propertiesPath, e.ToString()),
                                        verbose: true);
                return(null);
            }
            // Unfortunately the package name is simply based upon the path within the SDK.
            var sdkPackage = new AndroidSdkPackage {
                Path = packageDirectory
            };
            const string VERSION_FIELD_NAME     = "Pkg.Revision=";
            const string DESCRIPTION_FIELD_NAME = "Pkg.Desc=";

            foreach (var rawLine in CommandLine.SplitLines(propertiesText))
            {
                var line = rawLine.Trim();
                // Ignore comments.
                if (line.StartsWith("#"))
                {
                    continue;
                }
                // Parse fields
                if (line.StartsWith(VERSION_FIELD_NAME))
                {
                    sdkPackage.VersionString = line.Substring(VERSION_FIELD_NAME.Length);
                }
                else if (line.StartsWith(DESCRIPTION_FIELD_NAME))
                {
                    sdkPackage.Description = line.Substring(DESCRIPTION_FIELD_NAME.Length);
                }
            }
            return(sdkPackage);
        }
        /// <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))
            {
                PlayServicesSupport.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);
        }
        /// <summary>
        /// Resolve dependencies.
        /// </summary>
        /// <param name="resolutionComplete">Delegate called when resolution is complete
        /// with a parameter that indicates whether it succeeded or failed.</param>
        /// <param name="forceResolution">Whether resolution should be executed when no dependencies
        /// have changed.  This is useful if a dependency specifies a wildcard in the version
        /// expression.</param>
        private static void ResolveUnsafe(Action <bool> resolutionComplete = null,
                                          bool forceResolution             = false)
        {
            JavaUtilities.CheckJdkForApiLevel();

            if (!buildConfigChanged)
            {
                DeleteFiles(Resolver.OnBuildSettings());
            }

            xmlDependencies.ReadAll(logger);

            if (forceResolution)
            {
                DeleteLabeledAssets();
            }
            else
            {
                // Only resolve if user specified dependencies changed or the output files
                // differ to what is present in the project.
                var currentState  = DependencyState.GetState();
                var previousState = DependencyState.ReadFromFile();
                if (previousState != null)
                {
                    if (currentState.Equals(previousState))
                    {
                        if (resolutionComplete != null)
                        {
                            resolutionComplete(true);
                        }
                        return;
                    }
                    // Delete all labeled assets to make sure we don't leave any stale transitive
                    // dependencies in the project.
                    DeleteLabeledAssets();
                }
            }

            System.IO.Directory.CreateDirectory(GooglePlayServices.SettingsDialog.PackageDir);
            PlayServicesSupport.Log("Resolving...", verbose: true);

            lastError = "";
            Resolver.DoResolution(svcSupport, GooglePlayServices.SettingsDialog.PackageDir,
                                  (oldDependency, newDependency) => {
                return(Resolver.ShouldReplaceDependency(oldDependency,
                                                        newDependency));
            },
                                  () => {
                System.Action complete = () => {
                    bool succeeded = String.IsNullOrEmpty(lastError);
                    AssetDatabase.Refresh();
                    DependencyState.GetState().WriteToFile();
                    PlayServicesSupport.Log(String.Format(
                                                "Resolution {0}.\n\n{1}",
                                                succeeded ? "Succeeded" : "Failed",
                                                lastError), verbose: true);
                    if (resolutionComplete != null)
                    {
                        resolutionComplete(succeeded);
                    }
                };
                updateQueue.Enqueue(complete);
            });
        }
        /// <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)
        {
            PlayServicesSupport.Log(String.Format("ProcessAar {0} {1} antProject={2}",
                                                  dir, aarFile, antProject), verbose: true);
            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");
                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)
                {
                    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 currentAbi = PlayServicesResolver.AndroidTargetDeviceAbi;
                var activeAbis = GetSelectedABIDirs(currentAbi);
                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);
        }
        // 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);
                PlayServicesSupport.Log(
                    String.Format("TargetSDK is set to Auto-detect, and the latest Platform has been " +
                                  "detected as: android-{0}", targetSdkVersion),
                    level: PlayServicesSupport.LogLevel.Info, verbose: true);

                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)
                {
                    PlayServicesSupport.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: PlayServicesSupport.LogLevel.Error);
                    return;
                }
                else
                {
                    PlayServicesSupport.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: PlayServicesSupport.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 = svcSupport.LoadDependencies(true, true, false);
            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))
                            {
                                PlayServicesSupport.DeleteExistingFileOrDirectory(
                                    directory, includeMetaFiles: true);
                            }
                        }
                    }
                    if (Directory.Exists(outDir))
                    {
                        PlayServicesResolver.LabelAssets(new [] { outDir }, true, true);
                    }
                    AssetDatabase.Refresh();
                    resolutionComplete();
                }
            });
        }