From f5a820beb9a4f8d7e1e33e049b14dac21227adf3 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Tue, 24 Feb 2026 14:59:22 +0000 Subject: [PATCH 1/6] Add EmulatorRunner for emulator CLI operations - StartAvd(): Start an emulator for an AVD - ListAvdNamesAsync(): List installed AVD names Minimal implementation without external dependencies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Runners/EmulatorRunner.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs new file mode 100644 index 00000000..7110550c --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs @@ -0,0 +1,91 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools +{ +/// +/// Runs Android Emulator commands. +/// +public class EmulatorRunner +{ +readonly Func getSdkPath; + +public EmulatorRunner (Func getSdkPath) +{ +this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); +} + +public string? EmulatorPath { +get { +var sdkPath = getSdkPath (); +if (string.IsNullOrEmpty (sdkPath)) +return null; +var ext = OS.IsWindows ? ".exe" : ""; +var path = Path.Combine (sdkPath, "emulator", "emulator" + ext); +return File.Exists (path) ? path : null; +} +} + +public bool IsAvailable => EmulatorPath != null; + +/// +/// Starts an AVD and returns the process. +/// +public Process StartAvd (string avdName, bool coldBoot = false, string? additionalArgs = null) +{ +if (!IsAvailable) +throw new InvalidOperationException ("Android Emulator not found."); + +var args = $"-avd \"{avdName}\""; +if (coldBoot) +args += " -no-snapshot-load"; +if (!string.IsNullOrEmpty (additionalArgs)) +args += " " + additionalArgs; + +var psi = new ProcessStartInfo { +FileName = EmulatorPath!, +Arguments = args, +UseShellExecute = false, +CreateNoWindow = true +}; + +var process = new Process { StartInfo = psi }; +process.Start (); +return process; +} + +/// +/// Lists the names of installed AVDs. +/// +public async Task> ListAvdNamesAsync (CancellationToken cancellationToken = default) +{ +if (!IsAvailable) +throw new InvalidOperationException ("Android Emulator not found."); + +var stdout = new StringWriter (); +var psi = new ProcessStartInfo { +FileName = EmulatorPath!, +Arguments = "-list-avds", +UseShellExecute = false, +CreateNoWindow = true +}; + +var exitCode = await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); + +var avds = new List (); +foreach (var line in stdout.ToString ().Split ('\n')) { +var trimmed = line.Trim (); +if (!string.IsNullOrEmpty (trimmed)) +avds.Add (trimmed); +} +return avds; +} +} +} From d9769636a85816e23624ee131f5082165a6ba10c Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Tue, 24 Feb 2026 18:10:11 +0000 Subject: [PATCH 2/6] Refactor EmulatorRunner per copilot instructions - Add XML documentation to all public members - Use 'is not null' instead of '!= null' - Improve code formatting - Remove unused variable assignment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Runners/EmulatorRunner.cs | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs index 7110550c..82502d47 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs @@ -17,27 +17,45 @@ public class EmulatorRunner { readonly Func getSdkPath; +/// +/// Creates a new . +/// +/// Function that returns the Android SDK path. +/// Thrown when is null. public EmulatorRunner (Func getSdkPath) { this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); } +/// +/// Gets the path to the emulator executable, or null if not found. +/// public string? EmulatorPath { get { var sdkPath = getSdkPath (); if (string.IsNullOrEmpty (sdkPath)) return null; + var ext = OS.IsWindows ? ".exe" : ""; var path = Path.Combine (sdkPath, "emulator", "emulator" + ext); + return File.Exists (path) ? path : null; } } -public bool IsAvailable => EmulatorPath != null; +/// +/// Gets whether the Android Emulator is available. +/// +public bool IsAvailable => EmulatorPath is not null; /// /// Starts an AVD and returns the process. /// +/// The name of the AVD to start. +/// Whether to perform a cold boot (ignore snapshots). +/// Additional command-line arguments. +/// The emulator process. +/// Thrown when Android Emulator is not found. public Process StartAvd (string avdName, bool coldBoot = false, string? additionalArgs = null) { if (!IsAvailable) @@ -58,12 +76,16 @@ public Process StartAvd (string avdName, bool coldBoot = false, string? addition var process = new Process { StartInfo = psi }; process.Start (); + return process; } /// /// Lists the names of installed AVDs. /// +/// Cancellation token. +/// A list of AVD names. +/// Thrown when Android Emulator is not found. public async Task> ListAvdNamesAsync (CancellationToken cancellationToken = default) { if (!IsAvailable) @@ -77,7 +99,7 @@ public async Task> ListAvdNamesAsync (CancellationToken cancellatio CreateNoWindow = true }; -var exitCode = await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); +await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); var avds = new List (); foreach (var line in stdout.ToString ().Split ('\n')) { @@ -85,6 +107,7 @@ public async Task> ListAvdNamesAsync (CancellationToken cancellatio if (!string.IsNullOrEmpty (trimmed)) avds.Add (trimmed); } + return avds; } } From fa4ca63cc8872fad1cbbe4712a06f77ebfdb7865 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Tue, 24 Feb 2026 19:44:46 +0000 Subject: [PATCH 3/6] Fix indentation: add tab indentation per Mono coding guidelines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Runners/EmulatorRunner.cs | 202 +++++++++--------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs index 82502d47..5a4ab226 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs @@ -10,105 +10,105 @@ namespace Xamarin.Android.Tools { -/// -/// Runs Android Emulator commands. -/// -public class EmulatorRunner -{ -readonly Func getSdkPath; - -/// -/// Creates a new . -/// -/// Function that returns the Android SDK path. -/// Thrown when is null. -public EmulatorRunner (Func getSdkPath) -{ -this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); -} - -/// -/// Gets the path to the emulator executable, or null if not found. -/// -public string? EmulatorPath { -get { -var sdkPath = getSdkPath (); -if (string.IsNullOrEmpty (sdkPath)) -return null; - -var ext = OS.IsWindows ? ".exe" : ""; -var path = Path.Combine (sdkPath, "emulator", "emulator" + ext); - -return File.Exists (path) ? path : null; -} -} - -/// -/// Gets whether the Android Emulator is available. -/// -public bool IsAvailable => EmulatorPath is not null; - -/// -/// Starts an AVD and returns the process. -/// -/// The name of the AVD to start. -/// Whether to perform a cold boot (ignore snapshots). -/// Additional command-line arguments. -/// The emulator process. -/// Thrown when Android Emulator is not found. -public Process StartAvd (string avdName, bool coldBoot = false, string? additionalArgs = null) -{ -if (!IsAvailable) -throw new InvalidOperationException ("Android Emulator not found."); - -var args = $"-avd \"{avdName}\""; -if (coldBoot) -args += " -no-snapshot-load"; -if (!string.IsNullOrEmpty (additionalArgs)) -args += " " + additionalArgs; - -var psi = new ProcessStartInfo { -FileName = EmulatorPath!, -Arguments = args, -UseShellExecute = false, -CreateNoWindow = true -}; - -var process = new Process { StartInfo = psi }; -process.Start (); - -return process; -} - -/// -/// Lists the names of installed AVDs. -/// -/// Cancellation token. -/// A list of AVD names. -/// Thrown when Android Emulator is not found. -public async Task> ListAvdNamesAsync (CancellationToken cancellationToken = default) -{ -if (!IsAvailable) -throw new InvalidOperationException ("Android Emulator not found."); - -var stdout = new StringWriter (); -var psi = new ProcessStartInfo { -FileName = EmulatorPath!, -Arguments = "-list-avds", -UseShellExecute = false, -CreateNoWindow = true -}; - -await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); - -var avds = new List (); -foreach (var line in stdout.ToString ().Split ('\n')) { -var trimmed = line.Trim (); -if (!string.IsNullOrEmpty (trimmed)) -avds.Add (trimmed); -} - -return avds; -} -} + /// + /// Runs Android Emulator commands. + /// + public class EmulatorRunner + { + readonly Func getSdkPath; + + /// + /// Creates a new . + /// + /// Function that returns the Android SDK path. + /// Thrown when is null. + public EmulatorRunner (Func getSdkPath) + { + this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); + } + + /// + /// Gets the path to the emulator executable, or null if not found. + /// + public string? EmulatorPath { + get { + var sdkPath = getSdkPath (); + if (string.IsNullOrEmpty (sdkPath)) + return null; + + var ext = OS.IsWindows ? ".exe" : ""; + var path = Path.Combine (sdkPath, "emulator", "emulator" + ext); + + return File.Exists (path) ? path : null; + } + } + + /// + /// Gets whether the Android Emulator is available. + /// + public bool IsAvailable => EmulatorPath is not null; + + /// + /// Starts an AVD and returns the process. + /// + /// The name of the AVD to start. + /// Whether to perform a cold boot (ignore snapshots). + /// Additional command-line arguments. + /// The emulator process. + /// Thrown when Android Emulator is not found. + public Process StartAvd (string avdName, bool coldBoot = false, string? additionalArgs = null) + { + if (!IsAvailable) + throw new InvalidOperationException ("Android Emulator not found."); + + var args = $"-avd \"{avdName}\""; + if (coldBoot) + args += " -no-snapshot-load"; + if (!string.IsNullOrEmpty (additionalArgs)) + args += " " + additionalArgs; + + var psi = new ProcessStartInfo { + FileName = EmulatorPath!, + Arguments = args, + UseShellExecute = false, + CreateNoWindow = true + }; + + var process = new Process { StartInfo = psi }; + process.Start (); + + return process; + } + + /// + /// Lists the names of installed AVDs. + /// + /// Cancellation token. + /// A list of AVD names. + /// Thrown when Android Emulator is not found. + public async Task> ListAvdNamesAsync (CancellationToken cancellationToken = default) + { + if (!IsAvailable) + throw new InvalidOperationException ("Android Emulator not found."); + + var stdout = new StringWriter (); + var psi = new ProcessStartInfo { + FileName = EmulatorPath!, + Arguments = "-list-avds", + UseShellExecute = false, + CreateNoWindow = true + }; + + await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); + + var avds = new List (); + foreach (var line in stdout.ToString ().Split ('\n')) { + var trimmed = line.Trim (); + if (!string.IsNullOrEmpty (trimmed)) + avds.Add (trimmed); + } + + return avds; + } + } } From 863be746a82f0e8c46bb9cb95513ece898a0492b Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Wed, 25 Feb 2026 17:09:39 +0000 Subject: [PATCH 4/6] Add JAVA_HOME and ANDROID_HOME environment support - Add optional Func getJdkPath constructor parameter - ConfigureEnvironment() sets JAVA_HOME and ANDROID_HOME on all ProcessStartInfo - Applied to StartAvd and ListAvdNamesAsync - Backward compatible: existing 1-arg constructor still works Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Runners/EmulatorRunner.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs index 5a4ab226..6ab35b28 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs @@ -16,6 +16,7 @@ namespace Xamarin.Android.Tools public class EmulatorRunner { readonly Func getSdkPath; + readonly Func? getJdkPath; /// /// Creates a new . @@ -23,8 +24,20 @@ public class EmulatorRunner /// Function that returns the Android SDK path. /// Thrown when is null. public EmulatorRunner (Func getSdkPath) + : this (getSdkPath, null) + { + } + + /// + /// Creates a new . + /// + /// Function that returns the Android SDK path. + /// Optional function that returns the JDK path. When provided, sets JAVA_HOME for emulator processes. + /// Thrown when is null. + public EmulatorRunner (Func getSdkPath, Func? getJdkPath) { this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); + this.getJdkPath = getJdkPath; } /// @@ -48,6 +61,17 @@ public string? EmulatorPath { /// public bool IsAvailable => EmulatorPath is not null; + 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; + } + /// /// Starts an AVD and returns the process. /// @@ -73,6 +97,7 @@ public Process StartAvd (string avdName, bool coldBoot = false, string? addition UseShellExecute = false, CreateNoWindow = true }; + ConfigureEnvironment (psi); var process = new Process { StartInfo = psi }; process.Start (); @@ -98,6 +123,7 @@ public async Task> ListAvdNamesAsync (CancellationToken cancellatio UseShellExecute = false, CreateNoWindow = true }; + ConfigureEnvironment (psi); await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); @@ -112,3 +138,4 @@ public async Task> ListAvdNamesAsync (CancellationToken cancellatio } } } + From 9b53526b93f54e91917a1246c0bb533615a3264f Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 26 Feb 2026 13:34:49 +0000 Subject: [PATCH 5/6] Trim verbose XML docs, make EmulatorPath internal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Runners/EmulatorRunner.cs | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs index 6ab35b28..ec6f77a7 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs @@ -18,32 +18,18 @@ public class EmulatorRunner readonly Func getSdkPath; readonly Func? getJdkPath; - /// - /// Creates a new . - /// - /// Function that returns the Android SDK path. - /// Thrown when is null. public EmulatorRunner (Func getSdkPath) : this (getSdkPath, null) { } - /// - /// Creates a new . - /// - /// Function that returns the Android SDK path. - /// Optional function that returns the JDK path. When provided, sets JAVA_HOME for emulator processes. - /// Thrown when is null. public EmulatorRunner (Func getSdkPath, Func? getJdkPath) { this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); this.getJdkPath = getJdkPath; } - /// - /// Gets the path to the emulator executable, or null if not found. - /// - public string? EmulatorPath { + internal string? EmulatorPath { get { var sdkPath = getSdkPath (); if (string.IsNullOrEmpty (sdkPath)) @@ -56,9 +42,6 @@ public string? EmulatorPath { } } - /// - /// Gets whether the Android Emulator is available. - /// public bool IsAvailable => EmulatorPath is not null; void ConfigureEnvironment (ProcessStartInfo psi) @@ -72,14 +55,6 @@ void ConfigureEnvironment (ProcessStartInfo psi) psi.EnvironmentVariables ["JAVA_HOME"] = jdkPath; } - /// - /// Starts an AVD and returns the process. - /// - /// The name of the AVD to start. - /// Whether to perform a cold boot (ignore snapshots). - /// Additional command-line arguments. - /// The emulator process. - /// Thrown when Android Emulator is not found. public Process StartAvd (string avdName, bool coldBoot = false, string? additionalArgs = null) { if (!IsAvailable) @@ -105,12 +80,6 @@ public Process StartAvd (string avdName, bool coldBoot = false, string? addition return process; } - /// - /// Lists the names of installed AVDs. - /// - /// Cancellation token. - /// A list of AVD names. - /// Thrown when Android Emulator is not found. public async Task> ListAvdNamesAsync (CancellationToken cancellationToken = default) { if (!IsAvailable) From b998452360a539ed09adb29329ceaef155a52f86 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 26 Feb 2026 23:58:27 +0000 Subject: [PATCH 6/6] Make EmulatorPath public for consumer access Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs index ec6f77a7..d745a717 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs @@ -29,7 +29,7 @@ public EmulatorRunner (Func getSdkPath, Func? getJdkPath) this.getJdkPath = getJdkPath; } - internal string? EmulatorPath { + public string? EmulatorPath { get { var sdkPath = getSdkPath (); if (string.IsNullOrEmpty (sdkPath))