Skip to content

Conversation

@gmegidish
Copy link
Member

@gmegidish gmegidish commented Nov 26, 2025

Summary by CodeRabbit

  • New Features

    • Added performance instrumentation with timing metrics for device discovery operations, providing visibility into detection duration and device counts.
  • Improvements

    • Optimized Android Virtual Device detection using direct configuration file parsing for improved performance.
    • Enhanced iOS Simulator discovery through filesystem-based detection for greater reliability.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Nov 26, 2025

Walkthrough

The changes replace command-execution and JSON-parsing based device discovery for both Android Virtual Devices and iOS Simulators with direct filesystem-based approaches using INI and plist file parsing. Timing instrumentation is added to track device enumeration performance.

Changes

Cohort / File(s) Summary
AVD Discovery Refactoring
devices/avd.go
Replaced executor-based AVD discovery with INI file parsing. Updated AVDInfo struct: removed Path, Target, BasedOn; added AvdId. New getAVDDetails() function builds map of AVDs from ~/.android/avd/ directory. Removed listAllAVDs(). Reworked getOfflineAndroidEmulators() to compare AVD IDs against online devices. Updated imports to include os, filepath, and gopkg.in/ini.v1.
Simulator Discovery Refactoring
devices/simulator.go
Replaced xcrun-based simulator enumeration with filesystem-based discovery reading CoreSimulator Devices directory. Added devicePlist struct for parsing device.plist files. GetSimulators() now iterates device folders, extracts data from plist files (UDID, Name, Runtime, State), and maps numeric State values to human-readable strings. Improved error handling with graceful skipping of invalid/missing plist entries.
Instrumentation
devices/common.go
Added timing measurements for Android, offline Android, iOS, and simulator discovery steps in GetAllControllableDevices(). Computes device counts and logs consolidated verbose message with timing metrics. No signature changes.
Dependency Management
go.mod
Added direct dependencies: gopkg.in/ini.v1 v1.67.0 and howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5. Moved howett.net/plist from indirect to direct require block.

Sequence Diagram(s)

sequenceDiagram
    participant caller as Caller
    participant common as GetAllControllableDevices()
    participant avd as getAVDDetails()
    participant sim as GetSimulators()
    participant fs as Filesystem
    
    caller ->> common: Request all devices
    
    rect rgb(200, 220, 255)
    note over common: Android Discovery
    common ->> avd: Start timing
    avd ->> fs: Read ~/.android/avd/
    fs -->> avd: List .ini files
    avd ->> fs: Parse config.ini files
    fs -->> avd: AVD metadata
    avd -->> common: map[AvdId]AVDInfo
    common ->> common: Stop timing (Android)
    end
    
    rect rgb(220, 200, 255)
    note over common: iOS Simulator Discovery
    common ->> sim: Start timing
    sim ->> fs: Read CoreSimulator Devices
    fs -->> sim: Device directories
    sim ->> fs: Read device.plist files
    fs -->> sim: Plist data (UDID, State)
    sim -->> common: []Simulator
    common ->> common: Stop timing (Simulators)
    end
    
    common ->> common: Log consolidated metrics
    common -->> caller: All devices + timings
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • devices/avd.go: Dense refactoring with new INI parsing logic, struct field changes, and map-based data flow; requires verification of parsing correctness and offline device matching logic
  • devices/simulator.go: Significant control flow change from JSON to plist parsing; requires validation of State mappings and error handling for missing/malformed files
  • devices/common.go: Straightforward timing instrumentation, but integration with refactored discovery functions needs verification
  • Cross-file dependency: Both AVD and Simulator changes affect how devices are enumerated; need to verify end-to-end device collection behavior

Poem

🐰 Once we dug through commands and JSON streams so deep,
Now we nibble INI files and plist files to keep,
Hopping through the filesystem with timing metrics bright,
Discovery made simpler—faster, cleaner, right! 🏃‍♂️💨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective of the PR—removing the dependency on avdmanager by replacing executor-based AVD discovery with direct INI-file parsing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-remove-dependency-on-avdmanager

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
devices/simulator.go (2)

130-134: Consider graceful handling when CoreSimulator directory doesn't exist.

