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);
+ }
}