/// <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);
        }
        // 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);
        }
Example #4
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);
        }
        /// <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);
        }
        /// <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);
        }