Skip to content

Commit f715c14

Browse files
[xabt] Use shared AdbRunner from dotnet/android-tools (#10916)
Delegates the `adb devices -l` parsing, description building, and device/emulator merging logic from the `GetAvailableAndroidDevices` MSBuild task to the shared `AdbRunner` in `Xamarin.Android.Tools.AndroidSdk` (via the `external/xamarin-android-tools` submodule). This removes ~200 lines of duplicated parsing/formatting/merging code from dotnet/android and consolidates it in the shared android-tools library where it can be reused by other consumers (e.g., the MAUI DevTools CLI). ## Changes - **GetAvailableAndroidDevices.cs**: Rewritten to delegate to `AdbRunner.ParseAdbDevicesOutput(IEnumerable<string>)`, `AdbRunner.BuildDeviceDescription`, and `AdbRunner.MergeDevicesAndEmulators` instead of having its own parsing logic. Added `ConvertToTaskItems` to bridge `AdbDeviceInfo` -> `ITaskItem`. - **GetAvailableAndroidDevicesTests.cs**: Updated to use `AdbRunner`/`AdbDeviceInfo` directly instead of reflection. All tests preserved with equivalent coverage. Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
1 parent 55816c2 commit f715c14

2 files changed

Lines changed: 134 additions & 348 deletions

File tree

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

Lines changed: 49 additions & 215 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
using System;
44
using System.Collections.Generic;
5-
using System.Globalization;
65
using System.IO;
7-
using System.Text.RegularExpressions;
86
using Microsoft.Android.Build.Tasks;
97
using Microsoft.Build.Framework;
108
using Microsoft.Build.Utilities;
9+
using Xamarin.Android.Tools;
1110

1211
namespace Xamarin.Android.Tasks;
1312

@@ -16,20 +15,11 @@ namespace Xamarin.Android.Tasks;
1615
/// and 'emulator -list-avds'. Merges the results to provide a complete list of available
1716
/// devices including emulators that are not currently running.
1817
/// Returns a list of devices with metadata for device selection in dotnet run.
18+
///
19+
/// Parsing and merging logic is delegated to <see cref="AdbRunner"/> in Xamarin.Android.Tools.AndroidSdk.
1920
/// </summary>
2021
public class GetAvailableAndroidDevices : AndroidAdb
2122
{
22-
enum DeviceType
23-
{
24-
Device,
25-
Emulator
26-
}
27-
28-
// Pattern to match device lines: <serial> <state> [key:value ...]
29-
// Example: emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64
30-
static readonly Regex AdbDevicesRegex = new(@"^([^\s]+)\s+(device|offline|unauthorized|no permissions)\s*(.*)$", RegexOptions.Compiled);
31-
static readonly Regex ApiRegex = new(@"\bApi\b", RegexOptions.Compiled);
32-
3323
readonly List<string> output = [];
3424

3525
/// <summary>
@@ -64,23 +54,63 @@ public override bool RunTask ()
6454
if (!base.RunTask ())
6555
return false;
6656

67-
// Parse devices from adb
68-
var adbDevices = ParseAdbDevicesOutput (output);
57+
// Parse devices from adb using shared AdbRunner logic
58+
var adbDevices = AdbRunner.ParseAdbDevicesOutput (output);
6959
Log.LogDebugMessage ($"Found {adbDevices.Count} device(s) from adb");
7060

61+
// For emulators, query AVD names
62+
var logger = this.CreateTaskLogger ();
63+
foreach (var device in adbDevices) {
64+
if (device.Type == AdbDeviceType.Emulator) {
65+
device.AvdName = GetEmulatorAvdName (device.Serial);
66+
device.Description = AdbRunner.BuildDeviceDescription (device, logger);
67+
}
68+
}
69+
7170
// Get available emulators from 'emulator -list-avds'
7271
var availableEmulators = GetAvailableEmulators ();
7372
Log.LogDebugMessage ($"Found {availableEmulators.Count} available emulator(s) from 'emulator -list-avds'");
7473

75-
// Merge the lists
76-
var mergedDevices = MergeDevicesAndEmulators (adbDevices, availableEmulators);
77-
Devices = mergedDevices.ToArray ();
74+
// Merge using shared logic
75+
var mergedDevices = AdbRunner.MergeDevicesAndEmulators (adbDevices, availableEmulators, logger);
76+
77+
// Convert to ITaskItem array
78+
Devices = ConvertToTaskItems (mergedDevices);
7879

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

8182
return !Log.HasLoggedErrors;
8283
}
8384

85+
/// <summary>
86+
/// Converts AdbDeviceInfo list to ITaskItem array for MSBuild output.
87+
/// </summary>
88+
internal static ITaskItem [] ConvertToTaskItems (IReadOnlyList<AdbDeviceInfo> devices)
89+
{
90+
var items = new ITaskItem [devices.Count];
91+
for (int i = 0; i < devices.Count; i++) {
92+
var device = devices [i];
93+
var item = new TaskItem (device.Serial);
94+
item.SetMetadata ("Description", device.Description);
95+
item.SetMetadata ("Type", device.Type.ToString ());
96+
item.SetMetadata ("Status", device.Status.ToString ());
97+
98+
if (!device.AvdName.IsNullOrEmpty ())
99+
item.SetMetadata ("AvdName", device.AvdName);
100+
if (!device.Model.IsNullOrEmpty ())
101+
item.SetMetadata ("Model", device.Model);
102+
if (!device.Product.IsNullOrEmpty ())
103+
item.SetMetadata ("Product", device.Product);
104+
if (!device.Device.IsNullOrEmpty ())
105+
item.SetMetadata ("Device", device.Device);
106+
if (!device.TransportId.IsNullOrEmpty ())
107+
item.SetMetadata ("TransportId", device.TransportId);
108+
109+
items [i] = item;
110+
}
111+
return items;
112+
}
113+
84114
/// <summary>
85115
/// Gets the list of available AVDs using 'emulator -list-avds'.
86116
/// </summary>
@@ -125,186 +155,7 @@ protected virtual List<string> GetAvailableEmulators ()
125155
}
126156

