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
263 changes: 48 additions & 215 deletions src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.Android.Build.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Xamarin.Android.Tools;

namespace Xamarin.Android.Tasks;

Expand All @@ -16,20 +15,11 @@ namespace Xamarin.Android.Tasks;
/// and 'emulator -list-avds'. Merges the results to provide a complete list of available
/// devices including emulators that are not currently running.
/// Returns a list of devices with metadata for device selection in dotnet run.
///
/// Parsing and merging logic is delegated to <see cref="AdbRunner"/> in Xamarin.Android.Tools.AndroidSdk.
/// </summary>
public class GetAvailableAndroidDevices : AndroidAdb
{
enum DeviceType
{
Device,
Emulator
}

// Pattern to match device lines: <serial> <state> [key:value ...]
// Example: emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64
static readonly Regex AdbDevicesRegex = new(@"^([^\s]+)\s+(device|offline|unauthorized|no permissions)\s*(.*)$", RegexOptions.Compiled);
static readonly Regex ApiRegex = new(@"\bApi\b", RegexOptions.Compiled);

readonly List<string> output = [];

/// <summary>
Expand Down Expand Up @@ -64,23 +54,62 @@ public override bool RunTask ()
if (!base.RunTask ())
return false;

// Parse devices from adb
var adbDevices = ParseAdbDevicesOutput (output);
// Parse devices from adb using shared AdbRunner logic
var adbDevices = AdbRunner.ParseAdbDevicesOutput (output);
Log.LogDebugMessage ($"Found {adbDevices.Count} device(s) from adb");

// For emulators, query AVD names
foreach (var device in adbDevices) {
if (device.Type == AdbDeviceType.Emulator) {
device.AvdName = GetEmulatorAvdName (device.Serial);
device.Description = AdbRunner.BuildDeviceDescription (device, this.CreateTaskLogger ());
}
}
Comment on lines +61 to +67
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

this.CreateTaskLogger() is invoked repeatedly (once per emulator, and again for merging). Create it once (e.g., var logger = this.CreateTaskLogger();) and reuse it for all AdbRunner calls to avoid repeated delegate allocations and keep logging configuration consistent.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

can we fix this and just save a local var logger and reuse it?


// Get available emulators from 'emulator -list-avds'
var availableEmulators = GetAvailableEmulators ();
Log.LogDebugMessage ($"Found {availableEmulators.Count} available emulator(s) from 'emulator -list-avds'");

// Merge the lists
var mergedDevices = MergeDevicesAndEmulators (adbDevices, availableEmulators);
Devices = mergedDevices.ToArray ();
// Merge using shared logic
var mergedDevices = AdbRunner.MergeDevicesAndEmulators (adbDevices, availableEmulators, this.CreateTaskLogger ());
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

this.CreateTaskLogger() is invoked repeatedly (once per emulator, and again for merging). Create it once (e.g., var logger = this.CreateTaskLogger();) and reuse it for all AdbRunner calls to avoid repeated delegate allocations and keep logging configuration consistent.

Copilot uses AI. Check for mistakes.

// Convert to ITaskItem array
Devices = ConvertToTaskItems (mergedDevices);

Log.LogDebugMessage ($"Total {Devices.Length} Android device(s)/emulator(s) after merging");

return !Log.HasLoggedErrors;
}

/// <summary>
/// Converts AdbDeviceInfo list to ITaskItem array for MSBuild output.
/// </summary>
internal static ITaskItem [] ConvertToTaskItems (IReadOnlyList<AdbDeviceInfo> devices)
{
var items = new ITaskItem [devices.Count];
for (int i = 0; i < devices.Count; i++) {
var device = devices [i];
var item = new TaskItem (device.Serial);
item.SetMetadata ("Description", device.Description);
item.SetMetadata ("Type", device.Type.ToString ());
item.SetMetadata ("Status", device.Status.ToString ());
Comment on lines +93 to +95
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

MSBuild item metadata values are effectively part of the task’s output contract. Relying on enum.ToString() couples that contract to enum member names (which can change if the shared library renames members). Consider mapping AdbDeviceType/AdbDeviceStatus to explicit, stable strings that match the existing expectations (e.g., "Device"/"Emulator" and "Online"/"Offline"/"Unauthorized"/"NoPermissions"/"NotRunning"/"Unknown").

Copilot uses AI. Check for mistakes.

if (!device.AvdName.IsNullOrEmpty ())
item.SetMetadata ("AvdName", device.AvdName);
if (!device.Model.IsNullOrEmpty ())
item.SetMetadata ("Model", device.Model);
if (!device.Product.IsNullOrEmpty ())
item.SetMetadata ("Product", device.Product);
if (!device.Device.IsNullOrEmpty ())
item.SetMetadata ("Device", device.Device);
if (!device.TransportId.IsNullOrEmpty ())
item.SetMetadata ("TransportId", device.TransportId);

items [i] = item;
}
return items;
}

