Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ public class AdbDeviceInfo
/// </summary>
public string? TransportId { get; set; }

/// <summary>
/// Primary CPU ABI from ro.product.cpu.abilist (e.g., "arm64-v8a", "x86_64").
/// </summary>
public string? CpuAbi { get; set; }

/// <summary>
/// Device manufacturer from ro.product.manufacturer (e.g., "Google", "Samsung").
/// </summary>
public string? Manufacturer { get; set; }

/// <summary>
/// Android release version from ro.build.version.release (e.g., "16", "14").
/// </summary>
public string? ReleaseVersion { get; set; }

/// <summary>
/// Android SDK level from ro.build.version.sdk (e.g., "36", "34").
/// </summary>
public string? SdkVersion { get; set; }

/// <summary>
/// Whether this device is an emulator.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string!, string!>? environmentVariables = null, System.Action<System.Diagnostics.TraceLevel, string!>? 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<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbDeviceInfo!>!>!
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!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string!, string!>? environmentVariables = null, System.Action<System.Diagnostics.TraceLevel, string!>? 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<System.Collections.Generic.IReadOnlyList<Xamarin.Android.Tools.AdbDeviceInfo!>!>!
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!
Expand Down
83 changes: 82 additions & 1 deletion src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,50 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati
return FirstNonEmptyLine (stdout.ToString ());
}

/// <summary>
/// Populates null device metadata fields from <c>adb shell getprop</c>.
/// Skips offline devices and preserves existing non-empty values.
/// </summary>
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);
}

/// <summary>
/// Runs a shell command on a device via 'adb -s &lt;serial&gt; shell &lt;command&gt;'.
/// Returns the full stdout output trimmed, or <c>null</c> on failure.
Expand Down Expand Up @@ -244,6 +288,44 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati
return null;
}

async Task<string?> 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;
}
}

/// <summary>
/// Extracts the first ABI from a comma-separated <c>ro.product.cpu.abilist</c> value.
/// </summary>
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;
}

/// <summary>
/// Returns the trimmed value, or null for null/empty/whitespace input.
/// </summary>
static string? TrimOrNull (string? value)
{
if (string.IsNullOrWhiteSpace (value))
return null;

return value.Trim ();
}

/// <summary>
/// Sets up reverse port forwarding via 'adb -s &lt;serial&gt; reverse &lt;remote&gt; &lt;local&gt;'.
/// </summary>
Expand Down Expand Up @@ -668,4 +750,3 @@ public static IReadOnlyList<AdbDeviceInfo> MergeDevicesAndEmulators (IReadOnlyLi
return result;
}
}

102 changes: 102 additions & 0 deletions tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> ();
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<string, System.Threading.CancellationToken, Task<IReadOnlyList<AdbPortRule>>> listForwards;
Expand All @@ -1320,6 +1408,20 @@ public override Task RemoveForwardPortAsync (string serial, AdbPortSpec local, S
=> removeForward (serial, local, cancellationToken);
}

sealed class EnrichmentTestAdbRunner : AdbRunner
{
readonly Func<string, string, System.Threading.CancellationToken, Task<string>> getShellProperty;

public EnrichmentTestAdbRunner (Func<string, string, System.Threading.CancellationToken, Task<string>> getShellProperty)
: base ("/fake/sdk/platform-tools/adb")
{
this.getShellProperty = getShellProperty;
}

public override Task<string> GetShellPropertyAsync (string serial, string propertyName, System.Threading.CancellationToken cancellationToken = default)
=> getShellProperty (serial, propertyName, cancellationToken);
}

[Test]
public void ListForwardPortsAsync_EmptySerial_ThrowsArgumentException ()
{
Expand Down