diff --git a/nupkgs/Xamarin.Android.Tools.AndroidSdk.1.0.169.nupkg b/nupkgs/Xamarin.Android.Tools.AndroidSdk.1.0.169.nupkg new file mode 100644 index 00000000..9258520f Binary files /dev/null and b/nupkgs/Xamarin.Android.Tools.AndroidSdk.1.0.169.nupkg differ diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdInfo.cs b/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdInfo.cs new file mode 100644 index 00000000..70f8f3d3 --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdInfo.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Xamarin.Android.Tools +{ + public class AvdInfo + { + public string Name { get; set; } = string.Empty; + public string? DeviceProfile { get; set; } + public string? Path { get; set; } + } +} diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs new file mode 100644 index 00000000..3f24d914 --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools +{ + /// + /// Runs Android Virtual Device Manager (avdmanager) commands. + /// + public class AvdManagerRunner + { + readonly Func getSdkPath; + readonly Func? getJdkPath; + + public AvdManagerRunner (Func getSdkPath) + : this (getSdkPath, null) + { + } + + public AvdManagerRunner (Func getSdkPath, Func? getJdkPath) + { + this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); + this.getJdkPath = getJdkPath; + } + + public string? AvdManagerPath { + get { + var sdkPath = getSdkPath (); + if (string.IsNullOrEmpty (sdkPath)) + return null; + + var ext = OS.IsWindows ? ".bat" : ""; + var cmdlineToolsPath = Path.Combine (sdkPath, "cmdline-tools", "latest", "bin", "avdmanager" + ext); + if (File.Exists (cmdlineToolsPath)) + return cmdlineToolsPath; + + var toolsPath = Path.Combine (sdkPath, "tools", "bin", "avdmanager" + ext); + + return File.Exists (toolsPath) ? toolsPath : null; + } + } + + public bool IsAvailable => !string.IsNullOrEmpty (AvdManagerPath); + + void ConfigureEnvironment (ProcessStartInfo psi) + { + var sdkPath = getSdkPath (); + if (!string.IsNullOrEmpty (sdkPath)) + psi.EnvironmentVariables ["ANDROID_HOME"] = sdkPath; + + var jdkPath = getJdkPath?.Invoke (); + if (!string.IsNullOrEmpty (jdkPath)) + psi.EnvironmentVariables ["JAVA_HOME"] = jdkPath; + } + + public async Task> ListAvdsAsync (CancellationToken cancellationToken = default) + { + if (!IsAvailable) + throw new InvalidOperationException ("AVD Manager not found."); + + var stdout = new StringWriter (); + var psi = new ProcessStartInfo { + FileName = AvdManagerPath!, + Arguments = "list avd", + UseShellExecute = false, + CreateNoWindow = true + }; + ConfigureEnvironment (psi); + await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); + + return ParseAvdListOutput (stdout.ToString ()); + } + + public async Task CreateAvdAsync (string name, string systemImage, string? deviceProfile = null, + bool force = false, CancellationToken cancellationToken = default) + { + if (!IsAvailable) + throw new InvalidOperationException ("AVD Manager not found."); + if (string.IsNullOrEmpty (name)) + throw new ArgumentNullException (nameof (name)); + if (string.IsNullOrEmpty (systemImage)) + throw new ArgumentNullException (nameof (systemImage)); + + // Check if AVD already exists — return it instead of failing + if (!force) { + var existing = (await ListAvdsAsync (cancellationToken).ConfigureAwait (false)) + .FirstOrDefault (a => string.Equals (a.Name, name, StringComparison.OrdinalIgnoreCase)); + if (existing is not null) + return existing; + } + + // Detect orphaned AVD directory (folder exists without .ini registration). + // Use --force to overwrite the orphaned directory. + var avdDir = Path.Combine ( + Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), + ".android", "avd", $"{name}.avd"); + if (Directory.Exists (avdDir)) + force = true; + + var args = $"create avd -n \"{name}\" -k \"{systemImage}\""; + if (!string.IsNullOrEmpty (deviceProfile)) + args += $" -d \"{deviceProfile}\""; + if (force) + args += " --force"; + + var stdout = new StringWriter (); + var stderr = new StringWriter (); + var psi = new ProcessStartInfo { + FileName = AvdManagerPath!, + Arguments = args, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardInput = true + }; + ConfigureEnvironment (psi); + + // avdmanager prompts "Do you wish to create a custom hardware profile?" — answer "no" + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, + onStarted: p => { + try { + p.StandardInput.WriteLine ("no"); + p.StandardInput.Close (); + } catch (System.IO.IOException) { + // Process may have already exited + } + }).ConfigureAwait (false); + + if (exitCode != 0) { + var errorOutput = stderr.ToString ().Trim (); + if (string.IsNullOrEmpty (errorOutput)) + errorOutput = stdout.ToString ().Trim (); + throw new InvalidOperationException ($"Failed to create AVD '{name}': {errorOutput}"); + } + + return new AvdInfo { + Name = name, + DeviceProfile = deviceProfile, + }; + } + + public async Task DeleteAvdAsync (string name, CancellationToken cancellationToken = default) + { + if (!IsAvailable) + throw new InvalidOperationException ("AVD Manager not found."); + + var psi = new ProcessStartInfo { + FileName = AvdManagerPath!, + Arguments = $"delete avd --name \"{name}\"", + UseShellExecute = false, + CreateNoWindow = true + }; + ConfigureEnvironment (psi); + await ProcessUtils.StartProcess (psi, null, null, cancellationToken).ConfigureAwait (false); + } + + internal static List ParseAvdListOutput (string output) + { + var avds = new List (); + string? currentName = null, currentDevice = null, currentPath = null; + + foreach (var line in output.Split ('\n')) { + var trimmed = line.Trim (); + if (trimmed.StartsWith ("Name:", StringComparison.OrdinalIgnoreCase)) { + if (currentName is not null) + avds.Add (new AvdInfo { Name = currentName, DeviceProfile = currentDevice, Path = currentPath }); + currentName = trimmed.Substring (5).Trim (); + currentDevice = currentPath = null; + } + else if (trimmed.StartsWith ("Device:", StringComparison.OrdinalIgnoreCase)) + currentDevice = trimmed.Substring (7).Trim (); + else if (trimmed.StartsWith ("Path:", StringComparison.OrdinalIgnoreCase)) + currentPath = trimmed.Substring (5).Trim (); + } + + if (currentName is not null) + avds.Add (new AvdInfo { Name = currentName, DeviceProfile = currentDevice, Path = currentPath }); + + return avds; + } + } +} +