/// <summary>
/// Gets the list of available AVDs using 'emulator -list-avds'.
/// </summary>
Expand Down Expand Up @@ -125,186 +154,7 @@ protected virtual List<string> GetAvailableEmulators ()
}

/// <summary>
/// Merges devices from adb with available emulators.
/// Running emulators (already in adb list) are not duplicated.
/// Non-running emulators are added with Status="NotRunning".
/// Results are sorted: online devices first, then not-running emulators, alphabetically by description within each group.
/// </summary>
internal List<ITaskItem> MergeDevicesAndEmulators (List<ITaskItem> adbDevices, List<string> availableEmulators)
{
var result = new List<ITaskItem> (adbDevices);

// Build a set of AVD names that are already running (from adb devices)
var runningAvdNames = new HashSet<string> (StringComparer.OrdinalIgnoreCase);
foreach (var device in adbDevices) {
var avdName = device.GetMetadata ("AvdName");
if (!avdName.IsNullOrEmpty ()) {
runningAvdNames.Add (avdName);
}
}

Log.LogDebugMessage ($"Running emulators AVD names: {string.Join (", ", runningAvdNames)}");

// Add non-running emulators
foreach (var avdName in availableEmulators) {
if (runningAvdNames.Contains (avdName)) {
Log.LogDebugMessage ($"Emulator '{avdName}' is already running, skipping");
continue;
}

// Create item for non-running emulator
// Use the AVD name as the ItemSpec since there's no serial yet
var item = new TaskItem (avdName);
var displayName = FormatDisplayName (avdName, avdName);
item.SetMetadata ("Description", $"{displayName} (Not Running)");
item.SetMetadata ("Type", DeviceType.Emulator.ToString ());
item.SetMetadata ("Status", "NotRunning");
item.SetMetadata ("AvdName", avdName);

result.Add (item);
Log.LogDebugMessage ($"Added non-running emulator: {avdName}");
}

// Sort: online devices first, then not-running emulators, alphabetically by description within each group
result.Sort ((a, b) => {
var aNotRunning = string.Equals (a.GetMetadata ("Status"), "NotRunning", StringComparison.OrdinalIgnoreCase);
var bNotRunning = string.Equals (b.GetMetadata ("Status"), "NotRunning", StringComparison.OrdinalIgnoreCase);

if (aNotRunning != bNotRunning) {
return aNotRunning ? 1 : -1;
}

return string.Compare (a.GetMetadata ("Description"), b.GetMetadata ("Description"), StringComparison.OrdinalIgnoreCase);
});

return result;
}

