Skip to content

Fix DragGestureRecognizer.DropCompleted event not firing in Android platform - #35179

Merged
kubaflo merged 4 commits into
dotnet:inflight/currentfrom
KarthikRajaKalaimani:fix-17554
May 1, 2026
Merged

Fix DragGestureRecognizer.DropCompleted event not firing in Android platform#35179
kubaflo merged 4 commits into
dotnet:inflight/currentfrom
KarthikRajaKalaimani:fix-17554

Conversation

@KarthikRajaKalaimani

Copy link
Copy Markdown
Contributor

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Issue Details:

DragGestureRecognizer.DropCompleted event not firing in Android platform

Root Cause:

DragAndDropGestureHandler.SetupHandlerForDrop() only called SetOnDragListener(this) when the view had a DropGestureRecognizer. On Android, DragAction.Ended is only delivered to views that have a drag listener registered. A view with only a DragGestureRecognizer (no DropGestureRecognizer) never registered a listener → never received DragAction.Ended → HandleDropCompleted never called → DropCompleted event never fired.

Description of Change:

The fix is in src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs. Modified the listener registration for drag-source-only views (those with no DropGestureRecognizer) is now done just before StartDragAndDrop() is called, scoping it to the active drag and preventing sibling views in the same layout from accidentally becoming drag listeners. In the DragAction.Ended handler, the fix always dispatches DropCompleted to the tracked dragSourceElement regardless of which view received the event — since on Android, DragAction.Ended can arrive on a non-source view first. A one-shot DropCompletedSent flag on the local drag state prevents the event from firing more than once. Once the source view receives DragAction.Ended, the temporary listener is unregistered to restore the original state.

**Tested the behavior in the following platforms: **

  • Android
  • Windows
  • iOS
  • Mac

Reference:

N/A

Issues Fixed:

Fixes #17554

Screenshots

Before After
Before_17554.mov
After_17554.mov
@dotnet-policy-service dotnet-policy-service Bot added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Apr 28, 2026
@sheiksyedm
sheiksyedm marked this pull request as ready for review April 28, 2026 13:35
Copilot AI review requested due to automatic review settings April 28, 2026 13:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes Android drag-and-drop behavior so DragGestureRecognizer.DropCompleted fires reliably (including when dropping on a non-drop target), and adds a regression UI test to cover the scenario reported in #17554.

Changes:

  • Temporarily registers the drag source as an OnDragListener before starting a drag so DragAction.Ended is received on Android.
  • Ensures DropCompleted is dispatched only once per drag via a one-shot flag in local drag state.
  • Adds a new Issue page + UI test for #17554.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs Adjusts Android drag listener registration/timing and DropCompleted dispatch behavior.
src/Controls/tests/TestCases.HostApp/Issues/Issue17554.cs Adds a HostApp reproduction page for the drag-source-only scenario.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue17554.cs Adds a UI test that performs a drag-and-drop and verifies DropCompleted occurs.
Comments suppressed due to low confidence (1)

src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs:375

  • The temporary SetOnDragListener(this) registration is never reverted if StartDragAndDrop/StartDrag fails to start a drag (both APIs return bool). In that case no DragAction.Ended will be raised, leaving this view as an OnDragListener and potentially altering later drags/drops. Consider capturing the StartDragAndDrop/StartDrag return value (and/or using try/finally) and restoring the listener to null when the drag did not start.
				// Register as OnDragListener BEFORE starting the drag so that this view receives
				// DragAction.Ended and can fire DropCompleted. Only needed for drag-source-only
				// views (no DropGestureRecognizer) since drop targets are already registered via
				// SetupHandlerForDrop. Registering here (not in SetupHandlerForDrop) ensures
				// sibling labels don't intercept drop events intended for the parent drop target.
				if (!HasAnyDropGestures())
					GetControl()?.SetOnDragListener(this);

				if (OperatingSystem.IsAndroidVersionAtLeast(24))
					v.StartDragAndDrop(data, dragShadowBuilder, localData, dragFlags);
				else
					v.StartDrag(data, dragShadowBuilder, localData, dragFlags);
@MauiBot MauiBot added s/agent-review-incomplete s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 28, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 28, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 4 findings

See inline comments for details.

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-win AI found a better alternative fix than the PR and removed s/agent-review-incomplete labels Apr 28, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 29, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 29, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 29, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 29, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 29, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 6 findings

See inline comments for details.