127157
/// <summary>
128-
/// Merges devices from adb with available emulators.
129-
/// Running emulators (already in adb list) are not duplicated.
130-
/// Non-running emulators are added with Status="NotRunning".
131-
/// Results are sorted: online devices first, then not-running emulators, alphabetically by description within each group.
132-
/// </summary>
133-
internal List<ITaskItem> MergeDevicesAndEmulators (List<ITaskItem> adbDevices, List<string> availableEmulators)
134-
{
135-
var result = new List<ITaskItem> (adbDevices);
136-
137-
// Build a set of AVD names that are already running (from adb devices)
138-
var runningAvdNames = new HashSet<string> (StringComparer.OrdinalIgnoreCase);
139-
foreach (var device in adbDevices) {
140-
var avdName = device.GetMetadata ("AvdName");
141-
if (!avdName.IsNullOrEmpty ()) {
142-
runningAvdNames.Add (avdName);
143-
}
144-
}
145-
146-
Log.LogDebugMessage ($"Running emulators AVD names: {string.Join (", ", runningAvdNames)}");
147-
148-
// Add non-running emulators
149-
foreach (var avdName in availableEmulators) {
150-
if (runningAvdNames.Contains (avdName)) {
151-
Log.LogDebugMessage ($"Emulator '{avdName}' is already running, skipping");
152-
continue;
153-
}
154-
155-
// Create item for non-running emulator
156-
// Use the AVD name as the ItemSpec since there's no serial yet
157-
var item = new TaskItem (avdName);
158-
var displayName = FormatDisplayName (avdName, avdName);
159-
item.SetMetadata ("Description", $"{displayName} (Not Running)");
160-
item.SetMetadata ("Type", DeviceType.Emulator.ToString ());
161-
item.SetMetadata ("Status", "NotRunning");
162-
item.SetMetadata ("AvdName", avdName);
163-
164-
result.Add (item);
165-
Log.LogDebugMessage ($"Added non-running emulator: {avdName}");
166-
}
167-
168-
// Sort: online devices first, then not-running emulators, alphabetically by description within each group
169-
result.Sort ((a, b) => {
170-
var aNotRunning = string.Equals (a.GetMetadata ("Status"), "NotRunning", StringComparison.OrdinalIgnoreCase);
171-
var bNotRunning = string.Equals (b.GetMetadata ("Status"), "NotRunning", StringComparison.OrdinalIgnoreCase);
172-
173-
if (aNotRunning != bNotRunning) {
174-
return aNotRunning ? 1 : -1;
175-
}
176-
177-
return string.Compare (a.GetMetadata ("Description"), b.GetMetadata ("Description"), StringComparison.OrdinalIgnoreCase);
178-
});
179-
180-
return result;
181-
}
182-
183-
/// <summary>
184-
/// Parses the output of 'adb devices -l' command.
185-
/// Example output:
186-
/// List of devices attached
187-
/// emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1
188-
/// 0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2
189-
/// </summary>
190-
List<ITaskItem> ParseAdbDevicesOutput (List<string> lines)
191-
{
192-
var devices = new List<ITaskItem> ();
193-
194-
foreach (var line in lines) {
195-
// Skip the header line "List of devices attached"
196-
if (line.Contains ("List of devices") || line.IsNullOrWhiteSpace ())
197-
continue;
198-
199-
var match = AdbDevicesRegex.Match (line);
200-
if (!match.Success)
201-
continue;
202-
203-
var serial = match.Groups [1].Value.Trim ();
204-
var state = match.Groups [2].Value.Trim ();
205-
var properties = match.Groups [3].Value.Trim ();
206-
207-
// Parse key:value pairs from the properties string
208-
var propDict = new Dictionary<string, string> (StringComparer.OrdinalIgnoreCase);
209-
if (!properties.IsNullOrWhiteSpace ()) {
210-
// Split by whitespace and parse key:value pairs
211-
var pairs = properties.Split ([' '], StringSplitOptions.RemoveEmptyEntries);
212-
foreach (var pair in pairs) {
213-
var colonIndex = pair.IndexOf (':');
214-
if (colonIndex > 0 && colonIndex < pair.Length - 1) {
215-
var key = pair.Substring (0, colonIndex);
216-
var value = pair.Substring (colonIndex + 1);
217-
propDict [key] = value;
218-
}
219-
}
220-
}
221-
222-
// Determine device type: Emulator or Device
223-
var deviceType = serial.StartsWith ("emulator-", StringComparison.OrdinalIgnoreCase) ? DeviceType.Emulator : DeviceType.Device;
224-
225-
// For emulators, get the AVD name for duplicate detection
226-
string? avdName = null;
227-
if (deviceType == DeviceType.Emulator) {
228-
avdName = GetEmulatorAvdName (serial);
229-
}
230-
231-
// Build a friendly description
232-
var description = BuildDeviceDescription (serial, propDict, deviceType, avdName);
233-
234-
// Map adb state to device status
235-
var status = MapAdbStateToStatus (state);
236-
237-
// Create the MSBuild item
238-
var item = new TaskItem (serial);
239-
item.SetMetadata ("Description", description);
240-
item.SetMetadata ("Type", deviceType.ToString ());
241-
item.SetMetadata ("Status", status);
242-
243-
// Add AVD name for emulators (used for duplicate detection)
244-
if (!avdName.IsNullOrEmpty ()) {
245-
item.SetMetadata ("AvdName", avdName);
246-
}
247-
248-
// Add optional metadata for additional information
249-
if (propDict.TryGetValue ("model", out var model))
250-
item.SetMetadata ("Model", model);
251-
if (propDict.TryGetValue ("product", out var product))
252-
item.SetMetadata ("Product", product);
253-
if (propDict.TryGetValue ("device", out var device))
254-
item.SetMetadata ("Device", device);
255-
if (propDict.TryGetValue ("transport_id", out var transportId))
256-
item.SetMetadata ("TransportId", transportId);
257-
258-
devices.Add (item);
259-
}
260-
261-
return devices;
262-
}
263-
264-
string BuildDeviceDescription (string serial, Dictionary<string, string> properties, DeviceType deviceType, string? avdName)
265-
{
266-
// Try to build a human-friendly description
267-
// Priority: AVD name (for emulators) > model > product > device > serial
268-
269-
// For emulators, try to get the AVD display name
270-
if (deviceType == DeviceType.Emulator && !avdName.IsNullOrEmpty ()) {
271-
return FormatDisplayName (serial, avdName!);
272-
}
273-
274-
if (properties.TryGetValue ("model", out var model) && !model.IsNullOrEmpty ()) {
275-
// Clean up model name - replace underscores with spaces
276-
model = model.Replace ('_', ' ');
277-
return model;
278-
}
279-
280-
if (properties.TryGetValue ("product", out var product) && !product.IsNullOrEmpty ()) {
281-
product = product.Replace ('_', ' ');
282-
return product;
283-
}
284-
285-
if (properties.TryGetValue ("device", out var device) && !device.IsNullOrEmpty ()) {
286-
device = device.Replace ('_', ' ');
287-
return device;
288-
}
289-
290-
// Fallback to serial number
291-
return serial;
292-
}
293-
294-
static string MapAdbStateToStatus (string adbState)
295-
{
296-
// Map adb device states to the spec's status values
297-
return adbState.ToLowerInvariant () switch {
298-
"device" => "Online",
299-
"offline" => "Offline",
300-
"unauthorized" => "Unauthorized",
301-
"no permissions" => "NoPermissions",
302-
_ => "Unknown",
303-
};
304-
}
305-
306-
/// <summary>
307-
/// Queries the emulator for its AVD name using 'adb -s <serial> emu avd name'.
158+
/// Queries the emulator for its AVD name using 'adb -s &lt;serial&gt; emu avd name'.
308159
/// Returns the raw AVD name (not formatted).
309160
/// </summary>
310161
protected virtual string? GetEmulatorAvdName (string serial)
@@ -339,21 +190,4 @@ static string MapAdbStateToStatus (string adbState)
339190

340191
return null;
341192
}
342-
343-
/// <summary>
344-
/// Formats the AVD name into a more user-friendly display name. Replace underscores with spaces and title case.
345-
/// </summary>
346-
public string FormatDisplayName (string serial, string avdName)
347-
{
348-
Log.LogDebugMessage ($"Emulator {serial}, original AVD name: {avdName}");
349-
350-
// Title case and replace underscores with spaces
351-
var textInfo = CultureInfo.InvariantCulture.TextInfo;
352-
avdName = textInfo.ToTitleCase (avdName.Replace ('_', ' '));
353-
354-
// Replace "Api" with "API"
355-
avdName = ApiRegex.Replace (avdName, "API");
356-
Log.LogDebugMessage ($"Emulator {serial}, formatted AVD display name: {avdName}");
357-
return avdName;
358-
}
359193
}

0 commit comments

Comments
 (0)