Skip to content

Commit 02bb295

Browse files
rmarinhoCopilot
andcommitted
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>
1 parent a428178 commit 02bb295

5 files changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Xamarin.Android.Tools;
5+
6+
/// <summary>
7+
/// Represents a hardware device profile (e.g., "pixel_7", "Nexus 5X") from <c>avdmanager list device</c>.
8+
/// </summary>
9+
public record AvdDeviceProfile (string Id, string Name, string? Oem = null, string? Tag = null);

src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,14 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
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!
201201
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!
202+
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203+
Xamarin.Android.Tools.AvdDeviceProfile
204+
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id, string! Name, string? Oem = null, string? Tag = null) -> void
205+
Xamarin.Android.Tools.AvdDeviceProfile.Id.get -> string!
206+
Xamarin.Android.Tools.AvdDeviceProfile.Id.init -> void
207+
Xamarin.Android.Tools.AvdDeviceProfile.Name.get -> string!
208+
Xamarin.Android.Tools.AvdDeviceProfile.Name.init -> void
209+
Xamarin.Android.Tools.AvdDeviceProfile.Oem.get -> string?
210+
Xamarin.Android.Tools.AvdDeviceProfile.Oem.init -> void
211+
Xamarin.Android.Tools.AvdDeviceProfile.Tag.get -> string?
212+
Xamarin.Android.Tools.AvdDeviceProfile.Tag.init -> void

src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,14 @@ virtual Xamarin.Android.Tools.AdbRunner.ListReversePortsAsync(string! serial, Sy
199199
virtual Xamarin.Android.Tools.AdbRunner.RemoveAllReversePortsAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
200200
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!
201201
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!
202+
Xamarin.Android.Tools.AvdManagerRunner.ListDeviceProfilesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AvdDeviceProfile!>!>!
203+
Xamarin.Android.Tools.AvdDeviceProfile
204+
Xamarin.Android.Tools.AvdDeviceProfile.AvdDeviceProfile(string! Id, string! Name, string? Oem = null, string? Tag = null) -> void
205+
Xamarin.Android.Tools.AvdDeviceProfile.Id.get -> string!
206+
Xamarin.Android.Tools.AvdDeviceProfile.Id.init -> void
207+
Xamarin.Android.Tools.AvdDeviceProfile.Name.get -> string!
208+
Xamarin.Android.Tools.AvdDeviceProfile.Name.init -> void
209+
Xamarin.Android.Tools.AvdDeviceProfile.Oem.get -> string?
210+
Xamarin.Android.Tools.AvdDeviceProfile.Oem.init -> void
211+
Xamarin.Android.Tools.AvdDeviceProfile.Tag.get -> string?
212+
Xamarin.Android.Tools.AvdDeviceProfile.Tag.init -> void

src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,63 @@ public async Task DeleteAvdAsync (string name, CancellationToken cancellationTok
122122
ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager delete avd --name {name}", stderr);
123123
}
124124

125+
/// <summary>
126+
/// Lists available device profiles (hardware definitions) using <c>avdmanager list device</c>.
127+
/// </summary>
128+
public async Task<IReadOnlyList<AvdDeviceProfile>> ListDeviceProfilesAsync (CancellationToken cancellationToken = default)
129+
{
130+
using var stdout = new StringWriter ();
131+
using var stderr = new StringWriter ();
132+
var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "list", "device");
133+
logger.Invoke (TraceLevel.Verbose, "Running: avdmanager list device");
134+
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false);
135+
136+
ProcessUtils.ThrowIfFailed (exitCode, "avdmanager list device", stderr, stdout);
137+
138+
return ParseDeviceListOutput (stdout.ToString ());
139+
}
140+
141+
internal static IReadOnlyList<AvdDeviceProfile> ParseDeviceListOutput (string output)
142+
{
143+
var profiles = new List<AvdDeviceProfile> ();
144+
string? currentId = null, currentName = null, currentOem = null, currentTag = null;
145+
146+
foreach (var line in output.Split ('\n')) {
147+
var trimmed = line.Trim ();
148+
if (trimmed.StartsWith ("id:", StringComparison.OrdinalIgnoreCase)) {
149+
if (currentId is not null)
150+
profiles.Add (new AvdDeviceProfile (currentId, currentName ?? currentId, currentOem, currentTag));
151+
// Parse: id: 0 or "automotive_1024p_landscape"
152+
currentOem = currentTag = null;
153+
currentName = null;
154+
var orIndex = trimmed.IndexOf (" or ", StringComparison.Ordinal);
155+
if (orIndex >= 0) {
156+
var rawId = trimmed.Substring (orIndex + 4).Trim ().Trim ('"');
157+
currentId = rawId;
158+
} else {
159+
currentId = trimmed.Substring (3).Trim ().Trim ('"');
160+
}
161+
}
162+
else if (trimmed.StartsWith ("Name:", StringComparison.OrdinalIgnoreCase))
163+
currentName = trimmed.Substring (5).Trim ();
164+
else if (trimmed.StartsWith ("OEM", StringComparison.OrdinalIgnoreCase)) {
165+
var colonIndex = trimmed.IndexOf (':');
166+
if (colonIndex >= 0)
167+
currentOem = trimmed.Substring (colonIndex + 1).Trim ();
168+
}
169+
else if (trimmed.StartsWith ("Tag", StringComparison.OrdinalIgnoreCase)) {
170+
var colonIndex = trimmed.IndexOf (':');
171+
if (colonIndex >= 0)
172+
currentTag = trimmed.Substring (colonIndex + 1).Trim ();
173+
}
174+
}
175+
176+
if (currentId is not null)
177+
profiles.Add (new AvdDeviceProfile (currentId, currentName ?? currentId, currentOem, currentTag));
178+
179+
return profiles;
180+
}
181+
125182
internal static IReadOnlyList<AvdInfo> ParseAvdListOutput (string output)
126183
{
127184
var avds = new List<AvdInfo> ();

tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,4 +293,97 @@ public void FindCmdlineTool_PrefersStableOverPreRelease ()
293293
Directory.Delete (tempDir, true);
294294
}
295295
}
296+
297+
// --- ParseDeviceListOutput tests ---
298+
299+
[Test]
300+
public void ParseDeviceListOutput_MultipleProfiles ()
301+
{
302+
var output =
303+
"Available devices definitions:\n" +
304+
"id: 0 or \"automotive_1024p_landscape\"\n" +
305+
" Name: Automotive (1024p landscape)\n" +
306+
" OEM : Google\n" +
307+
" Tag : android-automotive-playstore\n" +
308+
"---------\n" +
309+
"id: 1 or \"pixel_7\"\n" +
310+
" Name: Pixel 7\n" +
311+
" OEM : Google\n" +
312+
"---------\n" +
313+
"id: 2 or \"Nexus 5X\"\n" +
314+
" Name: Nexus 5X\n" +
315+
" OEM : Google\n";
316+
317+
var profiles = AvdManagerRunner.ParseDeviceListOutput (output);
318+
319+
Assert.AreEqual (3, profiles.Count);
320+
321+
Assert.AreEqual ("automotive_1024p_landscape", profiles [0].Id);
322+
Assert.AreEqual ("Automotive (1024p landscape)", profiles [0].Name);
323+
Assert.AreEqual ("Google", profiles [0].Oem);
324+
Assert.AreEqual ("android-automotive-playstore", profiles [0].Tag);
325+
326+
Assert.AreEqual ("pixel_7", profiles [1].Id);
327+
Assert.AreEqual ("Pixel 7", profiles [1].Name);
328+
Assert.AreEqual ("Google", profiles [1].Oem);
329+
Assert.IsNull (profiles [1].Tag);
330+
331+
Assert.AreEqual ("Nexus 5X", profiles [2].Id);
332+
Assert.AreEqual ("Nexus 5X", profiles [2].Name);
333+
}
334+
335+
[Test]
336+
public void ParseDeviceListOutput_EmptyOutput ()
337+
{
338+
var profiles = AvdManagerRunner.ParseDeviceListOutput ("");
339+
Assert.AreEqual (0, profiles.Count);
340+
}
341+
342+
[Test]
343+
public void ParseDeviceListOutput_WindowsNewlines ()
344+
{
345+
var output =
346+
"Available devices definitions:\r\n" +
347+
"id: 0 or \"pixel_fold\"\r\n" +
348+
" Name: Pixel Fold\r\n" +
349+
" OEM : Google\r\n" +
350+
" Tag : default\r\n";
351+
352+
var profiles = AvdManagerRunner.ParseDeviceListOutput (output);
353+
354+
Assert.AreEqual (1, profiles.Count);
355+
Assert.AreEqual ("pixel_fold", profiles [0].Id);
356+
Assert.AreEqual ("Pixel Fold", profiles [0].Name);
357+
Assert.AreEqual ("Google", profiles [0].Oem);
358+
Assert.AreEqual ("default", profiles [0].Tag);
359+
}
360+
361+
[Test]
362+
public void ParseDeviceListOutput_NoNameFallsBackToId ()
363+
{
364+
var output =
365+
"id: 0 or \"custom_device\"\n" +
366+
" OEM : SomeOem\n";
367+
368+
var profiles = AvdManagerRunner.ParseDeviceListOutput (output);
369+
370+
Assert.AreEqual (1, profiles.Count);
371+
Assert.AreEqual ("custom_device", profiles [0].Id);
372+
Assert.AreEqual ("custom_device", profiles [0].Name);
373+
}
374+
375+
[Test]
376+
public void ParseDeviceListOutput_HeaderOnly ()
377+
{
378+
var output = "Available devices definitions:\n";
379+
var profiles = AvdManagerRunner.ParseDeviceListOutput (output);
380+
Assert.AreEqual (0, profiles.Count);
381+
}
382+
383+
[Test]
384+
public void ParseDeviceListOutput_ReturnsIReadOnlyList ()
385+
{
386+
var profiles = AvdManagerRunner.ParseDeviceListOutput ("");
387+
Assert.IsInstanceOf<IReadOnlyList<AvdDeviceProfile>> (profiles);
388+
}
296389
}

0 commit comments

Comments
 (0)