From 1028f418685d9cc9e73437652bf8171050d19eaf Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Tue, 24 Feb 2026 15:00:08 +0000 Subject: [PATCH 1/7] Add AdbRunner for adb CLI operations - ListDevicesAsync(): List connected devices - StopEmulatorAsync(): Stop a running emulator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Runners/AdbRunner.cs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs new file mode 100644 index 00000000..8c75d991 --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -0,0 +1,101 @@ +// 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.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Xamarin.Android.Tools +{ +/// +/// Runs Android Debug Bridge (adb) commands. +/// +public class AdbRunner +{ +readonly Func getSdkPath; +static readonly Regex DetailsRegex = new Regex (@"(\w+):(\S+)", RegexOptions.Compiled); + +public AdbRunner (Func getSdkPath) +{ +this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); +} + +public string? AdbPath { +get { +var sdkPath = getSdkPath (); +if (!string.IsNullOrEmpty (sdkPath)) { +var ext = OS.IsWindows ? ".exe" : ""; +var sdkAdb = Path.Combine (sdkPath, "platform-tools", "adb" + ext); +if (File.Exists (sdkAdb)) +return sdkAdb; +} +return ProcessUtils.FindExecutablesInPath ("adb").FirstOrDefault (); +} +} + +public bool IsAvailable => AdbPath != null; + +/// +/// Lists connected devices. +/// +public async Task> ListDevicesAsync (CancellationToken cancellationToken = default) +{ +if (!IsAvailable) +throw new InvalidOperationException ("ADB not found."); + +var stdout = new StringWriter (); +var psi = new ProcessStartInfo { FileName = AdbPath!, Arguments = "devices -l", UseShellExecute = false, CreateNoWindow = true }; +await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); + +var devices = new List (); +foreach (var line in stdout.ToString ().Split ('\n')) { +var trimmed = line.Trim (); +if (string.IsNullOrEmpty (trimmed) || trimmed.StartsWith ("List of", StringComparison.OrdinalIgnoreCase)) +continue; + +var parts = trimmed.Split (new [] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); +if (parts.Length < 2) +continue; + +var device = new AdbDeviceInfo { Serial = parts [0], State = parts [1] }; +foreach (Match match in DetailsRegex.Matches (trimmed)) { +switch (match.Groups [1].Value.ToLowerInvariant ()) { +case "model": device.Model = match.Groups [2].Value; break; +case "device": device.Device = match.Groups [2].Value; break; +} +} +devices.Add (device); +} +return devices; +} + +/// +/// Stops a running emulator. +/// +public async Task StopEmulatorAsync (string serial, CancellationToken cancellationToken = default) +{ +if (!IsAvailable) +throw new InvalidOperationException ("ADB not found."); + +var psi = new ProcessStartInfo { FileName = AdbPath!, Arguments = $"-s \"{serial}\" emu kill", UseShellExecute = false, CreateNoWindow = true }; +await ProcessUtils.StartProcess (psi, null, null, cancellationToken).ConfigureAwait (false); +} +} + +/// +/// Information about a connected Android device. +/// +public class AdbDeviceInfo +{ +public string Serial { get; set; } = string.Empty; +public string? State { get; set; } +public string? Model { get; set; } +public string? Device { get; set; } +public bool IsEmulator => Serial.StartsWith ("emulator-"); +} +} From 745dbcd3c0387708fe7c3f80e11cd22e43050deb Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Tue, 24 Feb 2026 18:09:34 +0000 Subject: [PATCH 2/7] Refactor AdbRunner per copilot instructions - Add XML documentation to all public members - Split AdbDeviceInfo into its own file (one type per file) - Use 'is not null' instead of '!= null' - Improve code formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Models/AdbDeviceInfo.cs | 36 ++++++++++++++ .../Runners/AdbRunner.cs | 47 +++++++++++++------ 2 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs b/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs new file mode 100644 index 00000000..1fff737f --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs @@ -0,0 +1,36 @@ +// 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 +{ +/// +/// Information about a connected Android device. +/// +public class AdbDeviceInfo +{ +/// +/// Gets or sets the device serial number. +/// +public string Serial { get; set; } = string.Empty; + +/// +/// Gets or sets the device state (e.g., "device", "offline"). +/// +public string? State { get; set; } + +/// +/// Gets or sets the device model name. +/// +public string? Model { get; set; } + +/// +/// Gets or sets the device code name. +/// +public string? Device { get; set; } + +/// +/// Gets whether this device is an emulator. +/// +public bool IsEmulator => Serial.StartsWith ("emulator-"); +} +} diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index 8c75d991..bc27a166 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -20,11 +20,19 @@ public class AdbRunner readonly Func getSdkPath; static readonly Regex DetailsRegex = new Regex (@"(\w+):(\S+)", RegexOptions.Compiled); +/// +/// Creates a new . +/// +/// Function that returns the Android SDK path. +/// Thrown when is null. public AdbRunner (Func getSdkPath) { this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); } +/// +/// Gets the path to the adb executable, or null if not found. +/// public string? AdbPath { get { var sdkPath = getSdkPath (); @@ -34,22 +42,34 @@ public string? AdbPath { if (File.Exists (sdkAdb)) return sdkAdb; } + return ProcessUtils.FindExecutablesInPath ("adb").FirstOrDefault (); } } -public bool IsAvailable => AdbPath != null; +/// +/// Gets whether ADB is available. +/// +public bool IsAvailable => AdbPath is not null; /// /// Lists connected devices. /// +/// Cancellation token. +/// A list of connected devices. +/// Thrown when ADB is not found. public async Task> ListDevicesAsync (CancellationToken cancellationToken = default) { if (!IsAvailable) throw new InvalidOperationException ("ADB not found."); var stdout = new StringWriter (); -var psi = new ProcessStartInfo { FileName = AdbPath!, Arguments = "devices -l", UseShellExecute = false, CreateNoWindow = true }; +var psi = new ProcessStartInfo { +FileName = AdbPath!, +Arguments = "devices -l", +UseShellExecute = false, +CreateNoWindow = true +}; await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); var devices = new List (); @@ -71,31 +91,28 @@ public async Task> ListDevicesAsync (CancellationToken cance } devices.Add (device); } + return devices; } /// /// Stops a running emulator. /// +/// The emulator serial number (e.g., "emulator-5554"). +/// Cancellation token. +/// Thrown when ADB is not found. public async Task StopEmulatorAsync (string serial, CancellationToken cancellationToken = default) { if (!IsAvailable) throw new InvalidOperationException ("ADB not found."); -var psi = new ProcessStartInfo { FileName = AdbPath!, Arguments = $"-s \"{serial}\" emu kill", UseShellExecute = false, CreateNoWindow = true }; +var psi = new ProcessStartInfo { +FileName = AdbPath!, +Arguments = $"-s \"{serial}\" emu kill", +UseShellExecute = false, +CreateNoWindow = true +}; await ProcessUtils.StartProcess (psi, null, null, cancellationToken).ConfigureAwait (false); } } - -/// -/// Information about a connected Android device. -/// -public class AdbDeviceInfo -{ -public string Serial { get; set; } = string.Empty; -public string? State { get; set; } -public string? Model { get; set; } -public string? Device { get; set; } -public bool IsEmulator => Serial.StartsWith ("emulator-"); -} } From ec5bc7d292bdbedbdcabd7d9ff420bc552f1e693 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Tue, 24 Feb 2026 19:44:08 +0000 Subject: [PATCH 3/7] Fix indentation: add tab indentation per Mono coding guidelines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Models/AdbDeviceInfo.cs | 52 ++--- .../Runners/AdbRunner.cs | 182 +++++++++--------- 2 files changed, 117 insertions(+), 117 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs b/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs index 1fff737f..6374b6d2 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs @@ -3,34 +3,34 @@ namespace Xamarin.Android.Tools { -/// -/// Information about a connected Android device. -/// -public class AdbDeviceInfo -{ -/// -/// Gets or sets the device serial number. -/// -public string Serial { get; set; } = string.Empty; + /// + /// Information about a connected Android device. + /// + public class AdbDeviceInfo + { + /// + /// Gets or sets the device serial number. + /// + public string Serial { get; set; } = string.Empty; -/// -/// Gets or sets the device state (e.g., "device", "offline"). -/// -public string? State { get; set; } + /// + /// Gets or sets the device state (e.g., "device", "offline"). + /// + public string? State { get; set; } -/// -/// Gets or sets the device model name. -/// -public string? Model { get; set; } + /// + /// Gets or sets the device model name. + /// + public string? Model { get; set; } -/// -/// Gets or sets the device code name. -/// -public string? Device { get; set; } + /// + /// Gets or sets the device code name. + /// + public string? Device { get; set; } -/// -/// Gets whether this device is an emulator. -/// -public bool IsEmulator => Serial.StartsWith ("emulator-"); -} + /// + /// Gets whether this device is an emulator. + /// + public bool IsEmulator => Serial.StartsWith ("emulator-"); + } } diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index bc27a166..f55cbead 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -12,107 +12,107 @@ namespace Xamarin.Android.Tools { -/// -/// Runs Android Debug Bridge (adb) commands. -/// -public class AdbRunner -{ -readonly Func getSdkPath; -static readonly Regex DetailsRegex = new Regex (@"(\w+):(\S+)", RegexOptions.Compiled); + /// + /// Runs Android Debug Bridge (adb) commands. + /// + public class AdbRunner + { + readonly Func getSdkPath; + static readonly Regex DetailsRegex = new Regex (@"(\w+):(\S+)", RegexOptions.Compiled); -/// -/// Creates a new . -/// -/// Function that returns the Android SDK path. -/// Thrown when is null. -public AdbRunner (Func getSdkPath) -{ -this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); -} + /// + /// Creates a new . + /// + /// Function that returns the Android SDK path. + /// Thrown when is null. + public AdbRunner (Func getSdkPath) + { + this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); + } -/// -/// Gets the path to the adb executable, or null if not found. -/// -public string? AdbPath { -get { -var sdkPath = getSdkPath (); -if (!string.IsNullOrEmpty (sdkPath)) { -var ext = OS.IsWindows ? ".exe" : ""; -var sdkAdb = Path.Combine (sdkPath, "platform-tools", "adb" + ext); -if (File.Exists (sdkAdb)) -return sdkAdb; -} + /// + /// Gets the path to the adb executable, or null if not found. + /// + public string? AdbPath { + get { + var sdkPath = getSdkPath (); + if (!string.IsNullOrEmpty (sdkPath)) { + var ext = OS.IsWindows ? ".exe" : ""; + var sdkAdb = Path.Combine (sdkPath, "platform-tools", "adb" + ext); + if (File.Exists (sdkAdb)) + return sdkAdb; + } -return ProcessUtils.FindExecutablesInPath ("adb").FirstOrDefault (); -} -} + return ProcessUtils.FindExecutablesInPath ("adb").FirstOrDefault (); + } + } -/// -/// Gets whether ADB is available. -/// -public bool IsAvailable => AdbPath is not null; + /// + /// Gets whether ADB is available. + /// + public bool IsAvailable => AdbPath is not null; -/// -/// Lists connected devices. -/// -/// Cancellation token. -/// A list of connected devices. -/// Thrown when ADB is not found. -public async Task> ListDevicesAsync (CancellationToken cancellationToken = default) -{ -if (!IsAvailable) -throw new InvalidOperationException ("ADB not found."); + /// + /// Lists connected devices. + /// + /// Cancellation token. + /// A list of connected devices. + /// Thrown when ADB is not found. + public async Task> ListDevicesAsync (CancellationToken cancellationToken = default) + { + if (!IsAvailable) + throw new InvalidOperationException ("ADB not found."); -var stdout = new StringWriter (); -var psi = new ProcessStartInfo { -FileName = AdbPath!, -Arguments = "devices -l", -UseShellExecute = false, -CreateNoWindow = true -}; -await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); + var stdout = new StringWriter (); + var psi = new ProcessStartInfo { + FileName = AdbPath!, + Arguments = "devices -l", + UseShellExecute = false, + CreateNoWindow = true + }; + await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); -var devices = new List (); -foreach (var line in stdout.ToString ().Split ('\n')) { -var trimmed = line.Trim (); -if (string.IsNullOrEmpty (trimmed) || trimmed.StartsWith ("List of", StringComparison.OrdinalIgnoreCase)) -continue; + var devices = new List (); + foreach (var line in stdout.ToString ().Split ('\n')) { + var trimmed = line.Trim (); + if (string.IsNullOrEmpty (trimmed) || trimmed.StartsWith ("List of", StringComparison.OrdinalIgnoreCase)) + continue; -var parts = trimmed.Split (new [] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); -if (parts.Length < 2) -continue; + var parts = trimmed.Split (new [] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 2) + continue; -var device = new AdbDeviceInfo { Serial = parts [0], State = parts [1] }; -foreach (Match match in DetailsRegex.Matches (trimmed)) { -switch (match.Groups [1].Value.ToLowerInvariant ()) { -case "model": device.Model = match.Groups [2].Value; break; -case "device": device.Device = match.Groups [2].Value; break; -} -} -devices.Add (device); -} + var device = new AdbDeviceInfo { Serial = parts [0], State = parts [1] }; + foreach (Match match in DetailsRegex.Matches (trimmed)) { + switch (match.Groups [1].Value.ToLowerInvariant ()) { + case "model": device.Model = match.Groups [2].Value; break; + case "device": device.Device = match.Groups [2].Value; break; + } + } + devices.Add (device); + } -return devices; -} + return devices; + } -/// -/// Stops a running emulator. -/// -/// The emulator serial number (e.g., "emulator-5554"). -/// Cancellation token. -/// Thrown when ADB is not found. -public async Task StopEmulatorAsync (string serial, CancellationToken cancellationToken = default) -{ -if (!IsAvailable) -throw new InvalidOperationException ("ADB not found."); + /// + /// Stops a running emulator. + /// + /// The emulator serial number (e.g., "emulator-5554"). + /// Cancellation token. + /// Thrown when ADB is not found. + public async Task StopEmulatorAsync (string serial, CancellationToken cancellationToken = default) + { + if (!IsAvailable) + throw new InvalidOperationException ("ADB not found."); -var psi = new ProcessStartInfo { -FileName = AdbPath!, -Arguments = $"-s \"{serial}\" emu kill", -UseShellExecute = false, -CreateNoWindow = true -}; -await ProcessUtils.StartProcess (psi, null, null, cancellationToken).ConfigureAwait (false); -} -} + var psi = new ProcessStartInfo { + FileName = AdbPath!, + Arguments = $"-s \"{serial}\" emu kill", + UseShellExecute = false, + CreateNoWindow = true + }; + await ProcessUtils.StartProcess (psi, null, null, cancellationToken).ConfigureAwait (false); + } + } } From 85815d03a740df146b596599771a2e402b30ace2 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Wed, 25 Feb 2026 17:09:11 +0000 Subject: [PATCH 4/7] Add ANDROID_HOME environment and WaitForDeviceAsync - ConfigureEnvironment() sets ANDROID_HOME on all ProcessStartInfo - Add WaitForDeviceAsync with optional serial filter, timeout, and TimeoutException - Uses CancellationTokenSource.CreateLinkedTokenSource for timeout handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Runners/AdbRunner.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index f55cbead..020f601e 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -52,6 +52,13 @@ public string? AdbPath { /// public bool IsAvailable => AdbPath is not null; + void ConfigureEnvironment (ProcessStartInfo psi) + { + var sdkPath = getSdkPath (); + if (!string.IsNullOrEmpty (sdkPath)) + psi.EnvironmentVariables ["ANDROID_HOME"] = sdkPath; + } + /// /// Lists connected devices. /// @@ -70,6 +77,7 @@ public async Task> ListDevicesAsync (CancellationToken cance UseShellExecute = false, CreateNoWindow = true }; + ConfigureEnvironment (psi); await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); var devices = new List (); @@ -95,6 +103,40 @@ public async Task> ListDevicesAsync (CancellationToken cance return devices; } + /// + /// Waits for a device to become available. + /// + /// Optional device serial number. When null, waits for any device. + /// Maximum time to wait. Defaults to 60 seconds. + /// Cancellation token. + /// Thrown when ADB is not found. + /// Thrown when no device becomes available within the timeout. + public async Task WaitForDeviceAsync (string? serial = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) + { + if (!IsAvailable) + throw new InvalidOperationException ("ADB not found."); + + var effectiveTimeout = timeout ?? TimeSpan.FromSeconds (60); + var args = string.IsNullOrEmpty (serial) ? "wait-for-device" : $"-s \"{serial}\" wait-for-device"; + + var psi = new ProcessStartInfo { + FileName = AdbPath!, + Arguments = args, + UseShellExecute = false, + CreateNoWindow = true + }; + ConfigureEnvironment (psi); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); + cts.CancelAfter (effectiveTimeout); + + try { + await ProcessUtils.StartProcess (psi, null, null, cts.Token).ConfigureAwait (false); + } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { + throw new TimeoutException ($"Timed out waiting for device after {effectiveTimeout.TotalSeconds}s."); + } + } + /// /// Stops a running emulator. /// @@ -112,7 +154,9 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati UseShellExecute = false, CreateNoWindow = true }; + ConfigureEnvironment (psi); await ProcessUtils.StartProcess (psi, null, null, cancellationToken).ConfigureAwait (false); } } } + From a7d34f8bf49501f92dbda7a358066ea8a1fef10f Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 26 Feb 2026 13:33:06 +0000 Subject: [PATCH 5/7] Trim verbose XML docs, make AdbPath internal Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Models/AdbDeviceInfo.cs | 22 -------------- .../Runners/AdbRunner.cs | 30 +------------------ 2 files changed, 1 insertion(+), 51 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs b/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs index 6374b6d2..a6be7d71 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs @@ -3,34 +3,12 @@ namespace Xamarin.Android.Tools { - /// - /// Information about a connected Android device. - /// public class AdbDeviceInfo { - /// - /// Gets or sets the device serial number. - /// public string Serial { get; set; } = string.Empty; - - /// - /// Gets or sets the device state (e.g., "device", "offline"). - /// public string? State { get; set; } - - /// - /// Gets or sets the device model name. - /// public string? Model { get; set; } - - /// - /// Gets or sets the device code name. - /// public string? Device { get; set; } - - /// - /// Gets whether this device is an emulator. - /// public bool IsEmulator => Serial.StartsWith ("emulator-"); } } diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index 020f601e..e19f0d55 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -20,20 +20,12 @@ public class AdbRunner readonly Func getSdkPath; static readonly Regex DetailsRegex = new Regex (@"(\w+):(\S+)", RegexOptions.Compiled); - /// - /// Creates a new . - /// - /// Function that returns the Android SDK path. - /// Thrown when is null. public AdbRunner (Func getSdkPath) { this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); } - /// - /// Gets the path to the adb executable, or null if not found. - /// - public string? AdbPath { + internal string? AdbPath { get { var sdkPath = getSdkPath (); if (!string.IsNullOrEmpty (sdkPath)) { @@ -59,12 +51,6 @@ void ConfigureEnvironment (ProcessStartInfo psi) psi.EnvironmentVariables ["ANDROID_HOME"] = sdkPath; } - /// - /// Lists connected devices. - /// - /// Cancellation token. - /// A list of connected devices. - /// Thrown when ADB is not found. public async Task> ListDevicesAsync (CancellationToken cancellationToken = default) { if (!IsAvailable) @@ -103,14 +89,6 @@ public async Task> ListDevicesAsync (CancellationToken cance return devices; } - /// - /// Waits for a device to become available. - /// - /// Optional device serial number. When null, waits for any device. - /// Maximum time to wait. Defaults to 60 seconds. - /// Cancellation token. - /// Thrown when ADB is not found. - /// Thrown when no device becomes available within the timeout. public async Task WaitForDeviceAsync (string? serial = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) { if (!IsAvailable) @@ -137,12 +115,6 @@ public async Task WaitForDeviceAsync (string? serial = null, TimeSpan? timeout = } } - /// - /// Stops a running emulator. - /// - /// The emulator serial number (e.g., "emulator-5554"). - /// Cancellation token. - /// Thrown when ADB is not found. public async Task StopEmulatorAsync (string serial, CancellationToken cancellationToken = default) { if (!IsAvailable) From d3046c13ce05ec3ebbd22673d411234caea4a859 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 26 Feb 2026 13:53:22 +0000 Subject: [PATCH 6/7] Refactor AdbRunner: extract CreateAdbProcess helper, add serial validation, reduce duplication - Extract RequireAdb() to centralize availability check - Extract CreateAdbProcess() to eliminate PSI creation duplication - Add whitespace validation on serial parameter in StopEmulatorAsync - Use params string[] args pattern for cleaner argument passing - Remove redundant XML doc comments Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Runners/AdbRunner.cs | 63 ++++++++----------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index e19f0d55..7a0ba18b 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -34,36 +34,38 @@ internal string? AdbPath { if (File.Exists (sdkAdb)) return sdkAdb; } - return ProcessUtils.FindExecutablesInPath ("adb").FirstOrDefault (); } } - /// - /// Gets whether ADB is available. - /// public bool IsAvailable => AdbPath is not null; - void ConfigureEnvironment (ProcessStartInfo psi) + string RequireAdb () + { + return AdbPath ?? throw new InvalidOperationException ("ADB not found."); + } + + ProcessStartInfo CreateAdbProcess (string adbPath, params string [] args) { + var psi = new ProcessStartInfo { + FileName = adbPath, + Arguments = string.Join (" ", args), + UseShellExecute = false, + CreateNoWindow = true + }; + var sdkPath = getSdkPath (); if (!string.IsNullOrEmpty (sdkPath)) psi.EnvironmentVariables ["ANDROID_HOME"] = sdkPath; + + return psi; } public async Task> ListDevicesAsync (CancellationToken cancellationToken = default) { - if (!IsAvailable) - throw new InvalidOperationException ("ADB not found."); - + var adb = RequireAdb (); var stdout = new StringWriter (); - var psi = new ProcessStartInfo { - FileName = AdbPath!, - Arguments = "devices -l", - UseShellExecute = false, - CreateNoWindow = true - }; - ConfigureEnvironment (psi); + var psi = CreateAdbProcess (adb, "devices", "-l"); await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); var devices = new List (); @@ -85,25 +87,19 @@ public async Task> ListDevicesAsync (CancellationToken cance } devices.Add (device); } - return devices; } public async Task WaitForDeviceAsync (string? serial = null, TimeSpan? timeout = null, CancellationToken cancellationToken = default) { - if (!IsAvailable) - throw new InvalidOperationException ("ADB not found."); - + var adb = RequireAdb (); var effectiveTimeout = timeout ?? TimeSpan.FromSeconds (60); - var args = string.IsNullOrEmpty (serial) ? "wait-for-device" : $"-s \"{serial}\" wait-for-device"; - var psi = new ProcessStartInfo { - FileName = AdbPath!, - Arguments = args, - UseShellExecute = false, - CreateNoWindow = true - }; - ConfigureEnvironment (psi); + var args = string.IsNullOrEmpty (serial) + ? new [] { "wait-for-device" } + : new [] { "-s", serial, "wait-for-device" }; + + var psi = CreateAdbProcess (adb, args); using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); cts.CancelAfter (effectiveTimeout); @@ -117,16 +113,11 @@ public async Task WaitForDeviceAsync (string? serial = null, TimeSpan? timeout = public async Task StopEmulatorAsync (string serial, CancellationToken cancellationToken = default) { - if (!IsAvailable) - throw new InvalidOperationException ("ADB not found."); + if (string.IsNullOrWhiteSpace (serial)) + throw new ArgumentException ("Serial must not be empty.", nameof (serial)); - var psi = new ProcessStartInfo { - FileName = AdbPath!, - Arguments = $"-s \"{serial}\" emu kill", - UseShellExecute = false, - CreateNoWindow = true - }; - ConfigureEnvironment (psi); + var adb = RequireAdb (); + var psi = CreateAdbProcess (adb, "-s", serial, "emu", "kill"); await ProcessUtils.StartProcess (psi, null, null, cancellationToken).ConfigureAwait (false); } } From d378294912f6ea4e30f3e8e4a986f8d4a48a88b2 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 26 Feb 2026 23:58:35 +0000 Subject: [PATCH 7/7] Make AdbPath public for consumer access Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index 7a0ba18b..4b598ac5 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -25,7 +25,7 @@ public AdbRunner (Func getSdkPath) this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath)); } - internal string? AdbPath { + public string? AdbPath { get { var sdkPath = getSdkPath (); if (!string.IsNullOrEmpty (sdkPath)) {