Skip to content
Open
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
128 changes: 128 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue33070.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue33070"
Title="Issue 33070">

<ScrollView>
<VerticalStackLayout AutomationId="ScrollViewContent"
Padding="20"
Spacing="15">
<Label Text="Test Drawable Mutation Fix"
FontSize="20"
FontAttributes="Bold"/>

<Label Text="Rapidly changing colors on shared drawables should not crash the app."
TextColor="Gray"/>

<!-- ActivityIndicator Test -->
<Border Padding="10"
Stroke="LightGray"
StrokeThickness="1">
<VerticalStackLayout Spacing="10">
<Label Text="ActivityIndicator"
FontAttributes="Bold"/>
<ActivityIndicator x:Name="ActivityIndicatorTest"
IsRunning="True"
Color="Red"
AutomationId="ActivityIndicator"/>
<Button Text="Change ActivityIndicator Color"
Clicked="OnChangeActivityIndicatorColor"
AutomationId="ChangeActivityIndicatorButton"/>
<Label x:Name="ActivityIndicatorStatus"
Text="Ready"
AutomationId="ActivityIndicatorStatus"/>
</VerticalStackLayout>
</Border>

<!-- Entry Clear Button Test -->
<Border Padding="10"
Stroke="LightGray"
StrokeThickness="1">
<VerticalStackLayout Spacing="10">
<Label Text="Entry Clear Button"
FontAttributes="Bold"/>
<Entry x:Name="EntryTest"
Text="Test Entry"
ClearButtonVisibility="WhileEditing"
TextColor="Blue"
AutomationId="Entry"/>
<Button Text="Change Entry Text Color"
Clicked="OnChangeEntryColor"
AutomationId="ChangeEntryButton"/>
<Label x:Name="EntryStatus"
Text="Ready"
AutomationId="EntryStatus"/>
</VerticalStackLayout>
</Border>

<!-- Switch Test -->
<Border Padding="10"
Stroke="LightGray"
StrokeThickness="1">
<VerticalStackLayout Spacing="10">
<Label Text="Switch Track/Thumb"
FontAttributes="Bold"/>
<Switch x:Name="SwitchTest"
IsToggled="True"
ThumbColor="Green"
OnColor="LightGreen"
AutomationId="Switch"/>
<Button Text="Change Switch Colors"
Clicked="OnChangeSwitchColors"
AutomationId="ChangeSwitchButton"/>
<Label x:Name="SwitchStatus"
Text="Ready"
AutomationId="SwitchStatus"/>
</VerticalStackLayout>
</Border>

<!-- SearchBar Test -->
<Border Padding="10"
Stroke="LightGray"
StrokeThickness="1">
<VerticalStackLayout Spacing="10">
<Label Text="SearchBar Icons"
FontAttributes="Bold"/>
<SearchBar x:Name="SearchBarTest"
Placeholder="Search..."
TextColor="Purple"
PlaceholderColor="LightGray"
CancelButtonColor="Red"
AutomationId="SearchBar"/>
<Button Text="Change SearchBar Colors"
Clicked="OnChangeSearchBarColors"
AutomationId="ChangeSearchBarButton"/>
<Label x:Name="SearchBarStatus"
Text="Ready"
AutomationId="SearchBarStatus"/>
</VerticalStackLayout>
</Border>