/// <summary>
/// Parses the output of 'adb devices -l' command.
/// Example output:
/// List of devices attached
/// emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1
/// 0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2
/// </summary>
List<ITaskItem> ParseAdbDevicesOutput (List<string> lines)
{
var devices = new List<ITaskItem> ();

foreach (var line in lines) {
// Skip the header line "List of devices attached"
if (line.Contains ("List of devices") || line.IsNullOrWhiteSpace ())
continue;

var match = AdbDevicesRegex.Match (line);
if (!match.Success)
continue;

var serial = match.Groups [1].Value.Trim ();
var state = match.Groups [2].Value.Trim ();
var properties = match.Groups [3].Value.Trim ();

// Parse key:value pairs from the properties string
var propDict = new Dictionary<string, string> (StringComparer.OrdinalIgnoreCase);
if (!properties.IsNullOrWhiteSpace ()) {
// Split by whitespace and parse key:value pairs
var pairs = properties.Split ([' '], StringSplitOptions.RemoveEmptyEntries);
foreach (var pair in pairs) {
var colonIndex = pair.IndexOf (':');
if (colonIndex > 0 && colonIndex < pair.Length - 1) {
var key = pair.Substring (0, colonIndex);
var value = pair.Substring (colonIndex + 1);
propDict [key] = value;
}
}
}

// Determine device type: Emulator or Device
var deviceType = serial.StartsWith ("emulator-", StringComparison.OrdinalIgnoreCase) ? DeviceType.Emulator : DeviceType.Device;

// For emulators, get the AVD name for duplicate detection
string? avdName = null;
if (deviceType == DeviceType.Emulator) {
avdName = GetEmulatorAvdName (serial);
}

// Build a friendly description
var description = BuildDeviceDescription (serial, propDict, deviceType, avdName);

// Map adb state to device status
var status = MapAdbStateToStatus (state);

// Create the MSBuild item
var item = new TaskItem (serial);
item.SetMetadata ("Description", description);
item.SetMetadata ("Type", deviceType.ToString ());
item.SetMetadata ("Status", status);

// Add AVD name for emulators (used for duplicate detection)
if (!avdName.IsNullOrEmpty ()) {
item.SetMetadata ("AvdName", avdName);
}

// Add optional metadata for additional information
if (propDict.TryGetValue ("model", out var model))
item.SetMetadata ("Model", model);
if (propDict.TryGetValue ("product", out var product))
item.SetMetadata ("Product", product);
if (propDict.TryGetValue ("device", out var device))
item.SetMetadata ("Device", device);
if (propDict.TryGetValue ("transport_id", out var transportId))
item.SetMetadata ("TransportId", transportId);

devices.Add (item);
}

return devices;
}

string BuildDeviceDescription (string serial, Dictionary<string, string> properties, DeviceType deviceType, string? avdName)
{
// Try to build a human-friendly description
// Priority: AVD name (for emulators) > model > product > device > serial

// For emulators, try to get the AVD display name
if (deviceType == DeviceType.Emulator && !avdName.IsNullOrEmpty ()) {
return FormatDisplayName (serial, avdName!);
}

if (properties.TryGetValue ("model", out var model) && !model.IsNullOrEmpty ()) {
// Clean up model name - replace underscores with spaces
model = model.Replace ('_', ' ');
return model;
}

if (properties.TryGetValue ("product", out var product) && !product.IsNullOrEmpty ()) {
product = product.Replace ('_', ' ');
return product;
}

if (properties.TryGetValue ("device", out var device) && !device.IsNullOrEmpty ()) {
device = device.Replace ('_', ' ');
return device;
}

// Fallback to serial number
return serial;
}

static string MapAdbStateToStatus (string adbState)
{
// Map adb device states to the spec's status values
return adbState.ToLowerInvariant () switch {
"device" => "Online",
"offline" => "Offline",
"unauthorized" => "Unauthorized",
"no permissions" => "NoPermissions",
_ => "Unknown",
};
}

/// <summary>
/// Queries the emulator for its AVD name using 'adb -s <serial> emu avd name'.
/// Queries the emulator for its AVD name using 'adb -s &lt;serial&gt; emu avd name'.
/// Returns the raw AVD name (not formatted).
/// </summary>
protected virtual string? GetEmulatorAvdName (string serial)
Expand Down Expand Up @@ -339,21 +189,4 @@ static string MapAdbStateToStatus (string adbState)

return null;
}

/// <summary>
/// Formats the AVD name into a more user-friendly display name. Replace underscores with spaces and title case.
/// </summary>
public string FormatDisplayName (string serial, string avdName)
{
Log.LogDebugMessage ($"Emulator {serial}, original AVD name: {avdName}");

// Title case and replace underscores with spaces
var textInfo = CultureInfo.InvariantCulture.TextInfo;
avdName = textInfo.ToTitleCase (avdName.Replace ('_', ' '));

// Replace "Api" with "API"
avdName = ApiRegex.Replace (avdName, "API");
Log.LogDebugMessage ($"Emulator {serial}, formatted AVD display name: {avdName}");
return avdName;
}
}
Loading