Skip to content

Commit 0d39966

Browse files
rmarinhoCopilot
andauthored
[xabt] Fix dotnet run failing with multiple AVDs present (#11001)
Fixes: #10998 When multiple AVD emulator images are defined but only one device is running, 'dotnet run' incorrectly considers all AVDs (including non-running ones) as available targets, causing a device selection prompt or error in non-interactive mode. Add FilterDevicesForSelection() to GetAvailableAndroidDevices that filters the merged device list: when any online devices exist, only online devices are returned (enabling auto-selection when a single device is running). When no online devices exist, all devices are returned including non-running emulators so the user can pick one to boot. ## Address review: avoid allocation when no filtering needed Two-pass approach in FilterDevicesForSelection: first determine if filtering is needed, then allocate only when mixed online/offline states exist. Returns the original list when all devices share the same status. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6b94e21 commit 0d39966

File tree

2 files changed

+132
-4
lines changed

2 files changed

+132
-4
lines changed

src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ namespace Xamarin.Android.Tasks;
1212

1313
/// <summary>
1414
/// MSBuild task that queries available Android devices and emulators using 'adb devices -l'
15-
/// and 'emulator -list-avds'. Merges the results to provide a complete list of available
16-
/// devices including emulators that are not currently running.
15+
/// and 'emulator -list-avds'. Merges the results and filters them for device selection:
16+
/// when any online devices exist, only online devices are returned (enabling auto-selection
17+
/// when a single device is running). When no online devices exist, all devices are returned
18+
/// including non-running emulators (allowing the user to pick one to boot).
1719
/// Returns a list of devices with metadata for device selection in dotnet run.
1820
///
1921
/// Parsing and merging logic is delegated to <see cref="AdbRunner"/> in Xamarin.Android.Tools.AndroidSdk.
@@ -74,14 +76,40 @@ public override bool RunTask ()
7476
// Merge using shared logic
7577
var mergedDevices = AdbRunner.MergeDevicesAndEmulators (adbDevices, availableEmulators, logger);
7678

79+
// Filter: if any online devices exist, return only those so auto-selection works
80+
// when a single device is running. If none are online, return all (including
81+
// non-running emulators) so the user can pick one to boot.
82+
var filteredDevices = FilterDevicesForSelection (mergedDevices);
83+
Log.LogDebugMessage ($"Filtered from {mergedDevices.Count} to {filteredDevices.Count} device(s) (online devices take priority)");
84+
7785
// Convert to ITaskItem array
78-
Devices = ConvertToTaskItems (mergedDevices);
86+
Devices = ConvertToTaskItems (filteredDevices);
7987

80-
Log.LogDebugMessage ($"Total {Devices.Length} Android device(s)/emulator(s) after merging");
88+
Log.LogDebugMessage ($"Total {Devices.Length} Android device(s)/emulator(s) after filtering");
8189

8290
return !Log.HasLoggedErrors;
8391
}
8492

93+
/// <summary>
94+
/// Filters the merged device list for device selection:
95+
/// - If any online devices exist, returns only those (so auto-selection works with a single running device)
96+
/// - If no online devices exist, returns all (including non-running emulators for user selection)
97+
/// </summary>
98+
internal static IReadOnlyList<AdbDeviceInfo> FilterDevicesForSelection (IReadOnlyList<AdbDeviceInfo> devices)
99+
{
100+
var onlineDevices = new List<AdbDeviceInfo> (devices.Count);
101+
foreach (var device in devices) {
102+
if (device.Status == AdbDeviceStatus.Online) {
103+
onlineDevices.Add (device);
104+
}
105+
}
106+
107+
if (onlineDevices.Count == 0)
108+
return devices;
109+
110+
return onlineDevices;
111+
}
112+
85113
/// <summary>
86114
/// Converts AdbDeviceInfo list to ITaskItem array for MSBuild output.
87115
/// </summary>

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetAvailableAndroidDevicesTests.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,106 @@ public void MergeDevicesAndEmulators_NonRunningEmulatorHasFormattedDescription (
706706
Assert.AreEqual ("Pixel 7 Pro API 35 (Not Running)", result [0].GetMetadata ("Description"), "Description should be formatted with (Not Running) suffix");
707707
}
708708

