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: 2 additions & 0 deletions eng/pipelines/arcade/setup-test-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ steps:
fetchDepth: 1
clean: true

- template: /eng/pipelines/common/enable-kvm.yml@self

- template: /eng/pipelines/common/provision.yml@self
parameters:
checkoutDirectory: '$(System.DefaultWorkingDirectory)'
Expand Down
14 changes: 9 additions & 5 deletions eng/pipelines/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ parameters:
- ImageOverride -equals ACES_VM_SharedPool_Tahoe
label: macOS

- name: AndroidPoolLinux
type: object
default:
name: MAUI-DNCENG
demands:
- ImageOverride -equals 1ESPT-Ubuntu22.04


# Condition for MacOSPool comparison lanes (non-ARM64)
# Runs on: (non-PR on main/net*.0/release/*/inflight/*) OR (PR targeting net*.0/release/*/inflight/*)
Expand Down Expand Up @@ -277,12 +284,9 @@ stages:
# TODO: macOSTemplates and AOT template categories
- name: mac_runandroid_tests
${{ if eq(variables['Build.DefinitionName'], 'maui-pr') }}:
pool:
name: AcesShared
demands:
- ImageOverride -equals ACES_arm64_Sequoia_Xcode
pool: ${{ parameters.AndroidPoolLinux }}
${{ else }}:
pool: ${{ parameters.MacOSPool.internal }}
pool: ${{ parameters.AndroidPoolLinux }}
timeout: 240
testCategory: RunOnAndroid

Expand Down
8 changes: 1 addition & 7 deletions eng/pipelines/common/device-tests-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,8 @@ steps:
continueOnError: true
timeoutInMinutes: 60

# Enable KVM for Android builds on Linux
- ${{ if and(ne(parameters.buildType, 'buildOnly'), eq(parameters.platform, 'android')) }}:
- bash: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
displayName: Enable KVM
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- template: enable-kvm.yml

# Provision the various SDKs that are needed
- template: provision.yml
Expand Down
8 changes: 8 additions & 0 deletions eng/pipelines/common/enable-kvm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Enable KVM for Android tests on Linux
steps:
- bash: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
displayName: Enable KVM
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
8 changes: 1 addition & 7 deletions eng/pipelines/common/ui-tests-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,8 @@ steps:
continueOnError: true
timeoutInMinutes: 60

# Enable KVM for Android builds on Linux
- ${{ if and(ne(parameters.buildType, 'buildOnly'), eq(parameters.platform, 'android')) }}:
- bash: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
displayName: Enable KVM
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
- template: enable-kvm.yml