On non-macOS systems or fresh macOS installs without Xcode, the ~/Library/Developer/CoreSimulator/Devices directory won't exist. Returning an error may be too aggressive. Consider returning an empty slice with a verbose warning instead:

 	devicesPath := filepath.Join(homeDir, "Library", "Developer", "CoreSimulator", "Devices")
 	entries, err := os.ReadDir(devicesPath)
 	if err != nil {
+		if os.IsNotExist(err) {
+			// CoreSimulator not installed or no simulators created yet
+			return []Simulator{}, nil
+		}
 		return nil, fmt.Errorf("failed to read devices directory: %w", err)
 	}

156-162: State mapping may be incomplete.

The comment mentions state 1 and 3, but iOS simulators can have additional states (e.g., 2 for "Booting", 4 for "ShuttingDown"). While defaulting to "Shutdown" is a safe fallback, consider logging unknown states for debugging:

 		// convert state integer to string
-		// state 1 = Shutdown (offline)
-		// state 3 = Booted (online)
+		// Known states: 1 = Shutdown, 2 = Booting, 3 = Booted, 4 = ShuttingDown
 		stateStr := "Shutdown"
 		if device.State == 3 {
 			stateStr = "Booted"
+		} else if device.State == 2 {
+			stateStr = "Booting"
 		}
devices/avd.go (1)

102-107: Name and Device fields are set to the same value.

Both fields are assigned displayName, making one redundant. If they serve different purposes, consider populating Device with the hardware device type (e.g., from hw.device.name in config.ini) instead:

+		hwDevice := configData.Section("").Key("hw.device.name").String()
+		if hwDevice == "" {
+			hwDevice = displayName
+		}
+
 		avdMap[avdName] = AVDInfo{
 			Name:     displayName,
-			Device:   displayName,
+			Device:   hwDevice,
 			APILevel: apiLevel,
 			AvdId:    avdId,
 		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3470934 and 852c8fe.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • devices/avd.go (2 hunks)
  • devices/common.go (3 hunks)
  • devices/simulator.go (4 hunks)
  • go.mod (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
devices/common.go (4)
devices/android.go (1)
  • GetAndroidDevices (478-493)
utils/logger.go (1)
  • Verbose (19-23)
devices/ios.go (1)
  • ListIOSDevices (89-107)
devices/simulator.go (1)
  • GetSimulators (124-175)
devices/avd.go (2)
utils/logger.go (1)
  • Verbose (19-23)
devices/common.go (1)
  • ControllableDevice (26-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: lint
🔇 Additional comments (5)
devices/common.go (1)

62-139: LGTM! Clean timing instrumentation.

The timing metrics implementation is well-structured: it tracks each device category separately, initializes counters correctly, and consolidates the output into a single verbose log message. The conditional formatting based on includeOffline avoids logging zeroed-out offline metrics when not requested.

devices/simulator.go (1)

36-42: LGTM! Well-structured plist model.

The devicePlist struct correctly uses plist tags matching the CoreSimulator device.plist schema.

devices/avd.go (2)

49-111: LGTM! Solid filesystem-based AVD discovery.

The implementation correctly parses the two-level INI structure (.android/avd/*.inipathconfig.ini), handles errors gracefully with continue, and extracts the necessary fields. This removes the dependency on avdmanager command execution.


148-153: Offline device ID uses avdName which may not match expected format.

The offline device is created with id: avdName (the .ini filename without extension). When the device boots, adb devices typically reports it as emulator-XXXX. This ID mismatch could cause issues when:

  1. The same device appears as two different devices (offline by avdName, online by emulator serial)
  2. Device-specific operations fail to find the offline device after boot

Consider documenting this expected behavior or using a consistent identifier scheme.

go.mod (1)

3-3: Go version 1.25.0 is valid and released.

Go 1.25.0 was released on August 12, 2025, and as of November 2025, version 1.25.4 is the latest patch release in the 1.25 series. The concern about this being an unreleased future version is incorrect. Using Go 1.25.0 in go.mod will not cause build issues due to version availability.

Comment on lines +128 to +131
for avdName, info := range avdDetails {
// check if this AVD is already online by checking if AvdId is in online device IDs
// the avdName from the .ini file should match the device ID when online
_, isOnline := onlineDeviceIDs[info.AvdId]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Script executed:

#!/bin/bash
# First, let's examine the file structure and then read the relevant section
wc -l devices/avd.go
echo "---"
# Read around lines 128-131 with context
sed -n '100,160p' devices/avd.go

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

158 devices/avd.go
---
		avdId := configData.Section("").Key("AvdId").String()

		avdMap[avdName] = AVDInfo{
			Name:     displayName,
			Device:   displayName,
			APILevel: apiLevel,
			AvdId:    avdId,
		}
	}

	return avdMap, nil
}

// getOfflineAndroidEmulators returns a list of offline Android emulators (AVDs not currently running)
func getOfflineAndroidEmulators(onlineDeviceIDs map[string]bool) ([]ControllableDevice, error) {
	var offlineDevices []ControllableDevice

	// get detailed info about AVDs by reading .ini files directly
	avdDetails, err := getAVDDetails()
	if err != nil {
		return offlineDevices, err
	}

	if len(avdDetails) == 0 {
		return offlineDevices, nil
	}

	// create offline device entries for AVDs that are not running
	for avdName, info := range avdDetails {
		// check if this AVD is already online by checking if AvdId is in online device IDs
		// the avdName from the .ini file should match the device ID when online
		_, isOnline := onlineDeviceIDs[info.AvdId]

		if !isOnline {
			displayName := strings.ReplaceAll(avdName, "_", " ")
			version := ""

			if info.Device != "" {
				// use device name if available (e.g., "pixel_6 (Google)")
				displayName = info.Device
				// clean up the device name
				if idx := strings.Index(displayName, "("); idx > 0 {
					displayName = strings.TrimSpace(displayName[:idx])
				}
				displayName = strings.ReplaceAll(displayName, "_", " ")
			}
			version = convertAPILevelToVersion(info.APILevel)

			offlineDevices = append(offlineDevices, &AndroidDevice{
				id:      avdName,
				name:    displayName,
				version: version,
				state:   "offline",
			})
		}
	}

	return offlineDevices, nil
}

</shell_result>


Script executed:

#!/bin/bash
# Search for where onlineDeviceIDs is created or used
rg -n "onlineDeviceIDs" devices/avd.go -A 2 -B 2

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

112-
113-// getOfflineAndroidEmulators returns a list of offline Android emulators (AVDs not currently running)
114:func getOfflineAndroidEmulators(onlineDeviceIDs map[string]bool) ([]ControllableDevice, error) {
115-	var offlineDevices []ControllableDevice
116-
--
129-		// check if this AVD is already online by checking if AvdId is in online device IDs
130-		// the avdName from the .ini file should match the device ID when online
131:		_, isOnline := onlineDeviceIDs[info.AvdId]
132-
133-		if !isOnline {

</shell_result>


Script executed:

#!/bin/bash
# Look at the full avd.go file to understand the flow
cat -n devices/avd.go

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

     1	package devices
     2	
     3	import (
     4		"os"
     5		"path/filepath"
     6		"strings"
     7	
     8		"github.com/mobile-next/mobilecli/utils"
     9		"gopkg.in/ini.v1"
    10	)
    11	
    12	// AVDInfo represents information about an Android Virtual Device
    13	type AVDInfo struct {
    14		Name     string
    15		Device   string
    16		APILevel string
    17		AvdId    string
    18	}
    19	
    20	// apiLevelToVersion maps Android API levels to version strings
    21	var apiLevelToVersion = map[string]string{
    22		"36": "16.0",
    23		"35": "15.0",
    24		"34": "14.0",
    25		"33": "13.0",
    26		"32": "12.1", // Android 12L
    27		"31": "12.0",
    28		"30": "11.0",
    29		"29": "10.0",
    30		"28": "9.0",
    31		"27": "8.1",
    32		"26": "8.0",
    33		"25": "7.1",
    34		"24": "7.0",
    35		"23": "6.0",
    36		"22": "5.1",
    37		"21": "5.0",
    38	}
    39	
    40	// convertAPILevelToVersion converts an API level to Android version string
    41	func convertAPILevelToVersion(apiLevel string) string {
    42		if version, ok := apiLevelToVersion[apiLevel]; ok {
    43			return version
    44		}
    45		// if no mapping found, return the API level as-is
    46		return apiLevel
    47	}
    48	
    49	// getAVDDetails retrieves AVD information by reading .ini files directly
    50	func getAVDDetails() (map[string]AVDInfo, error) {
    51		avdMap := make(map[string]AVDInfo)
    52	
    53		homeDir, err := os.UserHomeDir()
    54		if err != nil {
    55			return avdMap, err
    56		}
    57	
    58		avdDir := filepath.Join(homeDir, ".android", "avd")
    59		pattern := filepath.Join(avdDir, "*.ini")
    60	
    61		matches, err := filepath.Glob(pattern)
    62		if err != nil {
    63			return avdMap, err
    64		}
    65	
    66		for _, iniFile := range matches {
    67			// extract avd name from .ini filename
    68			avdName := strings.TrimSuffix(filepath.Base(iniFile), ".ini")
    69	
    70			// read the .ini file to get the path
    71			iniConfig, err := ini.Load(iniFile)
    72			if err != nil {
    73				utils.Verbose("Failed to read %s: %v", iniFile, err)
    74				continue
    75			}
    76	
    77			avdPath := iniConfig.Section("").Key("path").String()
    78			if avdPath == "" {
    79				continue
    80			}
    81	
    82			// read the config.ini inside the .avd directory
    83			configPath := filepath.Join(avdPath, "config.ini")
    84			configData, err := ini.Load(configPath)
    85			if err != nil {
    86				utils.Verbose("Failed to read %s: %v", configPath, err)
    87				continue
    88			}
    89	
    90			displayName := configData.Section("").Key("avd.ini.displayname").String()
    91			if displayName == "" {
    92				continue
    93			}
    94	
    95			// extract API level from target (e.g., "android-31" -> "31")
    96			target := configData.Section("").Key("target").String()
    97			apiLevel := strings.TrimPrefix(target, "android-")
    98	
    99			// get AvdId for matching with online devices
   100			avdId := configData.Section("").Key("AvdId").String()
   101	
   102			avdMap[avdName] = AVDInfo{
   103				Name:     displayName,
   104				Device:   displayName,
   105				APILevel: apiLevel,
   106				AvdId:    avdId,
   107			}
   108		}
   109	
   110		return avdMap, nil
   111	}
   112	
   113	// getOfflineAndroidEmulators returns a list of offline Android emulators (AVDs not currently running)
   114	func getOfflineAndroidEmulators(onlineDeviceIDs map[string]bool) ([]ControllableDevice, error) {
   115		var offlineDevices []ControllableDevice
   116	
   117		// get detailed info about AVDs by reading .ini files directly
   118		avdDetails, err := getAVDDetails()
   119		if err != nil {
   120			return offlineDevices, err
   121		}
   122	
   123		if len(avdDetails) == 0 {
   124			return offlineDevices, nil
   125		}
   126	
   127		// create offline device entries for AVDs that are not running
   128		for avdName, info := range avdDetails {
   129			// check if this AVD is already online by checking if AvdId is in online device IDs
   130			// the avdName from the .ini file should match the device ID when online
   131			_, isOnline := onlineDeviceIDs[info.AvdId]
   132	
   133			if !isOnline {
   134				displayName := strings.ReplaceAll(avdName, "_", " ")
   135				version := ""
   136	
   137				if info.Device != "" {
   138					// use device name if available (e.g., "pixel_6 (Google)")
   139					displayName = info.Device
   140					// clean up the device name
   141					if idx := strings.Index(displayName, "("); idx > 0 {
   142						displayName = strings.TrimSpace(displayName[:idx])
   143					}
   144					displayName = strings.ReplaceAll(displayName, "_", " ")
   145				}
   146				version = convertAPILevelToVersion(info.APILevel)
   147	
   148				offlineDevices = append(offlineDevices, &AndroidDevice{
   149					id:      avdName,
   150					name:    displayName,
   151					version: version,
   152					state:   "offline",
   153				})
   154			}
   155		}
   156	
   157		return offlineDevices, nil
   158	}

</shell_result>


Script executed:

#!/bin/bash
# Search for calls to getOfflineAndroidEmulators and how onlineDeviceIDs is built
rg -n "getOfflineAndroidEmulators|onlineDeviceIDs" --type go -A 3 -B 3

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

devices/avd.go-110-	return avdMap, nil
devices/avd.go-111-}
devices/avd.go-112-
devices/avd.go:113:// getOfflineAndroidEmulators returns a list of offline Android emulators (AVDs not currently running)
devices/avd.go:114:func getOfflineAndroidEmulators(onlineDeviceIDs map[string]bool) ([]ControllableDevice, error) {
devices/avd.go-115-	var offlineDevices []ControllableDevice
devices/avd.go-116-
devices/avd.go-117-	// get detailed info about AVDs by reading .ini files directly
--
devices/avd.go-128-	for avdName, info := range avdDetails {
devices/avd.go-129-		// check if this AVD is already online by checking if AvdId is in online device IDs
devices/avd.go-130-		// the avdName from the .ini file should match the device ID when online
devices/avd.go:131:		_, isOnline := onlineDeviceIDs[info.AvdId]
devices/avd.go-132-
devices/avd.go-133-		if !isOnline {
devices/avd.go-134-			displayName := strings.ReplaceAll(avdName, "_", " ")
--
devices/common.go-79-	if includeOffline {
devices/common.go-80-		startOfflineAndroid := time.Now()
devices/common.go-81-		// build map of online device IDs for quick lookup
devices/common.go:82:		onlineDeviceIDs := make(map[string]bool)
devices/common.go-83-		for _, device := range androidDevices {
devices/common.go:84:			onlineDeviceIDs[device.ID()] = true
devices/common.go-85-		}
devices/common.go-86-
devices/common.go:87:		offlineEmulators, err := getOfflineAndroidEmulators(onlineDeviceIDs)
devices/common.go-88-		offlineAndroidDuration = time.Since(startOfflineAndroid).Milliseconds()
devices/common.go-89-		if err != nil {
devices/common.go-90-			utils.Verbose("Warning: Failed to get offline Android emulators: %v", err)

</shell_result>


Script executed:

#!/bin/bash
# Search for AndroidDevice type definition and ID() method
rg -n "type AndroidDevice|func.*ID\(\)" --type go -A 5 -B 2

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

devices/simulator.go-70-}
devices/simulator.go-71-
devices/simulator.go:72:func (s SimulatorDevice) ID() string         { return s.UDID }
devices/simulator.go-73-func (s SimulatorDevice) Name() string       { return s.Simulator.Name }
devices/simulator.go-74-func (s SimulatorDevice) Platform() string   { return "ios" }
devices/simulator.go-75-func (s SimulatorDevice) DeviceType() string { return "simulator" }
devices/simulator.go-76-func (s SimulatorDevice) Version() string    { return parseSimulatorVersion(s.Runtime) }
devices/simulator.go-77-func (s SimulatorDevice) State() string {
--
devices/ios.go-37-}
devices/ios.go-38-
devices/ios.go:39:func (d IOSDevice) ID() string {
devices/ios.go-40-	return d.Udid
devices/ios.go-41-}
devices/ios.go-42-
devices/ios.go-43-func (d IOSDevice) Name() string {
devices/ios.go-44-	return d.DeviceName
--
devices/android.go-20-
devices/android.go-21-// AndroidDevice implements the ControllableDevice interface for Android devices
devices/android.go:22:type AndroidDevice struct {
devices/android.go-23-	id          string
devices/android.go-24-	name        string
devices/android.go-25-	version     string
devices/android.go-26-	state       string // "online" or "offline"
devices/android.go-27-	transportID string // adb transport ID (e.g., "emulator-5554"), only set for online devices
devices/android.go-28-}
devices/android.go-29-
devices/android.go:30:func (d *AndroidDevice) ID() string {
devices/android.go-31-	return d.id
devices/android.go-32-}
devices/android.go-33-
devices/android.go-34-func (d *AndroidDevice) Name() string {
devices/android.go-35-	return d.name

</shell_result>


Script executed:

#!/bin/bash
# Search for AndroidDevice creation and adb devices parsing
rg -n "&AndroidDevice{" --type go -A 10 -B 5

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 2

stderr:

regex parse error:
    &AndroidDevice{
                  ^
error: unclosed counted repetition

</shell_result>


Script executed:

#!/bin/bash
# Search for AndroidDevice instantiation
rg -n "AndroidDevice\{" --type go -A 10 -B 5

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

devices/android.go-420-					if avdName != "" {
devices/android.go-421-						deviceID = avdName
devices/android.go-422-					}
devices/android.go-423-				}
devices/android.go-424-
devices/android.go:425:				devices = append(devices, &AndroidDevice{
devices/android.go-426-					id:          deviceID,
devices/android.go-427-					transportID: transportID,
devices/android.go-428-					name:        getAndroidDeviceName(transportID),
devices/android.go-429-					version:     getAndroidDeviceVersion(transportID),
devices/android.go-430-					state:       "online",
devices/android.go-431-				})
devices/android.go-432-			}
devices/android.go-433-		}
devices/android.go-434-	}
devices/android.go-435-
--
devices/avd.go-143-				}
devices/avd.go-144-				displayName = strings.ReplaceAll(displayName, "_", " ")
devices/avd.go-145-			}
devices/avd.go-146-			version = convertAPILevelToVersion(info.APILevel)
devices/avd.go-147-
devices/avd.go:148:			offlineDevices = append(offlineDevices, &AndroidDevice{
devices/avd.go-149-				id:      avdName,
devices/avd.go-150-				name:    displayName,
devices/avd.go-151-				version: version,
devices/avd.go-152-				state:   "offline",
devices/avd.go-153-			})
devices/avd.go-154-		}
devices/avd.go-155-	}
devices/avd.go-156-
devices/avd.go-157-	return offlineDevices, nil
devices/avd.go-158-}

</shell_result>


Script executed:

#!/bin/bash
# Read context around AndroidDevice creation in android.go
sed -n '395,440p' devices/android.go

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

		_, err := d.runAdbCommand(cmd...)
		if err != nil {
			return fmt.Errorf("failed to execute gesture action %s: %v", action.Type, err)
		}
	}

	return nil
}

func parseAdbDevicesOutput(output string) []ControllableDevice {
	var devices []ControllableDevice

	lines := strings.Split(output, "\n")
	for i := 1; i < len(lines); i++ {
		line := strings.TrimSpace(lines[i])
		parts := strings.Fields(line)
		if len(parts) == 2 {
			transportID := parts[0]
			status := parts[1]
			if status == "device" {
				deviceID := transportID

				// for emulators, use AVD name as the consistent ID
				if strings.HasPrefix(transportID, "emulator-") {
					avdName := getAVDName(transportID)
					if avdName != "" {
						deviceID = avdName
					}
				}

				devices = append(devices, &AndroidDevice{
					id:          deviceID,
					transportID: transportID,
					name:        getAndroidDeviceName(transportID),
					version:     getAndroidDeviceVersion(transportID),
					state:       "online",
				})
			}
		}
	}

	return devices
}

// getAVDName returns the AVD name for an emulator, or empty string if not an emulator
func getAVDName(transportID string) string {

</shell_result>


Script executed:

#!/bin/bash
# Find getAVDName implementation
rg -n "func getAVDName" --type go -A 20

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

devices/android.go:440:func getAVDName(transportID string) string {
devices/android.go-441-	avdCmd := exec.Command(getAdbPath(), "-s", transportID, "shell", "getprop", "ro.boot.qemu.avd_name")
devices/android.go-442-	avdOutput, err := avdCmd.CombinedOutput()
devices/android.go-443-	if err == nil && len(avdOutput) > 0 {
devices/android.go-444-		avdName := strings.TrimSpace(string(avdOutput))
devices/android.go-445-		return avdName
devices/android.go-446-	}
devices/android.go-447-	return ""
devices/android.go-448-}
devices/android.go-449-
devices/android.go-450-func getAndroidDeviceName(deviceID string) string {
devices/android.go-451-	// try getting AVD name first (for emulators)
devices/android.go-452-	avdName := getAVDName(deviceID)
devices/android.go-453-	if avdName != "" {
devices/android.go-454-		return strings.ReplaceAll(avdName, "_", " ")
devices/android.go-455-	}
devices/android.go-456-
devices/android.go-457-	// fall back to product model
devices/android.go-458-	modelCmd := exec.Command(getAdbPath(), "-s", deviceID, "shell", "getprop", "ro.product.model")
devices/android.go-459-	modelOutput, err := modelCmd.CombinedOutput()
devices/android.go-460-	if err == nil && len(modelOutput) > 0 {

</shell_result>


Android emulator config.ini AvdId field format


<web_search_result>
The AvdId in config.ini is the AVD's identifier — typically the AVD directory/name (the name you gave the device, e.g. Pixel2 or Nexus_6P_API_28). It is a plain string (no special encoding) used as the emulator's ID and normally matches the .avd folder name. [1][2]

Sources:

  • Android emulator AVD template config showing AvdId lines. [1]
  • Example AVD config.ini from Stack Overflow showing AvdId usage. [2]
    </web_search_result>

Script executed:

#!/bin/bash
# Trace the complete flow: how online devices are added to map vs how offline are checked
echo "=== Online device ID assignment (from android.go) ==="
sed -n '415,432p' devices/android.go

echo -e "\n=== Map population (from common.go) ==="
sed -n '79,90p' devices/common.go

echo -e "\n=== Offline lookup (from avd.go) ==="
sed -n '128,132p' devices/avd.go

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

=== Online device ID assignment (from android.go) ===
				deviceID := transportID

				// for emulators, use AVD name as the consistent ID
				if strings.HasPrefix(transportID, "emulator-") {
					avdName := getAVDName(transportID)
					if avdName != "" {
						deviceID = avdName
					}
				}

				devices = append(devices, &AndroidDevice{
					id:          deviceID,
					transportID: transportID,
					name:        getAndroidDeviceName(transportID),
					version:     getAndroidDeviceVersion(transportID),
					state:       "online",
				})
			}

=== Map population (from common.go) ===
	if includeOffline {
		startOfflineAndroid := time.Now()
		// build map of online device IDs for quick lookup
		onlineDeviceIDs := make(map[string]bool)
		for _, device := range androidDevices {
			onlineDeviceIDs[device.ID()] = true
		}

		offlineEmulators, err := getOfflineAndroidEmulators(onlineDeviceIDs)
		offlineAndroidDuration = time.Since(startOfflineAndroid).Milliseconds()
		if err != nil {
			utils.Verbose("Warning: Failed to get offline Android emulators: %v", err)

=== Offline lookup (from avd.go) ===
	for avdName, info := range avdDetails {
		// check if this AVD is already online by checking if AvdId is in online device IDs
		// the avdName from the .ini file should match the device ID when online
		_, isOnline := onlineDeviceIDs[info.AvdId]

</shell_result>


Script executed:

#!/bin/bash
# Check the getAVDDetails function for how it handles missing AvdId
sed -n '49,111p' devices/avd.go | cat -n

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

     1	// getAVDDetails retrieves AVD information by reading .ini files directly
     2	func getAVDDetails() (map[string]AVDInfo, error) {
     3		avdMap := make(map[string]AVDInfo)
     4	
     5		homeDir, err := os.UserHomeDir()
     6		if err != nil {
     7			return avdMap, err
     8		}
     9	
    10		avdDir := filepath.Join(homeDir, ".android", "avd")
    11		pattern := filepath.Join(avdDir, "*.ini")
    12	
    13		matches, err := filepath.Glob(pattern)
    14		if err != nil {
    15			return avdMap, err
    16		}
    17	
    18		for _, iniFile := range matches {
    19			// extract avd name from .ini filename
    20			avdName := strings.TrimSuffix(filepath.Base(iniFile), ".ini")
    21	
    22			// read the .ini file to get the path
    23			iniConfig, err := ini.Load(iniFile)
    24			if err != nil {
    25				utils.Verbose("Failed to read %s: %v", iniFile, err)
    26				continue
    27			}
    28	
    29			avdPath := iniConfig.Section("").Key("path").String()
    30			if avdPath == "" {
    31				continue
    32			}
    33	
    34			// read the config.ini inside the .avd directory
    35			configPath := filepath.Join(avdPath, "config.ini")
    36			configData, err := ini.Load(configPath)
    37			if err != nil {
    38				utils.Verbose("Failed to read %s: %v", configPath, err)
    39				continue
    40			}
    41	
    42			displayName := configData.Section("").Key("avd.ini.displayname").String()
    43			if displayName == "" {
    44				continue
    45			}
    46	
    47			// extract API level from target (e.g., "android-31" -> "31")
    48			target := configData.Section("").Key("target").String()
    49			apiLevel := strings.TrimPrefix(target, "android-")
    50	
    51			// get AvdId for matching with online devices
    52			avdId := configData.Section("").Key("AvdId").String()
    53	
    54			avdMap[avdName] = AVDInfo{
    55				Name:     displayName,
    56				Device:   displayName,
    57				APILevel: apiLevel,
    58				AvdId:    avdId,
    59			}
    60		}
    61	
    62		return avdMap, nil
    63	}

</shell_result>


Android AVD config.ini AvdId field mandatory required always present


<web_search_result>
Short answer: No — AvdId in an AVD's config.ini is not strictly mandatory. The emulator's code accepts a missing config.ini (and older AVDs may omit AvdId), though modern AVD templates and tools usually write AvdId into config.ini and some external tools expect it. [1][2][3]

Sources:

  • emulator/AVD code showing config.ini may be absent and is optional. [1]
  • official AVD config.ini template including AvdId (so current tools typically include it). [2]
  • avdmanager/AVD tooling docs (AVD layout and tools generate .ini files). [3]
    </web_search_result>

Script executed:

#!/bin/bash
# Search for any tests or examples that might show AvdId usage
rg -n "AvdId" --type go -B 2 -A 2

Repository: mobile-next/mobilecli


<shell_result>
Repository: mobile-next/mobilecli
Exit code: 0

stdout:

devices/avd.go-15-	Device   string
devices/avd.go-16-	APILevel string
devices/avd.go:17:	AvdId    string
devices/avd.go-18-}
devices/avd.go-19-
--
devices/avd.go-97-		apiLevel := strings.TrimPrefix(target, "android-")
devices/avd.go-98-
devices/avd.go:99:		// get AvdId for matching with online devices
devices/avd.go:100:		avdId := configData.Section("").Key("AvdId").String()
devices/avd.go-101-
devices/avd.go-102-		avdMap[avdName] = AVDInfo{
--
devices/avd.go-104-			Device:   displayName,
devices/avd.go-105-			APILevel: apiLevel,
devices/avd.go:106:			AvdId:    avdId,
devices/avd.go-107-		}
devices/avd.go-108-	}
--
devices/avd.go-127-	// create offline device entries for AVDs that are not running
devices/avd.go-128-	for avdName, info := range avdDetails {
devices/avd.go:129:		// check if this AVD is already online by checking if AvdId is in online device IDs
devices/avd.go-130-		// the avdName from the .ini file should match the device ID when online
devices/avd.go:131:		_, isOnline := onlineDeviceIDs[info.AvdId]
devices/avd.go-132-
devices/avd.go-133-		if !isOnline {

</shell_result>


Based on my verification, I've found that:

  1. AvdId source: Line 100 in avd.go reads AvdId from config.ini using .String(), which returns an empty string if the key doesn't exist.

  2. Online device matching: Lines 415-421 in android.go show that online emulators use the AVD name from the ro.boot.qemu.avd_name property (retrieved via adb).

  3. The actual issue: According to Android documentation, AvdId is not mandatory in config.ini. Older AVDs may omit it entirely. When missing, info.AvdId becomes an empty string, causing the lookup at line 131 to fail.

  4. No fallback logic: The code doesn't fall back to avdName if AvdId is empty, meaning emulators without the AvdId field will always appear offline.


Verify that AvdId is always populated; use avdName as fallback if missing.

The code matches offline emulators against online devices using info.AvdId from config.ini (line 131). However, AvdId is not mandatory in config.ini—older AVDs may omit it entirely. When missing, info.AvdId becomes an empty string, causing the online device lookup to fail. This results in emulators without an explicit AvdId field appearing offline even when running. Add a fallback to use avdName if AvdId is empty to ensure all AVDs are correctly matched against running devices.

🤖 Prompt for AI Agents
In devices/avd.go around lines 128-131, the code checks onlineDeviceIDs using
info.AvdId which can be an empty string because AvdId is optional in config.ini;
change the lookup to first use info.AvdId and if that is empty use the avdName
as a fallback (e.g., derive an idVar := info.AvdId; if idVar == "" { idVar =
avdName } and then check onlineDeviceIDs[idVar]) so emulators that omit AvdId
are correctly matched as online.

@gmegidish gmegidish merged commit 8cd12f5 into main Nov 26, 2025
15 checks passed
@gmegidish gmegidish deleted the fix-remove-dependency-on-avdmanager branch November 26, 2025 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants