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..f415a7b6 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,4 @@ 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! +static Xamarin.Android.Tools.AvdManagerRunner.ListAvdSkins(string! sdkPath, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IReadOnlyList! 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..f415a7b6 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,4 @@ 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! +static Xamarin.Android.Tools.AvdManagerRunner.ListAvdSkins(string! sdkPath, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IReadOnlyList! diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs index b233ae38..199e846d 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs @@ -122,6 +122,66 @@ public async Task DeleteAvdAsync (string name, CancellationToken cancellationTok ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager delete avd --name {name}", stderr); } + /// + /// Lists available AVD skins by scanning the SDK skins/ directory + /// and system-images/.../skins/ directories. + /// + /// Root path of the Android SDK. + /// Cancellation token checked during directory enumeration. + /// Sorted list of unique skin directory names. + public static IReadOnlyList ListAvdSkins (string sdkPath, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace (sdkPath)) + throw new ArgumentException ("SDK path must not be empty.", nameof (sdkPath)); + + return EnumerateSkins (sdkPath, cancellationToken); + } + + internal static IReadOnlyList EnumerateSkins (string sdkPath, CancellationToken cancellationToken = default) + { + var skins = new SortedSet (StringComparer.OrdinalIgnoreCase); + + // Standalone skins: /skins// + var skinsDir = Path.Combine (sdkPath, "skins"); + AddSkinDirectories (skins, skinsDir); + + // System image skins: /system-images////skins// + var systemImagesDir = Path.Combine (sdkPath, "system-images"); + if (Directory.Exists (systemImagesDir)) { + try { + foreach (var apiDir in Directory.EnumerateDirectories (systemImagesDir)) { + cancellationToken.ThrowIfCancellationRequested (); + try { + foreach (var tagDir in Directory.EnumerateDirectories (apiDir)) { + foreach (var abiDir in Directory.EnumerateDirectories (tagDir)) { + var imgSkinsDir = Path.Combine (abiDir, "skins"); + AddSkinDirectories (skins, imgSkinsDir); + } + } + } catch (IOException) { + } catch (UnauthorizedAccessException) { + } + } + } catch (IOException) { + } catch (UnauthorizedAccessException) { + } + } + + return skins.ToList (); + } + + static void AddSkinDirectories (SortedSet skins, string directory) + { + if (!Directory.Exists (directory)) + return; + try { + foreach (var skinDir in Directory.EnumerateDirectories (directory)) + skins.Add (Path.GetFileName (skinDir)); + } catch (IOException) { + } catch (UnauthorizedAccessException) { + } + } + 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..9ba211e5 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AvdManagerRunnerTests.cs @@ -293,4 +293,80 @@ public void FindCmdlineTool_PrefersStableOverPreRelease () Directory.Delete (tempDir, true); } } + + // --- EnumerateSkins tests --- + + [Test] + public void EnumerateSkins_FindsStandaloneSkins () + { + var sdkDir = Path.Combine (Path.GetTempPath (), $"sdk-skin-test-{Path.GetRandomFileName ()}"); + try { + Directory.CreateDirectory (Path.Combine (sdkDir, "skins", "pixel_7_pro")); + Directory.CreateDirectory (Path.Combine (sdkDir, "skins", "nexus_5x")); + + var skins = AvdManagerRunner.EnumerateSkins (sdkDir); + + Assert.AreEqual (2, skins.Count); + Assert.That (skins, Contains.Item ("nexus_5x")); + Assert.That (skins, Contains.Item ("pixel_7_pro")); + } finally { + Directory.Delete (sdkDir, true); + } + } + + [Test] + public void EnumerateSkins_FindsSystemImageSkins () + { + var sdkDir = Path.Combine (Path.GetTempPath (), $"sdk-skin-test-{Path.GetRandomFileName ()}"); + try { + var imgSkinsDir = Path.Combine (sdkDir, "system-images", "android-35", "google_apis", "x86_64", "skins"); + Directory.CreateDirectory (Path.Combine (imgSkinsDir, "pixel_tablet")); + + var skins = AvdManagerRunner.EnumerateSkins (sdkDir); + + Assert.AreEqual (1, skins.Count); + Assert.AreEqual ("pixel_tablet", skins [0]); + } finally { + Directory.Delete (sdkDir, true); + } + } + + [Test] + public void EnumerateSkins_DeduplicatesAndSorts () + { + var sdkDir = Path.Combine (Path.GetTempPath (), $"sdk-skin-test-{Path.GetRandomFileName ()}"); + try { + Directory.CreateDirectory (Path.Combine (sdkDir, "skins", "pixel_7")); + var imgSkinsDir = Path.Combine (sdkDir, "system-images", "android-35", "google_apis", "x86_64", "skins"); + Directory.CreateDirectory (Path.Combine (imgSkinsDir, "pixel_7")); + Directory.CreateDirectory (Path.Combine (imgSkinsDir, "auto_skin")); + + var skins = AvdManagerRunner.EnumerateSkins (sdkDir); + + Assert.AreEqual (2, skins.Count); + Assert.AreEqual ("auto_skin", skins [0]); + Assert.AreEqual ("pixel_7", skins [1]); + } finally { + Directory.Delete (sdkDir, true); + } + } + + [Test] + public void EnumerateSkins_MissingSdkDir_ReturnsEmpty () + { + var skins = AvdManagerRunner.EnumerateSkins (Path.Combine (Path.GetTempPath (), "nonexistent-sdk-dir")); + Assert.AreEqual (0, skins.Count); + } + + [Test] + public void ListAvdSkins_NullSdkPath_ThrowsArgumentException () + { + Assert.Throws (() => AvdManagerRunner.ListAvdSkins (null!)); + } + + [Test] + public void ListAvdSkins_EmptySdkPath_ThrowsArgumentException () + { + Assert.Throws (() => AvdManagerRunner.ListAvdSkins ("")); + } }