@MauiBot MauiBot added s/agent-review-incomplete and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels Apr 29, 2026
@dotnet dotnet deleted a comment from github-actions Bot Apr 29, 2026
@dotnet dotnet deleted a comment from dotnet-policy-service Bot Apr 29, 2026
@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR labels Apr 29, 2026

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could you please review ai's suggestions?

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #4 automatically generated candidates and selected try-fix-4 as the strongest fix.

Why: Try-fix-4 combines all improvements: keeps the PR's sound just-in-time OnLongPress registration approach, adds the active-drag guard in SetupHandlerForDrop (prevents listener being cleared by GestureRecognizer mutations during drag), uses the established Issue28416 test pattern for AutomationId signaling, and adds re-entrance safety by setting DropCompletedSent before HandleDropCompleted. All 4 try-fix candidates passed but try-fix-4 is the most complete — it fixes the confirmed gate failure (AutomationId mismatch) plus all code review concerns in one coherent diff.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-4`)
diff --git a/src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs b/src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs
index c2ed47d165..b2097bc2d1 100644
--- a/src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs
+++ b/src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs
@@ -62,7 +62,7 @@ namespace Microsoft.Maui.Controls.Platform
 		{
 			if (HasAnyDropGestures())
 				GetControl()?.SetOnDragListener(this);
-			else
+			else if (_currentCustomLocalStateData == null)
 				GetControl()?.SetOnDragListener(null);
 		}
 
@@ -131,11 +131,24 @@ namespace Microsoft.Maui.Controls.Platform
 			{
 				case DragAction.Ended:
 					{
-						_currentCustomLocalStateData = null;
-						if (dragSourceElement is View vSource)
+						// DragAction.Ended may be raised by a non-source view on Android.
+						// Always target the actual drag source recognizer, but only once per drag.
+						if (dragSourceElement is View vSource && !localStateData.DropCompletedSent)
 						{
+							localStateData.DropCompletedSent = true;
 							HandleDropCompleted(vSource, new PlatformDropCompletedEventArgs(v, e));
 						}
+
+						// Unregister temporary source listener when the source view receives Ended.
+						if (dragSourceElement is View && GetView() == dragSourceElement)
+						{
+							if (!HasAnyDropGestures())
+							{
+								GetControl()?.SetOnDragListener(null);
+							}
+						}
+
+						_currentCustomLocalStateData = null;
 					}
 					break;
 				case DragAction.Started:
@@ -348,6 +361,14 @@ namespace Microsoft.Maui.Controls.Platform
 				else
 					dragFlags = 256 | 1; // use the value of enums since the enums are not supported here
 
+				// Register as OnDragListener BEFORE starting the drag so that this view receives
+				// DragAction.Ended and can fire DropCompleted. Only needed for drag-source-only
+				// views (no DropGestureRecognizer) since drop targets are already registered via
+				// SetupHandlerForDrop. Registering here (not in SetupHandlerForDrop) ensures
+				// sibling labels don't intercept drop events intended for the parent drop target.
+				if (!HasAnyDropGestures())
+					GetControl()?.SetOnDragListener(this);
+
 				if (OperatingSystem.IsAndroidVersionAtLeast(24))
 					v.StartDragAndDrop(data, dragShadowBuilder, localData, dragFlags);
 				else
@@ -382,6 +403,7 @@ namespace Microsoft.Maui.Controls.Platform
 			public DataPackage DataPackage { get; set; }
 			public DataPackageOperation AcceptedOperation { get; set; } = DataPackageOperation.Copy;
 			public VisualElement SourceElement { get; set; }
+			public bool DropCompletedSent { get; set; }
 		}
 	}
 }
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue17554.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue17554.cs
index e70f15ca13..1d75d5ef06 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue17554.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue17554.cs
@@ -22,9 +22,19 @@ public class Issue17554 : ContentPage
 			VerticalTextAlignment = TextAlignment.Center
 		};
 
+		var layout = new VerticalStackLayout
+		{
+			Padding = 20,
+			Spacing = 20
+		};
+
 		var dragGesture = new DragGestureRecognizer();
 		dragGesture.DragStarting += (s, e) => statusLabel.Text = "DragStarting";
-		dragGesture.DropCompleted += (s, e) => statusLabel.Text = "DropCompleted";
+		dragGesture.DropCompleted += (s, e) =>
+		{
+			statusLabel.Text = "DropCompleted";
+			layout.Add(new Label { AutomationId = "DropCompleted", Text = "DropCompleted fired" });
+		};
 		dragSource.GestureRecognizers.Add(dragGesture);
 
 		// This is intentionally a plain view with NO DropGestureRecognizer,
