22
33using System ;
44using System . Collections . Generic ;
5- using System . Globalization ;
65using System . IO ;
7- using System . Text . RegularExpressions ;
86using Microsoft . Android . Build . Tasks ;
97using Microsoft . Build . Framework ;
108using Microsoft . Build . Utilities ;
9+ using Xamarin . Android . Tools ;
1110
1211namespace 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>
2021public 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 <serial> 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