// // Copyright (C) 2017 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // namespace Google.Android.AppBundle.Editor.Internal.PlayServices { using System; using System.Diagnostics; using System.IO; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using UnityEngine; /// /// Subset of Android SDK package metadata required for installation. /// public class AndroidSdkPackageNameVersion { /// Converts and old "android" package manager package name to a new "sdkmanager" package /// name. private static Dictionary OLD_TO_NEW_PACKAGE_NAME_PREFIX = new Dictionary { { "extra;", "extras;" } }; /// List package name components to not convert hyphens / semi-colons. /// Some package names contain hyphens and therefore can't simply be converted by /// replacing - with ;. This list of name components is used to preserved when /// converting between new and legacy package names. /// This list was dervied from: /// sdkmanager --verbose --list | \ /// grep -vE '^(Info:| |---)' | \ /// grep '-' | \ /// tr ';' '\n' | \ /// sort | \ /// uniq | \ /// grep '-' | \ /// sed 's/.*/"&",/' private static List PRESERVED_PACKAGE_NAME_COMPONENTS = new List { new Regex("\\d+.\\d+.\\d+-[a-zA-Z0-9]+"), new Regex("add-ons"), new Regex("addon-google_apis-google-\\d+"), new Regex("android-\\d+"), new Regex("arm64-v8a"), new Regex("armeabi-v7a"), new Regex("build-tools"), new Regex("constraint-layout"), new Regex("constraint-layout-solver"), new Regex("ndk-bundle"), new Regex("platform-tools"), new Regex("system-images"), new Regex("support-[a-zA-Z0-9]+"), }; /// /// Name of the package. /// public string Name { set; get; } /// /// Escape components that should not be converted by LegacyName. /// /// Escaped package name. private string EscapeComponents(string packageName) { foreach (var componentRegex in PRESERVED_PACKAGE_NAME_COMPONENTS) { var match = componentRegex.Match(packageName); if (match.Success) { var prefix = packageName.Substring(0, match.Index); var postfix = packageName.Substring(match.Index + match.Length); // Exclaimation marks are guaranteed - at the moment - to not be // part of a package name / path. packageName = prefix + match.Value.Replace("-", "!") + postfix; } } return packageName; } /// /// Un-escaped components that should not be converted by LegacyName. /// /// Un-escaped package name. private string UnescapeComponents(string packageName) { return packageName.Replace("!", "-"); } /// /// Convert to / from a legacy package name. /// public string LegacyName { set { var packageName = UnescapeComponents(EscapeComponents(value).Replace("-", ";")); foreach (var kv in OLD_TO_NEW_PACKAGE_NAME_PREFIX) { if (packageName.StartsWith(kv.Key)) { packageName = kv.Value + packageName.Substring(kv.Key.Length); break; } } Name = packageName; } get { var packageName = Name; foreach (var kv in OLD_TO_NEW_PACKAGE_NAME_PREFIX) { if (packageName.StartsWith(kv.Value)) { packageName = kv.Key + packageName.Substring(kv.Value.Length); break; } } return packageName.Replace(";", "-"); } } /// /// Convert to / from a package path to name. /// /// Android SDK package names are derived from their path relative to the SDK directory. public string Path { set { Name = value.Replace("\\", "/").Replace("/", ";"); } get { return Name.Replace(";", System.IO.Path.PathSeparator.ToString()); } } /// /// String representation of the package version. /// public string VersionString { set; get; } /// /// 64-bit integer representation of the package version. /// public long Version { get { return ConvertVersionStringToInteger(VersionString); } } /// /// Get a string representation of this object. /// public override string ToString() { return String.Format("{0} ({1})", Name, VersionString); } /// /// Hash the name of this package. /// public override int GetHashCode() { return Name.GetHashCode(); } /// /// Compares two package names. /// /// Object to compare with. /// true if both objects have the same name, false otherwise. public override bool Equals(System.Object obj) { var pkg = obj as AndroidSdkPackageNameVersion; return pkg != null && pkg.Name == Name; } /// /// Convert an N component version string into an integer. /// /// Version string to convert. /// Value to multiply each component by. /// An integer representation of the version string. public static long ConvertVersionStringToInteger(string versionString, long componentMultiplier = 1000000) { if (String.IsNullOrEmpty(versionString)) return 0; var components = versionString.Split(new [] { '.' }); long versionInteger = 0; long currentMultiplier = 1; Array.Reverse(components); foreach (var component in components) { long componentInteger = 0; try { componentInteger = Convert.ToInt64(component); } catch (FormatException) { PlayServicesResolver.Log( String.Format("Unable to convert version string {0} to " + "integer value", versionString), level: LogLevel.Warning); return 0; } versionInteger += (componentInteger * currentMultiplier); currentMultiplier *= componentMultiplier; } return versionInteger; } /// /// Convert a list of package name / versions to a bulleted string list. /// /// List of packages to write to a string. /// Bulleted list of package name / versions. public static string ListToString( IEnumerable packages) { var packageAndVersion = new List(); foreach (var pkg in packages) { packageAndVersion.Add(String.Format( "* {0} {1}", pkg.Name, !String.IsNullOrEmpty(pkg.VersionString) ? String.Format("({0})", pkg.VersionString) : "")); } return String.Join("\n", packageAndVersion.ToArray()); } } /// /// Describes an Android SDK package. /// public class AndroidSdkPackage : AndroidSdkPackageNameVersion { /// /// Human readable description of the package. /// public string Description { set; get; } /// /// Whether the package is installed. /// public bool Installed { set; get; } /// /// Read package metadata from the source.properties file within the specified directory. /// /// Android SDK directory to query. /// Directory containing the package relative to /// sdkDirectory. 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) { PlayServicesResolver.Log(String.Format("Unable to read {0}\n{1}\n", propertiesPath, e.ToString()), level: LogLevel.Verbose); 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; } } /// /// Collection of AndroidSdkPackage instances indexed by package name. /// public class AndroidSdkPackageCollection { private Dictionary> packages = new Dictionary>(); /// /// Get the set of package names in the collection. /// public List PackageNames { get { return new List(packages.Keys); } } /// /// Get the list of package metadata by package name. /// /// List of package metadata. public List this[string packageName] { get { List packageList = null; if (!packages.TryGetValue(packageName, out packageList)) { packageList = new List(); packages[packageName] = packageList; } return packageList; } } /// /// Get the most recent available version of a specified package, prefering installed. /// /// The package if it's available, null otherwise. public AndroidSdkPackage GetMostRecentAvailablePackage(string packageName) { var list = this[packageName]; if (list == null || !list.Any()) { return null; } var maxVersion = list.Max(p => p.Version); AndroidSdkPackage mostRecentPackage = null; foreach (var package in list) { if (package.Version == maxVersion && (mostRecentPackage == null || !mostRecentPackage.Installed)) { mostRecentPackage = package; } } return mostRecentPackage; } /// /// Get installed package metadata by package name. /// /// The package if it's installed, null otherwise. public AndroidSdkPackage GetInstalledPackage(string packageName) { foreach (var sdkPackage in this[packageName]) { if (sdkPackage.Installed) return sdkPackage; } return null; } } /// /// Interface used to interact with Android SDK managers. /// public interface IAndroidSdkManager { /// /// Use the package manager to retrieve the set of installed and available packages. /// /// Called when the query is complete. void QueryPackages(Action complete); /// /// Install a set of packages. /// /// Set of packages to install / upgrade. /// Called when installation is complete. void InstallPackages(HashSet packages, Action complete); } // Answers Android SDK manager license questions. internal class LicenseResponder : CommandLine.LineReader { // String to match in order to respond. private string question; // Response to provide to the question. private string response; /// /// Initialize the class to respond "yes" or "no" to license questions. /// /// Question to respond to. /// Response to provide. public LicenseResponder(string question, string response) { this.question = question; this.response = response; LineHandler += CheckAndRespond; } // Respond license questions with the "response". public void CheckAndRespond(Process process, StreamWriter stdin, CommandLine.StreamData data) { if (process.HasExited) return; if ((data.data != null && data.text.Contains(question)) || CommandLine.LineReader.Aggregate(GetBufferedData(0)).text.Contains(question)) { Flush(); // Ignore I/O exceptions as this could race with the process exiting. try { foreach (byte b in System.Text.Encoding.UTF8.GetBytes( response + System.Environment.NewLine)) { stdin.BaseStream.WriteByte(b); } stdin.BaseStream.Flush(); } catch (System.IO.IOException) { } } } } /// /// Utility methods for implementations of IAndroidSdkManager. /// internal class SdkManagerUtil { /// /// Message displayed if a package query operation fails. /// const string PACKAGES_MISSING = "Unable to determine which Android packages are installed.\n{0}"; /// /// Title of the installation dialog. /// private const string DIALOG_TITLE = "Installing Android SDK packages"; /// /// Use the package manager to retrieve the set of installed and available packages. /// /// Tool to run. /// Arguments to pass to the tool. /// Called when the query is complete. public static void QueryPackages(string toolPath, string toolArguments, Action complete) { var window = CommandLineDialog.CreateCommandLineDialog( "Querying Android SDK packages"); PlayServicesResolver.Log(String.Format("Query Android SDK packages\n" + "\n" + "{0} {1}\n", toolPath, toolArguments), level: LogLevel.Verbose); window.summaryText = "Getting Installed Android SDK packages."; window.modal = false; // Note: not displaying progress bar window.autoScrollToBottom = true; window.RunAsync( toolPath, toolArguments, (CommandLine.Result result) => { HandleProcessExited(result, window); complete(result); }, maxProgressLines: 50); window.Show(); } /// /// Retrieve package licenses, display license dialog and then install packages. /// /// Tool to run. /// Arguments to pass to the tool. /// List of package versions to install / upgrade. /// License question to respond to. /// String used to agree to a license. /// String used to decline a license. /// Regex which matches the line which is the start of a /// license agreement. /// Called when installation is complete. public static void InstallPackages( string toolPath, string toolArguments, HashSet packages, string licenseQuestion, string licenseAgree, string licenseDecline, Regex licenseTextHeader, Action complete) { PlayServicesResolver.Log(String.Format("Install Android SDK packages\n" + "\n" + "{0} {1}\n", toolPath, toolArguments), level: LogLevel.Verbose); // Display the license retrieval dialog. DisplayInstallLicenseDialog( toolPath, toolArguments, true, new LicenseResponder(licenseQuestion, licenseDecline), packages, (CommandLine.Result licensesResult) => { if (licensesResult.exitCode != 0) { complete(false); return; } // Get the license text. var licensesLines = new List(); bool foundLicenses = false; foreach (var line in CommandLine.SplitLines(licensesResult.stdout)) { foundLicenses = foundLicenses || licenseTextHeader.Match(line).Success; if (foundLicenses) licensesLines.Add(line); } if (licensesLines.Count == 0) { LogInstallLicenseResult(toolPath, toolArguments, false, packages, licensesResult); complete(true); return; } // Display the license agreement dialog. DisplayLicensesDialog( String.Join("\n", licensesLines.ToArray()), (bool agreed) => { if (!agreed) { complete(false); return; } // Finally install the packages agreeing to the license questions. DisplayInstallLicenseDialog( toolPath, toolArguments, false, new LicenseResponder(licenseQuestion, licenseAgree), packages, (CommandLine.Result installResult) => { complete(installResult.exitCode == 0); }); }); }); } /// /// Log a message that describes the installation / license fetching operation. /// /// Tool that was executed. /// Arguments to passed to the tool. /// Whether the command is retrieving licenses. /// List of package versions to install / upgrade.. /// Result of the tool's execution. private static void LogInstallLicenseResult( string toolPath, string toolArguments, bool retrievingLicenses, IEnumerable packages, CommandLine.Result toolResult) { bool succeeded = toolResult.exitCode == 0; if (!retrievingLicenses || !succeeded) { var failedMessage = retrievingLicenses ? "Failed to retrieve Android SDK package licenses.\n\n" + "Aborted installation of the following packages:\n" : "Android package installation failed.\n\n" + "Failed when installing the following packages:\n"; PlayServicesResolver.Log( String.Format( "{0}\n" + "{1}\n\n" + "{2}\n", succeeded ? "Successfully installed Android packages.\n\n" : failedMessage, AndroidSdkPackageNameVersion.ListToString(packages), toolResult.message), level: succeeded ? LogLevel.Info : LogLevel.Warning); } } /// /// Open a install / license window and execute a command. /// /// Tool to run. /// Arguments to pass to the tool. /// Whether the command is retrieving licenses. /// Responds to license questions. /// List of package versions to install / upgrade. /// Called when installation is complete. private static void DisplayInstallLicenseDialog( string toolPath, string toolArguments, bool retrievingLicenses, LicenseResponder licenseResponder, IEnumerable packages, Action complete) { var summary = retrievingLicenses ? "Attempting Android SDK package installation..." : DIALOG_TITLE + "..."; var window = CommandLineDialog.CreateCommandLineDialog(DIALOG_TITLE); window.summaryText = summary; window.modal = false; window.bodyText = String.Format("{0} {1}\n\n", toolPath, toolArguments); // Note: not displaying progress bar window.autoScrollToBottom = true; CommandLine.IOHandler ioHandler = null; if (licenseResponder != null) ioHandler = licenseResponder.AggregateLine; PlayServicesResolver.Log(String.Format("{0} {1}", toolPath, toolArguments), level: LogLevel.Verbose); window.RunAsync( toolPath, toolArguments, (CommandLine.Result result) => { HandleProcessExited(result, window); LogInstallLicenseResult(toolPath, toolArguments, retrievingLicenses, packages, result); complete(result); }, ioHandler: ioHandler, maxProgressLines: retrievingLicenses ? 250 : 500); window.Show(); } /// /// Handles window behavior when the CommandLineDialog finishes, closing the window automatically if /// execution succeeded and leaving it open with a "Close" button if it failed. /// /// The result of the process execution. /// The window that should be adjusted based on the process execution result. private static void HandleProcessExited(CommandLine.Result result, CommandLineDialog window) { if (result.exitCode == 0) { window.Close(); } else { PlayServicesResolver.Log(string.Format(PACKAGES_MISSING, result.message)); window.noText = "Close"; // After adding the button we need to scroll down a little more. window.scrollPosition.y = Mathf.Infinity; window.Repaint(); } } /// /// Display license dialog. /// /// String containing the licenses to display. /// Called when the user agrees / disagrees to the licenses. private static void DisplayLicensesDialog(string licenses, Action complete) { var window = CommandLineDialog.CreateCommandLineDialog(DIALOG_TITLE); window.summaryText = "License agreement(s) required to install Android SDK packages"; window.modal = false; window.bodyText = licenses; window.yesText = "agree"; window.noText = "decline"; window.result = false; window.Repaint(); window.buttonClicked = (TextAreaDialog dialog) => { window.Close(); if (!dialog.result) { complete(false); return; } complete(true); }; window.Show(); } } /// /// Interacts with the legacy Android SDK manager "android". /// internal class AndroidToolSdkManager : IAndroidSdkManager { /// Name of the SDK manager command line tool. public const string TOOL_NAME = "android"; /// /// Extracts the package identifer from the SDK list output. /// private static Regex PACKAGE_ID_REGEX = new Regex( "^id:\\W+\\d+\\W+or\\W+\"([^\"]+)\""); /// /// Extracts the package description from the SDK list output. /// private static Regex PACKAGE_DESCRIPTION_REGEX = new Regex( "^\\WDesc:\\W+(.+)"); /// /// Extracts the install location from the SDK list output. /// private static Regex PACKAGE_INSTALL_LOCATION_REGEX = new Regex( "^\\W+Install[^:]+:\\W+([^ ]+)"); // Path to the SDK manager tool. private string toolPath; // Path to the Android SDK. private string sdkPath; /// /// Initialize this instance. /// /// Path of the android tool. /// Required to validate that a package is really installed. public AndroidToolSdkManager(string toolPath, string sdkPath) { this.toolPath = toolPath; this.sdkPath = sdkPath; } /// /// Determines whether this is the legacy tool or the sdkmanager wrapper. /// public bool IsWrapper { get { // It's only possible to differentiate between the "android" package manager or // sdkmanager wrapper by searching the output string for "deprecated" which is // present in the wrapper. var result = CommandLine.Run( toolPath, "list sdk", envVars: new Dictionary { { "USE_SDK_WRAPPER", "1" } }); if (result.stdout.IndexOf("deprecated") >= 0) { return true; } return false; } } /// /// Parse "android list sdk -u -e -a" output. /// private AndroidSdkPackageCollection ParseAndroidListSdkOutput( string androidListSdkOutput) { var packages = new AndroidSdkPackageCollection(); AndroidSdkPackage currentPackage = null; foreach (string line in CommandLine.SplitLines(androidListSdkOutput)) { // Check for the start of a new package entry. if (line.StartsWith("---")) { currentPackage = null; continue; } Match match; // If this is the start of a package description, add a package. match = PACKAGE_ID_REGEX.Match(line); if (match.Success) { // TODO(smiles): Convert the legacy package name to a new package name. currentPackage = new AndroidSdkPackage { LegacyName = match.Groups[1].Value }; packages[currentPackage.Name].Add(currentPackage); continue; } if (currentPackage == null) continue; // Add a package description. match = PACKAGE_DESCRIPTION_REGEX.Match(line); if (match.Success) { currentPackage.Description = match.Groups[1].Value; continue; } // Parse the install path and record whether the package is installed. match = PACKAGE_INSTALL_LOCATION_REGEX.Match(line); if (match.Success) { currentPackage.Installed = File.Exists( Path.Combine(Path.Combine(sdkPath, match.Groups[1].Value), "source.properties")); } } return packages; } /// /// Use the package manager to retrieve the set of installed and available packages. /// /// Called when the query is complete. public void QueryPackages(Action complete) { SdkManagerUtil.QueryPackages( toolPath, "list sdk -u -e -a", (CommandLine.Result result) => { complete(result.exitCode == 0 ? ParseAndroidListSdkOutput(result.stdout) : null); }); } /// /// Install a set of packages. /// /// List of package versions to install / upgrade. /// Called when installation is complete. public void InstallPackages(HashSet packages, Action complete) { var packageNames = new List(); foreach (var pkg in packages) packageNames.Add(pkg.LegacyName); SdkManagerUtil.InstallPackages( toolPath, String.Format( "update sdk -a -u -t {0}", String.Join(",", packageNames.ToArray())), packages, "Do you accept the license", "yes", "no", new Regex("^--------"), complete); } } /// /// Interacts with the Android SDK manager "sdkmanager". /// internal class SdkManager : IAndroidSdkManager { /// Name of the SDK manager command line tool. public const string TOOL_NAME = "sdkmanager"; // Marker followed by the list of installed packages. private const string INSTALLED_PACKAGES_HEADER = "installed packages:"; // Marker followed by the list of available packages. private const string AVAILABLE_PACKAGES_HEADER = "available packages:"; // Marker followed by the list of available package updates. private const string AVAILABLE_UPDATES_HEADER = "available updates:"; /// Minimum version of the package that supports the --verbose flag. public static long MINIMUM_VERSION_FOR_VERBOSE_OUTPUT = AndroidSdkPackageNameVersion.ConvertVersionStringToInteger("26.0.2"); // Path to the SDK manager tool. private string toolPath; // Metadata for this package. private AndroidSdkPackage toolsPackage; /// /// Initialize this instance. /// /// Path of the android tool. public SdkManager(string toolPath) { this.toolPath = toolPath; var toolsDir = Path.GetDirectoryName(Path.GetDirectoryName(toolPath)); var sdkDir = Path.GetDirectoryName(toolsDir); toolsPackage = AndroidSdkPackage.ReadFromSourceProperties( sdkDir, toolsDir.Substring((sdkDir + Path.PathSeparator).Length)); } /// /// Read the metadata for the package that contains the package manager. /// public AndroidSdkPackage Package { get { return toolsPackage; } } /// /// Parse "sdkmanager --list --verbose" output. /// NOTE: The --verbose output format is only reported by sdkmanager 26.0.2 and above. /// private AndroidSdkPackageCollection ParseListVerboseOutput( string sdkManagerListVerboseOutput) { var packages = new AndroidSdkPackageCollection(); // Whether we're parsing a set of packages. bool parsingPackages = false; // Whether we're parsing within the set of installed packages vs. available packages. bool parsingInstalledPackages = false; // Fields of the package being parsed. AndroidSdkPackage currentPackage = null; foreach (var rawLine in CommandLine.SplitLines(sdkManagerListVerboseOutput)) { var line = rawLine.Trim(); var lowerCaseLine = line.ToLower(); if (lowerCaseLine == AVAILABLE_UPDATES_HEADER) { parsingPackages = false; continue; } bool installedPackagesLine = lowerCaseLine == INSTALLED_PACKAGES_HEADER; bool availablePackagesLine = lowerCaseLine == AVAILABLE_PACKAGES_HEADER; if (installedPackagesLine || availablePackagesLine) { parsingPackages = true; parsingInstalledPackages = installedPackagesLine; continue; } else if (line.StartsWith("---")) { // Ignore section separators. continue; } else if (String.IsNullOrEmpty(line)) { if (currentPackage != null && !(String.IsNullOrEmpty(currentPackage.Name) || String.IsNullOrEmpty(currentPackage.VersionString))) { packages[currentPackage.Name].Add(currentPackage); } currentPackage = null; continue; } else if (!parsingPackages) { continue; } // Fields of the package are indented. bool indentedLine = rawLine.StartsWith(" "); if (!indentedLine) { // If this isn't an indented line it should be a package name. if (currentPackage == null) { currentPackage = new AndroidSdkPackage { Name = line, Installed = parsingInstalledPackages }; } } else if (currentPackage != null) { // Parse the package field. var fieldSeparatorIndex = line.IndexOf(":"); if (fieldSeparatorIndex >= 0) { var fieldName = line.Substring(0, fieldSeparatorIndex).Trim().ToLower(); var fieldValue = line.Substring(fieldSeparatorIndex + 1).Trim(); if (fieldName == "description") { currentPackage.Description = fieldValue; } else if (fieldName == "version") { currentPackage.VersionString = fieldValue; } } } } return packages; } /// /// Parse "sdkmanager --list" output. /// /// Dictionary of packages bucketed by package name private AndroidSdkPackageCollection ParseListOutput( string sdkManagerListOutput) { var packages = new AndroidSdkPackageCollection(); // Whether we're parsing a set of packages. bool parsingPackages = false; // Whether we're parsing within the set of installed packages vs. available packages. bool parsingInstalledPackages = false; // Whether we're parsing the contents of the package table vs. the header. bool inPackageTable = false; foreach (var rawLine in CommandLine.SplitLines(sdkManagerListOutput)) { var line = rawLine.Trim(); var lowerCaseLine = line.ToLower(); if (lowerCaseLine == AVAILABLE_UPDATES_HEADER) { parsingPackages = false; continue; } bool installedPackagesLine = lowerCaseLine == INSTALLED_PACKAGES_HEADER; bool availablePackagesLine = lowerCaseLine == AVAILABLE_PACKAGES_HEADER; if (installedPackagesLine || availablePackagesLine) { parsingPackages = true; parsingInstalledPackages = installedPackagesLine; inPackageTable = false; continue; } if (!parsingPackages) continue; if (!inPackageTable) { // If we've reached end of the table header, start parsing the set of packages. if (line.StartsWith("----")) { inPackageTable = true; } continue; } // Split into the fields package_name|version|description|location. // Where "location" is an optional field that contains the install path. var rawTokens = line.Split(new [] { '|' }); if (rawTokens.Length < 3 || String.IsNullOrEmpty(line)) { parsingPackages = false; continue; } // Each field is surrounded by whitespace so trim the fields. string[] tokens = new string[rawTokens.Length]; for (int i = 0; i < rawTokens.Length; ++i) { tokens[i] = rawTokens[i].Trim(); } var packageName = tokens[0]; packages[packageName].Add(new AndroidSdkPackage { Name = packageName, Description = tokens[2], VersionString = tokens[1], Installed = parsingInstalledPackages }); } return packages; } /// /// Use the package manager to retrieve the set of installed and available packages. /// /// Called when the query is complete. public void QueryPackages(Action complete) { bool useVerbose = Package != null && Package.Version >= MINIMUM_VERSION_FOR_VERBOSE_OUTPUT; SdkManagerUtil.QueryPackages( toolPath, "--list" + (useVerbose ? " --verbose" : ""), (CommandLine.Result result) => { complete(result.exitCode == 0 ? useVerbose ? ParseListVerboseOutput(result.stdout) : ParseListOutput(result.stdout) : null); }); } /// /// Install a set of packages. /// /// List of package versions to install / upgrade. /// Called when installation is complete. public void InstallPackages(HashSet packages, Action complete) { var packageNames = new List(); 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); } } /// /// Interacts with the available Android SDK package manager. /// public class AndroidSdkManager { /// /// Find a tool in the Android SDK. /// /// Name of the tool to search for. /// SDK path to search for the tool. If this is null or empty, the // system path is searched instead. /// String with the path to the tool if found, null otherwise. private static string FindAndroidSdkTool(string toolName, string sdkPath = null) { if (String.IsNullOrEmpty(sdkPath)) { PlayServicesResolver.Log(String.Format( "Falling back to searching for the Android SDK tool {0} in the system path.", toolName)); } else { var extensions = new List { 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; } /// /// Log an error and complete a Create() operation. /// /// Action called with null. private static void CreateFailed(Action complete) { PlayServicesResolver.Log(String.Format( "Unable to find either the {0} or {1} command line tool.\n\n" + "It is not possible to query or install Android SDK packages.\n" + "To resolve this issue, open the Android Package Manager" + "and install the latest tools package.", SdkManager.TOOL_NAME, AndroidToolSdkManager.TOOL_NAME)); complete(null); } /// /// Create an instance of this class. /// /// If the package manager is out of date, the user is prompted to update it. /// /// The path to the Android SDK root folder. /// Used to report a AndroidSdkManager instance if a SDK manager is /// available, returns null otherwise. public static void Create(string androidSdkRoot, Action complete) { // Search for the new package manager var sdkManagerTool = FindAndroidSdkTool(SdkManager.TOOL_NAME, androidSdkRoot); if (sdkManagerTool != null) { var sdkManager = new SdkManager(sdkManagerTool); var sdkManagerPackage = sdkManager.Package; if (sdkManagerPackage != null) { // If the package manager is out of date, try updating it. if (sdkManagerPackage.Version < SdkManager.MINIMUM_VERSION_FOR_VERBOSE_OUTPUT) { sdkManager.QueryPackages( (AndroidSdkPackageCollection packages) => { sdkManagerPackage = packages.GetMostRecentAvailablePackage( sdkManagerPackage.Name); if (sdkManagerPackage != null) { sdkManager.InstallPackages( new HashSet( new [] { sdkManagerPackage }), (bool success) => { complete(success ? sdkManager : null); }); } else { CreateFailed(complete); } }); } else { complete(sdkManager); } return; } } // Search for the legacy package manager. var androidTool = FindAndroidSdkTool("android", androidSdkRoot); if (androidTool != null) { var sdkManager = new AndroidToolSdkManager(androidTool, androidSdkRoot); if (!sdkManager.IsWrapper) { complete(sdkManager); return; } } CreateFailed(complete); } } }