Commit d3c269d
## Summary
Wraps `adb` CLI operations for device management. Addresses #279.
Parsing/formatting/merging logic ported from `dotnet/android` `GetAvailableAndroidDevices` MSBuild task, enabling code sharing via the `external/xamarin-android-tools` submodule. See draft PR: dotnet/android#10880
## Public API
```csharp
public class AdbRunner
{
// Constructor — requires full path to adb executable
public AdbRunner(string adbPath, IDictionary<string, string>? environmentVariables = null);
// Instance methods (async, invoke adb process)
public Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync(CancellationToken ct = default);
public Task WaitForDeviceAsync(string? serial = null, TimeSpan? timeout = null, CancellationToken ct = default);
public Task StopEmulatorAsync(string serial, CancellationToken ct = default);
// Static helpers — public so dotnet/android can call without instantiating AdbRunner
public static List<AdbDeviceInfo> ParseAdbDevicesOutput(IEnumerable<string> lines);
public static AdbDeviceStatus MapAdbStateToStatus(string adbState);
public static string BuildDeviceDescription(AdbDeviceInfo device, Action<TraceLevel, string>? logger = null);
public static string FormatDisplayName(string avdName);
public static List<AdbDeviceInfo> MergeDevicesAndEmulators(
IReadOnlyList<AdbDeviceInfo> adbDevices,
IReadOnlyList<string> availableEmulators,
Action<TraceLevel, string>? logger = null);
}
```
**Internal methods** (not part of public API):
- `GetEmulatorAvdNameAsync` — queries AVD name via `adb emu avd name` with TCP console fallback
- `ProcessUtils.ThrowIfFailed` — shared exit code validation (string and StringWriter overloads)
## Key Design Decisions
- **Constructor takes `string adbPath`**: Callers pass the resolved path; no lazy `Func<>` indirection. Optional `environmentVariables` dictionary for ANDROID_HOME/JAVA_HOME/PATH.
- **Static parsing methods** are `public static` so `dotnet/android` can call them without instantiating `AdbRunner` (e.g., `GetAvailableAndroidDevices` MSBuild task passes `List<string>` to `ParseAdbDevicesOutput`)
- **`IEnumerable<string>` overload**: `dotnet/android` passes `List<string>` directly from output lines
- **Logger parameter**: `BuildDeviceDescription` and `MergeDevicesAndEmulators` accept `Action<TraceLevel, string>?` — `dotnet/android` passes `this.CreateTaskLogger()` for MSBuild trace output
- **Regex with explicit state list**: Uses `\s+` separator to match one or more whitespace characters (spaces or tabs). Matches explicit known states with `IgnoreCase`. Daemon startup lines (`*`) are pre-filtered.
- **Exit code checking**: `ListDevicesAsync`, `WaitForDeviceAsync`, and `StopEmulatorAsync` throw `InvalidOperationException` with stderr context on non-zero exit via `ProcessUtils.ThrowIfFailed` (internal)
- **`MapAdbStateToStatus` as switch expression**: Simple value mapping uses C# switch expression for conciseness
- **Property patterns instead of null-forgiving**: Uses `is { Length: > 0 }` patterns throughout for null checks on `netstandard2.0` where `string.IsNullOrEmpty()` lacks `[NotNullWhen(false)]`
- **FormatDisplayName**: Lowercases before `ToTitleCase` to normalize mixed-case input (e.g., "PiXeL" → "Pixel")
- **Environment variables via `StartProcess`**: Runners pass env vars dictionary to `ProcessUtils.StartProcess`. `AndroidEnvironmentHelper.GetEnvironmentVariables()` builds the dict.
## Tests
**45 unit tests** (`AdbRunnerTests.cs`):
- **ParseAdbDevicesOutput**: real-world data, empty output, single/multiple devices, mixed states, daemon messages, IP:port, Windows newlines, recovery/sideload, tab-separated output
- **FormatDisplayName**: underscores, title case, API capitalization, mixed case, special chars, empty
- **MapAdbStateToStatus**: all known states + unknown (recovery, sideload)
- **MergeDevicesAndEmulators**: no emulators, no running, mixed, case-insensitive dedup, sorting
- **Constructor**: valid path, null/empty throws
- **WaitForDeviceAsync**: timeout validation (negative, zero)
**4 integration tests** (`RunnerIntegrationTests.cs`):
- Run only when `TF_BUILD=True` or `CI=true` (case-insensitive truthy check), skipped locally
- Require pre-installed JDK (`JAVA_HOME`) and Android SDK (`ANDROID_HOME`) on CI agent
- `Assert.Ignore` when `ANDROID_HOME` missing (no bootstrap/network dependency)
- Cover: constructor, `ListDevicesAsync`, `WaitForDeviceAsync` timeout, tool discovery
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
1 parent f4d2f2d commit d3c269d
File tree
12 files changed
+1244
-19
lines changed- .github
- src/Xamarin.Android.Tools.AndroidSdk
- Models
- Runners
- tests/Xamarin.Android.Tools.AndroidSdk-Tests
12 files changed
+1244
-19
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| 52 | + | |
| 53 | + | |
52 | 54 | | |
53 | 55 | | |
54 | 56 | | |
| |||
Lines changed: 12 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
43 | 55 | | |
44 | 56 | | |
Lines changed: 62 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
Lines changed: 17 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
Lines changed: 13 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
203 | 203 | | |
204 | 204 | | |
205 | 205 | | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
206 | 233 | | |
207 | 234 | | |
208 | 235 | | |
| |||
0 commit comments