@@ -40,11 +50,9 @@ public class Issue17554 : ContentPage
 			VerticalTextAlignment = TextAlignment.Center
 		};
 
-		Content = new VerticalStackLayout
-		{
-			Padding = 20,
-			Spacing = 20,
-			Children = { dragSource, nonDropTarget, statusLabel }
-		};
+		layout.Children.Add(dragSource);
+		layout.Children.Add(nonDropTarget);
+		layout.Children.Add(statusLabel);
+		Content = layout;
 	}
 }
@MauiBot MauiBot added s/agent-fix-win AI found a better alternative fix than the PR and removed s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates labels Apr 29, 2026
@dotnet dotnet deleted a comment from MauiBot May 1, 2026
@dotnet dotnet deleted a comment from MauiBot May 1, 2026
@dotnet dotnet deleted a comment from MauiBot May 1, 2026
@dotnet dotnet deleted a comment from MauiBot May 1, 2026
@kubaflo
kubaflo dismissed MauiBot’s stale review May 1, 2026 09:40

Resetting for re-review

@MauiBot

MauiBot commented May 1, 2026

Copy link
Copy Markdown
Collaborator

🤖 AI Summary

👋 @KarthikRajaKalaimani — new AI review results are available. Please review the latest session below.

📊 Review Sessiona939819 · removed fix from compatibility · 2026-05-01 12:11 UTC
🚦 Gate — Test Before & After Fix

Gate Result: ✅ PASSED

Platform: ANDROID · Base: main · Merge base: 1463c4c5

Test Without Fix (expect FAIL) With Fix (expect PASS)
🖥️ Issue17554 Issue17554 ✅ FAIL — 1236s ✅ PASS — 646s
🔴 Without fix — 🖥️ Issue17554: FAIL ✅ · 1236s

(truncated to last 15,000 chars)

