Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<string!>!
Original file line number Diff line number Diff line change
Expand Up @@ -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<string!>!
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,66 @@ public async Task DeleteAvdAsync (string name, CancellationToken cancellationTok
ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager delete avd --name {name}", stderr);
}

/// <summary>
/// Lists available AVD skins by scanning the SDK <c>skins/</c> directory
/// and <c>system-images/.../skins/</c> directories.
/// </summary>
/// <param name="sdkPath">Root path of the Android SDK.</param>
/// <param name="cancellationToken">Cancellation token checked during directory enumeration.</param>
/// <returns>Sorted list of unique skin directory names.</returns>
public static IReadOnlyList<string> ListAvdSkins (string sdkPath, CancellationToken cancellationToken = default)
{
Comment on lines +125 to +133
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says the new API is ListAvdSkinsAsync(sdkPath) but the implementation adds a synchronous ListAvdSkins(...) method. Either update the PR description/issues to match the shipped API, or rename/shape the API to match the intended async contract so consumers aren’t surprised.

Copilot uses AI. Check for mistakes.
if (string.IsNullOrWhiteSpace (sdkPath))
throw new ArgumentException ("SDK path must not be empty.", nameof (sdkPath));

return EnumerateSkins (sdkPath, cancellationToken);
}

internal static IReadOnlyList<string> EnumerateSkins (string sdkPath, CancellationToken cancellationToken = default)
{
var skins = new SortedSet<string> (StringComparer.OrdinalIgnoreCase);

// Standalone skins: <sdk>/skins/<skinName>/
var skinsDir = Path.Combine (sdkPath, "skins");
AddSkinDirectories (skins, skinsDir);

// System image skins: <sdk>/system-images/<api>/<tag>/<abi>/skins/<skinName>/
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);
Comment on lines +148 to +158
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancellationToken is documented as being checked during directory enumeration, but it’s only checked once per API directory and isn’t checked while iterating tag/ABI/skin directories (and AddSkinDirectories doesn’t observe it). Please either tighten cancellation handling (check in inner loops and/or pass the token down) or update the docs/parameter if cancellation isn’t intended to be honored for the full scan.

Copilot uses AI. Check for mistakes.
}
}
} catch (IOException) {
} catch (UnauthorizedAccessException) {
}
}
} catch (IOException) {
} catch (UnauthorizedAccessException) {
}
}

return skins.ToList ();
}

static void AddSkinDirectories (SortedSet<string> 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<AvdInfo> ParseAvdListOutput (string output)
{
var avds = new List<AvdInfo> ();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentException> (() => AvdManagerRunner.ListAvdSkins (null!));
}

[Test]
public void ListAvdSkins_EmptySdkPath_ThrowsArgumentException ()
{
Assert.Throws<ArgumentException> (() => AvdManagerRunner.ListAvdSkins (""));
}
}