- ${{ if eq(parameters.platform, 'catalyst')}}:
- bash: |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using Android.Content;
Expand All @@ -24,6 +25,10 @@ internal static class ToolbarExtensions
{
static ColorStateList? _defaultTitleTextColor;
static int? _defaultNavigationIconColor;

// Track which ToolbarItem should currently be associated with each MenuItem ID to prevent race conditions
// This prevents stale async icon loading callbacks from updating the wrong toolbar items during navigation
static readonly ConcurrentDictionary<int, WeakReference<ToolbarItem>> _menuItemToolbarItemMap = new();

public static void UpdateIsVisible(this AToolbar nativeToolbar, Toolbar toolbar)
{
Expand Down Expand Up @@ -242,6 +247,9 @@ public static void UpdateMenuItems(this AToolbar toolbar,
var previousMenuItem = previousMenuItems[j];
if (menu.FindItem(previousMenuItem.ItemId) == null)
{
// Clean up the mapping for disposed MenuItems
_menuItemToolbarItemMap.TryRemove(previousMenuItem.ItemId, out _);

previousMenuItem.Dispose();
previousMenuItems.RemoveAt(j);
}
Expand All @@ -261,8 +269,13 @@ public static void UpdateMenuItems(this AToolbar toolbar,
int toolBarItemCount = i;
while (toolBarItemCount < previousMenuItems.Count)
{
menu?.RemoveItem(previousMenuItems[toolBarItemCount].ItemId);
previousMenuItems[toolBarItemCount].Dispose();
var menuItemToRemove = previousMenuItems[toolBarItemCount];
menu?.RemoveItem(menuItemToRemove.ItemId);

// Clean up the mapping for disposed MenuItems
_menuItemToolbarItemMap.TryRemove(menuItemToRemove.ItemId, out _);

menuItemToRemove.Dispose();
previousMenuItems.RemoveAt(toolBarItemCount);
}

Expand Down Expand Up @@ -335,6 +348,12 @@ static void UpdateMenuItem(AToolbar toolbar,
menuitem.SetEnabled(item.IsEnabled);
menuitem.SetTitleOrContentDescription(item);

// Track which ToolbarItem should be associated with this MenuItem to prevent race conditions
_menuItemToolbarItemMap[menuitem.ItemId] = new WeakReference<ToolbarItem>(item);

// NOTE: Custom updateMenuItemIcon callbacks are responsible for their own
// race condition handling. The _menuItemToolbarItemMap guard only applies
// to the default UpdateMenuItemIcon path.
if (updateMenuItemIcon != null)
updateMenuItemIcon(context, menuitem, item);
else
Expand Down Expand Up @@ -370,6 +389,13 @@ internal static void UpdateMenuItemIcon(this IMauiContext mauiContext, IMenuItem
return;
}

if (!_menuItemToolbarItemMap.TryGetValue(menuItem.ItemId, out var weakRef)
|| !weakRef.TryGetTarget(out var currentToolbarItem)
|| !ReferenceEquals(currentToolbarItem, toolBarItem))
{
return;
}

Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

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

The race condition check has a gap: if the MenuItem ID is reused for a new ToolbarItem before the async callback executes, but the WeakReference has been garbage collected, the check will pass even though it should abort. Add an additional check: if the menuItem.ItemId exists in the map but the WeakReference target is null (collected), the callback should return early rather than proceeding with the update.

Suggested change
// Additional check: if the menuItem.ItemId exists in the map but the WeakReference target is null (collected), abort
if (_menuItemToolbarItemMap.TryGetValue(menuItem.ItemId, out var wr) && !wr.TryGetTarget(out _))
{
return;
}

Copilot uses AI. Check for mistakes.
if (baseDrawable != null)
{
using (var constant = baseDrawable.GetConstantState())
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue32886.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 32886, "[Android, iOS, Mac] Entry ClearButton not visible on dark theme", PlatformAffected.Android | PlatformAffected.iOS | PlatformAffected.macOS)]
public class Issue32886 : TestContentPage
{
protected override void Init()
{
Title = "Issue32886";

// Create the UITestEntry with ClearButtonVisibility
var entry = new UITestEntry
{
Text = "Entry Text",
IsCursorVisible = false,
IsSpellCheckEnabled = false,
IsTextPredictionEnabled = false,
AutomationId = "TestEntry",
ClearButtonVisibility = ClearButtonVisibility.WhileEditing
};

var button = new Button
{
Text = "Change theme",
AutomationId = "ThemeButton"
};
button.Clicked += Button_Clicked;

var layout = new VerticalStackLayout();
layout.Children.Add(entry);
layout.Children.Add(button);

Content = layout;

// Set background color based on app theme
this.SetAppThemeColor(BackgroundColorProperty, Colors.White, Colors.Black);
}

private void Button_Clicked(object sender, EventArgs e)
{
if (Application.Current is not null)
{
Application.Current.UserAppTheme = Application.Current.UserAppTheme != AppTheme.Dark ? AppTheme.Dark : AppTheme.Light;
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue32886 : _IssuesUITest
{
public Issue32886(TestDevice device) : base(device)
{
}

public override string Issue => "[Android, iOS, Mac] Entry ClearButton not visible on dark theme";

[Test, Order(1)]
[Category(UITestCategories.Entry)]
public void EntryClearButtonShouldBeVisibleOnLightTheme()
{
App.WaitForElement("TestEntry");
App.Tap("TestEntry");
#if ANDROID // On Android, to address CI flakiness, the keyboard is dismissed.
if (App.WaitForKeyboardToShow(timeout: TimeSpan.FromSeconds(1)))
{
App.DismissKeyboard();
}
#endif

#if IOS
// On iOS, the virtual keyboard appears inconsistent with keyboard characters casing, can cause flaky test results. As this test verifying only the entry clear button color, crop the bottom portion of the screenshot to exclude the keyboard.
// Using DismissKeyboard() would unfocus the control in iOS, so we're using cropping instead to maintain focus during testing.
VerifyScreenshot(cropBottom: 1550);
#else
VerifyScreenshot();
#endif
}

[Test, Order(2)]
[Category(UITestCategories.Entry)]
public void EntryClearButtonShouldBeVisibleOnDarkTheme()
{
App.WaitForElement("TestEntry");
App.Tap("ThemeButton");
#if WINDOWS // On Windows, the clear button isn't visible when Entry loses focus, so manually focused to check its icon color.
App.Tap("TestEntry");
#endif

#if IOS
// On iOS, the virtual keyboard appears inconsistent with keyboard characters casing, can cause flaky test results. As this test verifying only the entry clear button color, crop the bottom portion of the screenshot to exclude the keyboard.
// Using DismissKeyboard() would unfocus the control in iOS, so we're using cropping instead to maintain focus during testing.
VerifyScreenshot(cropBottom: 1550);
#else
VerifyScreenshot();
#endif
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 17 additions & 1 deletion src/Core/src/Platform/Android/EditTextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,23 @@ internal static void UpdateClearButtonColor(this EditText editText, Graphics.Col
}
else
{
clearButtonDrawable?.ClearColorFilter();
if (OperatingSystem.IsAndroidVersionAtLeast(23) && editText.Context?.Theme is Resources.Theme theme)
{
using var ta = theme.ObtainStyledAttributes([global::Android.Resource.Attribute.TextColorPrimary]);
var cs = ta.GetColorStateList(0);

if (cs is not null)
{
// Clear button is only visible when enabled, so just use the enabled state
int[] enabledState = [global::Android.Resource.Attribute.StateEnabled];
var color = new global::Android.Graphics.Color(cs.GetColorForState(enabledState, Colors.Black.ToPlatform()));
clearButtonDrawable?.SetColorFilter(color, FilterMode.SrcIn);
}
}
else
{
clearButtonDrawable?.ClearColorFilter();
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/Core/src/Platform/iOS/TextFieldExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ internal static void UpdateClearButtonColor(this UITextField textField, IEntry e

if (entry.TextColor is null)
{
clearButton.SetImage(defaultClearImage, UIControlState.Normal);
clearButton.SetImage(defaultClearImage, UIControlState.Highlighted);
// Setting TintColor to null allows the system to automatically apply the appropriate color based on the current theme (light or dark mode)
clearButton.TintColor = null;
}
else
{
Expand Down
4 changes: 3 additions & 1 deletion src/DotNet/DotNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@
<!-- Run 'dotnet workload install' for the current running 'dotnet' install -->
<ItemGroup>
<_WorkloadSource Include="$(NugetArtifactsPath)" />
<_LocalWorkloadIds Include="maui" />
<!-- On Linux, install maui-android instead of maui to avoid iOS/macOS dependencies -->
<_LocalWorkloadIds Include="maui-android" Condition="$([MSBuild]::IsOSPlatform('linux'))" />
<_LocalWorkloadIds Include="maui" Condition="!$([MSBuild]::IsOSPlatform('linux'))" />

<_LocalWorkloadIds Include="tizen" Condition=" '$(IncludeTizenTargetFrameworks)' == 'true' " />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Microsoft.Maui.IntegrationTests.Android;

namespace Microsoft.Maui.IntegrationTests
Expand Down Expand Up @@ -94,6 +95,12 @@ public void RunOnAndroid(string id, string framework, string config, string? tri
Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output),
$"Unable to create template {id}. Check test output for errors.");

// On Linux, only the maui-android workload is installed. Previous .NET
// templates may still include iOS/macOS TFMs causing NETSDK1178 errors
// during restore. Strip them so only Android remains.
if (TestEnvironment.IsLinux)
StripNonAndroidTfms(projectFile, framework);

var buildProps = BuildProps;
if (!string.IsNullOrEmpty(trimMode))
{
Expand Down Expand Up @@ -128,5 +135,20 @@ void AddInstrumentation(string projectDir)
"MainLauncher = true, Name = \"com.microsoft.mauitemplate.MainActivity\"");
}

static void StripNonAndroidTfms(string projectFile, string framework)
{
var content = File.ReadAllText(projectFile);
var androidTfm = $"{framework}-android";
// Remove conditional TargetFrameworks lines (iOS/macOS/Windows additions)
content = Regex.Replace(content,
@"\s*<TargetFrameworks\s+Condition=""[^""]*"">[^<]*</TargetFrameworks>",
"");
// Set the base TargetFrameworks to Android only
content = Regex.Replace(content,
@"<TargetFrameworks>[^<]*</TargetFrameworks>",
$"<TargetFrameworks>{androidTfm}</TargetFrameworks>");
File.WriteAllText(projectFile, content);
}

}
}
Loading