diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7316c0565e8..32b29dd094f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,6 +1,6 @@ # Instructions for AIs -**.NET for Android** (formerly Xamarin.Android) - Open-source Android development bindings for .NET languages. `main` branch targets **.NET 10**. +**.NET for Android** (formerly Xamarin.Android) - Open-source Android development bindings for .NET languages. `main` branch targets **.NET 11**. ## Architecture - `src/Mono.Android/` - Android SDK bindings in C# diff --git a/Documentation/docs-mobile/building-apps/build-properties.md b/Documentation/docs-mobile/building-apps/build-properties.md index 39f87be1dc9..a5a3acaf7a1 100644 --- a/Documentation/docs-mobile/building-apps/build-properties.md +++ b/Documentation/docs-mobile/building-apps/build-properties.md @@ -1535,6 +1535,22 @@ If `DebugType` is not set or is the empty string, then the `DebugSymbols` property controls whether or not the Application is debuggable. +## Device + +Specifies which Android device or emulator to target when using +`dotnet run --device ` or MSBuild targets that interact with +devices (such as `Run`, `Install`, or `Uninstall`). + +The value must be the full device serial number or identifier as +returned by `adb devices`. For example, if the device serial is +`emulator-5554`, you must use `-p:Device=emulator-5554`. + +When set, this property is used to initialize the +[`AdbTarget`](#adbtarget) property with the value `-s ""`. + +For more information about device selection, see the +[.NET SDK device selection specification](https://github.com/dotnet/sdk/blob/2b9fc02a265c735f2132e4e3626e94962e48bdf5/documentation/specs/dotnet-run-for-maui.md). + ## DiagnosticAddress A value provided by `dotnet-dsrouter` such as `127.0.0.1`, the IP diff --git a/Documentation/docs-mobile/building-apps/build-targets.md b/Documentation/docs-mobile/building-apps/build-targets.md index b611efedb30..bd43e2dbc7f 100644 --- a/Documentation/docs-mobile/building-apps/build-targets.md +++ b/Documentation/docs-mobile/building-apps/build-targets.md @@ -36,6 +36,36 @@ Added in Xamarin.Android 10.2. Removes all files generated by the build process. +## ComputeAvailableDevices + +Queries and returns a list of available Android devices and emulators that can be used with `dotnet run`. + +This target is called automatically by the .NET SDK's `dotnet run` command to support device selection via the `--device` option. It returns an `@(Devices)` item group where each device has the following metadata: + +- **Description**: A human-friendly name (e.g., "Pixel 7 - API 35" for emulators, "Pixel 6 Pro" for physical devices) +- **Type**: Either "Device" or "Emulator" +- **Status**: Device status - "Online", "Offline", "Unauthorized", or "NoPermissions" +- **Model**: The device model identifier (optional) +- **Product**: The product name (optional) +- **Device**: The device name (optional) +- **TransportId**: The adb transport ID (optional) + +For example, to list all available devices: + +```shell +dotnet build -t:ComputeAvailableDevices +``` + +This target is part of the [.NET SDK device selection specification](https://github.com/dotnet/sdk/blob/2b9fc02a265c735f2132e4e3626e94962e48bdf5/documentation/specs/dotnet-run-for-maui.md) and enables commands like: + +```shell +dotnet run --device +``` + +When a device is selected via the `$(Device)` property, the [`$(AdbTarget)`](build-properties.md#adbtarget) property is automatically set to target that specific device for all adb operations. + +Added in .NET 11. + ## FinishAotProfiling Must be called *after* the [BuildAndStartAotProfiling](#buildandstartaotprofiling) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets index f248af1c83e..b28e987017f 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets @@ -9,6 +9,7 @@ This file contains targets specific for Android application projects. + @@ -26,6 +27,27 @@ This file contains targets specific for Android application projects. + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs new file mode 100644 index 00000000000..fe041ee49df --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs @@ -0,0 +1,211 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Xamarin.Android.Tasks; + +/// +/// MSBuild task that queries available Android devices and emulators using 'adb devices -l'. +/// Returns a list of devices with metadata for device selection in dotnet run. +/// +public class GetAvailableAndroidDevices : AndroidAdb +{ + enum DeviceType + { + Device, + Emulator + } + + // Pattern to match device lines: [key:value ...] + // Example: emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 + static readonly Regex AdbDevicesRegex = new(@"^([^\s]+)\s+(device|offline|unauthorized|no permissions)\s*(.*)$", RegexOptions.Compiled); + + readonly List output = []; + + [Output] + public ITaskItem [] Devices { get; set; } = []; + + public GetAvailableAndroidDevices () + { + Command = "devices"; + Arguments = "-l"; + } + + protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance) + { + base.LogEventsFromTextOutput (singleLine, messageImportance); + output.Add (singleLine); + } + + protected override void LogToolCommand (string message) => Log.LogDebugMessage (message); + + public override bool RunTask () + { + if (!base.RunTask ()) + return false; + + var devices = ParseAdbDevicesOutput (output); + Devices = devices.ToArray (); + + Log.LogDebugMessage ($"Found {Devices.Length} Android device(s)/emulator(s)"); + + return !Log.HasLoggedErrors; + } + + /// + /// Parses the output of 'adb devices -l' command. + /// Example output: + /// List of devices attached + /// emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1 + /// 0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2 + /// + List ParseAdbDevicesOutput (List lines) + { + var devices = new List (); + + foreach (var line in lines) { + // Skip the header line "List of devices attached" + if (line.Contains ("List of devices") || string.IsNullOrWhiteSpace (line)) + continue; + + var match = AdbDevicesRegex.Match (line); + if (!match.Success) + continue; + + var serial = match.Groups [1].Value.Trim (); + var state = match.Groups [2].Value.Trim (); + var properties = match.Groups [3].Value.Trim (); + + // Parse key:value pairs from the properties string + var propDict = new Dictionary (StringComparer.OrdinalIgnoreCase); + if (!string.IsNullOrWhiteSpace (properties)) { + // Split by whitespace and parse key:value pairs + var pairs = properties.Split ([' '], StringSplitOptions.RemoveEmptyEntries); + foreach (var pair in pairs) { + var colonIndex = pair.IndexOf (':'); + if (colonIndex > 0 && colonIndex < pair.Length - 1) { + var key = pair.Substring (0, colonIndex); + var value = pair.Substring (colonIndex + 1); + propDict [key] = value; + } + } + } + + // Determine device type: Emulator or Device + var deviceType = serial.StartsWith ("emulator-", StringComparison.OrdinalIgnoreCase) ? DeviceType.Emulator : DeviceType.Device; + + // Build a friendly description + var description = BuildDeviceDescription (serial, propDict, deviceType); + + // Map adb state to device status + var status = MapAdbStateToStatus (state); + + // Create the MSBuild item + var item = new TaskItem (serial); + item.SetMetadata ("Description", description); + item.SetMetadata ("Type", deviceType.ToString ()); + item.SetMetadata ("Status", status); + + // Add optional metadata for additional information + if (propDict.TryGetValue ("model", out var model)) + item.SetMetadata ("Model", model); + if (propDict.TryGetValue ("product", out var product)) + item.SetMetadata ("Product", product); + if (propDict.TryGetValue ("device", out var device)) + item.SetMetadata ("Device", device); + if (propDict.TryGetValue ("transport_id", out var transportId)) + item.SetMetadata ("TransportId", transportId); + + devices.Add (item); + } + + return devices; + } + + string BuildDeviceDescription (string serial, Dictionary properties, DeviceType deviceType) + { + // Try to build a human-friendly description + // Priority: AVD name (for emulators) > model > product > device > serial + + // For emulators, try to get the AVD display name + if (deviceType == DeviceType.Emulator) { + var avdName = GetEmulatorAvdDisplayName (serial); + if (!string.IsNullOrEmpty (avdName)) + return avdName!; + } + + if (properties.TryGetValue ("model", out var model) && !string.IsNullOrEmpty (model)) { + // Clean up model name - replace underscores with spaces + model = model.Replace ('_', ' '); + return model; + } + + if (properties.TryGetValue ("product", out var product) && !string.IsNullOrEmpty (product)) { + product = product.Replace ('_', ' '); + return product; + } + + if (properties.TryGetValue ("device", out var device) && !string.IsNullOrEmpty (device)) { + device = device.Replace ('_', ' '); + return device; + } + + // Fallback to serial number + return serial; + } + + static string MapAdbStateToStatus (string adbState) + { + // Map adb device states to the spec's status values + return adbState.ToLowerInvariant () switch { + "device" => "Online", + "offline" => "Offline", + "unauthorized" => "Unauthorized", + "no permissions" => "NoPermissions", + _ => "Unknown", + }; + } + + /// + /// Queries the emulator for its AVD name using 'adb -s emu avd name' + /// and formats it as a friendly display name. + /// + protected virtual string? GetEmulatorAvdDisplayName (string serial) + { + try { + var adbPath = System.IO.Path.Combine (ToolPath, ToolExe); + var outputLines = new List (); + + var exitCode = MonoAndroidHelper.RunProcess ( + adbPath, + $"-s {serial} emu avd name", + Log, + onOutput: (sender, e) => { + if (!string.IsNullOrEmpty (e.Data)) { + outputLines.Add (e.Data); + base.LogEventsFromTextOutput (e.Data, MessageImportance.Normal); + } + }, + logWarningOnFailure: false + ); + + if (exitCode == 0 && outputLines.Count > 0) { + var avdName = outputLines [0].Trim (); + // Verify it's not the "OK" response + if (!string.IsNullOrEmpty (avdName) && !avdName.Equals ("OK", StringComparison.OrdinalIgnoreCase)) { + // Format the AVD name: replace underscores with spaces + return avdName.Replace ('_', ' '); + } + } + } catch (Exception ex) { + Log.LogDebugMessage ($"Failed to get AVD display name for {serial}: {ex}"); + } + + return null; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetAvailableAndroidDevicesTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetAvailableAndroidDevicesTests.cs new file mode 100644 index 00000000000..ccbb78aea38 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetAvailableAndroidDevicesTests.cs @@ -0,0 +1,440 @@ +using Microsoft.Build.Framework; +using NUnit.Framework; +using System.Collections.Generic; +using System.Reflection; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + public class GetAvailableAndroidDevicesTests : BaseTest + { + List errors; + List warnings; + List messages; + MockBuildEngine engine; + + [SetUp] + public void Setup () + { + engine = new MockBuildEngine (TestContext.Out, errors = [], warnings = [], messages = []); + } + + /// + /// Mock version of GetAvailableAndroidDevices that returns known AVD names + /// + class MockGetAvailableAndroidDevices : GetAvailableAndroidDevices + { + readonly Dictionary AvdNames = new () { + { "emulator-5554", "Pixel 7 - API 35" }, + { "emulator-5556", "Pixel 9 Pro XL - API 36" } + }; + + protected override string? GetEmulatorAvdDisplayName (string serial) + { + if (AvdNames.TryGetValue (serial, out var name)) + return name; + return null; + } + } + + /// + /// Helper method to invoke the private ParseAdbDevicesOutput method via reflection + /// + ITaskItem [] ParseAdbDevicesOutput (MockGetAvailableAndroidDevices task, List lines) + { + var method = typeof (GetAvailableAndroidDevices).GetMethod ("ParseAdbDevicesOutput", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.IsNotNull (method, "ParseAdbDevicesOutput method should exist"); + var result = (List) method.Invoke (task, [lines]); + return result.ToArray (); + } + + [Test] + public void ParseRealWorldData () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + // Real data from adb devices -l + var lines = new List { + "List of devices attached", + "0A041FDD400327 device product:redfin model:Pixel_5 device:redfin transport_id:2", + "emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (2, devices.Length, "Should return two devices"); + + // First device: Pixel 5 (physical device) + var device1 = devices [0]; + Assert.AreEqual ("0A041FDD400327", device1.ItemSpec, "Device serial should match"); + Assert.AreEqual ("Device", device1.GetMetadata ("Type"), "Type should be Device"); + Assert.AreEqual ("Online", device1.GetMetadata ("Status"), "Status should be Online"); + Assert.AreEqual ("Pixel 5", device1.GetMetadata ("Description"), "Description should be 'Pixel 5'"); + Assert.AreEqual ("Pixel_5", device1.GetMetadata ("Model"), "Model metadata should be 'Pixel_5'"); + Assert.AreEqual ("redfin", device1.GetMetadata ("Product"), "Product metadata should be 'redfin'"); + Assert.AreEqual ("redfin", device1.GetMetadata ("Device"), "Device metadata should be 'redfin'"); + Assert.AreEqual ("2", device1.GetMetadata ("TransportId"), "TransportId should be '2'"); + + // Second device: Emulator - should have AVD name replacement applied + var device2 = devices [1]; + Assert.AreEqual ("emulator-5554", device2.ItemSpec, "Emulator serial should match"); + Assert.AreEqual ("Emulator", device2.GetMetadata ("Type"), "Type should be Emulator"); + Assert.AreEqual ("Online", device2.GetMetadata ("Status"), "Status should be Online"); + Assert.AreEqual ("Pixel 7 - API 35", device2.GetMetadata ("Description"), "Description should be replaced with AVD name"); + Assert.AreEqual ("sdk_gphone64_x86_64", device2.GetMetadata ("Model"), "Model metadata should be 'sdk_gphone64_x86_64'"); + Assert.AreEqual ("sdk_gphone64_x86_64", device2.GetMetadata ("Product"), "Product metadata should be 'sdk_gphone64_x86_64'"); + Assert.AreEqual ("emu64xa", device2.GetMetadata ("Device"), "Device metadata should be 'emu64xa'"); + Assert.AreEqual ("1", device2.GetMetadata ("TransportId"), "TransportId should be '1'"); + } + + [Test] + public void ParseEmptyOutput () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (0, devices.Length, "Should return no devices for empty list"); + } + + [Test] + public void ParseSingleEmulator () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (1, devices.Length, "Should return one device"); + var device = devices [0]; + Assert.AreEqual ("emulator-5554", device.ItemSpec, "Device serial should match"); + Assert.AreEqual ("Emulator", device.GetMetadata ("Type"), "Type should be Emulator"); + Assert.AreEqual ("Online", device.GetMetadata ("Status"), "Status should be Online"); + Assert.AreEqual ("Pixel 7 - API 35", device.GetMetadata ("Description"), "Description should be replaced with AVD name"); + Assert.AreEqual ("sdk_gphone64_arm64", device.GetMetadata ("Model"), "Model metadata should be present"); + Assert.AreEqual ("1", device.GetMetadata ("TransportId"), "TransportId should be present"); + } + + [Test] + public void ParseSinglePhysicalDevice () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (1, devices.Length, "Should return one device"); + var device = devices [0]; + Assert.AreEqual ("0A041FDD400327", device.ItemSpec, "Device serial should match"); + Assert.AreEqual ("Device", device.GetMetadata ("Type"), "Type should be Device"); + Assert.AreEqual ("Online", device.GetMetadata ("Status"), "Status should be Online"); + Assert.AreEqual ("Pixel 6 Pro", device.GetMetadata ("Description"), "Description should be cleaned up"); + Assert.AreEqual ("Pixel_6_Pro", device.GetMetadata ("Model"), "Model metadata should be present"); + Assert.AreEqual ("raven", device.GetMetadata ("Product"), "Product metadata should be present"); + Assert.AreEqual ("2", device.GetMetadata ("TransportId"), "TransportId should be present"); + } + + [Test] + public void ParseMultipleDevices () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1", + "emulator-5556 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64x transport_id:3", + "0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (3, devices.Length, "Should return three devices"); + + Assert.AreEqual ("emulator-5554", devices [0].ItemSpec); + Assert.AreEqual ("Emulator", devices [0].GetMetadata ("Type")); + Assert.AreEqual ("Online", devices [0].GetMetadata ("Status")); + Assert.AreEqual ("Pixel 7 - API 35", devices [0].GetMetadata ("Description"), "Emulator should have AVD name"); + + Assert.AreEqual ("emulator-5556", devices [1].ItemSpec); + Assert.AreEqual ("Emulator", devices [1].GetMetadata ("Type")); + Assert.AreEqual ("Online", devices [1].GetMetadata ("Status")); + Assert.AreEqual ("Pixel 9 Pro XL - API 36", devices [1].GetMetadata ("Description"), "Emulator should have AVD name"); + + Assert.AreEqual ("0A041FDD400327", devices [2].ItemSpec); + Assert.AreEqual ("Device", devices [2].GetMetadata ("Type")); + Assert.AreEqual ("Online", devices [2].GetMetadata ("Status")); + Assert.AreEqual ("Pixel 6 Pro", devices [2].GetMetadata ("Description"), "Physical device should use model name"); + } + + [Test] + public void ParseOfflineDevice () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "emulator-5554 offline product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (1, devices.Length, "Should return one device"); + var device = devices [0]; + Assert.AreEqual ("emulator-5554", device.ItemSpec); + Assert.AreEqual ("Offline", device.GetMetadata ("Status"), "Status should be Offline"); + Assert.AreEqual ("Pixel 7 - API 35", device.GetMetadata ("Description"), "Offline emulator should still get AVD name"); + } + + [Test] + public void ParseUnauthorizedDevice () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "0A041FDD400327 unauthorized usb:1-1" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (1, devices.Length, "Should return one device"); + var device = devices [0]; + Assert.AreEqual ("0A041FDD400327", device.ItemSpec); + Assert.AreEqual ("Unauthorized", device.GetMetadata ("Status"), "Status should be Unauthorized"); + Assert.AreEqual ("Device", device.GetMetadata ("Type")); + } + + [Test] + public void ParseNoPermissionsDevice () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "???????????????? no permissions usb:1-1" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (1, devices.Length, "Should return one device"); + var device = devices [0]; + Assert.AreEqual ("????????????????", device.ItemSpec); + Assert.AreEqual ("NoPermissions", device.GetMetadata ("Status"), "Status should be NoPermissions"); + } + + [Test] + public void ParseDeviceWithMinimalMetadata () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "ABC123 device" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (1, devices.Length, "Should return one device"); + var device = devices [0]; + Assert.AreEqual ("ABC123", device.ItemSpec); + Assert.AreEqual ("Device", device.GetMetadata ("Type")); + Assert.AreEqual ("Online", device.GetMetadata ("Status")); + Assert.AreEqual ("ABC123", device.GetMetadata ("Description"), "Description should fall back to serial"); + } + + [Test] + public void ParseDeviceWithProductOnly () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "emulator-5554 device product:aosp_x86_64" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (1, devices.Length, "Should return one device"); + var device = devices [0]; + Assert.AreEqual ("Pixel 7 - API 35", device.GetMetadata ("Description"), "Emulator should get AVD name replacement"); + Assert.AreEqual ("aosp_x86_64", device.GetMetadata ("Product")); + } + + [Test] + public void ParseInvalidLines () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "", + " ", + "Some random text", + "* daemon not running; starting now at tcp:5037", + "* daemon started successfully", + "emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (1, devices.Length, "Should only return valid device lines"); + Assert.AreEqual ("emulator-5554", devices [0].ItemSpec); + Assert.AreEqual ("Pixel 7 - API 35", devices [0].GetMetadata ("Description"), "Emulator should have AVD name"); + } + + [Test] + public void ParseMixedDeviceStates () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "emulator-5554 device product:sdk_gphone64_arm64 model:Pixel_7 device:emu64a", + "emulator-5556 offline", + "0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro", + "0B123456789ABC unauthorized usb:1-2" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (4, devices.Length, "Should return all devices regardless of state"); + + Assert.AreEqual ("Online", devices [0].GetMetadata ("Status")); + Assert.AreEqual ("Pixel 7 - API 35", devices [0].GetMetadata ("Description"), "Emulator should have AVD name"); + + Assert.AreEqual ("Offline", devices [1].GetMetadata ("Status")); + Assert.AreEqual ("Pixel 9 Pro XL - API 36", devices [1].GetMetadata ("Description"), "Offline emulator should still get AVD name"); + + Assert.AreEqual ("Online", devices [2].GetMetadata ("Status")); + Assert.AreEqual ("Pixel 6 Pro", devices [2].GetMetadata ("Description")); + + Assert.AreEqual ("Unauthorized", devices [3].GetMetadata ("Status")); + } + + [Test] + public void ParseDeviceWithSpecialCharacters () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var lines = new List { + "List of devices attached", + "192.168.1.100:5555 device product:sdk_gphone64_arm64 model:Remote_Device" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (1, devices.Length, "Should handle IP:port format devices"); + var device = devices [0]; + Assert.AreEqual ("192.168.1.100:5555", device.ItemSpec); + Assert.AreEqual ("Device", device.GetMetadata ("Type"), "IP devices should be classified as Device not Emulator"); + Assert.AreEqual ("Remote Device", device.GetMetadata ("Description")); + } + + [Test] + public void DescriptionPriorityOrder () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + // Test that model takes priority over product + var lines1 = new List { + "List of devices attached", + "device1 device product:product_name model:model_name device:device_name" + }; + var devices1 = ParseAdbDevicesOutput (task, lines1); + Assert.AreEqual ("model name", devices1 [0].GetMetadata ("Description"), "Model should have highest priority"); + + // Test that product takes priority over device name when model is missing + var lines2 = new List { + "List of devices attached", + "device2 device product:product_name device:device_name" + }; + var devices2 = ParseAdbDevicesOutput (task, lines2); + Assert.AreEqual ("product name", devices2 [0].GetMetadata ("Description"), "Product should have second priority"); + + // Test that device name is used when model and product are missing + var lines3 = new List { + "List of devices attached", + "device3 device device:device_name" + }; + var devices3 = ParseAdbDevicesOutput (task, lines3); + Assert.AreEqual ("device name", devices3 [0].GetMetadata ("Description"), "Device should have third priority"); + } + + [Test] + public void ParseAdbDaemonStarting () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + // Output when adb daemon is not running and starting up + var lines = new List { + "* daemon not running; starting now at tcp:5037", + "* daemon started successfully", + "List of devices attached", + "emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1", + "0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2" + }; + + var devices = ParseAdbDevicesOutput (task, lines); + + Assert.AreEqual (2, devices.Length, "Should parse devices even with daemon startup messages"); + + // First device - emulator + var emulator = devices [0]; + Assert.AreEqual ("emulator-5554", emulator.ItemSpec); + Assert.AreEqual ("Pixel 7 - API 35", emulator.GetMetadata ("Description"), "Emulator should get AVD name"); + Assert.AreEqual ("Emulator", emulator.GetMetadata ("Type")); + Assert.AreEqual ("Online", emulator.GetMetadata ("Status")); + + // Second device - physical device + var physical = devices [1]; + Assert.AreEqual ("0A041FDD400327", physical.ItemSpec); + Assert.AreEqual ("Pixel 6 Pro", physical.GetMetadata ("Description")); + Assert.AreEqual ("Device", physical.GetMetadata ("Type")); + Assert.AreEqual ("Online", physical.GetMetadata ("Status")); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index be4fe8d8d28..ee24d3a843d 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -821,6 +821,11 @@ because xbuild doesn't support framework reference assemblies. android.app.Application + + + + -s "$(Device)" +