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 () {