@@ -129,24 +129,85 @@ class AndroidEmulatorManager: ObservableObject {
129129
130130 var emulators : [ ( name: String , device: String , apiLevel: String , target: String ) ] = [ ]
131131
132+ // Try to get detailed info using avdmanager list avd
133+ let avdManagerOutput = await executeCommand ( " avdmanager " , arguments: [ " list " , " avd " ] )
134+ let avdDetails = parseAVDManagerOutput ( avdManagerOutput)
135+
132136 for line in lines {
133137 let name = line. trimmingCharacters ( in: . whitespacesAndNewlines)
134138
135- // Try to get config info from the AVD directory
136- let configPath = " \( NSHomeDirectory ( ) ) /.android/avd/ \( name) .avd/config.ini "
137- let configContent = await readFile ( at: configPath)
138-
139- let device = extractValue ( from: configContent, key: " hw.device.name " ) ??
140- extractValue ( from: configContent, key: " hw.device.manufacturer " ) ?? " Unknown Device "
141- let apiLevel = extractValue ( from: configContent, key: " image.sysdir.1 " ) ? . components ( separatedBy: " / " ) . last ?? " Unknown API "
142- let target = extractValue ( from: configContent, key: " target " ) ?? " Unknown Target "
143-
144- emulators. append ( ( name: name, device: device, apiLevel: apiLevel, target: target) )
139+ // First try to get info from avdmanager command
140+ if let avdInfo = avdDetails [ name] {
141+ emulators. append ( (
142+ name: name,
143+ device: avdInfo. device,
144+ apiLevel: avdInfo. apiLevel,
145+ target: avdInfo. target
146+ ) )
147+ } else {
148+ // Fallback to config file parsing
149+ let configPath = " \( NSHomeDirectory ( ) ) /.android/avd/ \( name) .avd/config.ini "
150+ let configContent = await readFile ( at: configPath)
151+
152+ let device = extractDeviceName ( from: configContent)
153+ let apiLevel = extractAPILevel ( from: configContent, avdName: name)
154+ let target = extractTarget ( from: configContent, avdName: name)
155+
156+ emulators. append ( ( name: name, device: device, apiLevel: apiLevel, target: target) )
157+ }
145158 }
146159
147160 return emulators
148161 }
149162
163+ private func parseAVDManagerOutput( _ output: String ) -> [ String : ( device: String , apiLevel: String , target: String ) ] {
164+ var avdDetails : [ String : ( device: String , apiLevel: String , target: String ) ] = [ : ]
165+
166+ let lines = output. components ( separatedBy: . newlines)
167+ var currentAVD : String ?
168+ var currentDevice = " Unknown Device "
169+ var currentAPI = " Unknown "
170+ var currentTarget = " Android "
171+
172+ for line in lines {
173+ let trimmedLine = line. trimmingCharacters ( in: . whitespacesAndNewlines)
174+
175+ if trimmedLine. hasPrefix ( " Name: " ) {
176+ currentAVD = String ( trimmedLine. dropFirst ( " Name: " . count) ) . trimmingCharacters ( in: . whitespacesAndNewlines)
177+ } else if trimmedLine. hasPrefix ( " Device: " ) {
178+ currentDevice = String ( trimmedLine. dropFirst ( " Device: " . count) ) . trimmingCharacters ( in: . whitespacesAndNewlines)
179+ } else if trimmedLine. hasPrefix ( " Target: " ) {
180+ let targetString = String ( trimmedLine. dropFirst ( " Target: " . count) ) . trimmingCharacters ( in: . whitespacesAndNewlines)
181+ currentTarget = targetString
182+
183+ // Extract API level from target string (e.g., "Google APIs (Google Inc.) - API Level 33")
184+ if let apiRange = targetString. range ( of: " API Level " ) {
185+ let apiSubstring = targetString [ apiRange. upperBound... ]
186+ let apiComponents = apiSubstring. components ( separatedBy: " " )
187+ if let firstComponent = apiComponents. first, Int ( firstComponent) != nil {
188+ currentAPI = firstComponent
189+ }
190+ }
191+ } else if trimmedLine. contains ( " --------- " ) && currentAVD != nil {
192+ // End of current AVD block
193+ if let avdName = currentAVD {
194+ avdDetails [ avdName] = ( device: currentDevice, apiLevel: currentAPI, target: currentTarget)
195+ }
196+ currentAVD = nil
197+ currentDevice = " Unknown Device "
198+ currentAPI = " Unknown "
199+ currentTarget = " Android "
200+ }
201+ }
202+
203+ // Handle last AVD if no separator line at the end
204+ if let avdName = currentAVD {
205+ avdDetails [ avdName] = ( device: currentDevice, apiLevel: currentAPI, target: currentTarget)
206+ }
207+
208+ return avdDetails
209+ }
210+
150211 private func getRunningEmulatorNames( ) async -> Set < String > {
151212 // Method 1: Check running processes for emulator commands
152213 let processOutput = await executeCommand ( " ps " , arguments: [ " aux " ] )
@@ -256,6 +317,96 @@ class AndroidEmulatorManager: ObservableObject {
256317 return nil
257318 }
258319
320+ private func extractDeviceName( from content: String ) -> String {
321+ // Try different device name keys
322+ if let deviceName = extractValue ( from: content, key: " hw.device.name " ) {
323+ return deviceName
324+ }
325+ if let deviceManufacturer = extractValue ( from: content, key: " hw.device.manufacturer " ) {
326+ return deviceManufacturer
327+ }
328+ if let avdDisplayName = extractValue ( from: content, key: " avd.ini.displayname " ) {
329+ return avdDisplayName
330+ }
331+ return " Unknown Device "
332+ }
333+
334+ private func extractAPILevel( from content: String , avdName: String ) -> String {
335+ // Method 1: Try to extract from image.sysdir.1
336+ if let sysDir = extractValue ( from: content, key: " image.sysdir.1 " ) {
337+ let components = sysDir. components ( separatedBy: " / " )
338+ for component in components {
339+ if component. hasPrefix ( " android- " ) {
340+ let apiString = String ( component. dropFirst ( " android- " . count) )
341+ if Int ( apiString) != nil {
342+ return apiString
343+ }
344+ }
345+ }
346+ }
347+
348+ // Method 2: Try target
349+ if let target = extractValue ( from: content, key: " target " ) {
350+ if target. hasPrefix ( " android- " ) {
351+ let apiString = String ( target. dropFirst ( " android- " . count) )
352+ if Int ( apiString) != nil {
353+ return apiString
354+ }
355+ }
356+ }
357+
358+ // Method 3: Try tag.id combined with tag.display
359+ if let tagId = extractValue ( from: content, key: " tag.id " ) ,
360+ let tagDisplay = extractValue ( from: content, key: " tag.display " ) {
361+ if tagDisplay. contains ( " API " ) {
362+ let components = tagDisplay. components ( separatedBy: " " )
363+ for component in components {
364+ if Int ( component) != nil {
365+ return component
366+ }
367+ }
368+ }
369+ }
370+
371+ // Method 4: Try PlayStore tag approach
372+ if let playStoreTag = extractValue ( from: content, key: " PlayStore.enabled " ) {
373+ // If PlayStore is enabled, it's likely a Google APIs version
374+ if let target = extractValue ( from: content, key: " target " ) {
375+ return target. replacingOccurrences ( of: " android- " , with: " " )
376+ }
377+ }
378+
379+ return " Unknown "
380+ }
381+
382+ private func extractTarget( from content: String , avdName: String ) -> String {
383+ // Try to get a human-readable target name
384+ if let target = extractValue ( from: content, key: " target " ) {
385+ if target. hasPrefix ( " android- " ) {
386+ let apiLevel = String ( target. dropFirst ( " android- " . count) )
387+
388+ // Check if it has Google APIs
389+ if let tagId = extractValue ( from: content, key: " tag.id " ) {
390+ if tagId. contains ( " google_apis " ) {
391+ return " Android \( apiLevel) (Google APIs) "
392+ } else if tagId. contains ( " playstore " ) || tagId. contains ( " google_apis_playstore " ) {
393+ return " Android \( apiLevel) (Google Play) "
394+ }
395+ }
396+
397+ return " Android \( apiLevel) "
398+ }
399+ return target
400+ }
401+
402+ // Fallback to tag display if available
403+ if let tagDisplay = extractValue ( from: content, key: " tag.display " ) {
404+ return tagDisplay
405+ }
406+
407+ return " Android "
408+ }
409+
259410 @discardableResult
260411 private func executeCommand( _ command: String , arguments: [ String ] = [ ] , background: Bool = false ) async -> String {
261412 return await withCheckedContinuation { continuation in
0 commit comments