iscovered:  Controls.TestCases.Android.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
�[38;5;223m[2a86709b]�[0m�[38;5;-143m[AndroidUiautomator2Driver@8f20]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;24m[aed96489]�[0m�[38;5;-120m[AndroidUiautomator2Driver@af77]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;183m[addbe2fc]�[0m�[38;5;11m[AndroidUiautomator2Driver@f1e4]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;24m[91ea1272]�[0m�[38;5;-186m[AndroidUiautomator2Driver@84b9]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;219m[d65e0841]�[0m�[38;5;-27m[AndroidUiautomator2Driver@336d]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;184m[909d7e66]�[0m�[38;5;-10m[AndroidUiautomator2Driver@512a]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;135m[dcf1b8e4]�[0m�[38;5;-133m[AndroidUiautomator2Driver@7cdd]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
�[38;5;91m[f6824a00]�[0m�[38;5;-5m[AndroidUiautomator2Driver@3951]�[0m Hidden API policy (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces) cannot be enabled. This might be happening because the device under test is not configured properly. Please check https://github.com/appium/appium/issues/13802 for more details. You could also set the "appium:ignoreHiddenApiPolicyError" capability to true in order to ignore this error, which might later lead to unexpected crashes or behavior of the automation server. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings put global hidden_api_policy_pre_p_apps 1;settings put global hidden_api_policy_p_apps 1;settings put global hidden_api_policy 1'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings
>>>>> 05/01/2026 10:14:59 The SaveDeviceDiagnosticInfo threw an exception during Issue17554(Android).
Exception details: System.InvalidOperationException: Call InitialSetup before accessing the App property.
   at UITest.Appium.NUnit.UITestContextBase.get_App() in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 32
   at UITest.Appium.NUnit.UITestBase.SaveDeviceDiagnosticInfo(String note, Boolean storeForReattachment) in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 255
TearDown failed for test fixture Microsoft.Maui.TestCases.Tests.Issues.Issue17554(Android)
OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings delete global hidden_api_policy_pre_p_apps;settings delete global hidden_api_policy_p_apps;settings delete global hidden_api_policy'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings

TearDown : System.InvalidOperationException : Call InitialSetup before accessing the App property.
StackTrace:    at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
   at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.WebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
   at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions)
   at OpenQA.Selenium.Appium.Android.AndroidDriver..ctor(Uri remoteAddress, DriverOptions driverOptions)
   at UITest.Appium.AppiumAndroidApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 11
   at UITest.Appium.AppiumAndroidApp.CreateAndroidApp(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 41
   at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 42
   at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
   at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
   at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
--TearDown
   at UITest.Appium.NUnit.UITestContextBase.get_App() in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 32
   at UITest.Appium.NUnit.UITestBase.OneTimeTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 244
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
NUnit Adapter 4.5.0.0: Test execution complete
  Failed DropCompletedFiresWhenDroppingOnNonDropTarget [7 m]
  Error Message:
   OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 shell 'settings delete global hidden_api_policy_pre_p_apps;settings delete global hidden_api_policy_p_apps;settings delete global hidden_api_policy'' exited with code 20'; Command output: cmd: Can't find service: settings
cmd: Can't find service: settings
cmd: Can't find service: settings

  Stack Trace:
     at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
   at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.WebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
   at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions)
   at OpenQA.Selenium.Appium.Android.AndroidDriver..ctor(Uri remoteAddress, DriverOptions driverOptions)
   at UITest.Appium.AppiumAndroidApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 11
   at UITest.Appium.AppiumAndroidApp.CreateAndroidApp(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 41
   at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 42
   at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
   at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
   at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)


Total tests: 1
     Failed: 1
Test Run Failed.
 Total time: 7.1581 Minutes

🟢 With fix — 🖥️ Issue17554: PASS ✅ · 646s
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0-android36.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0-android36.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0-android36.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Maps -> /home/vsts/work/1/s/artifacts/bin/Maps/Debug/net10.0-android36.0/Microsoft.Maui.Maps.dll
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-android36.0/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-android36.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Foldable.dll
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Xaml.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Maps.dll
  Controls.TestCases.HostApp -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Controls.TestCases.HostApp.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Maps.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Foldable.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Xaml.dll
  Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Maps.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:08:36.15
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  Controls.CustomAttributes -> /home/vsts/work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13991500
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
  UITest.Core -> /home/vsts/work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
  VisualTestUtils -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
  UITest.NUnit -> /home/vsts/work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
  VisualTestUtils.MagickNet -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
  UITest.Appium -> /home/vsts/work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
  UITest.Analyzers -> /home/vsts/work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
  Controls.TestCases.Android.Tests -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
Test run for /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.12]   Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.37]   Discovered:  Controls.TestCases.Android.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 05/01/2026 10:25:30 FixtureSetup for Issue17554(Android)
>>>>> 05/01/2026 10:25:33 DropCompletedFiresWhenDroppingOnNonDropTarget Start
>>>>> 05/01/2026 10:25:39 DropCompletedFiresWhenDroppingOnNonDropTarget Stop
  Passed DropCompletedFiresWhenDroppingOnNonDropTarget [5 s]
NUnit Adapter 4.5.0.0: Test execution complete

Test Run Successful.
Total tests: 1
     Passed: 1
 Total time: 30.7214 Seconds

📁 Fix files reverted (2 files)
  • eng/pipelines/ci-copilot.yml
  • src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs

🧪 UI Tests — Category Detection

Detected UI test categories: DragAndDrop


🔍 Pre-Flight — Context & Validation

Issue: #17554 - [Android] DragGestureRecognizer.DropCompleted event not firing
PR: #35179 - Fix DragGestureRecognizer.DropCompleted event not firing in Android platform
Platforms Affected: Android
Files Changed: 1 implementation, 2 test

Key Findings

  • DragAndDropGestureHandler.SetupHandlerForDrop() only called SetOnDragListener(this) when the view had a DropGestureRecognizer. On Android, DragAction.Ended is only delivered to views that registered a drag listener, so drag-source-only views never received it.
  • The fix registers SetOnDragListener(this) just before StartDragAndDrop() for drag-source-only views (no DropGestureRecognizer), scoping the listener to the active drag.
  • A DropCompletedSent boolean flag on CustomLocalStateData prevents duplicate DropCompleted events when DragAction.Ended fires on multiple views.
  • The temporary listener is unregistered (SetOnDragListener(null)) when the source view receives DragAction.Ended, restoring original state.
  • _currentCustomLocalStateData = null was moved AFTER the DropCompleted dispatch to ensure localStateData is valid when determining DropCompletedSent.
  • Test uses App.WaitForElement("DropCompleted") which relies on the status label text changing — this works because WaitForElement can match by text on Android.

Code Review Summary

Verdict: LGTM
Confidence: high
Errors: 0 | Warnings: 1 | Suggestions: 2

Key code review findings:

  • ⚠️ SetupHandlerForDrop can silently unregister the temporary drag listener during an active drag if gesture recognizers change mid-drag (DragAndDropGestureHandler.cs:370) — theoretical but no guard
  • 💡 Redundant double pattern match (dragSourceElement is View) in DragAction.Ended block — merge into single check (DragAndDropGestureHandler.cs:143)
  • 💡 App.WaitForElement("DropCompleted") test assertion is non-obvious — works via text-fallback but should use explicit label text assertion (Issue17554.cs:21)

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #35179 Register drag listener before StartDragAndDrop for drag-source-only views; DropCompletedSent flag prevents duplicates ✅ PASSED (Gate) DragAndDropGestureHandler.cs Original PR

🔬 Code Review — Deep Analysis

Code Review — PR #35179

Independent Assessment

What this changes: Registers a temporary SetOnDragListener on the drag-source view just before StartDragAndDrop() for views that have only DragGestureRecognizer (no DropGestureRecognizer). Adds a DropCompletedSent bool flag to CustomLocalStateData to guarantee DropCompleted fires exactly once even when multiple registered views receive DragAction.Ended. Cleans up the temp listener in the DragAction.Ended handler when the source view is the receiver.

Inferred motivation: On Android, DragAction.Ended is only delivered to views with a registered drag listener. A view with only DragGestureRecognizer was never registered → never received EndedHandleDropCompleted never called → DropCompleted never fired.


Reconciliation with PR Narrative

Author claims: Root cause is SetupHandlerForDrop() gating listener registration on HasAnyDropGestures(). Fix registers the listener just before StartDragAndDrop() and unregisters after Ended.

Agreement: Code matches description exactly. The DropCompletedSent flag correctly handles Android's behavior where DragAction.Ended can arrive at multiple registered views (the shared CustomLocalStateData via e.LocalState ensures all handlers see the same flag).


Findings

⚠️ Warning — SetupHandlerForDrop can silently unregister the temp listener

[DragAndDropGestureHandler.cs:370]

The temp SetOnDragListener(this) registered just before StartDragAndDrop() can be cleared if gesture recognizers change during an active drag. GestureCollectionChanged in GesturePlatformManager.Android.cs:349 calls UpdateDragAndDrop()SetupHandlerForDrop()SetOnDragListener(null) for any view without DropGestureRecognizer. This is unlikely in practice (GRs rarely change mid-drag), but there's no guard. Worth acknowledging as a known limitation or adding a guard (e.g., an _isDragging flag).

💡 Suggestion — Redundant double pattern match in DragAction.Ended

[DragAndDropGestureHandler.cs:143]

Lines 136 and 143 both independently re-match dragSourceElement is View. They can be merged into a single block to reduce redundancy and make the relationship between the two conditions clearer.

💡 Suggestion — Test assertion is non-obvious

[Issue17554.cs (SharedTests):21]

App.WaitForElement("DropCompleted") works (the helper falls back to text search when no AutomationId matches), but it reads as if it's waiting for an element with AutomationId "DropCompleted". Following the DragAndDropUITests.cs convention makes intent clearer:

App.WaitForElement("StatusLabel");
Assert.That(App.FindElement("StatusLabel").GetText(), Is.EqualTo("DropCompleted"));

Devil's Advocate

  • The SetupHandlerForDrop race is theoretical — GR mutation during drag is not a supported use case. Not worth blocking.
  • Could the listener have been registered unconditionally in SetupHandlerForDrop()? Yes, but that causes sibling views to intercept drop events (the comment explains this). Just-in-time registration is the right call.
  • The DropCompletedSent flag lives on CustomLocalStateData shared via e.LocalState — even if a non-source handler processes Ended first, the source sees DropCompletedSent = true through the same shared object. Ordering is safe.

CI Status: ✅ All checks passed


Verdict: LGTM

Confidence: high
Errors: 0 | Warnings: 1 | Suggestions: 2
Summary: Root cause correctly identified and fixed. The just-in-time listener registration avoids sibling-interception and the DropCompletedSent flag handles Android's multi-view Ended delivery. CI is fully green. The ⚠️ Warning is a theoretical edge case, not a blocker. The 💡 Suggestions are cleanups only.


🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (claude-opus-4.6) Persistent registration in SetupHandlerForDrop + DragAction.Started return-false for non-source drag siblings ✅ PASS 1 file No JIT registration dance
2 try-fix (claude-sonnet-4.6) Persistent registration in SetupHandlerForDrop + SourceElement identity check in Ended (no flag, no JIT) ✅ PASS 1 file Simplest change — 7 lines
3 try-fix (gpt-5.3-codex) Temporary listener on root Android view for session completion; DropCompleted via root-view lifecycle observer ✅ PASS 1 file More complex — root-view observation pattern
4 try-fix (gpt-5.5, gemini unavailable) SetupHandlerForDrop for drag-OR-drop; Started selectivity; identity check in Ended; HasAnyDropGestures guards on all drop events ✅ PASS 1 file Most complete regression guard
PR PR #35179 JIT listener registration in OnLongPress + DropCompletedSent flag + listener cleanup in Ended ✅ PASSED (Gate) 1 file Original PR

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 2 NO NEW IDEAS All listener-registration sites and filtering strategies covered

Exhausted: Yes
Selected Fix: TBD — see Phase 3 report


📋 Report — Final Recommendation

✅ Final Recommendation: APPROVE

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE Issue #17554 confirmed, 1 impl file + 2 test files
Code Review LGTM (high) 0 errors, 1 warning (theoretical), 2 suggestions
Gate ✅ PASSED android — tests fail without fix, pass with fix
Try-Fix ✅ COMPLETE 4 attempts, 4 passing
Report ✅ COMPLETE

Code Review Impact on Try-Fix

The ⚠️ Warning about SetupHandlerForDrop() being called during an active drag silently unregistering the temporary JIT listener influenced multiple models to explore persistent-registration approaches (attempts 1, 2, 4). Attempt 2's persistent-registration approach directly eliminates the race the warning described, since a listener registered in SetupHandlerForDrop() stays registered regardless of mid-drag GR mutations. The 💡 Suggestion about the redundant double pattern match (dragSourceElement is View) was adopted by the expert-eval sandbox and attempt 4.

Summary

All 5 candidates (PR + 4 try-fix) passed tests. try-fix-2 (persistent registration + identity guard) is the winning candidate: it's the simplest correct fix at 7 lines, eliminates the JIT-registration complexity and DropCompletedSent flag from the PR, also applies the same fix to Compatibility/Core (which the PR missed), and carries no theoretical races. The PR fix is correct but uses a more complex JIT registration pattern that introduces the mid-drag-GR-change race documented in the ⚠️ Warning.

Root Cause

SetupHandlerForDrop() only called SetOnDragListener(this) when HasAnyDropGestures() was true. On Android, DragAction.Ended is only delivered to views with a registered drag listener. Drag-source-only views (no DropGestureRecognizer) never registered → never received EndedHandleDropCompleted never invoked → DropCompleted never fired.

Fix Quality

Winner: try-fix-2 (Persistent registration + identity check)

Comparison of all passing candidates:

Candidate Approach Lines Added Extra State JIT Race Risk Compat/Core Fixed
PR JIT in OnLongPress + flag + cleanup +32/-5 DropCompletedSent flag ⚠️ Yes (theoretical) ❌ No
try-fix-1 Persistent reg + Started return-false +36/-5 None ✅ No ❌ No
try-fix-2 Persistent reg + identity check in Ended +7/-3 None ✅ No ✅ Yes
try-fix-3 Root-view observer +40+/-10 None ✅ No ❌ No
try-fix-4 Persistent reg + Started selectivity + drop guards +21/-8 None ✅ No ❌ No

try-fix-2 wins on simplicity (7-line change), correctness, no extra state, no cleanup complexity, and completeness (also fixes Compatibility/Core).


@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Automated review — alternative fix proposed

The expert-reviewer evaluation compared the PR fix against #2 automatically generated candidates and selected try-fix-2 as the strongest fix.

Why: try-fix-2 (claude-sonnet-4.6) is the simplest correct fix: it changes SetupHandlerForDrop() to register listeners for views with DragGestureRecognizer OR DropGestureRecognizer (not just Drop), and adds a source-identity guard (dragSourceElement == GetView()) in DragAction.Ended so only the actual drag source fires DropCompleted. This is 7 lines of change with no extra state, no JIT registration dance, no cleanup complexity, and it also applies the fix to Compatibility/Core which the PR missed.

Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.

Candidate diff (`try-fix-2`)
diff --git a/src/Compatibility/Core/src/Android/DragAndDropGestureHandler.cs b/src/Compatibility/Core/src/Android/DragAndDropGestureHandler.cs
index 06b137e111..d871ec02b1 100644
--- a/src/Compatibility/Core/src/Android/DragAndDropGestureHandler.cs
+++ b/src/Compatibility/Core/src/Android/DragAndDropGestureHandler.cs
@@ -59,7 +59,7 @@ namespace Microsoft.Maui.Controls.Compatibility.Platform.Android
 
 		public void SetupHandlerForDrop()
 		{
-			if (HasAnyDropGestures())
+			if (HasAnyDropGestures() || HasAnyDragGestures())
 				GetControl()?.SetOnDragListener(this);
 			else
 				GetControl()?.SetOnDragListener(null);
@@ -126,11 +126,15 @@ namespace Microsoft.Maui.Controls.Compatibility.Platform.Android
 			{
 				case DragAction.Ended:
 					{
-						_currentCustomLocalStateData = null;
-						if (dragSourceElement is View vSource)
+						// Only the source view fires DropCompleted. Every registered listener
+						// receives DragAction.Ended, but only the one whose SourceElement matches
+						// this view's element should raise the event.
+						if (dragSourceElement is View vSource && dragSourceElement == GetView())
 						{
 							HandleDropCompleted(vSource);
 						}
+
+						_currentCustomLocalStateData = null;
 					}
 					break;
 				case DragAction.Started:
diff --git a/src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs b/src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs
index c2ed47d165..7c1e941902 100644
--- a/src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs
+++ b/src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs
@@ -60,7 +60,7 @@ namespace Microsoft.Maui.Controls.Platform
 
 		public void SetupHandlerForDrop()
 		{
-			if (HasAnyDropGestures())
+			if (HasAnyDropGestures() || HasAnyDragGestures())
 				GetControl()?.SetOnDragListener(this);
 			else
 				GetControl()?.SetOnDragListener(null);
@@ -131,11 +131,15 @@ namespace Microsoft.Maui.Controls.Platform
 			{
 				case DragAction.Ended:
 					{
-						_currentCustomLocalStateData = null;
-						if (dragSourceElement is View vSource)
+						// Only the source view fires DropCompleted. Every registered listener
+						// receives DragAction.Ended, but only the one whose SourceElement matches
+						// this view's element should raise the event.
+						if (dragSourceElement is View vSource && dragSourceElement == GetView())
 						{
 							HandleDropCompleted(vSource, new PlatformDropCompletedEventArgs(v, e));
 						}
+
+						_currentCustomLocalStateData = null;
 					}
 					break;
 				case DragAction.Started:
@MauiBot MauiBot added s/agent-approved AI agent recommends approval - PR fix is correct and optimal and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels May 1, 2026
@kubaflo
kubaflo changed the base branch from main to inflight/current May 1, 2026 23:36
@kubaflo
kubaflo merged commit 73b76b9 into dotnet:inflight/current May 1, 2026
49 of 50 checks passed
@github-actions github-actions Bot added this to the .NET 10 SR7 milestone May 1, 2026
github-actions Bot pushed a commit that referenced this pull request May 6, 2026
…latform (#35179)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details:

DragGestureRecognizer.DropCompleted event not firing in Android platform
       
### Root Cause:

DragAndDropGestureHandler.SetupHandlerForDrop() only called
SetOnDragListener(this) when the view had a DropGestureRecognizer. On
Android, DragAction.Ended is only delivered to views that have a drag
listener registered. A view with only a DragGestureRecognizer (no
DropGestureRecognizer) never registered a listener → never received
DragAction.Ended → HandleDropCompleted never called → DropCompleted
event never fired.

### Description of Change:

The fix is in
src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs.
Modified the listener registration for drag-source-only views (those
with no DropGestureRecognizer) is now done just before
StartDragAndDrop() is called, scoping it to the active drag and
preventing sibling views in the same layout from accidentally becoming
drag listeners. In the DragAction.Ended handler, the fix always
dispatches DropCompleted to the tracked dragSourceElement regardless of
which view received the event — since on Android, DragAction.Ended can
arrive on a non-source view first. A one-shot DropCompletedSent flag on
the local drag state prevents the event from firing more than once. Once
the source view receives DragAction.Ended, the temporary listener is
unregistered to restore the original state.

**Tested the behavior in the following platforms: **

- [x] Android
- [ ] Windows
- [ ] iOS
- [ ] Mac

### Reference:

N/A

### Issues Fixed:

Fixes  #17554          

### Screenshots
| Before  | After  |
|---------|--------|
| <Video width="300" height="600"
src="https://github.com/user-attachments/assets/243dec05-a560-4d26-87dd-cb6e8b99ab27"
/> | <Video width="300" height="600"
src="https://github.com/user-attachments/assets/3137deb3-4f45-476e-8e96-15661609c546"
/> |
@kubaflo kubaflo added the s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) label May 20, 2026
PureWeen pushed a commit that referenced this pull request Jun 2, 2026
…latform (#35179)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details:

DragGestureRecognizer.DropCompleted event not firing in Android platform
       
### Root Cause:

DragAndDropGestureHandler.SetupHandlerForDrop() only called
SetOnDragListener(this) when the view had a DropGestureRecognizer. On
Android, DragAction.Ended is only delivered to views that have a drag
listener registered. A view with only a DragGestureRecognizer (no
DropGestureRecognizer) never registered a listener → never received
DragAction.Ended → HandleDropCompleted never called → DropCompleted
event never fired.

### Description of Change:

The fix is in
src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs.
Modified the listener registration for drag-source-only views (those
with no DropGestureRecognizer) is now done just before
StartDragAndDrop() is called, scoping it to the active drag and
preventing sibling views in the same layout from accidentally becoming
drag listeners. In the DragAction.Ended handler, the fix always
dispatches DropCompleted to the tracked dragSourceElement regardless of
which view received the event — since on Android, DragAction.Ended can
arrive on a non-source view first. A one-shot DropCompletedSent flag on
the local drag state prevents the event from firing more than once. Once
the source view receives DragAction.Ended, the temporary listener is
unregistered to restore the original state.

**Tested the behavior in the following platforms: **

- [x] Android
- [ ] Windows
- [ ] iOS
- [ ] Mac

### Reference:

N/A

### Issues Fixed:

Fixes  #17554          

### Screenshots
| Before  | After  |
|---------|--------|
| <Video width="300" height="600"
src="https://github.com/user-attachments/assets/243dec05-a560-4d26-87dd-cb6e8b99ab27"
/> | <Video width="300" height="600"
src="https://github.com/user-attachments/assets/3137deb3-4f45-476e-8e96-15661609c546"
/> |
PureWeen pushed a commit that referenced this pull request Jun 11, 2026
…latform (#35179)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Issue Details:

DragGestureRecognizer.DropCompleted event not firing in Android platform
       
### Root Cause:

DragAndDropGestureHandler.SetupHandlerForDrop() only called
SetOnDragListener(this) when the view had a DropGestureRecognizer. On
Android, DragAction.Ended is only delivered to views that have a drag
listener registered. A view with only a DragGestureRecognizer (no
DropGestureRecognizer) never registered a listener → never received
DragAction.Ended → HandleDropCompleted never called → DropCompleted
event never fired.

### Description of Change:

The fix is in
src/Controls/src/Core/Platform/Android/DragAndDropGestureHandler.cs.
Modified the listener registration for drag-source-only views (those
with no DropGestureRecognizer) is now done just before
StartDragAndDrop() is called, scoping it to the active drag and
preventing sibling views in the same layout from accidentally becoming
drag listeners. In the DragAction.Ended handler, the fix always
dispatches DropCompleted to the tracked dragSourceElement regardless of
which view received the event — since on Android, DragAction.Ended can
arrive on a non-source view first. A one-shot DropCompletedSent flag on
the local drag state prevents the event from firing more than once. Once
the source view receives DragAction.Ended, the temporary listener is
unregistered to restore the original state.

**Tested the behavior in the following platforms: **

- [x] Android
- [ ] Windows
- [ ] iOS
- [ ] Mac

### Reference:

N/A

### Issues Fixed:

Fixes  #17554          

### Screenshots
| Before  | After  |
|---------|--------|
| <Video width="300" height="600"
src="https://github.com/user-attachments/assets/243dec05-a560-4d26-87dd-cb6e8b99ab27"
/> | <Video width="300" height="600"
src="https://github.com/user-attachments/assets/3137deb3-4f45-476e-8e96-15661609c546"
/> |
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 20, 2026
@github-actions github-actions Bot modified the milestones: .NET 10 SR7, .NET 10 SR9 Jul 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-gestures Gesture types community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/android s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-win AI found a better alternative fix than the PR s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

6 participants