From 73bd69184cd8e33acca575a093ca005619becb94 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 2 Dec 2025 14:19:07 -0600 Subject: [PATCH 1/2] [xabt] title case `ComputeAvailableDevices` Context: https://github.com/dotnet/sdk/pull/51337 Context: https://github.com/dotnet/sdk/pull/51914 In 63f7cba00, we added the `ComputeAvailableDevices` MSBuild target, which I was able to test end-to-end: D:\src\helloandroid> D:\src\dotnet\sdk\artifacts\bin\redist\Debug\dotnet\dotnet.exe run -bl Select a device to run on: > 0A041FDD400327 - Pixel 5 emulator-5554 - pixel 7 - api 36 Type to search Unfortunately, the AVD name is returned from `adb emu avd name`, which simply returns the property: > adb -s emulator-5554 shell getprop | grep avd_name [ro.boot.qemu.avd_name]: [pixel_7_-_api_36] We can call `TextInfo.ToTitleCase()`, replace underscores with spaces, and replace "Api" with "API" to make the AVD name more user-friendly. The only other alternative I considered was parsing the `~/.android/avd/.ini` file to get the `displayname` property, but it would still require calling `adb emu avd name` to *get* the path to this `.ini` file. It felt more straightforward to just format the AVD name directly. --- .../Tasks/GetAvailableAndroidDevices.cs | 22 +++- .../Tasks/GetAvailableAndroidDevicesTests.cs | 120 ++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs index fe041ee49df..c0c43fe409f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Text.RegularExpressions; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; @@ -24,6 +25,7 @@ enum DeviceType // 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); + static readonly Regex ApiRegex = new(@"\bApi\b", RegexOptions.Compiled); readonly List output = []; @@ -198,8 +200,7 @@ static string MapAdbStateToStatus (string adbState) 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 ('_', ' '); + return FormatDisplayName(serial, avdName); } } } catch (Exception ex) { @@ -208,4 +209,21 @@ static string MapAdbStateToStatus (string adbState) return null; } + + /// + /// Formats the AVD name into a more user-friendly display name. Replace underscores with spaces and title case. + /// + public string FormatDisplayName(string serial, string avdName) + { + Log.LogDebugMessage ($"Emulator {serial}, original AVD name: {avdName}"); + + // Title case and replace underscores with spaces + var textInfo = CultureInfo.InvariantCulture.TextInfo; + avdName = textInfo.ToTitleCase(avdName.Replace ('_', ' ')); + + // Replace "Api" with "API" + avdName = ApiRegex.Replace (avdName, "API"); + Log.LogDebugMessage ($"Emulator {serial}, formatted AVD display name: {avdName}"); + return avdName; + } } 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 index ccbb78aea38..c1dd157648b 100644 --- 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 @@ -436,5 +436,125 @@ public void ParseAdbDaemonStarting () Assert.AreEqual ("Device", physical.GetMetadata ("Type")); Assert.AreEqual ("Online", physical.GetMetadata ("Status")); } + + [Test] + public void FormatDisplayName_ReplacesUnderscoresWithSpaces () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var result = task.FormatDisplayName ("emulator-5554", "pixel_7_pro"); + + Assert.AreEqual ("Pixel 7 Pro", result, "Should replace underscores with spaces"); + } + + [Test] + public void FormatDisplayName_AppliesTitleCase () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var result = task.FormatDisplayName ("emulator-5554", "pixel 7 pro"); + + Assert.AreEqual ("Pixel 7 Pro", result, "Should apply title case"); + } + + [Test] + public void FormatDisplayName_ReplacesApiWithAPIUppercase () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var result = task.FormatDisplayName ("emulator-5554", "pixel_5_api_34"); + + Assert.AreEqual ("Pixel 5 API 34", result, "Should replace 'Api' with 'API'"); + } + + [Test] + public void FormatDisplayName_HandlesMultipleApiOccurrences () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var result = task.FormatDisplayName ("emulator-5554", "test_api_device_api_35"); + + Assert.AreEqual ("Test API Device API 35", result, "Should replace all 'Api' occurrences with 'API'"); + } + + [Test] + public void FormatDisplayName_HandlesMixedCaseInput () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var result = task.FormatDisplayName ("emulator-5554", "PiXeL_7_API_35"); + + Assert.AreEqual ("Pixel 7 API 35", result, "Should normalize mixed case input"); + } + + [Test] + public void FormatDisplayName_HandlesComplexNames () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var result = task.FormatDisplayName ("emulator-5554", "pixel_9_pro_xl_api_36"); + + Assert.AreEqual ("Pixel 9 Pro Xl API 36", result, "Should format complex names correctly"); + } + + [Test] + public void FormatDisplayName_PreservesNumbersAndSpecialChars () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var result = task.FormatDisplayName ("emulator-5554", "pixel_7-pro_api_35"); + + Assert.AreEqual ("Pixel 7-Pro API 35", result, "Should preserve hyphens and numbers"); + } + + [Test] + public void FormatDisplayName_HandlesEmptyString () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var result = task.FormatDisplayName ("emulator-5554", ""); + + Assert.AreEqual ("", result, "Should handle empty string"); + } + + [Test] + public void FormatDisplayName_HandlesSingleWord () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var result = task.FormatDisplayName ("emulator-5554", "pixel"); + + Assert.AreEqual ("Pixel", result, "Should capitalize single word"); + } + + [Test] + public void FormatDisplayName_DoesNotReplaceApiInsideWords () + { + var task = new MockGetAvailableAndroidDevices { + BuildEngine = engine, + }; + + var result = task.FormatDisplayName ("emulator-5554", "erapidevice"); + + Assert.AreEqual ("Erapidevice", result, "Should not replace 'api' when it's part of a larger word"); + } } } From c8dbb57cbb37ef38e53be2c3093b1f399c7af90a Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 2 Dec 2025 15:56:22 -0600 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Tasks/GetAvailableAndroidDevices.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs index c0c43fe409f..9475e66c8a7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs @@ -200,7 +200,7 @@ static string MapAdbStateToStatus (string adbState) var avdName = outputLines [0].Trim (); // Verify it's not the "OK" response if (!string.IsNullOrEmpty (avdName) && !avdName.Equals ("OK", StringComparison.OrdinalIgnoreCase)) { - return FormatDisplayName(serial, avdName); + return FormatDisplayName (serial, avdName); } } } catch (Exception ex) { @@ -213,13 +213,13 @@ static string MapAdbStateToStatus (string adbState) /// /// Formats the AVD name into a more user-friendly display name. Replace underscores with spaces and title case. /// - public string FormatDisplayName(string serial, string avdName) + public string FormatDisplayName (string serial, string avdName) { Log.LogDebugMessage ($"Emulator {serial}, original AVD name: {avdName}"); // Title case and replace underscores with spaces var textInfo = CultureInfo.InvariantCulture.TextInfo; - avdName = textInfo.ToTitleCase(avdName.Replace ('_', ' ')); + avdName = textInfo.ToTitleCase (avdName.Replace ('_', ' ')); // Replace "Api" with "API" avdName = ApiRegex.Replace (avdName, "API");