diff --git a/src/Essentials/src/Platform/ActivityStateManager.android.cs b/src/Essentials/src/Platform/ActivityStateManager.android.cs
index a66a1f2d7007..c7ee19523354 100644
--- a/src/Essentials/src/Platform/ActivityStateManager.android.cs
+++ b/src/Essentials/src/Platform/ActivityStateManager.android.cs
@@ -73,6 +73,11 @@ class ActivityStateManagerImplementation : IActivityStateManager
public void Init(Application application)
{
+ if (lifecycleListener is not null)
+ {
+ return;
+ }
+
lifecycleListener = new ActivityLifecycleContextListener(OnActivityStateChanged);
application.RegisterActivityLifecycleCallbacks(lifecycleListener);
}
diff --git a/src/Essentials/test/DeviceTests/Tests/ActivityStateManager_Tests.cs b/src/Essentials/test/DeviceTests/Tests/ActivityStateManager_Tests.cs
new file mode 100644
index 000000000000..4d44e55baa36
--- /dev/null
+++ b/src/Essentials/test/DeviceTests/Tests/ActivityStateManager_Tests.cs
@@ -0,0 +1,104 @@
+// Device tests for GitHub issue #36035:
+// ActivityStateManager listener leak — Init(Application) was called on every
+// Activity recreation without a guard, registering a new listener each time.
+// Android's RegisterActivityLifecycleCallbacks is additive, so all old listeners
+// accumulated and every ActivityStateChanged event fired N+1 times after N recreations.
+
+#if __ANDROID__
+using System.Linq;
+using System.Threading;
+using Microsoft.Maui.ApplicationModel;
+using Xunit;
+
+namespace Microsoft.Maui.Essentials.DeviceTests
+{
+ using MauiPlatform = Microsoft.Maui.ApplicationModel.Platform;
+
+ [Category("ActivityStateManager")]
+ public class ActivityStateManager_Tests
+ {
+ ///
+ /// Verifies the fix for issue #36035: calling Init(Application) multiple times
+ /// (which happens on every Activity recreation) must keep the SAME listener
+ /// and NOT register additional ones.
+ /// Before the fix: each call created a new listener → N calls = N listeners.
+ /// After the fix: the guard `if (lifecycleListener is not null) return;` ensures
+ /// only the first call registers a listener.
+ ///
+ [Fact]
+ public void Init_CalledMultipleTimes_SameListenerIsKept()
+ {
+ var app = (global::Android.App.Application)global::Android.App.Application.Context;
+ var manager = new ActivityStateManagerImplementation();
+
+ manager.Init(app);
+ var listenerAfterFirst = GetListener(manager);
+
+ manager.Init(app);
+ var listenerAfterSecond = GetListener(manager);
+
+ manager.Init(app);
+ var listenerAfterThird = GetListener(manager);
+
+ Assert.NotNull(listenerAfterFirst);
+ Assert.Same(listenerAfterFirst, listenerAfterSecond);
+ Assert.Same(listenerAfterFirst, listenerAfterThird);
+ }
+
+ ///
+ /// Verifies that ActivityStateChanged fires exactly once per lifecycle event
+ /// even after Init(Application) is called multiple times.
+ /// Before the fix: N calls = N listeners → event fired N times per real event.
+ /// After the fix: always exactly 1 listener → event fires exactly once.
+ ///
+ /// IMPORTANT: we capture each listener BEFORE the next Init call because
+ /// the bug overwrites the internal field each time. We then fire on ALL captured
+ /// listeners (simulating Android dispatching to every registered callback).
+ /// With the fix all three are the same object → fires once.
+ /// With the bug all three are different objects → fires three times.
+ ///
+ [Fact]
+ public void Init_CalledMultipleTimes_ActivityStateChangedFiresOnce()
+ {
+ var app = (global::Android.App.Application)global::Android.App.Application.Context;
+ var activity = MauiPlatform.CurrentActivity;
+
+ var manager = new ActivityStateManagerImplementation();
+
+ int invocations = 0;
+ manager.ActivityStateChanged += (_, _) => Interlocked.Increment(ref invocations);
+
+ // Capture the listener after EACH Init call, before the next one overwrites it.
+ manager.Init(app);
+ var l1 = GetListener(manager) as global::Android.App.Application.IActivityLifecycleCallbacks;
+
+ manager.Init(app);
+ var l2 = GetListener(manager) as global::Android.App.Application.IActivityLifecycleCallbacks;
+
+ manager.Init(app);
+ var l3 = GetListener(manager) as global::Android.App.Application.IActivityLifecycleCallbacks;
+
+ // Simulate Android dispatching a Resumed event to every distinct registered listener.
+ // With fix: l1 == l2 == l3 (same object) → 1 unique listener → 1 invocation ✅
+ // With bug: l1 != l2 != l3 (different) → 3 unique listeners → 3 invocations ❌
+ foreach (var l in new[] { l1, l2, l3 }.Distinct())
+ l?.OnActivityResumed(activity);
+
+ Assert.Equal(1, invocations);
+ }
+
+ // Reads the private 'lifecycleListener' field from ActivityStateManagerImplementation
+ // via reflection. InternalsVisibleTo in AssemblyInfo.shared.cs grants access to
+ // internal types; reflection is needed only for the private field itself.
+ static ActivityLifecycleContextListener GetListener(ActivityStateManagerImplementation manager)
+ {
+ var field = typeof(ActivityStateManagerImplementation)
+ .GetField("lifecycleListener",
+ System.Reflection.BindingFlags.NonPublic |
+ System.Reflection.BindingFlags.Instance);
+
+ return field?.GetValue(manager) as ActivityLifecycleContextListener;
+ }
+ }
+}
+#endif