Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
76b01e4
Fix Glide IllegalArgumentException in PlatformInterop for destroyed a…
Copilot Feb 2, 2026
fbdaec5
[iOS] Fix MauiCALayer and StaticCAShapeLayer crash on finalizer threa…
pshoey Feb 2, 2026
322f69e
[Android] Fixed duplicate title icon when setting TitleIconImageSourc…
SubhikshaSf4851 Feb 3, 2026
6b07ab0
[Android] Fix Numeric Entry not accepting the appropriate Decimal Sep…
devanathan-vaithiyanathan Feb 3, 2026
fd276c6
[Android & iOS] Entry/Editor: Dismiss keyboard when control becomes i…
prakashKannanSf3972 Feb 3, 2026
c57f77c
[Android] Shell: Fix OnBackButtonPressed not firing for navigation ba…
kubaflo Feb 4, 2026
061a200
[Android] Fixed PointerGestureRecognizer not triggering PointerMoved …
KarthikRajaKalaimani Feb 5, 2026
5ad59f0
Fixed Editor vertical text alignment not working after toggling IsVis…
NanthiniMahalingam Feb 13, 2026
eee0535
[iOS] Fixed Shell Navigating event showing same current and target va…
Vignesh-SF3580 Feb 13, 2026
757447a
Fix ImageButton not rendering correctly based on its bounds (#28309)
Shalini-Ashokan Feb 17, 2026
2133acd
[Android] Fixed issue where group Header/Footer template was applied …
Tamilarasan-Paranthaman Feb 17, 2026
116bf55
[Android] Fixed CollectionView items do not reorder correctly when us…
NanthiniMahalingam Feb 17, 2026
7b77352
[Android] Fix for incorrect scroll position when using ScrollTo with …
SyedAbdulAzeemSF4852 Feb 18, 2026
d8d5cb7
Fixed the crash on iOS when setting HeightRequest on WebView inside a…
Ahamed-Ali Feb 19, 2026
6ed4d42
[iOS, macOS] Fixed Shell Flyout Icon is always black in iOS 26 (#32997)
Dhivya-SF4094 Feb 20, 2026
b50b700
Fix Incorrect Scrolling Behavior in CollectionView ScrollTo Method Us…
Shalini-Ashokan Feb 20, 2026
15e8a3c
[Android] Fixed TransformProperties issue when a wrapper view is pres…
Ahamed-Ali Feb 20, 2026
fb104dc
[Android] Fix System.IndexOutOfRangeException when scrolling Collecti…
devanathan-vaithiyanathan Feb 20, 2026
746d160
[Android] Fix VerticalOffset Update When Modifying CollectionView.Ite…
devanathan-vaithiyanathan Feb 20, 2026
9291950
[Testing] Fix for enable uitests ios26 (#33686)
TamilarasanSF4853 Feb 21, 2026
4058e34
[Testing] Fixed Test case failure in PR 34173 - [02/21/2026] Candidat…
TamilarasanSF4853 Feb 23, 2026
66a814e
[Android] Fix PointerMoved and PointerReleased not firing in PointerG…
KarthikRajaKalaimani Feb 24, 2026
4b17d62
[Testing] Fixed Test case failure in PR 34173 - [02/21/2026] Candidat…
TamilarasanSF4853 Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
261 changes: 261 additions & 0 deletions .github/agent-pr-session/pr-31487.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
# PR Review: #31487 - [Android] Fixed duplicate title icon when setting TitleIconImageSource Multiple times

**Date:** 2026-01-08 | **Issue:** [#31445](https://github.com/dotnet/maui/issues/31445) | **PR:** [#31487](https://github.com/dotnet/maui/pull/31487)

## ✅ Final Recommendation: APPROVE

| Phase | Status |
|-------|--------|
| Pre-Flight | ✅ COMPLETE |
| 🧪 Tests | ✅ COMPLETE |
| 🚦 Gate | ✅ PASSED |
| 🔧 Fix | ✅ COMPLETE |
| 📋 Report | ✅ COMPLETE |

---

<details>
<summary><strong>📋 Issue Summary</strong></summary>

On Android, calling `NavigationPage.SetTitleIconImageSource(page, "image.png")` more than once for the same page results in the icon being rendered multiple times in the navigation bar.

**Steps to Reproduce:**
1. Launch app on Android
2. Tap "Set TitleIconImageSource" once: icon appears
3. Tap it again: a second identical icon appears

**Expected:** Single toolbar icon regardless of how many times SetTitleIconImageSource is called.

**Actual:** Each repeated call adds an additional duplicate icon.

**Platforms Affected:**
- [ ] iOS
- [x] Android
- [ ] Windows
- [ ] MacCatalyst

**Version:** 9.0.100 SR10

</details>

<details>
<summary><strong>📁 Files Changed</strong></summary>

| File | Type | Changes |
|------|------|---------|
| `src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs` | Fix | +17/-6 |
| `src/Controls/tests/TestCases.HostApp/Issues/Issue31445.cs` | Test | +38 |
| `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31445.cs` | Test | +23 |
| `snapshots/android/Issue31445DuplicateTitleIconDoesNotAppear.png` | Snapshot | binary |
| `snapshots/mac/Issue31445DuplicateTitleIconDoesNotAppear.png` | Snapshot | binary |
| `snapshots/windows/Issue31445DuplicateTitleIconDoesNotAppear.png` | Snapshot | binary |
| `snapshots/ios/Issue31445DuplicateTitleIconDoesNotAppear.png` | Snapshot | binary |

</details>

<details>
<summary><strong>💬 PR Discussion Summary</strong></summary>

**Key Comments:**
- Issue verified by LogishaSelvarajSF4525 on MAUI 9.0.0 & 9.0.100
- PR triggered UI tests by jsuarezruiz
- PureWeen requested rebase

**Reviewer Feedback:**
- Copilot review: Suggested testing with different image sources or rapid succession to validate fix better

**Disagreements to Investigate:**
| File:Line | Reviewer Says | Author Says | Status |
|-----------|---------------|-------------|--------|
| Issue31445.cs:31 | Test with different images or rapid calls | N/A | ⚠️ INVESTIGATE |

**Author Uncertainty:**
- None noted

</details>

<details>
<summary><strong>🧪 Tests</strong></summary>

**Status**: ✅ COMPLETE

- [x] PR includes UI tests
- [x] Tests reproduce the issue
- [x] Tests follow naming convention (`Issue31445`)

**Test Files:**
- HostApp: `src/Controls/tests/TestCases.HostApp/Issues/Issue31445.cs`
- NUnit: `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31445.cs`

**Test Behavior:**
- Uses snapshot verification (`VerifyScreenshot()`)
- Navigates to test page, taps button to trigger duplicate icon scenario
- Verified to compile successfully

</details>

<details>
<summary><strong>🚦 Gate - Test Verification</strong></summary>

**Status**: ✅ PASSED

- [x] Tests FAIL without fix (bug reproduced - duplicate icons appeared)
- [x] Tests PASS with fix (single icon as expected)

**Result:** PASSED ✅

**Verification Details:**
- Platform: Android (emulator-5554)
- Without fix: Test FAILED (screenshot mismatch - duplicate icons)
- With fix: Test PASSED (single icon verified)

</details>

<details>
<summary><strong>🔧 Fix Candidates</strong></summary>

**Status**: ✅ COMPLETE

| # | Source | Approach | Test Result | Files Changed | Model | Notes |
|---|--------|----------|-------------|---------------|-------|-------|
| 1 | try-fix | Check for existing icon view at position 0, reuse if exists, only create new if needed | ✅ PASS | `ToolbarExtensions.cs` (+7) | Opus 4.5 | Works! Independently arrived at same solution logic as PR |
| 2 | try-fix | Dedupe defensively by scanning all toolbar children, keep first `ToolbarTitleIconImageView`, remove extras; then reuse/create | ✅ PASS | `ToolbarExtensions.cs` (+22/-5) | GPT 5.2 | More robust if child ordering changes or duplicates already exist |
| 3 | try-fix | Use `FindViewWithTag` to uniquely identify/retrieve the MAUI title icon | ✅ PASS | `ToolbarExtensions.cs` (+20/-6) | Gemini 2.0 Flash | Explicit identification; avoids index assumptions and iteration; most robust against external view insertions |
| PR | PR #31487 | Check for existing ToolbarTitleIconImageView before adding new one | ✅ PASS (Gate) | `ToolbarExtensions.cs` (+17/-6) | Author | Original PR - validated by Gate |

**Exhausted:** Yes (3 passing alternatives found)

**Selected Fix:** PR's fix - It’s simplest and sufficient.
- #3 (Tag) is the most "correct" for robustness but adds Tag management overhead.
- #2 (Dedupe) is good for cleanup.
- PR/#1 (Index 0) are standard for this codebase's patterns.

**Comparison Notes:**
- PR/try-fix #1 rely on `GetChildAt(0)` being the title icon view when present
- try-fix #2 is more defensive: it collapses existing duplicates regardless of child index and then reuses/creates as needed
- try-fix #3 uses explicit tagging: precise but introduces new state (Tag) to manage

</details>

---

**Next Step:** Propose Alternative Fix #2 (Dedupe & Scan) to Author for Discussion

---

## 💬 Draft Comment for Author

Hi @PureWeen,

Reviewing the fix in this PR, it works correctly for the reported issue and tests pass.

I explored a couple of alternative approaches and found one that might offer slightly better robustness against edge cases, which I wanted to run by you:

**Alternative: Dedupe & Scan**
Instead of just checking index 0, we could scan all children of the toolbar to find any `ToolbarTitleIconImageView` instances.

```csharp
// Scan all children to find existing title icons
ToolbarTitleIconImageView? titleIcon = null;
for (int i = 0; i < nativeToolbar.ChildCount; i++)
{
var child = nativeToolbar.GetChildAt(i);
if (child is ToolbarTitleIconImageView icon)
{
if (titleIcon == null)
titleIcon = icon; // Keep the first one found
else
nativeToolbar.RemoveView(icon); // Remove any extras (self-healing)
}
}
```

**Why consider this?**
1. **Robustness against Injection:** If another library inserts a view at index 0 (e.g., search bar), the current PR fix (checking only index 0) would fail to see the existing icon and create a duplicate.
2. **Self-Healing:** If the toolbar is already in a bad state (multiple icons from previous bugs), this approach cleans them up.

**Trade-off:**
It involves a loop, so O(N) instead of O(1), but for a toolbar with very few items, this is negligible.

Do you think the added robustness is worth the change, or should we stick to the simpler Index 0 check (current PR) which matches the existing removal logic?

---

## 📋 Final Report

### Summary

PR #31487 correctly fixes the duplicate title icon issue on Android. The fix checks for an existing `ToolbarTitleIconImageView` at position 0 before creating a new one, preventing duplicate icons when `SetTitleIconImageSource` is called multiple times.

### Root Cause

The original `UpdateTitleIcon` method always created a new `ToolbarTitleIconImageView` and added it to position 0, without checking if one already existed. This caused duplicate icons when the method was called repeatedly.

### Validation

| Check | Result |
|-------|--------|
| Tests reproduce bug | ✅ Test fails without fix (duplicate icons) |
| Tests pass with fix | ✅ Test passes with fix (single icon) |
| Independent fix analysis | ✅ try-fix arrived at same solution |
| Code quality | ✅ Clean, minimal change |

### Regression Analysis

<details>
<summary><strong>📜 Git History Analysis</strong></summary>

**Original Implementation:** `e2f3aaa222` (Oct 2021) by Shane Neuville
- Part of "[Android] ToolbarHandler and fixes for various page nesting scenarios (#2781)"
- The bug has existed since the original implementation - it was never designed to handle repeated calls

**Key Finding:** The original code had a check for removing an existing icon when source is null/empty:
```csharp
if (nativeToolbar.GetChildAt(0) is ToolbarTitleIconImageView existingImageView)
nativeToolbar.RemoveView(existingImageView);
```
But this check was **only in the removal path**, not in the creation path. The fix extends this pattern to also check before adding.

**Related Toolbar Issues in This File:**
| Commit | Issue | Description |
|--------|-------|-------------|
| `a93e88c3de` | #7823 | Fix toolbar item icon not removed when navigating |
| `c04b7d79cc` | #19673 | Fixed android toolbar icon change |
| `158ed8b4f1` | #28767 | Removing outdated menu items after activity switch |

**Pattern:** Multiple fixes in this file address issues where Android toolbar state isn't properly cleaned up or reused. This PR follows the same pattern.

</details>

<details>
<summary><strong>🔄 Platform Comparison</strong></summary>

| Platform | TitleIcon Implementation | Duplicate Prevention |
|----------|-------------------------|---------------------|
| **Android** | Creates `ToolbarTitleIconImageView`, adds to position 0 | ❌ Was missing (now fixed by PR) |
| **Windows** | Sets `TitleIconImageSource` property directly | ✅ Property-based, no duplicates possible |
| **iOS** | Uses `NavigationRenderer` with property binding | ✅ Property-based approach |

**Why Android was vulnerable:** Android uses a view-based approach (adding/removing child views) while other platforms use property-based approaches. View management requires explicit duplicate checks.

</details>

<details>
<summary><strong>⚠️ Risk Assessment</strong></summary>

**Regression Risk: LOW**

1. **Minimal change** - Only modifies the creation logic, doesn't change removal
2. **Consistent pattern** - Uses same `GetChildAt(0)` check that already existed for removal
3. **Well-tested** - UI test verifies the specific scenario
4. **No side effects** - Reusing existing view is safe; `SetImageDrawable` handles updates

**Potential Edge Cases (from Copilot review suggestion):**
- Setting different image sources rapidly → Should work fine, image is updated on existing view
- Setting same source multiple times → Explicitly tested, works correctly

</details>

### Recommendation

**✅ APPROVE** - The PR's approach is correct and validated by independent analysis. The fix is minimal, focused, and addresses the root cause.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,6 @@ temp
# TypeScript source map files (generated artifacts)
# Note: CSS map files in templates (e.g., bootstrap) are intentionally tracked
*.js.map

# Gradle build reports
src/Core/AndroidNative/build/reports/
6 changes: 3 additions & 3 deletions eng/pipelines/ci-uitests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,12 @@ stages:
# BuildNativeAOT is false by default, but true in devdiv environment
BuildNativeAOT: ${{ or(parameters.BuildNativeAOT, and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['System.TeamProject'], 'devdiv'))) }}
RunNativeAOT: ${{ parameters.RunNativeAOT }}
${{ if or(parameters.BuildEverything, and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['System.TeamProject'], 'devdiv'))) }}:
${{ if or(parameters.BuildEverything, ne(variables['Build.Reason'], 'PullRequest')) }}:
androidApiLevels: [ 30 ]
iosVersions: [ '18.4' ]
iosVersions: [ '18.5', 'latest' ]
${{ else }}:
androidApiLevels: [ 30 ]
iosVersions: [ '18.4' ]
iosVersions: [ 'latest' ]
projects:
- name: controls
desc: Controls
Expand Down
9 changes: 5 additions & 4 deletions eng/pipelines/common/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ parameters:
androidApiLevelsExtended: [ 36 ] # API 36 for Material3 tests with Pixel 3 XL
iosVersions: [ 'latest' ]
provisionatorChannel: 'latest'
defaultiOSVersion: '26.0'
timeoutInMinutes: 180
skipProvisioning: true
BuildNativeAOT: false # Parameter to control whether NativeAOT artifacts should be built
Expand Down Expand Up @@ -295,7 +296,7 @@ stages:
parameters:
platform: ios
${{ if eq(version, 'latest') }}:
version: 18.5
version: ${{ parameters.defaultiOSVersion }}
${{ if ne(version, 'latest') }}:
version: ${{ version }}
path: ${{ project.ios }}
Expand Down Expand Up @@ -338,7 +339,7 @@ stages:
parameters:
platform: ios
${{ if eq(version, 'latest') }}:
version: 18.5
version: ${{ parameters.defaultiOSVersion }}
${{ if ne(version, 'latest') }}:
version: ${{ version }}
path: ${{ project.ios }}
Expand Down Expand Up @@ -382,7 +383,7 @@ stages:
parameters:
platform: ios
${{ if eq(version, 'latest') }}:
version: 18.5
version: ${{ parameters.defaultiOSVersion }}
${{ if ne(version, 'latest') }}:
version: ${{ version }}
path: ${{ project.ios }}
Expand Down Expand Up @@ -432,7 +433,7 @@ stages:
parameters:
platform: ios
${{ if eq(version, 'latest') }}:
version: 18.5
version: ${{ parameters.defaultiOSVersion }}
${{ if ne(version, 'latest') }}:
version: ${{ version }}
path: ${{ project.ios }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ protected async virtual void OnNavigateBack()
{
try
{
// Call OnBackButtonPressed to allow the page to intercept navigation
if (Page?.SendBackButtonPressed() == true)
return;

await Page.Navigation.PopAsync();
}
catch (Exception exc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Windows.Input;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Platform;
using UIKit;
using static Microsoft.Maui.Controls.Compatibility.Platform.iOS.AccessibilityExtensions;
Expand Down Expand Up @@ -498,13 +499,13 @@ void UpdateLeftToolbarItems()

UIImage? icon = null;

var foregroundColor = _context?.Shell.CurrentPage?.GetValue(Shell.ForegroundColorProperty) as Color ??
_context?.Shell.GetValue(Shell.ForegroundColorProperty) as Color;

if (image is not null)
{
icon = result?.Value;

var foregroundColor = _context?.Shell.CurrentPage?.GetValue(Shell.ForegroundColorProperty) ??
_context?.Shell.GetValue(Shell.ForegroundColorProperty);

if (foregroundColor is null)
{
icon = icon?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
Expand Down Expand Up @@ -542,6 +543,16 @@ void UpdateLeftToolbarItems()
{
NavigationItem.LeftBarButtonItem =
new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, (s, e) => LeftBarButtonItemHandler(ViewController, IsRootPage)) { Enabled = enabled };

// For iOS 26+, explicitly set the tint color on the bar button item
// because the navigation bar's tint color is not automatically inherited
if (OperatingSystem.IsIOSVersionAtLeast(26) || OperatingSystem.IsMacCatalystVersionAtLeast(26))
{
if (foregroundColor is not null)
{
NavigationItem.LeftBarButtonItem.TintColor = foregroundColor.ToPlatform();
}
}
Comment on lines +547 to +555
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

Consider adding a GitHub issue link reference to the comment explaining the iOS 26 tint color inheritance change. This would help track when the workaround can be removed if Apple fixes the issue in a future iOS release.

Copilot uses AI. Check for mistakes.
}
else
{
Expand Down
1 change: 1 addition & 0 deletions src/Controls/src/Core/Editor/Editor.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public partial class Editor

#if IOS || ANDROID
EditorHandler.Mapper.AppendToMapping(nameof(VisualElement.IsFocused), InputView.MapIsFocused);
EditorHandler.Mapper.AppendToMapping(nameof(VisualElement.IsVisible), InputView.MapIsVisible);
#endif

#if ANDROID
Expand Down
1 change: 1 addition & 0 deletions src/Controls/src/Core/Entry/Entry.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public partial class Entry

#if IOS || ANDROID
EntryHandler.Mapper.AppendToMapping(nameof(VisualElement.IsFocused), InputView.MapIsFocused);
EntryHandler.Mapper.AppendToMapping(nameof(VisualElement.IsVisible), InputView.MapIsVisible);
#endif

#if ANDROID
Expand Down
Loading
Loading