diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs index fe041ee49df..9475e66c8a7 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"); + } } }