709+
[Test]
710+
public void FilterDevicesForSelection_SingleOnlineDevice_WithNonRunningEmulators_ReturnsOnlyOnline ()
711+
{
712+
var devices = new List<AdbDeviceInfo> {
713+
CreateDeviceInfo ("emulator-5554", "Pixel 8 API 36 X64", AdbDeviceType.Emulator, AdbDeviceStatus.Online),
714+
CreateDeviceInfo ("pixel_3_xl_api_22_arm", "Pixel 3 XL API 22 Arm (Not Running)", AdbDeviceType.Emulator, AdbDeviceStatus.NotRunning),
715+
CreateDeviceInfo ("pixel_3_xl_api_22_x86", "Pixel 3 XL API 22 X86 (Not Running)", AdbDeviceType.Emulator, AdbDeviceStatus.NotRunning),
716+
CreateDeviceInfo ("pixel_4_api_30_amd64", "Pixel 4 API 30 Amd64 (Not Running)", AdbDeviceType.Emulator, AdbDeviceStatus.NotRunning),
717+
};
718+
719+
var filtered = GetAvailableAndroidDevices.FilterDevicesForSelection (devices);
720+
721+
Assert.AreEqual (1, filtered.Count, "Should return only the single running device");
722+
Assert.AreEqual ("emulator-5554", filtered [0].Serial);
723+
Assert.AreEqual (AdbDeviceStatus.Online, filtered [0].Status);
724+
}
725+
726+
[Test]
727+
public void FilterDevicesForSelection_SinglePhysicalDevice_WithNonRunningEmulators_ReturnsOnlyOnline ()
728+
{
729+
var devices = new List<AdbDeviceInfo> {
730+
CreateDeviceInfo ("0A041FDD400327", "Pixel 5", AdbDeviceType.Device, AdbDeviceStatus.Online),
731+
CreateDeviceInfo ("pixel_7_api_35", "Pixel 7 API 35 (Not Running)", AdbDeviceType.Emulator, AdbDeviceStatus.NotRunning),
732+
CreateDeviceInfo ("pixel_9_api_36", "Pixel 9 API 36 (Not Running)", AdbDeviceType.Emulator, AdbDeviceStatus.NotRunning),
733+
};
734+
735+
var filtered = GetAvailableAndroidDevices.FilterDevicesForSelection (devices);
736+
737+
Assert.AreEqual (1, filtered.Count, "Should return only the single running physical device");
738+
Assert.AreEqual ("0A041FDD400327", filtered [0].Serial);
739+
}
740+
741+
[Test]
742+
public void FilterDevicesForSelection_MultipleOnlineDevices_ReturnsOnlyOnline ()
743+
{
744+
var devices = new List<AdbDeviceInfo> {
745+
CreateDeviceInfo ("0A041FDD400327", "Pixel 5", AdbDeviceType.Device, AdbDeviceStatus.Online),
746+
CreateDeviceInfo ("emulator-5554", "Pixel 8 API 36", AdbDeviceType.Emulator, AdbDeviceStatus.Online),
747+
CreateDeviceInfo ("pixel_7_api_35", "Pixel 7 API 35 (Not Running)", AdbDeviceType.Emulator, AdbDeviceStatus.NotRunning),
748+
};
749+
750+
var filtered = GetAvailableAndroidDevices.FilterDevicesForSelection (devices);
751+
752+
Assert.AreEqual (2, filtered.Count, "Should return only the 2 online devices");
753+
Assert.IsTrue (filtered.All (d => d.Status == AdbDeviceStatus.Online), "All returned devices should be online");
754+
}
755+
756+
[Test]
757+
public void FilterDevicesForSelection_NoOnlineDevices_ReturnsAll ()
758+
{
759+
var devices = new List<AdbDeviceInfo> {
760+
CreateDeviceInfo ("pixel_7_api_35", "Pixel 7 API 35 (Not Running)", AdbDeviceType.Emulator, AdbDeviceStatus.NotRunning),
761+
CreateDeviceInfo ("pixel_9_api_36", "Pixel 9 API 36 (Not Running)", AdbDeviceType.Emulator, AdbDeviceStatus.NotRunning),
762+
};
763+
764+
var filtered = GetAvailableAndroidDevices.FilterDevicesForSelection (devices);
765+
766+
Assert.AreEqual (2, filtered.Count, "Should return all devices when none are online");
767+
}
768+
769+
[Test]
770+
public void FilterDevicesForSelection_EmptyList_ReturnsEmpty ()
771+
{
772+
var devices = new List<AdbDeviceInfo> ();
773+
774+
var filtered = GetAvailableAndroidDevices.FilterDevicesForSelection (devices);
775+
776+
Assert.AreEqual (0, filtered.Count, "Should return empty list");
777+
}
778+
779+
[Test]
780+
public void FilterDevicesForSelection_OnlineDeviceWithOfflineDevice_ReturnsOnlyOnline ()
781+
{
782+
var devices = new List<AdbDeviceInfo> {
783+
CreateDeviceInfo ("0A041FDD400327", "Pixel 5", AdbDeviceType.Device, AdbDeviceStatus.Online),
784+
CreateDeviceInfo ("DEADBEEF123456", "Pixel 3", AdbDeviceType.Device, AdbDeviceStatus.Offline),
785+
CreateDeviceInfo ("pixel_7_api_35", "Pixel 7 API 35 (Not Running)", AdbDeviceType.Emulator, AdbDeviceStatus.NotRunning),
786+
};
787+
788+
var filtered = GetAvailableAndroidDevices.FilterDevicesForSelection (devices);
789+
790+
Assert.AreEqual (1, filtered.Count, "Should return only the online device, filtering offline and non-running");
791+
Assert.AreEqual ("0A041FDD400327", filtered [0].Serial);
792+
}
793+
794+
[Test]
795+
public void FilterDevicesForSelection_OnlineDeviceWithUnauthorizedDevice_ReturnsOnlyOnline ()
796+
{
797+
var devices = new List<AdbDeviceInfo> {
798+
CreateDeviceInfo ("0A041FDD400327", "Pixel 5", AdbDeviceType.Device, AdbDeviceStatus.Online),
799+
CreateDeviceInfo ("UNAUTHORIZED001", "Unknown", AdbDeviceType.Device, AdbDeviceStatus.Unauthorized),
800+
CreateDeviceInfo ("pixel_7_api_35", "Pixel 7 API 35 (Not Running)", AdbDeviceType.Emulator, AdbDeviceStatus.NotRunning),
801+
};
802+
803+
var filtered = GetAvailableAndroidDevices.FilterDevicesForSelection (devices);
804+
805+
Assert.AreEqual (1, filtered.Count, "Should return only the online device, filtering unauthorized and non-running");
806+
Assert.AreEqual ("0A041FDD400327", filtered [0].Serial);
807+
}
808+
709809
/// <summary>
710810
/// Helper method to create an AdbDeviceInfo for testing
711811
/// </summary>

0 commit comments

Comments
 (0)