Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -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#
Expand Down
16 changes: 16 additions & 0 deletions Documentation/docs-mobile/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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 "<Device>"`.

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
Expand Down
30 changes: 30 additions & 0 deletions Documentation/docs-mobile/building-apps/build-targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <device-serial>
```

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This file contains targets specific for Android application projects.
<Project>
<UsingTask TaskName="Xamarin.Android.Tasks.AndroidAdb" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
<UsingTask TaskName="Xamarin.Android.Tasks.GetAndroidActivityName" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
<UsingTask TaskName="Xamarin.Android.Tasks.GetAvailableAndroidDevices" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
<UsingTask TaskName="Xamarin.Android.BuildTools.PrepTasks.XASleepInternal" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />

<PropertyGroup>
Expand All @@ -26,6 +27,27 @@ This file contains targets specific for Android application projects.
</_AndroidComputeRunArgumentsDependsOn>
</PropertyGroup>

<!--
***********************************************************************************************
ComputeAvailableDevices

Target that queries available Android devices and emulators.
This target is called by 'dotnet run' to support device selection.
Returns @(Devices) items with metadata: Description, Type, Status

See: https://github.com/dotnet/sdk/blob/2b9fc02a265c735f2132e4e3626e94962e48bdf5/documentation/specs/dotnet-run-for-maui.md
***********************************************************************************************
-->
<Target Name="ComputeAvailableDevices"
DependsOnTargets="_ResolveMonoAndroidSdks"
Returns="@(Devices)">
<GetAvailableAndroidDevices
ToolExe="$(AdbToolExe)"
ToolPath="$(AdbToolPath)">
<Output TaskParameter="Devices" ItemName="Devices" />
</GetAvailableAndroidDevices>
</Target>
Comment on lines +41 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any possible world where there's some way to do input/output tracking here for incrementality purposes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has to query each time because you could click the X on the emulator, unplug the device, etc.

It was taking 55ms on my machine, but we can see if there are different adb commands that are faster in the future. I was also thinking about reading files on disk like %userprofile%\.android\avd\pixel_7_-_api_36.ini that could be faster than launching a new process.


<Target Name="_AndroidComputeRunArguments"
BeforeTargets="ComputeRunArguments"
DependsOnTargets="$(_AndroidComputeRunArgumentsDependsOn)">
Expand Down
211 changes: 211 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
public class GetAvailableAndroidDevices : AndroidAdb
{
enum DeviceType
{
Device,
Emulator
}

// Pattern to match device lines: <serial> <state> [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<string> 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;
}

/// <summary>
/// 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
/// </summary>
List<ITaskItem> ParseAdbDevicesOutput (List<string> lines)
{
var devices = new List<ITaskItem> ();

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<string, string> (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<string, string> 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",
};
}

/// <summary>
/// Queries the emulator for its AVD name using 'adb -s <serial> emu avd name'
/// and formats it as a friendly display name.
/// </summary>
protected virtual string? GetEmulatorAvdDisplayName (string serial)
{
try {
var adbPath = System.IO.Path.Combine (ToolPath, ToolExe);
var outputLines = new List<string> ();

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;
}
}
Loading
Loading