From 02bb2952e0ff22c96e13da0181889d07d07dbb39 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Wed, 8 Apr 2026 17:35:35 +0100 Subject: [PATCH] Add AvdManagerRunner.ListDeviceProfilesAsync() for device profile enumeration Add ListDeviceProfilesAsync() and ParseDeviceListOutput() to AvdManagerRunner. Runs 'avdmanager list device' and parses the output into AvdDeviceProfile records (Id, Name, Oem, Tag). Includes AvdDeviceProfile record model, 6 unit tests, and PublicAPI entries for both net10.0 and netstandard2.0. Closes #321 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Models/AvdDeviceProfile.cs | 9 ++ .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 11 +++ .../netstandard2.0/PublicAPI.Unshipped.txt | 11 +++ .../Runners/AvdManagerRunner.cs | 57 ++++++++++++ .../AvdManagerRunnerTests.cs | 93 +++++++++++++++++++ 5 files changed, 181 insertions(+) create mode 100644 src/Xamarin.Android.Tools.AndroidSdk/Models/AvdDeviceProfile.cs diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdDeviceProfile.cs b/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdDeviceProfile.cs new file mode 100644 index 00000000..35f7e1fa --- /dev/null +++ b/src/Xamarin.Android.Tools.AndroidSdk/Models/AvdDeviceProfile.cs @@ -0,0 +1,9 @@ +// 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; + +/// +/// Represents a hardware device profile (e.g., "pixel_7", "Nexus 5X") from avdmanager list device. +/// +public record AvdDeviceProfile (string Id, string Name, string? Oem = null, string? Tag = null); diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 61fc0e0b..9eed5f39 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -199,3 +199,14 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.AvdDeviceProfile +Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id, string! Name, string? Oem = null, string? Tag = null) -> void +Xamarin.Android.Tools.AvdDeviceProfile.Id.get -> string! +Xamarin.Android.Tools.AvdDeviceProfile.Id.init -> void +Xamarin.Android.Tools.AvdDeviceProfile.Name.get -> string! +Xamarin.Android.Tools.AvdDeviceProfile.Name.init -> void +Xamarin.Android.Tools.AvdDeviceProfile.Oem.get -> string? +Xamarin.Android.Tools.AvdDeviceProfile.Oem.init -> void +Xamarin.Android.Tools.AvdDeviceProfile.Tag.get -> string? +Xamarin.Android.Tools.AvdDeviceProfile.Tag.init -> void diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 61fc0e0b..9eed5f39 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -199,3 +199,14 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.RemoveReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Xamarin.Android.Tools.AdbRunner.ReversePortAsync(string! serial, Xamarin.Android.Tools.AdbPortSpec! remote, Xamarin.Android.Tools.AdbPortSpec! local, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +Xamarin.Android.Tools.AvdDeviceProfile +Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id, string! Name, string? Oem = null, string? Tag = null) -> void +Xamarin.Android.Tools.AvdDeviceProfile.Id.get -> string! +Xamarin.Android.Tools.AvdDeviceProfile.Id.init -> void +Xamarin.Android.Tools.AvdDeviceProfile.Name.get -> string! +Xamarin.Android.Tools.AvdDeviceProfile.Name.init -> void +Xamarin.Android.Tools.AvdDeviceProfile.Oem.get -> string? +Xamarin.Android.Tools.AvdDeviceProfile.Oem.init -> void +Xamarin.Android.Tools.AvdDeviceProfile.Tag.get -> string? +Xamarin.Android.Tools.AvdDeviceProfile.Tag.init -> void diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs index b233ae38..2d8df305 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs @@ -122,6 +122,63 @@ public async Task DeleteAvdAsync (string name, CancellationToken cancellationTok ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager delete avd --name {name}", stderr); } + /// + /// Lists available device profiles (hardware definitions) using avdmanager list device. + /// + public async Task> ListDeviceProfilesAsync (CancellationToken cancellationToken = default) + { + using var stdout = new StringWriter (); + using var stderr = new StringWriter (); + var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "list", "device"); + logger.Invoke (TraceLevel.Verbose, "Running: avdmanager list device"); + var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); + + ProcessUtils.ThrowIfFailed (exitCode, "avdmanager list device", stderr, stdout); + + return ParseDeviceListOutput (stdout.ToString ()); + } + + internal static IReadOnlyList ParseDeviceListOutput (string output) + { + var profiles = new List (); + string? currentId = null, currentName = null, currentOem = null, currentTag = null; + + foreach (var line in output.Split ('\n')) { + var trimmed = line.Trim (); + if (trimmed.StartsWith ("id:", StringComparison.OrdinalIgnoreCase)) { + if (currentId is not null) + profiles.Add (new AvdDeviceProfile (currentId, currentName ?? currentId, currentOem, currentTag)); + // Parse: id: 0 or "automotive_1024p_landscape" + currentOem = currentTag = null; + currentName = null; + var orIndex = trimmed.IndexOf (" or ", StringComparison.Ordinal); + if (orIndex >= 0) { + var rawId = trimmed.Substring (orIndex + 4).Trim ().Trim ('"'); + currentId = rawId; + } else { + currentId = trimmed.Substring (3).Trim ().Trim ('"'); + } + } + else if (trimmed.StartsWith ("Name:", StringComparison.OrdinalIgnoreCase)) + currentName = trimmed.Substring (5).Trim (); + else if (trimmed.StartsWith ("OEM", StringComparison.OrdinalIgnoreCase)) { + var colonIndex = trimmed.IndexOf (':'); + if (colonIndex >= 0) + currentOem = trimmed.Substring (colonIndex + 1).Trim (); + } + else if (trimmed.StartsWith ("Tag", StringComparison.OrdinalIgnoreCase)) { + var colonIndex = trimmed.IndexOf (':'); + if (colonIndex >= 0) + currentTag = trimmed.Substring (colonIndex + 1).Trim (); + } + } + + if (currentId is not null) + profiles.Add (new AvdDeviceProfile (currentId, currentName ?? currentId, currentOem, currentTag)); + + return profiles; + } + internal static IReadOnlyList ParseAvdListOutput (string output) { var avds = new List (); diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs index 6ae30de8..c3e0cac9 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs @@ -293,4 +293,97 @@ public void FindCmdlineTool_PrefersStableOverPreRelease () Directory.Delete (tempDir, true); } } + + // --- ParseDeviceListOutput tests --- + + [Test] + public void ParseDeviceListOutput_MultipleProfiles () + { + var output = + "Available devices definitions:\n" + + "id: 0 or \"automotive_1024p_landscape\"\n" + + " Name: Automotive (1024p landscape)\n" + + " OEM : Google\n" + + " Tag : android-automotive-playstore\n" + + "---------\n" + + "id: 1 or \"pixel_7\"\n" + + " Name: Pixel 7\n" + + " OEM : Google\n" + + "---------\n" + + "id: 2 or \"Nexus 5X\"\n" + + " Name: Nexus 5X\n" + + " OEM : Google\n"; + + var profiles = AvdManagerRunner.ParseDeviceListOutput (output); + + Assert.AreEqual (3, profiles.Count); + + Assert.AreEqual ("automotive_1024p_landscape", profiles [0].Id); + Assert.AreEqual ("Automotive (1024p landscape)", profiles [0].Name); + Assert.AreEqual ("Google", profiles [0].Oem); + Assert.AreEqual ("android-automotive-playstore", profiles [0].Tag); + + Assert.AreEqual ("pixel_7", profiles [1].Id); + Assert.AreEqual ("Pixel 7", profiles [1].Name); + Assert.AreEqual ("Google", profiles [1].Oem); + Assert.IsNull (profiles [1].Tag); + + Assert.AreEqual ("Nexus 5X", profiles [2].Id); + Assert.AreEqual ("Nexus 5X", profiles [2].Name); + } + + [Test] + public void ParseDeviceListOutput_EmptyOutput () + { + var profiles = AvdManagerRunner.ParseDeviceListOutput (""); + Assert.AreEqual (0, profiles.Count); + } + + [Test] + public void ParseDeviceListOutput_WindowsNewlines () + { + var output = + "Available devices definitions:\r\n" + + "id: 0 or \"pixel_fold\"\r\n" + + " Name: Pixel Fold\r\n" + + " OEM : Google\r\n" + + " Tag : default\r\n"; + + var profiles = AvdManagerRunner.ParseDeviceListOutput (output); + + Assert.AreEqual (1, profiles.Count); + Assert.AreEqual ("pixel_fold", profiles [0].Id); + Assert.AreEqual ("Pixel Fold", profiles [0].Name); + Assert.AreEqual ("Google", profiles [0].Oem); + Assert.AreEqual ("default", profiles [0].Tag); + } + + [Test] + public void ParseDeviceListOutput_NoNameFallsBackToId () + { + var output = + "id: 0 or \"custom_device\"\n" + + " OEM : SomeOem\n"; + + var profiles = AvdManagerRunner.ParseDeviceListOutput (output); + + Assert.AreEqual (1, profiles.Count); + Assert.AreEqual ("custom_device", profiles [0].Id); + Assert.AreEqual ("custom_device", profiles [0].Name); + } + + [Test] + public void ParseDeviceListOutput_HeaderOnly () + { + var output = "Available devices definitions:\n"; + var profiles = AvdManagerRunner.ParseDeviceListOutput (output); + Assert.AreEqual (0, profiles.Count); + } + + [Test] + public void ParseDeviceListOutput_ReturnsIReadOnlyList () + { + var profiles = AvdManagerRunner.ParseDeviceListOutput (""); + Assert.IsInstanceOf> (profiles); + } }