From 91e984414960917f02e742c2b91ee8fa8e509a37 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 2 Jun 2026 21:54:04 +0000
Subject: [PATCH 1/2] Initial plan
From 9b988a78b21e306cbb28cc08161776f2fa6cd23c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 2 Jun 2026 22:04:16 +0000
Subject: [PATCH 2/2] Add AdbRunner device enrichment API and tests
Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com>
---
.../Models/AdbDeviceInfo.cs | 20 ++++
.../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 9 ++
.../netstandard2.0/PublicAPI.Unshipped.txt | 9 ++
.../Runners/AdbRunner.cs | 83 +++++++++++++-
.../AdbRunnerTests.cs | 102 ++++++++++++++++++
5 files changed, 222 insertions(+), 1 deletion(-)
diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs b/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs
index a14941ee..40cb21cc 100644
--- a/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs
+++ b/src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs
@@ -55,6 +55,26 @@ public class AdbDeviceInfo
///
public string? TransportId { get; set; }
+ ///
+ /// Primary CPU ABI from ro.product.cpu.abilist (e.g., "arm64-v8a", "x86_64").
+ ///
+ public string? CpuAbi { get; set; }
+
+ ///
+ /// Device manufacturer from ro.product.manufacturer (e.g., "Google", "Samsung").
+ ///
+ public string? Manufacturer { get; set; }
+
+ ///
+ /// Android release version from ro.build.version.release (e.g., "16", "14").
+ ///
+ public string? ReleaseVersion { get; set; }
+
+ ///
+ /// Android SDK level from ro.build.version.sdk (e.g., "36", "34").
+ ///
+ public string? SdkVersion { get; set; }
+
///
/// Whether this device is an emulator.
///
diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt
index 21b4baba..3fda55d1 100644
--- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt
+++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/net10.0/PublicAPI.Unshipped.txt
@@ -3,15 +3,23 @@ Xamarin.Android.Tools.AdbDeviceInfo
Xamarin.Android.Tools.AdbDeviceInfo.AdbDeviceInfo() -> void
Xamarin.Android.Tools.AdbDeviceInfo.AvdName.get -> string?
Xamarin.Android.Tools.AdbDeviceInfo.AvdName.set -> void
+Xamarin.Android.Tools.AdbDeviceInfo.CpuAbi.get -> string?
+Xamarin.Android.Tools.AdbDeviceInfo.CpuAbi.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Description.get -> string!
Xamarin.Android.Tools.AdbDeviceInfo.Description.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Device.get -> string?
Xamarin.Android.Tools.AdbDeviceInfo.Device.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.IsEmulator.get -> bool
+Xamarin.Android.Tools.AdbDeviceInfo.Manufacturer.get -> string?
+Xamarin.Android.Tools.AdbDeviceInfo.Manufacturer.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Model.get -> string?
Xamarin.Android.Tools.AdbDeviceInfo.Model.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Product.get -> string?
Xamarin.Android.Tools.AdbDeviceInfo.Product.set -> void
+Xamarin.Android.Tools.AdbDeviceInfo.ReleaseVersion.get -> string?
+Xamarin.Android.Tools.AdbDeviceInfo.ReleaseVersion.set -> void
+Xamarin.Android.Tools.AdbDeviceInfo.SdkVersion.get -> string?
+Xamarin.Android.Tools.AdbDeviceInfo.SdkVersion.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Serial.get -> string!
Xamarin.Android.Tools.AdbDeviceInfo.Serial.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Status.get -> Xamarin.Android.Tools.AdbDeviceStatus
@@ -32,6 +40,7 @@ Xamarin.Android.Tools.AdbDeviceType.Device = 0 -> Xamarin.Android.Tools.AdbDevic
Xamarin.Android.Tools.AdbDeviceType.Emulator = 1 -> Xamarin.Android.Tools.AdbDeviceType
Xamarin.Android.Tools.AdbRunner
Xamarin.Android.Tools.AdbRunner.AdbRunner(string! adbPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void
+virtual Xamarin.Android.Tools.AdbRunner.EnrichDeviceAsync(Xamarin.Android.Tools.AdbDeviceInfo! device, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
virtual Xamarin.Android.Tools.AdbRunner.ListDevicesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!
Xamarin.Android.Tools.AdbRunner.StopEmulatorAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Xamarin.Android.Tools.AdbRunner.WaitForDeviceAsync(string? serial = null, System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
diff --git a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
index 21b4baba..3fda55d1 100644
--- a/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/src/Xamarin.Android.Tools.AndroidSdk/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -3,15 +3,23 @@ Xamarin.Android.Tools.AdbDeviceInfo
Xamarin.Android.Tools.AdbDeviceInfo.AdbDeviceInfo() -> void
Xamarin.Android.Tools.AdbDeviceInfo.AvdName.get -> string?
Xamarin.Android.Tools.AdbDeviceInfo.AvdName.set -> void
+Xamarin.Android.Tools.AdbDeviceInfo.CpuAbi.get -> string?
+Xamarin.Android.Tools.AdbDeviceInfo.CpuAbi.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Description.get -> string!
Xamarin.Android.Tools.AdbDeviceInfo.Description.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Device.get -> string?
Xamarin.Android.Tools.AdbDeviceInfo.Device.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.IsEmulator.get -> bool
+Xamarin.Android.Tools.AdbDeviceInfo.Manufacturer.get -> string?
+Xamarin.Android.Tools.AdbDeviceInfo.Manufacturer.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Model.get -> string?
Xamarin.Android.Tools.AdbDeviceInfo.Model.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Product.get -> string?
Xamarin.Android.Tools.AdbDeviceInfo.Product.set -> void
+Xamarin.Android.Tools.AdbDeviceInfo.ReleaseVersion.get -> string?
+Xamarin.Android.Tools.AdbDeviceInfo.ReleaseVersion.set -> void
+Xamarin.Android.Tools.AdbDeviceInfo.SdkVersion.get -> string?
+Xamarin.Android.Tools.AdbDeviceInfo.SdkVersion.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Serial.get -> string!
Xamarin.Android.Tools.AdbDeviceInfo.Serial.set -> void
Xamarin.Android.Tools.AdbDeviceInfo.Status.get -> Xamarin.Android.Tools.AdbDeviceStatus
@@ -32,6 +40,7 @@ Xamarin.Android.Tools.AdbDeviceType.Device = 0 -> Xamarin.Android.Tools.AdbDevic
Xamarin.Android.Tools.AdbDeviceType.Emulator = 1 -> Xamarin.Android.Tools.AdbDeviceType
Xamarin.Android.Tools.AdbRunner
Xamarin.Android.Tools.AdbRunner.AdbRunner(string! adbPath, System.Collections.Generic.IDictionary? environmentVariables = null, System.Action? logger = null) -> void
+virtual Xamarin.Android.Tools.AdbRunner.EnrichDeviceAsync(Xamarin.Android.Tools.AdbDeviceInfo! device, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
virtual Xamarin.Android.Tools.AdbRunner.ListDevicesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!
Xamarin.Android.Tools.AdbRunner.StopEmulatorAsync(string! serial, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Xamarin.Android.Tools.AdbRunner.WaitForDeviceAsync(string? serial = null, System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
index eefb155c..7d77a1c7 100644
--- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
+++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
@@ -177,6 +177,50 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati
return FirstNonEmptyLine (stdout.ToString ());
}
+ ///
+ /// Populates null device metadata fields from adb shell getprop.
+ /// Skips offline devices and preserves existing non-empty values.
+ ///
+ public virtual async Task EnrichDeviceAsync (AdbDeviceInfo device, CancellationToken cancellationToken = default)
+ {
+ if (device is null)
+ throw new ArgumentNullException (nameof (device));
+
+ if (device.Status != AdbDeviceStatus.Online) {
+ logger.Invoke (TraceLevel.Verbose, $"Skipping enrichment for {device.Status} device {device.Serial}");
+ return;
+ }
+
+ var abiListTask = GetShellPropertySafeAsync (device.Serial, "ro.product.cpu.abilist", cancellationToken);
+ var abiTask = GetShellPropertySafeAsync (device.Serial, "ro.product.cpu.abi", cancellationToken);
+ var manufacturerTask = GetShellPropertySafeAsync (device.Serial, "ro.product.manufacturer", cancellationToken);
+ var brandTask = GetShellPropertySafeAsync (device.Serial, "ro.product.brand", cancellationToken);
+ var releaseTask = GetShellPropertySafeAsync (device.Serial, "ro.build.version.release", cancellationToken);
+ var sdkTask = GetShellPropertySafeAsync (device.Serial, "ro.build.version.sdk", cancellationToken);
+ var modelTask = GetShellPropertySafeAsync (device.Serial, "ro.product.model", cancellationToken);
+
+ await Task.WhenAll (abiListTask, abiTask, manufacturerTask, brandTask, releaseTask, sdkTask, modelTask).ConfigureAwait (false);
+
+ if (string.IsNullOrWhiteSpace (device.CpuAbi)) {
+ device.CpuAbi = ParsePrimaryAbi (abiListTask.Result) ??
+ TrimOrNull (abiTask.Result);
+ }
+
+ if (string.IsNullOrWhiteSpace (device.Manufacturer)) {
+ device.Manufacturer = TrimOrNull (manufacturerTask.Result) ??
+ TrimOrNull (brandTask.Result);
+ }
+
+ if (string.IsNullOrWhiteSpace (device.ReleaseVersion))
+ device.ReleaseVersion = TrimOrNull (releaseTask.Result);
+
+ if (string.IsNullOrWhiteSpace (device.SdkVersion))
+ device.SdkVersion = TrimOrNull (sdkTask.Result);
+
+ if (string.IsNullOrWhiteSpace (device.Model))
+ device.Model = TrimOrNull (modelTask.Result);
+ }
+
///
/// Runs a shell command on a device via 'adb -s <serial> shell <command>'.
/// Returns the full stdout output trimmed, or null on failure.
@@ -244,6 +288,44 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati
return null;
}
+ async Task GetShellPropertySafeAsync (string serial, string propertyName, CancellationToken cancellationToken)
+ {
+ try {
+ return await GetShellPropertyAsync (serial, propertyName, cancellationToken).ConfigureAwait (false);
+ } catch (OperationCanceledException) {
+ throw;
+ } catch (Exception ex) {
+ logger.Invoke (TraceLevel.Warning, $"adb shell getprop {propertyName} threw for {serial}: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Extracts the first ABI from a comma-separated ro.product.cpu.abilist value.
+ ///
+ static string? ParsePrimaryAbi (string? abiList)
+ {
+ var value = TrimOrNull (abiList);
+ if (value is null)
+ return null;
+
+ var comma = value.IndexOf (',');
+ return comma >= 0
+ ? TrimOrNull (value.Substring (0, comma))
+ : value;
+ }
+
+ ///
+ /// Returns the trimmed value, or null for null/empty/whitespace input.
+ ///
+ static string? TrimOrNull (string? value)
+ {
+ if (string.IsNullOrWhiteSpace (value))
+ return null;
+
+ return value.Trim ();
+ }
+
///
/// Sets up reverse port forwarding via 'adb -s <serial> reverse <remote> <local>'.
///
@@ -668,4 +750,3 @@ public static IReadOnlyList MergeDevicesAndEmulators (IReadOnlyLi
return result;
}
}
-
diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs
index acd29f1d..0163fa58 100644
--- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs
+++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs
@@ -1299,6 +1299,94 @@ public async Task RemoveAllForwardPortsAsync_EmptyList_IsNoOp ()
Assert.IsEmpty (removed, "No removes should be issued when the listing is empty.");
}
+ [Test]
+ public async Task EnrichDeviceAsync_Offline_NoOp ()
+ {
+ var calls = new List ();
+ var runner = new EnrichmentTestAdbRunner ((_, propertyName, _) => {
+ calls.Add (propertyName);
+ return Task.FromResult ("unexpected");
+ });
+ var device = new AdbDeviceInfo {
+ Serial = "emulator-5554",
+ Status = AdbDeviceStatus.Offline,
+ Type = AdbDeviceType.Emulator,
+ };
+
+ await runner.EnrichDeviceAsync (device);
+
+ Assert.IsEmpty (calls, "Offline devices should not be queried.");
+ Assert.IsNull (device.CpuAbi);
+ Assert.IsNull (device.Manufacturer);
+ Assert.IsNull (device.ReleaseVersion);
+ Assert.IsNull (device.SdkVersion);
+ Assert.IsNull (device.Model);
+ }
+
+ [Test]
+ public async Task EnrichDeviceAsync_PopulatesFieldsAndPreservesExistingValues ()
+ {
+ var runner = new EnrichmentTestAdbRunner ((_, propertyName, _) => Task.FromResult (propertyName switch {
+ "ro.product.cpu.abilist" => "arm64-v8a,armeabi-v7a",
+ "ro.product.cpu.abi" => "armeabi-v7a",
+ "ro.product.manufacturer" => "Google",
+ "ro.product.brand" => "google",
+ "ro.build.version.release" => "16",
+ "ro.build.version.sdk" => "36",
+ "ro.product.model" => "Pixel 9 Pro",
+ _ => null,
+ }));
+
+ var device = new AdbDeviceInfo {
+ Serial = "device-1",
+ Status = AdbDeviceStatus.Online,
+ Type = AdbDeviceType.Device,
+ Manufacturer = "ExistingManufacturer",
+ };
+
+ await runner.EnrichDeviceAsync (device);
+
+ Assert.AreEqual ("arm64-v8a", device.CpuAbi);
+ Assert.AreEqual ("ExistingManufacturer", device.Manufacturer, "Existing values should be preserved.");
+ Assert.AreEqual ("16", device.ReleaseVersion);
+ Assert.AreEqual ("36", device.SdkVersion);
+ Assert.AreEqual ("Pixel 9 Pro", device.Model);
+ }
+
+ [Test]
+ public async Task EnrichDeviceAsync_ToleratesFailuresAndUsesFallbacks ()
+ {
+ var runner = new EnrichmentTestAdbRunner ((_, propertyName, _) => {
+ if (propertyName == "ro.product.cpu.abilist")
+ throw new InvalidOperationException ("abilist failed");
+ if (propertyName == "ro.build.version.release")
+ throw new InvalidOperationException ("release failed");
+
+ return Task.FromResult (propertyName switch {
+ "ro.product.cpu.abi" => "x86_64",
+ "ro.product.manufacturer" => null,
+ "ro.product.brand" => "Google",
+ "ro.build.version.sdk" => "34",
+ "ro.product.model" => "Pixel 8",
+ _ => null,
+ });
+ });
+
+ var device = new AdbDeviceInfo {
+ Serial = "device-2",
+ Status = AdbDeviceStatus.Online,
+ Type = AdbDeviceType.Device,
+ };
+
+ await runner.EnrichDeviceAsync (device);
+
+ Assert.AreEqual ("x86_64", device.CpuAbi);
+ Assert.AreEqual ("Google", device.Manufacturer);
+ Assert.IsNull (device.ReleaseVersion);
+ Assert.AreEqual ("34", device.SdkVersion);
+ Assert.AreEqual ("Pixel 8", device.Model);
+ }
+
sealed class RecordingAdbRunner : AdbRunner
{
readonly Func>> listForwards;
@@ -1320,6 +1408,20 @@ public override Task RemoveForwardPortAsync (string serial, AdbPortSpec local, S
=> removeForward (serial, local, cancellationToken);
}
+ sealed class EnrichmentTestAdbRunner : AdbRunner
+ {
+ readonly Func> getShellProperty;
+
+ public EnrichmentTestAdbRunner (Func> getShellProperty)
+ : base ("/fake/sdk/platform-tools/adb")
+ {
+ this.getShellProperty = getShellProperty;
+ }
+
+ public override Task GetShellPropertyAsync (string serial, string propertyName, System.Threading.CancellationToken cancellationToken = default)
+ => getShellProperty (serial, propertyName, cancellationToken);
+ }
+
[Test]
public void ListForwardPortsAsync_EmptySerial_ThrowsArgumentException ()
{