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