<!-- Rapid Changes Test -->
<Border Padding="10"
Stroke="LightGray"
StrokeThickness="1"
BackgroundColor="LightYellow">
<VerticalStackLayout Spacing="10">
<Label Text="Stress Test - Rapid Color Changes"
FontAttributes="Bold"/>
<Label Text="This simulates the crash scenario from large apps with many screens"
FontSize="12"
TextColor="Gray"/>
<Button Text="Run Rapid Changes Test"
Clicked="OnRunRapidChangesTest"
AutomationId="RunRapidChangesButton"/>
<Label x:Name="RapidChangesStatus"
Text="Ready"
AutomationId="RapidChangesStatus"/>
<Label x:Name="RapidChangesProgress"
Text=""
AutomationId="RapidChangesProgress"
FontSize="12"
TextColor="Gray"/>
</VerticalStackLayout>
</Border>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
137 changes: 137 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue33070.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 33070, "Fix Android drawable mutation crash", PlatformAffected.Android)]
public partial class Issue33070 : ContentPage
{
private readonly Color[] _testColors = new[]
{
Colors.Red, Colors.Blue, Colors.Green, Colors.Purple,
Colors.Orange, Colors.Pink, Colors.Cyan, Colors.Yellow
};

private int _colorIndex = 0;
private int _rapidChangeCounter = 0;

public Issue33070()
{
InitializeComponent();
}

private void OnChangeActivityIndicatorColor(object sender, EventArgs e)
{
try
{
_colorIndex = (_colorIndex + 1) % _testColors.Length;
var newColor = _testColors[_colorIndex];
ActivityIndicatorTest.Color = newColor;
ActivityIndicatorStatus.Text = $"Color changed to {newColor}";
}
catch (Exception ex)
{
ActivityIndicatorStatus.Text = $"ERROR: {ex.Message}";
}
}

private void OnChangeEntryColor(object sender, EventArgs e)
{
try
{
_colorIndex = (_colorIndex + 1) % _testColors.Length;
var newColor = _testColors[_colorIndex];
EntryTest.TextColor = newColor;
EntryStatus.Text = $"Color changed to {newColor}";
}
catch (Exception ex)
{
EntryStatus.Text = $"ERROR: {ex.Message}";
}
}

private void OnChangeSwitchColors(object sender, EventArgs e)
{
try
{
_colorIndex = (_colorIndex + 1) % _testColors.Length;
var newThumbColor = _testColors[_colorIndex];
var newTrackColor = _testColors[(_colorIndex + 1) % _testColors.Length];

SwitchTest.ThumbColor = newThumbColor;
SwitchTest.OnColor = newTrackColor;

SwitchStatus.Text = $"Thumb: {newThumbColor}, Track: {newTrackColor}";
}
catch (Exception ex)
{
SwitchStatus.Text = $"ERROR: {ex.Message}";
}
}

private void OnChangeSearchBarColors(object sender, EventArgs e)
{
try
{
_colorIndex = (_colorIndex + 1) % _testColors.Length;
var newTextColor = _testColors[_colorIndex];
var newPlaceholderColor = _testColors[(_colorIndex + 1) % _testColors.Length];
var newCancelColor = _testColors[(_colorIndex + 2) % _testColors.Length];

SearchBarTest.TextColor = newTextColor;
SearchBarTest.PlaceholderColor = newPlaceholderColor;
SearchBarTest.CancelButtonColor = newCancelColor;

SearchBarStatus.Text = $"Colors changed successfully";
}
catch (Exception ex)
{
SearchBarStatus.Text = $"ERROR: {ex.Message}";
}
}

private async void OnRunRapidChangesTest(object sender, EventArgs e)
{
const int totalIterations = 50;
_rapidChangeCounter = 0;

try
{
RapidChangesStatus.Text = "Running...";

for (int i = 0; i < totalIterations; i++)
{
var color1 = _testColors[i % _testColors.Length];
var color2 = _testColors[(i + 1) % _testColors.Length];
var color3 = _testColors[(i + 2) % _testColors.Length];
var color4 = _testColors[(i + 3) % _testColors.Length];

// Rapidly change all controls
ActivityIndicatorTest.Color = color1;
EntryTest.TextColor = color2;
SwitchTest.ThumbColor = color3;
SwitchTest.OnColor = color4;
SearchBarTest.TextColor = color1;
SearchBarTest.PlaceholderColor = color2;
SearchBarTest.CancelButtonColor = color3;

_rapidChangeCounter++;

if (i % 10 == 0)
{
RapidChangesProgress.Text = $"Iteration {i + 1}/{totalIterations}";
await Task.Delay(10); // Small delay to allow UI update
}
}

RapidChangesStatus.Text = $"✅ Completed {_rapidChangeCounter} iterations";
RapidChangesProgress.Text = "No crashes!";
}
catch (Exception ex)
{
RapidChangesStatus.Text = $"❌ Failed at iteration {_rapidChangeCounter}";
RapidChangesProgress.Text = $"Error: {ex.Message}";
}
}
}
119 changes: 119 additions & 0 deletions src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33070.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue33070 : _IssuesUITest
{
public override string Issue => "Fix Android drawable mutation crash";

public Issue33070(TestDevice device) : base(device) { }

[Test]
[Category(UITestCategories.ActivityIndicator)]
public void ActivityIndicatorColorChangeShouldNotCrash()
{
App.WaitForElement("ActivityIndicator");

// Change color multiple times
for (int i = 0; i < 5; i++)
{
App.Tap("ChangeActivityIndicatorButton");
App.WaitForElement("ActivityIndicatorStatus");

var status = App.FindElement("ActivityIndicatorStatus").GetText();
Assert.That(status, Does.Contain("Color changed to"), $"Iteration {i + 1}: Color change failed");
}

// Verify no crash occurred
var finalStatus = App.FindElement("ActivityIndicatorStatus").GetText();
Assert.That(finalStatus, Does.Not.Contain("ERROR"));
}

[Test]
[Category(UITestCategories.Entry)]
public void EntryTextColorChangeShouldNotCrash()
{
App.WaitForElement("Entry");

// Change color multiple times
for (int i = 0; i < 5; i++)
{
App.Tap("ChangeEntryButton");
App.WaitForElement("EntryStatus");

var status = App.FindElement("EntryStatus").GetText();
Assert.That(status, Does.Contain("Color changed to"), $"Iteration {i + 1}: Color change failed");
}

// Verify no crash occurred
var finalStatus = App.FindElement("EntryStatus").GetText();
Assert.That(finalStatus, Does.Not.Contain("ERROR"));
}

[Test]
[Category(UITestCategories.Switch)]
public void SwitchColorChangeShouldNotCrash()
{
App.WaitForElement("Switch");

// Change colors multiple times
for (int i = 0; i < 5; i++)
{
App.Tap("ChangeSwitchButton");
App.WaitForElement("SwitchStatus");

var status = App.FindElement("SwitchStatus").GetText();
Assert.That(status, Does.Contain("Thumb:"), $"Iteration {i + 1}: Color change failed");
}

// Verify no crash occurred
var finalStatus = App.FindElement("SwitchStatus").GetText();
Assert.That(finalStatus, Does.Not.Contain("ERROR"));
}

[Test]
[Category(UITestCategories.SearchBar)]
public void SearchBarColorChangeShouldNotCrash()
{
App.WaitForElement("SearchBar");

// Change colors multiple times
for (int i = 0; i < 5; i++)
{
App.Tap("ChangeSearchBarButton");
App.WaitForElement("SearchBarStatus");

var status = App.FindElement("SearchBarStatus").GetText();
Assert.That(status, Does.Contain("Colors changed successfully"), $"Iteration {i + 1}: Color change failed");
}

// Verify no crash occurred
var finalStatus = App.FindElement("SearchBarStatus").GetText();
Assert.That(finalStatus, Does.Not.Contain("ERROR"));
}

[Test]
[Category(UITestCategories.ActivityIndicator)]
public void RapidColorChangesShouldNotCrash()
{
App.WaitForElement("ScrollViewContent");
App.ScrollDownTo("RunRapidChangesButton", "ScrollViewContent");

// Run the stress test
App.Tap("RunRapidChangesButton");

// Wait for completion (50 iterations with small delays)
App.WaitForElement("RapidChangesStatus", timeout: TimeSpan.FromSeconds(30));

// Check that test completed successfully
var status = App.FindElement("RapidChangesStatus").GetText();
Assert.That(status, Does.Contain("Completed"), "Rapid changes test did not complete");
Assert.That(status, Does.Contain("50 iterations"), "Expected 50 iterations");

// Verify no crashes
var progress = App.FindElement("RapidChangesProgress").GetText();
Assert.That(progress, Does.Contain("No crashes!"));
}
}
4 changes: 2 additions & 2 deletions src/Core/src/Platform/Android/ActivityIndicatorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public static void UpdateColor(this ProgressBar progressBar, IActivityIndicator
var color = activityIndicator.Color;

if (color != null)
progressBar.IndeterminateDrawable?.SetColorFilter(color.ToPlatform(), FilterMode.SrcIn);
progressBar.IndeterminateDrawable = progressBar.IndeterminateDrawable.SafeSetColorFilter(color.ToPlatform(), FilterMode.SrcIn);
else
progressBar.IndeterminateDrawable?.ClearColorFilter();
progressBar.IndeterminateDrawable = progressBar.IndeterminateDrawable.SafeClearColorFilter();
}
}
}
Loading
Loading