[Android] Fix ActivityStateManager leaking lifecycle callbacks on Activity recreation - #36161
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36161Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36161" |
|
Hey there @@Shalini-Ashokan! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
Fixes an Android lifecycle-callback leak in ActivityStateManager by making ActivityStateManagerImplementation.Init(Application) idempotent, preventing duplicate registrations during Activity recreation and avoiding multiplied Platform.ActivityStateChanged notifications.
Changes:
- Add an early-return guard in
ActivityStateManagerImplementation.Init(Application)to prevent re-registeringRegisterActivityLifecycleCallbacks. - Add Android-only device tests to validate listener reuse and single
ActivityStateChangedinvocation behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Essentials/src/Platform/ActivityStateManager.android.cs | Makes Init(Application) idempotent to avoid accumulating application lifecycle callbacks across activity recreations. |
| src/Essentials/test/DeviceTests/Tests/ActivityStateManager_Tests.cs | Adds regression coverage for the listener leak / event-multiplication scenario. |
| 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); | ||
| } |
| 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); | ||
| } |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 1 findings
See inline comments for details.
| var app = (global::Android.App.Application)global::Android.App.Application.Context; | ||
| var manager = new ActivityStateManagerImplementation(); | ||
|
|
||
| manager.Init(app); |
There was a problem hiding this comment.
[moderate] Regression Prevention / Memory Leak — These tests register a real Application.IActivityLifecycleCallbacks instance on the process-wide Android Application, but never unregister it. RegisterActivityLifecycleCallbacks is additive and the Application lives for the whole device-test process, so each test leaves behind a listener that still references its ActivityStateManagerImplementation and receives future activity lifecycle events. This can make later tests observe extra callbacks and prevents the test manager from being collected.
Please unregister the listener in finally (and do the same for the listener created around line 72), e.g. retrieve GetListener(manager) as Application.IActivityLifecycleCallbacks and call app.UnregisterActivityLifecycleCallbacks(listener) after assertions.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@Shalini-Ashokan — new AI review results are available based on this last commit:
b31f961. To request a fresh review after new comments or commits, comment/review rerun.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ✅ PASSED
Platform: ANDROID · Base: main · Merge base: 2a323a50
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
📱 ActivityStateManager_Tests (Init_CalledMultipleTimes_SameListenerIsKept, Init_CalledMultipleTimes_ActivityStateChangedFiresOnce) Category=ActivityStateManager |
✅ FAIL — 505s | ✅ PASS — 337s |
🔴 Without fix — 📱 ActivityStateManager_Tests (Init_CalledMultipleTimes_SameListenerIsKept, Init_CalledMultipleTimes_ActivityStateChangedFiresOnce): FAIL ✅ · 505s
(truncated to last 15,000 chars)
tionsPlacemark
06-27 10:47:32.495 8551 8705 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Maps_Tests 0.0015582 ms
06-27 10:47:32.495 8551 8705 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Shared.Android_FileSystemUtils_Tests
06-27 10:47:32.497 8551 8705 I DOTNET : [PASS] IsFileReadable_Returns_False_For_Inaccessible_Path
06-27 10:47:32.497 8551 8705 I DOTNET : [PASS] IsFileReadable_Returns_True_For_Readable_File
06-27 10:47:32.498 8551 8705 I DOTNET : [PASS] IsFileReadable_Returns_False_For_NonExistent_File
06-27 10:47:32.498 8551 8705 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Shared.Android_FileSystemUtils_Tests 0.0016787 ms
06-27 10:47:32.498 8551 8705 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Permissions_Tests
06-27 10:47:32.509 8551 8705 I DOTNET : [PASS] Check_Status
06-27 10:47:32.512 8551 8705 I DOTNET : [PASS] Check_Status
06-27 10:47:32.513 8551 8705 I DOTNET : [PASS] Ensure_Declared
06-27 10:47:32.516 8551 8705 I DOTNET : [PASS] Ensure_Declared
06-27 10:47:32.518 8551 8705 I DOTNET : [PASS] Request
06-27 10:47:32.520 8551 8705 I DOTNET : [PASS] Request
06-27 10:47:32.522 8551 8705 I DOTNET : [PASS] StorageAndroid13AlwaysGranted
06-27 10:47:32.522 8551 8705 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Permissions_Tests 0.0222112 ms
06-27 10:47:32.522 8551 8705 I DOTNET : UsesPreferences
06-27 10:47:32.526 8551 8705 I DOTNET : [PASS] Set_Get_Bool
06-27 10:47:32.527 8551 8705 I DOTNET : [PASS] Set_Get_Bool
06-27 10:47:32.535 8551 8705 I DOTNET : [PASS] Remove_Get_Double_NonStatic
06-27 10:47:32.536 8551 8705 I DOTNET : [PASS] Remove_Get_Double_NonStatic
06-27 10:47:32.539 8551 8705 I DOTNET : [PASS] Not_ContainsKey
06-27 10:47:32.539 8551 8705 I DOTNET : [PASS] Not_ContainsKey
06-27 10:47:32.557 8551 8705 I DOTNET : [PASS] FailsWithUnsupportedType
06-27 10:47:32.561 8551 8705 I DOTNET : [PASS] Set_Get_Int
06-27 10:47:32.562 8551 8705 I DOTNET : [PASS] Set_Get_Int
06-27 10:47:32.563 8551 8705 I DOTNET : [PASS] Set_Get_String
06-27 10:47:32.563 8551 8705 I DOTNET : [PASS] Set_Get_String
06-27 10:47:32.570 8551 8705 I DOTNET : [PASS] Set_Get_Long
06-27 10:47:32.571 8551 8705 I DOTNET : [PASS] Set_Get_Long
06-27 10:47:32.578 8551 8705 I DOTNET : [PASS] DateTimePreservesKind_NonStatic
06-27 10:47:32.581 8551 8705 I DOTNET : [PASS] DateTimePreservesKind_NonStatic
06-27 10:47:32.593 8551 8705 I DOTNET : [PASS] Set_Get_Float
06-27 10:47:32.594 8551 8705 I DOTNET : [PASS] Set_Get_Float
06-27 10:47:32.595 8551 8705 I DOTNET : [PASS] Remove_Get_String
06-27 10:47:32.595 8551 8705 I DOTNET : [PASS] Remove_Get_String
06-27 10:47:32.600 8551 8705 I DOTNET : [PASS] Set_Get_Double
06-27 10:47:32.601 8551 8705 I DOTNET : [PASS] Set_Get_Double
06-27 10:47:32.601 8551 8705 I DOTNET : [PASS] Set_Set_Null_Get_String_NonStatic
06-27 10:47:32.602 8551 8705 I DOTNET : [PASS] Set_Set_Null_Get_String_NonStatic
06-27 10:47:32.604 8551 8705 I DOTNET : [PASS] Clear
06-27 10:47:32.605 8551 8705 I DOTNET : [PASS] Clear
06-27 10:47:32.606 8551 8705 I DOTNET : [PASS] Remove_Get_Float_NonStatic
06-27 10:47:32.607 8551 8705 I DOTNET : [PASS] Remove_Get_Float_NonStatic
06-27 10:47:32.608 8551 8705 I DOTNET : [PASS] Not_ContainsKey_NonStatic
06-27 10:47:32.608 8551 8705 I DOTNET : [PASS] Not_ContainsKey_NonStatic
06-27 10:47:32.609 8551 8705 I DOTNET : [PASS] Set_Get_DateTime_NonStatic
06-27 10:47:32.609 8551 8705 I DOTNET : [PASS] Set_Get_DateTime_NonStatic
06-27 10:47:32.610 8551 8705 I DOTNET : [PASS] Remove_Get_Long
06-27 10:47:32.611 8551 8705 I DOTNET : [PASS] Remove_Get_Long
06-27 10:47:32.612 8551 8705 I DOTNET : [PASS] Remove_Get_Bool
06-27 10:47:32.612 8551 8705 I DOTNET : [PASS] Remove_Get_Bool
06-27 10:47:32.614 8551 8705 I DOTNET : [PASS] DateTimePreservesKind
06-27 10:47:32.614 8551 8705 I DOTNET : [PASS] DateTimePreservesKind
06-27 10:47:32.615 8551 8705 I DOTNET : [PASS] Remove_Get_Double
06-27 10:47:32.616 8551 8705 I DOTNET : [PASS] Remove_Get_Double
06-27 10:47:32.617 8551 8705 I DOTNET : [PASS] Clear_NonStatic
06-27 10:47:32.620 8551 8705 I DOTNET : [PASS] Clear_NonStatic
06-27 10:47:32.621 8551 8705 I DOTNET : [PASS] Set_Get_Long_NonStatic
06-27 10:47:32.622 8551 8705 I DOTNET : [PASS] Set_Get_Long_NonStatic
06-27 10:47:32.622 8551 8705 I DOTNET : [PASS] Set_Set_Null_Get_String
06-27 10:47:32.622 8551 8705 I DOTNET : [PASS] Set_Set_Null_Get_String
06-27 10:47:32.623 8551 8705 I DOTNET : [PASS] Set_Get_DateTime
06-27 10:47:32.623 8551 8705 I DOTNET : [PASS] Set_Get_DateTime
06-27 10:47:32.624 8551 8705 I DOTNET : [PASS] Set_Get_Float_NonStatic
06-27 10:47:32.626 8551 8705 I DOTNET : [PASS] Set_Get_Float_NonStatic
06-27 10:47:32.629 8551 8705 I DOTNET : [PASS] Remove
06-27 10:47:32.630 8551 8705 I DOTNET : [PASS] Remove
06-27 10:47:32.631 8551 8705 I DOTNET : [PASS] Remove_Get_Long_NonStatic
06-27 10:47:32.631 8551 8705 I DOTNET : [PASS] Remove_Get_Long_NonStatic
06-27 10:47:32.632 8551 8705 I DOTNET : [PASS] Remove_NonStatic
06-27 10:47:32.632 8551 8705 I DOTNET : [PASS] Remove_NonStatic
06-27 10:47:32.633 8551 8705 I DOTNET : [PASS] Does_ContainsKey_NonStatic
06-27 10:47:32.633 8551 8705 I DOTNET : [PASS] Does_ContainsKey_NonStatic
06-27 10:47:32.634 8551 8705 I DOTNET : [PASS] Remove_Get_Bool_NonStatic
06-27 10:47:32.634 8551 8705 I DOTNET : [PASS] Remove_Get_Bool_NonStatic
06-27 10:47:32.635 8551 8705 I DOTNET : [PASS] Set_Get_Int_NonStatic
06-27 10:47:32.636 8551 8705 I DOTNET : [PASS] Set_Get_Int_NonStatic
06-27 10:47:32.662 8551 8705 I DOTNET : [PASS] DateTimeOffsetPreservesOffset_NonStatic
06-27 10:47:32.664 8551 8705 I DOTNET : [PASS] DateTimeOffsetPreservesOffset_NonStatic
06-27 10:47:32.665 8551 8705 I DOTNET : [PASS] Remove_Get_Int_NonStatic
06-27 10:47:32.666 8551 8705 I DOTNET : [PASS] Remove_Get_Int_NonStatic
06-27 10:47:32.667 8551 8705 I DOTNET : [PASS] Set_Get_String_NonStatic
06-27 10:47:32.667 8551 8705 I DOTNET : [PASS] Set_Get_String_NonStatic
06-27 10:47:32.676 8551 8705 I DOTNET : [PASS] Does_ContainsKey
06-27 10:47:32.676 8551 8705 I DOTNET : [PASS] Does_ContainsKey
06-27 10:47:32.677 8551 8705 I DOTNET : [PASS] Remove_Get_Int
06-27 10:47:32.678 8551 8705 I DOTNET : [PASS] Remove_Get_Int
06-27 10:47:32.680 8551 8705 I DOTNET : [PASS] Remove_Get_Float
06-27 10:47:32.681 8551 8705 I DOTNET : [PASS] Remove_Get_Float
06-27 10:47:32.682 8551 8705 I DOTNET : [PASS] Remove_Get_String_NonStatic
06-27 10:47:32.683 8551 8705 I DOTNET : [PASS] Remove_Get_String_NonStatic
06-27 10:47:32.684 8551 8705 I DOTNET : [PASS] Set_Get_Double_NonStatic
06-27 10:47:32.688 8551 8705 I DOTNET : [PASS] Set_Get_Double_NonStatic
06-27 10:47:32.689 8551 8705 I DOTNET : [PASS] Set_Get_Bool_NonStatic
06-27 10:47:32.689 8551 8705 I DOTNET : [PASS] Set_Get_Bool_NonStatic
06-27 10:47:32.689 8551 8705 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Preferences_Tests 0.0910580 ms
06-27 10:47:32.863 8551 8715 I DOTNET : [PASS] Remove_All_Keys
06-27 10:47:32.924 8551 8720 I DOTNET : [PASS] Remove_All_Keys
06-27 10:47:36.007 8551 8727 I DOTNET : [PASS] Set_Get_Remove_Async_MultipleTimes
06-27 10:47:36.052 8551 8732 I DOTNET : [PASS] Asymmetric_to_Symmetric_API_Upgrade
06-27 10:47:36.071 8551 8732 I DOTNET : [PASS] Non_Existent_Key_Returns_Null
06-27 10:47:36.107 8551 8737 I DOTNET : [PASS] Saves_Same_Key_Twice
06-27 10:47:36.136 8551 8742 I DOTNET : [PASS] Saves_And_Loads
06-27 10:47:36.167 8551 8747 I DOTNET : [PASS] Saves_And_Loads
06-27 10:47:36.192 8551 8752 I DOTNET : [PASS] Saves_And_Loads
06-27 10:47:36.220 8551 8757 I DOTNET : [PASS] Saves_And_Loads
06-27 10:47:36.246 8551 8762 I DOTNET : [PASS] Saves_And_Loads
06-27 10:47:36.272 8551 8767 I DOTNET : [PASS] Saves_And_Loads
06-27 10:47:37.820 8551 8772 I DOTNET : [PASS] Set_Get_Async_MultipleTimes
06-27 10:47:37.900 8551 8777 I DOTNET : [PASS] Fix_Corrupt_Data
06-27 10:47:39.404 8551 8777 I DOTNET : [PASS] Set_Get_Wait_MultipleTimes
06-27 10:47:39.460 8551 8777 I DOTNET : [PASS] Remove_Key
06-27 10:47:39.490 8551 8777 I DOTNET : [PASS] Remove_Key
06-27 10:47:39.490 8551 8777 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.SecureStorage_Tests 6.7612552 ms
06-27 10:47:39.490 8551 8777 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.DeviceDisplay_Tests
06-27 10:47:39.492 8551 8777 I DOTNET : [PASS] Screen_Metrics_Are_Not_Null
06-27 10:47:39.492 8551 8777 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.DeviceDisplay_Tests 0.001864 ms
06-27 10:47:39.492 8551 8777 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Shared.Android_FileProvider_Tests
06-27 10:47:39.499 8551 8777 I DOTNET : [PASS] Get_Existing_Internal_Cache_Shareable_Uri
06-27 10:47:39.503 8551 8777 I DOTNET : [PASS] Get_Existing_Internal_Cache_Shareable_Uri
06-27 10:47:39.508 8551 8777 I DOTNET : [PASS] No_Media_Fails_Get_External_Cache_Shareable_Uri
06-27 10:47:39.541 8551 8777 I DOTNET : [PASS] Get_Shareable_Uri
06-27 10:47:39.552 8551 8777 I DOTNET : [PASS] Get_Shareable_Uri
06-27 10:47:39.555 8551 8777 I DOTNET : [PASS] Get_Existing_External_Shareable_Uri
06-27 10:47:39.560 8551 8777 I DOTNET : [PASS] Get_Existing_External_Shareable_Uri
06-27 10:47:39.562 8551 8777 I DOTNET : [PASS] Get_Existing_External_Cache_Shareable_Uri
06-27 10:47:39.567 8551 8777 I DOTNET : [PASS] Get_Existing_External_Cache_Shareable_Uri
06-27 10:47:39.570 8551 8777 I DOTNET : [PASS] Get_External_Cache_Shareable_Uri
06-27 10:47:39.570 8551 8777 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Shared.Android_FileProvider_Tests 0.0744645 ms
06-27 10:47:39.577 8551 8581 I DOTNET : Failed tests:
06-27 10:47:39.578 8551 8581 I DOTNET : 1) [FAIL] Init_CalledMultipleTimes_ActivityStateChangedFiresOnce Test name: Init_CalledMultipleTimes_ActivityStateChangedFiresOnce
06-27 10:47:39.578 8551 8581 I DOTNET : Assembly: [Microsoft.Maui.Essentials.DeviceTests, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null]
06-27 10:47:39.578 8551 8581 I DOTNET : Exception messages: Assert.Equal() Failure: Values differ
06-27 10:47:39.578 8551 8581 I DOTNET : Expected: 1
06-27 10:47:39.578 8551 8581 I DOTNET : Actual: 3 Exception stack traces: at Microsoft.Maui.Essentials.DeviceTests.ActivityStateManager_Tests.Init_CalledMultipleTimes_ActivityStateChangedFiresOnce()
06-27 10:47:39.578 8551 8581 I DOTNET : at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
06-27 10:47:39.578 8551 8581 I DOTNET : at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object , BindingFlags )
06-27 10:47:39.578 8551 8581 I DOTNET : Execution time: 0.0099839
06-27 10:47:39.578 8551 8581 I DOTNET : Test trait name: Category
06-27 10:47:39.578 8551 8581 I DOTNET : value: ActivityStateManager
06-27 10:47:39.578 8551 8581 I DOTNET :
06-27 10:47:39.578 8551 8581 I DOTNET : 2) [FAIL] Init_CalledMultipleTimes_SameListenerIsKept Test name: Init_CalledMultipleTimes_SameListenerIsKept
06-27 10:47:39.578 8551 8581 I DOTNET : Assembly: [Microsoft.Maui.Essentials.DeviceTests, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null]
06-27 10:47:39.578 8551 8581 I DOTNET : Exception messages: Assert.Same() Failure: Values are not the same instance
06-27 10:47:39.578 8551 8581 I DOTNET : Expected: crc64ba438d8f48cf7e75.ActivityLifecycleContextListener@10f09b1
06-27 10:47:39.578 8551 8581 I DOTNET : Actual: crc64ba438d8f48cf7e75.ActivityLifecycleContextListener@8413e96 Exception stack traces: at Microsoft.Maui.Essentials.DeviceTests.ActivityStateManager_Tests.Init_CalledMultipleTimes_SameListenerIsKept()
06-27 10:47:39.578 8551 8581 I DOTNET : at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
06-27 10:47:39.578 8551 8581 I DOTNET : at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object , BindingFlags )
06-27 10:47:39.578 8551 8581 I DOTNET : Execution time: 0.0007553
06-27 10:47:39.578 8551 8581 I DOTNET : Test trait name: Category
06-27 10:47:39.578 8551 8581 I DOTNET : value: ActivityStateManager
06-27 10:47:39.578 8551 8581 I DOTNET :
06-27 10:47:39.599 8551 8581 I DOTNET : Xml file was written to the provided writer.
06-27 10:47:39.599 8551 8581 I DOTNET : Tests run: 307 Passed: 275 Inconclusive: 0 Failed: 2 Ignored: 30
�[41m�[30mfail�[39m�[22m�[49m: Non-success instrumentation exit code: 1, expected: 0
�[40m�[32minfo�[39m�[22m�[49m: <<XHARNESS_RESULT_START>>
{
"version": 1,
"machineName": "runnervm6n5x7",
"exitCode": 1,
"exitCodeName": "TESTS_FAILED",
"platform": "android",
"instrumentationExitCode": 1,
"device": "emulator-5554",
"deviceOsVersion": "API 30",
"architecture": "x86_64",
"files": [
{
"name": "testResults.xml",
"type": "test-results"
},
{
"name": "adb-logcat-com.microsoft.maui.essentials.devicetests-default.log",
"type": "logcat"
}
]
}
<<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.essentials.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.essentials.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.essentials.devicetests
XHarness exit code: 1 (TESTS_FAILED)
Tests completed with exit code: 1
🟢 With fix — 📱 ActivityStateManager_Tests (Init_CalledMultipleTimes_SameListenerIsKept, Init_CalledMultipleTimes_ActivityStateChangedFiresOnce): PASS ✅ · 337s
(truncated to last 15,000 chars)
[PASS] Remove_NonStatic
06-27 10:53:02.388 10895 10972 I DOTNET : [PASS] Remove_NonStatic
06-27 10:53:02.389 10895 10972 I DOTNET : [PASS] Does_ContainsKey_NonStatic
06-27 10:53:02.389 10895 10972 I DOTNET : [PASS] Does_ContainsKey_NonStatic
06-27 10:53:02.390 10895 10972 I DOTNET : [PASS] Remove_Get_Bool_NonStatic
06-27 10:53:02.390 10895 10972 I DOTNET : [PASS] Remove_Get_Bool_NonStatic
06-27 10:53:02.391 10895 10972 I DOTNET : [PASS] Set_Get_Int_NonStatic
06-27 10:53:02.391 10895 10972 I DOTNET : [PASS] Set_Get_Int_NonStatic
06-27 10:53:02.416 10895 10972 I DOTNET : [PASS] DateTimeOffsetPreservesOffset_NonStatic
06-27 10:53:02.417 10895 10972 I DOTNET : [PASS] DateTimeOffsetPreservesOffset_NonStatic
06-27 10:53:02.418 10895 10972 I DOTNET : [PASS] Remove_Get_Int_NonStatic
06-27 10:53:02.418 10895 10972 I DOTNET : [PASS] Remove_Get_Int_NonStatic
06-27 10:53:02.419 10895 10972 I DOTNET : [PASS] Set_Get_String_NonStatic
06-27 10:53:02.419 10895 10972 I DOTNET : [PASS] Set_Get_String_NonStatic
06-27 10:53:02.419 10895 10972 I DOTNET : [PASS] Does_ContainsKey
06-27 10:53:02.420 10895 10972 I DOTNET : [PASS] Does_ContainsKey
06-27 10:53:02.421 10895 10972 I DOTNET : [PASS] Remove_Get_Int
06-27 10:53:02.421 10895 10972 I DOTNET : [PASS] Remove_Get_Int
06-27 10:53:02.422 10895 10972 I DOTNET : [PASS] Remove_Get_Float
06-27 10:53:02.423 10895 10972 I DOTNET : [PASS] Remove_Get_Float
06-27 10:53:02.424 10895 10972 I DOTNET : [PASS] Remove_Get_String_NonStatic
06-27 10:53:02.424 10895 10972 I DOTNET : [PASS] Remove_Get_String_NonStatic
06-27 10:53:02.425 10895 10972 I DOTNET : [PASS] Set_Get_Double_NonStatic
06-27 10:53:02.427 10895 10972 I DOTNET : [PASS] Set_Get_Double_NonStatic
06-27 10:53:02.427 10895 10972 I DOTNET : [PASS] Set_Get_Bool_NonStatic
06-27 10:53:02.428 10895 10972 I DOTNET : [PASS] Set_Get_Bool_NonStatic
06-27 10:53:02.428 10895 10972 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Preferences_Tests 0.0924499 ms
06-27 10:53:02.617 10895 10986 I DOTNET : [PASS] Remove_All_Keys
06-27 10:53:02.680 10895 10991 I DOTNET : [PASS] Remove_All_Keys
06-27 10:53:05.806 10895 10999 I DOTNET : [PASS] Set_Get_Remove_Async_MultipleTimes
06-27 10:53:05.852 10895 11004 I DOTNET : [PASS] Asymmetric_to_Symmetric_API_Upgrade
06-27 10:53:05.867 10895 11004 I DOTNET : [PASS] Non_Existent_Key_Returns_Null
06-27 10:53:05.901 10895 11009 I DOTNET : [PASS] Saves_Same_Key_Twice
06-27 10:53:05.929 10895 11014 I DOTNET : [PASS] Saves_And_Loads
06-27 10:53:05.956 10895 11019 I DOTNET : [PASS] Saves_And_Loads
06-27 10:53:05.984 10895 11024 I DOTNET : [PASS] Saves_And_Loads
06-27 10:53:06.016 10895 11029 I DOTNET : [PASS] Saves_And_Loads
06-27 10:53:06.040 10895 11034 I DOTNET : [PASS] Saves_And_Loads
06-27 10:53:06.067 10895 11039 I DOTNET : [PASS] Saves_And_Loads
06-27 10:53:07.986 10895 11069 I DOTNET : [PASS] Set_Get_Async_MultipleTimes
06-27 10:53:08.594 10895 11086 I DOTNET : [PASS] Fix_Corrupt_Data
06-27 10:53:10.320 10895 11086 I DOTNET : [PASS] Set_Get_Wait_MultipleTimes
06-27 10:53:10.375 10895 11086 I DOTNET : [PASS] Remove_Key
06-27 10:53:10.407 10895 11086 I DOTNET : [PASS] Remove_Key
06-27 10:53:10.408 10895 11086 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.SecureStorage_Tests 7.9261638 ms
06-27 10:53:10.408 10895 11086 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Shared.Android_FileProvider_Tests
06-27 10:53:10.439 10895 11086 I DOTNET : [PASS] Get_Existing_Internal_Cache_Shareable_Uri
06-27 10:53:10.445 10895 11086 I DOTNET : [PASS] Get_Existing_Internal_Cache_Shareable_Uri
06-27 10:53:10.449 10895 11086 I DOTNET : [PASS] No_Media_Fails_Get_External_Cache_Shareable_Uri
06-27 10:53:10.482 10895 11086 I DOTNET : [PASS] Get_Shareable_Uri
06-27 10:53:10.492 10895 11086 I DOTNET : [PASS] Get_Shareable_Uri
06-27 10:53:10.495 10895 11086 I DOTNET : [PASS] Get_Existing_External_Shareable_Uri
06-27 10:53:10.500 10895 11086 I DOTNET : [PASS] Get_Existing_External_Shareable_Uri
06-27 10:53:10.502 10895 11086 I DOTNET : [PASS] Get_Existing_External_Cache_Shareable_Uri
06-27 10:53:10.506 10895 11086 I DOTNET : [PASS] Get_Existing_External_Cache_Shareable_Uri
06-27 10:53:10.510 10895 11086 I DOTNET : [PASS] Get_External_Cache_Shareable_Uri
06-27 10:53:10.510 10895 11086 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Shared.Android_FileProvider_Tests 0.0983568 ms
06-27 10:53:10.510 10895 11086 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.FileSystem_Tests
06-27 10:53:10.510 10895 11086 I DOTNET : [PASS] AppDataDirectory_Is_Valid
06-27 10:53:10.524 10895 11091 I DOTNET : [PASS] CheckFileResultWithFilePath
06-27 10:53:10.524 10895 11091 I DOTNET : [PASS] CacheDirectory_Is_Valid
06-27 10:53:10.533 10895 11091 I DOTNET : [PASS] CheckFileResultOpenReadAsyncMultipleTimes
06-27 10:53:10.536 10895 11091 I DOTNET : [PASS] OpenAppPackageFileAsync_Can_Load_File
06-27 10:53:10.537 10895 11091 I DOTNET : [PASS] OpenAppPackageFileAsync_Can_Load_File
06-27 10:53:10.541 10895 11091 I DOTNET : [PASS] OpenAppPackageFileAsync_Throws_If_File_Is_Not_Found
06-27 10:53:10.541 10895 11091 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.FileSystem_Tests 0.0255523 ms
06-27 10:53:10.541 10895 11091 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.AppActions_Tests
06-27 10:53:10.572 10895 11091 I DOTNET : [PASS] GetSetItems
06-27 10:53:10.572 10895 11091 I DOTNET : [PASS] IsSupported
06-27 10:53:10.572 10895 11091 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.AppActions_Tests 0.0308267 ms
06-27 10:53:10.572 10895 11091 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.HapticFeedback_Tests
06-27 10:53:10.589 10895 11091 I DOTNET : [PASS] LongPress
06-27 10:53:10.590 10895 11091 I DOTNET : [PASS] Click
06-27 10:53:10.590 10895 11091 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.HapticFeedback_Tests 0.0173204 ms
06-27 10:53:10.590 10895 11091 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Share_Tests
06-27 10:53:10.592 10895 11091 I DOTNET : [PASS] Share_NullShareMultipleFilesRequest
06-27 10:53:10.593 10895 11091 I DOTNET : [PASS] Share_ShareMultipleFilesRequestWithEmptyFilesList
06-27 10:53:10.593 10895 11091 I DOTNET : [PASS] Share_ShareMultipleFilesRequestWithInvalidFilesList
06-27 10:53:10.594 10895 11091 I DOTNET : [PASS] Share_NullShareTextRequest
06-27 10:53:10.603 10895 11091 I DOTNET : [PASS] Share_SingleFileIntent_HasClipData
06-27 10:53:10.606 10895 11091 I DOTNET : [PASS] Share_ShareFileRequestWithInvalidFile
06-27 10:53:10.607 10895 11091 I DOTNET : [PASS] Share_FiletWithNullFilePath
06-27 10:53:10.614 10895 11091 I DOTNET : [PASS] Share_MultipleFilesIntent_HasClipData
06-27 10:53:10.615 10895 11091 I DOTNET : [PASS] Share_NullShareFileRequest
06-27 10:53:10.615 10895 11091 I DOTNET : [PASS] Share_FiletWithInvalidFilePath
06-27 10:53:10.615 10895 11091 I DOTNET : [PASS] Share_ShareTextRequestWithInvalidTextAndUri
06-27 10:53:10.615 10895 11091 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Share_Tests 0.0231955 ms
06-27 10:53:10.616 10895 11091 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.DeviceDisplay_Tests
06-27 10:53:10.618 10895 11091 I DOTNET : [PASS] Screen_Metrics_Are_Not_Null
06-27 10:53:10.618 10895 11091 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.DeviceDisplay_Tests 0.0020359 ms
06-27 10:53:10.618 10895 11091 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Permissions_Tests
06-27 10:53:10.623 10895 11091 I DOTNET : [PASS] Check_Status
06-27 10:53:10.634 10895 11091 I DOTNET : [PASS] Check_Status
06-27 10:53:10.635 10895 11091 I DOTNET : [PASS] Ensure_Declared
06-27 10:53:10.636 10895 11091 I DOTNET : [PASS] Ensure_Declared
06-27 10:53:10.638 10895 11091 I DOTNET : [PASS] Request
06-27 10:53:10.638 10895 11091 I DOTNET : [PASS] Request
06-27 10:53:10.640 10895 11091 I DOTNET : [PASS] StorageAndroid13AlwaysGranted
06-27 10:53:10.640 10895 11091 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Permissions_Tests 0.0197748 ms
06-27 10:53:10.640 10895 11091 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Barometer_Tests
06-27 10:53:10.641 10895 11091 I DOTNET : [PASS] IsSupported
06-27 10:53:10.714 10895 11098 I DOTNET : [PASS] Monitor
06-27 10:53:10.779 10895 11103 I DOTNET : [PASS] IsMonitoring
06-27 10:53:10.785 10895 11108 I DOTNET : [PASS] Stop_Monitor
06-27 10:53:10.785 10895 11108 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Barometer_Tests 0.1434026 ms
06-27 10:53:10.785 10895 11108 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.AppInfo_Tests
06-27 10:53:10.786 10895 11108 I DOTNET : [PASS] App_Versions_Are_Correct
06-27 10:53:10.790 10895 11108 I DOTNET : [PASS] App_RequestedLayoutDirection_Is_Correct
06-27 10:53:10.790 10895 11108 I DOTNET : [PASS] AppPackageName_Is_Correct
06-27 10:53:10.791 10895 11108 I DOTNET : [PASS] AppName_Is_Correct
06-27 10:53:10.791 10895 11108 I DOTNET : [PASS] App_Build_Is_Correct
06-27 10:53:10.794 10895 11108 I DOTNET : [PASS] App_Theme_Is_Correct
06-27 10:53:10.794 10895 11108 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.AppInfo_Tests 0.0078138 ms
06-27 10:53:10.794 10895 11108 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Screenshot_Tests
06-27 10:53:11.004 10895 11112 I DOTNET : [PASS] CaptureAsync
06-27 10:53:11.212 10895 11116 I DOTNET : [PASS] GetPngScreenshot
06-27 10:53:11.423 10895 11132 I DOTNET : [PASS] GetJpegScreenshot
06-27 10:53:11.741 10895 11154 I DOTNET : [PASS] CaptureStaticAsync
06-27 10:53:11.741 10895 11154 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Screenshot_Tests 0.8792264 ms
06-27 10:53:11.741 10895 11154 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Maps_Tests
06-27 10:53:11.762 10895 11154 I DOTNET : [PASS] LaunchMap_NullOptionsLocation
06-27 10:53:11.767 10895 11154 I DOTNET : [PASS] LaunchMap_NullPlacemark
06-27 10:53:11.772 10895 11154 I DOTNET : [PASS] LaunchMap_NullLocation
06-27 10:53:11.773 10895 11154 I DOTNET : [PASS] LaunchMap_NullOptionsPlacemark
06-27 10:53:11.773 10895 11154 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Maps_Tests 0.0299840 ms
06-27 10:53:11.773 10895 11154 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Connectivity_Tests
06-27 10:53:11.791 10895 11154 I DOTNET : [PASS] Network_Access
06-27 10:53:12.869 10895 11255 I DOTNET : [PASS] ConnectivityChanged_Does_Not_Crash
06-27 10:53:12.884 10895 11255 I DOTNET : [PASS] Connection_Profiles
06-27 10:53:12.889 10895 11255 I DOTNET : [PASS] Test
06-27 10:53:12.901 10895 11255 I DOTNET : [PASS] Distict_Connection_Profiles
06-27 10:53:12.901 10895 11255 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Connectivity_Tests 1.1179338 ms
06-27 10:53:12.901 10895 11255 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Accelerometer_Tests
06-27 10:53:12.958 10895 11261 I DOTNET : [PASS] Stop_Monitor
06-27 10:53:12.964 10895 11261 I DOTNET : [PASS] IsMonitoring
06-27 10:53:12.964 10895 11261 I DOTNET : [PASS] IsSupported
06-27 10:53:12.978 10895 11266 I DOTNET : [PASS] Monitor
06-27 10:53:12.978 10895 11266 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Accelerometer_Tests 0.0737711 ms
06-27 10:53:12.978 10895 11266 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Android_Geolocation_Tests
06-27 10:53:12.993 10895 11266 I DOTNET : [PASS] ToLocation_MslAltitude_UsesGeoidReferenceSystem
06-27 10:53:12.994 10895 11266 I DOTNET : [PASS] ToLocation_EllipsoidalAltitude_UsesEllipsoidReferenceSystem
06-27 10:53:12.994 10895 11266 I DOTNET : [PASS] ToLocation_MslAltitudeWithoutMslAccuracy_ReportsNullVerticalAccuracy
06-27 10:53:12.994 10895 11266 I DOTNET : [PASS] LocationCopyConstructor_PreservesAltitudeReferenceSystem
06-27 10:53:12.995 10895 11266 I DOTNET : [PASS] ToLocation_NoAltitude_UsesUnspecifiedReferenceSystem
06-27 10:53:12.995 10895 11266 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Android_Geolocation_Tests 0.0155517 ms
06-27 10:53:12.995 10895 11266 I DOTNET : Test collection for Microsoft.Maui.Essentials.DeviceTests.Magnetometer_Tests
06-27 10:53:13.015 10895 11272 I DOTNET : [PASS] Stop_Monitor
06-27 10:53:13.027 10895 11277 I DOTNET : [PASS] Monitor
06-27 10:53:13.048 10895 11283 I DOTNET : [PASS] IsMonitoring
06-27 10:53:13.048 10895 11283 I DOTNET : [PASS] IsSupported
06-27 10:53:13.048 10895 11283 I DOTNET : Microsoft.Maui.Essentials.DeviceTests.Magnetometer_Tests 0.0482184 ms
06-27 10:53:13.086 10895 10993 I DOTNET : Xml file was written to the provided writer.
06-27 10:53:13.086 10895 10993 I DOTNET : Tests run: 307 Passed: 277 Inconclusive: 0 Failed: 0 Ignored: 30
�[40m�[32minfo�[39m�[22m�[49m: <<XHARNESS_RESULT_START>>
{
"version": 1,
"machineName": "runnervm6n5x7",
"exitCode": 0,
"exitCodeName": "SUCCESS",
"platform": "android",
"instrumentationExitCode": 0,
"device": "emulator-5554",
"deviceOsVersion": "API 30",
"architecture": "x86_64",
"files": [
{
"name": "testResults.xml",
"type": "test-results"
},
{
"name": "adb-logcat-com.microsoft.maui.essentials.devicetests-default.log",
"type": "logcat"
}
]
}
<<XHARNESS_RESULT_END>>
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.essentials.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26230.4/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.essentials.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.essentials.devicetests
XHarness exit code: 0
Tests completed successfully
📁 Fix files reverted (1 files)
src/Essentials/src/Platform/ActivityStateManager.android.cs
📱 UI Tests — Essentials
Detected UI test categories: Essentials
⏭️ Deep UI tests — 0 passed, 0 failed across 1 category on platform-pool agent (replaces in-process counts above). 1 category reported 0 tests.
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Essentials |
0 tests | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
📋 Pre-Flight — Context & Validation
Issue: #36035 - ActivityStateManager listener leak on Android (inferred from PR test comments; GitHub metadata unavailable)
PR: #36161 - Fix repeated ActivityStateManager lifecycle callback registration on Android (inferred from local diff)
Platforms Affected: Android
Files Changed: 1 implementation, 1 test
Key Findings
- Local PR diff changes
ActivityStateManagerImplementation.Init(Application)so repeated initialization does not add multiple AndroidApplication.IActivityLifecycleCallbacksregistrations. - Added Android Essentials device tests verify repeated
Initkeeps one listener and emits oneActivityStateChangednotification per lifecycle event. - GitHub CLI is unauthenticated in this environment, so issue/PR body, comments, reviews, and required CI checks could not be fetched; local gate artifacts show the regression tests fail without the fix and pass with it.
- Regression cross-reference artifact reports no detected regression risks.
Code Review Summary
Verdict: NEEDS_DISCUSSION
Confidence: low
Errors: 0 | Warnings: 1 | Suggestions: 2
Key code review findings:
- ⚠
src/Essentials/test/DeviceTests/Tests/ActivityStateManager_Tests.cs:72registers a real app lifecycle callback in the test, so the event-count assertion could be sensitive to real lifecycle callbacks during the test window. - ℹ
src/Essentials/test/DeviceTests/Tests/ActivityStateManager_Tests.cs:64could explicitly guard nullCurrentActivityfor clearer diagnostics. - ℹ
src/Essentials/src/Platform/ActivityStateManager.android.cs:76-82is sequentially correct; a lock/atomic variant would also close a theoretical concurrentInitrace.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #36161 | Simple idempotency guard: return from Init(Application) when lifecycleListener is already initialized. |
✅ PASSED (Gate) | src/Essentials/src/Platform/ActivityStateManager.android.cs, src/Essentials/test/DeviceTests/Tests/ActivityStateManager_Tests.cs |
Original PR fix; local gate passed. |
🔬 Code Review — Deep Analysis
Code Review — PR #36161
Independent Assessment
What this changes: Android ActivityStateManagerImplementation.Init(Application) now returns early when lifecycleListener is already initialized, so repeated Platform.Init(...) / activity recreation does not register additional Application.IActivityLifecycleCallbacks instances. The PR also adds Android Essentials device tests that instantiate a fresh manager, call Init repeatedly, and verify the same listener is retained and ActivityStateChanged fires once when distinct registered callbacks are simulated.
Inferred motivation: The previous implementation overwrote lifecycleListener and called Application.RegisterActivityLifecycleCallbacks(...) on every init. Since Android callback registration is additive and the old callbacks were never unregistered, each activity lifecycle event could be observed multiple times and retain extra manager/listener instances for the process lifetime.
Reconciliation with PR Narrative
Author claims: GitHub PR title/body/comments could not be read because gh is unauthenticated in this environment. Local test comments reference GitHub issue #36035 and describe an ActivityStateManager listener leak caused by repeated Init(Application) calls during Activity recreation.
Agreement/disagreement: The local diff matches that inferred bug: src/Essentials/src/Platform/ActivityStateManager.android.cs:76-82 makes registration idempotent, while Init(Activity, Bundle?) still updates lifecycleListener!.Activity = activity after the guarded app init (ActivityStateManager.android.cs:96-97). Local gate artifacts confirm the new tests fail without the fix and pass with it.
Prior Review Reconciliation
| Prior ❌ Error Finding | Source | Status | Evidence |
|---|---|---|---|
Unavailable — gh pr view, pull review comments, and issue comments all failed with gh auth login / missing GH_TOKEN. |
GitHub metadata | Unknown | Local artifacts under CustomAgentLogsTmp/PRState/36161/PRAgent/ contain no prior ❌ Error findings; only the local expert-review findings created during this run were found. |
Blast Radius Assessment
- Runs for all instances: Yes, Android app/activity initialization is a process-wide Essentials/MAUI startup path. The change only affects repeated initialization after the first listener has been registered.
- Startup impact: Yes, the code executes during
Platform.Init(Application)/Platform.Init(Activity, Bundle?). The new branch is a null check and early return. - Static/shared state: No new static state. Existing
ActivityStateManager.Defaultremains singleton-backed; existing one-processApplicationlifecycle callback remains intentionally registered for the process lifetime. - Platform scope: Android-only (
.android.cs) with Android-only device tests guarded by#if __ANDROID__.
CI Status
- Required-check result: unavailable.
gh pr checks 36161 --repo dotnet/maui --requiredfailed because the GitHub CLI is unauthenticated. - Classification: undetermined for required CI. Local gate/regression artifacts are favorable: gate
PASSED; Android ActivityStateManager tests fail without the fix and pass with the fix; regression checkCLEANwith zero risks. - Action taken: Recorded GitHub/CI metadata as unavailable; capped confidence at low and did not use
LGTMbecause required CI could not be verified.
Findings
❌ Error — None
No blocking correctness issue was found in the production fix.
⚠️ Warning — Device test registers a real app lifecycle callback that can make the assertion sensitive to real lifecycle events
src/Essentials/test/DeviceTests/Tests/ActivityStateManager_Tests.cs:72 calls manager.Init(app) on the real Android Application after subscribing to manager.ActivityStateChanged at line 69. If the test-runner activity receives a real lifecycle callback between registration and Assert.Equal(1, invocations) at line 87, invocations can include both the real callback and the manual OnActivityResumed simulation, producing a flaky count. This is not a production-code blocker and the local gate passed, but a baseline/delta assertion or subscribing only after setup would make the regression test more isolated.
💡 Suggestion — Guard the test's current activity assumption
src/Essentials/test/DeviceTests/Tests/ActivityStateManager_Tests.cs:64 assigns MauiPlatform.CurrentActivity and passes it to OnActivityResumed at line 85. The current Android device-test run has an activity, but an explicit throw if it is null would preserve the non-null API contract and make failures easier to diagnose.
💡 Suggestion — Consider whether Init(Application) should be race-free
src/Essentials/src/Platform/ActivityStateManager.android.cs:76-82 uses a non-atomic null-check-then-assign. Normal MAUI/Android startup calls this from lifecycle/main-thread paths, so this is acceptable in practice, but a lock/Interlocked.CompareExchange would fully prevent duplicate registration if third-party code invoked the public interface concurrently.
Failure-Mode Probing
- Repeated
Init(Activity, Bundle?)after rotation/recreation: The guard skips re-registering callbacks, then line 97 still assigns the latestActivity, soGetCurrentActivity()remains current while duplicate callbacks stop accumulating. - App-level
Init(Application)followed by later Activity lifecycle callbacks: The first call still registers oneActivityLifecycleContextListener; itsOnActivityCreated/OnActivityResumedmethods continue updating the weak current-activity reference. - Existing consumers of
Platform.ActivityStateChangedandWaitForActivityAsync: They receive a single event per native callback instead of N duplicated events.WaitForActivityAsyncstill subscribes/unsubscribes its handler infinallyand benefits from reduced duplicate completion attempts. - Null/default startup values: Before any activity is known,
GetCurrentActivity()can still return null as before; the fix does not introduce a new null dereference. - Handler disconnect/reconnect: Not a handler change. The app lifecycle callback remains process-scoped; this PR reduces, rather than increases, long-lived listener accumulation.
- Test hermeticity: The new tests correctly reproduce the leak without the fix, but because they register with the real
Application, they are not fully isolated from real lifecycle dispatch.
Verdict: NEEDS_DISCUSSION
Confidence: low (Android startup/platform lifecycle path plus required CI unavailable via unauthenticated gh; local gate evidence is strong but not a substitute for required checks.)
Summary: The production fix is small and sound: it makes ActivityStateManager listener registration idempotent without breaking current-activity updates. I found no blocking production issue. The main residual concern is test isolation around real Android lifecycle callbacks, and the merge verdict cannot be LGTM because required CI/GitHub metadata is unavailable in this environment.
🛠️ Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | maui-expert-reviewer/code-review loop | Race-free locked initialization with rollback around ActivityStateManager listener registration. | ✅ PASS | 1 file | More robust than PR fix for theoretical concurrent Init(Application) calls; 0 self-review findings; more complex than PR. |
| PR | PR #36161 | Simple idempotency guard in Init(Application) when lifecycleListener is non-null. |
✅ PASSED (Gate) | 2 files | Original PR; simpler and gate-proven. |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| gpt-5.5 + maui-expert-reviewer | 1 | Yes | Proposed race-free locked initialization with rollback. Candidate passed all Android ActivityStateManager tests. |
Exhausted: No — stopped because Candidate #1 passed all targeted Android tests and is demonstrably more robust than the PR fix for concurrent initialization and registration-failure rollback. Further variants would be tradeoff exploration rather than necessary failure-driven iteration.
Selected Fix: Candidate #1 — It preserves the PR behavior while adding race-safety and rollback. The tradeoff is added complexity; if the team values simplicity over theoretical concurrency hardening, the PR fix remains acceptable.
🏁 Report — Final Recommendation
Comparative Report — PR #36161
Candidates compared
| Rank | Candidate | Regression result | Review result | Assessment |
|---|---|---|---|---|
| 1 | pr-plus-reviewer |
✅ Inherits gate-passing PR production fix; reviewer change is test cleanup only | ✅ Resolves the current expert inline finding | Best balance. Keeps the simple idempotency production fix, preserves the regression coverage, and cleans up real Android Application lifecycle callbacks registered by the tests. |
| 2 | try-fix-1 |
✅ PASS — Run-DeviceTests.ps1 -Project Essentials -Platform android -TestFilter "Category=ActivityStateManager" reported XHarness exit code 0 and Tests run: 307 Passed: 277 Inconclusive: 0 Failed: 0 Ignored: 30 |
✅ Self-review recorded 0 findings | Strong production alternative. Adds a lock and rollback around listener registration, which is more robust for theoretical concurrent Init(Application) calls, but also adds complexity for a path that normally runs on Android lifecycle/main-thread startup. It does not address the current expert finding against the PR's test callbacks unless combined with the reviewer cleanup. |
| 3 | pr |
✅ Gate passed — tests fail without fix and pass with fix | Production fix is sound and minimal, but the raw test code leaves process-wide lifecycle callbacks registered after the tests complete. |
No candidate with a failed regression result was ranked above a passing candidate.
Key tradeoffs
pr fixes the actual production leak with a small early-return guard. The main residual issue is test hermeticity: the newly registered real Android lifecycle callbacks should be unregistered so the test process does not retain listeners or receive extra callbacks later.
try-fix-1 is production-hardened beyond the PR by serializing lifecycleListener initialization and rolling back the field if Android callback registration throws. That is technically stronger if concurrent/off-main Init(Application) calls are considered in scope, but the observed bug and normal MAUI call path are sequential Android lifecycle initialization. The extra lock is therefore useful but not necessary to fix #36035.
pr-plus-reviewer is the safest merge recommendation because it keeps the author's simple production fix and applies the only current expert actionable feedback. It improves the submitted PR without introducing new production synchronization complexity.
Winning candidate
Winner: pr-plus-reviewer
Rationale: It preserves the gate-proven PR behavior, fixes the expert-review test leak, and keeps the production change narrowly scoped to idempotent Android lifecycle callback registration. try-fix-1 remains a valid alternative if maintainers explicitly want to harden Init(Application) for concurrent callers, but that is a broader tradeoff than required for the regression.
🧭 Next Steps — review latest findings
No alternative fix was selected for this run. Review the session findings and CI results before merging.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Tests Failure Analysis
Test Failure Review: Needs human investigation - click to expandOverall verdict: Needs human investigation All 12 extracted failures are Windows Helix device-test work items that exited non-zero while recording zero named test failures; the pattern (HybridWebView timeout, crash dump, or bare non-zero exit) is consistent with a Windows runner environment issue unrelated to the Android lifecycle change this PR makes, but deterministic attribution is unavailable and the maui-pr-uitests run was canceled mid-flight leaving its sample-app build legs unresolved. Coverage: 50 checks · 43 passing · 5 failing · 2 pending · 0 inaccessible · 1 unmapped · 0 unexplained build legs · 4 unaccounted failing checks · 4 aborted failing checks · 5 canceled-build checks · 9 device-test unverified · 12 unattributed · 0 regressed-vs-base. Deterministic ceiling: Needs human investigation — 2 checks pending (maui-pr-uitests, Build Analysis); 1 failing check has no inspectable AzDO evidence (agent); 4 unaccounted failing checks (cancelled maui-pr-uitests sample-app build legs); 12 unattributed failures (Windows device-test work items); 4 aborted failing checks; 5 canceled-build checks; 9 device-test checks unverified.
Recommended actionWait for the queued maui-pr-uitests run (build 1483275) to finish; a human reviewer should confirm whether the Windows Helix device-test work item non-clean exits (all 12 show 0 named failures, consistent with a runner environment or HybridWebView timeout issue unrelated to the Android lifecycle change) are pre-existing before merging. Evidence detailsPR scope: 2 files changed (1 test file), Android-only platform label, area-core-lifecycle. The fix targets Device test build: maui-pr-devicetests build 1483277 — result: succeeded. Helix job Windows device-test pattern: Every work item in the Windows Helix job exited non-zero but named 0 failed tests. The Controls items show "Timeout waiting for HybridWebView test results after 480 seconds | crash dump present" — a known flaky pattern on Windows Helix runners unrelated to Android lifecycle code. The remaining 10 work items show bare non-zero ExitCode with no detail, also consistent with runner-level failures. UI test cancellation: maui-pr-uitests build 1483275 was canceled (result: canceled). The 4 sample-app build legs (CoreCLR, Material3, Standard, Windows) all have CANCELLED conclusion. A fresh maui-pr-uitests run is currently QUEUED — once it completes, the UI test picture will be clearer. Baseline: maui-pr-devicetests base build 1484479 had 1 baseline failure; none of the 12 PR failures matched it exactly (0 of 12 also on base). Baseline comparison is therefore inconclusive for these work-item-level failures. Unmapped check: GitHub Actions Limitations: AzDO access was unauthenticated; Helix work item detail was read via the public API. Device-test verified status could not be positively confirmed (XHarness exits 0 on some failure modes), so 9 green device-test checks remain unverified by the gate. |
… crash defense Addresses the Copilot PR reviewer's L928 finding plus two bundled polish items on the /review tests gather script. L928 over-cap (Get-ConsoleFailureReason): the single regex that captured a human-readable failure reason ALSO set isIncomplete, and it matched "Test execution completed with exit code: N" -- which eng/devices/run-windows-devicetests.cmd:481 echoes UNCONDITIONALLY for every non-zero run (the cmd only ever does `exit /b 0|1`). Every failed Windows work item was therefore force-capped to "needs human investigation", defeating the Phase 2 intent of letting a cleanly-completed NAMED failure flow through to base/known-issue attribution. Split into a broad $reasonRegex (capture, unchanged) and a narrow $incompleteRegex that fires ONLY on genuine non-clean-finish markers (timeout/hang/crash/kill/wipeout), excluding the unconditional exit-code line and a bare "[FAIL] <test>". This is safe: a genuinely incomplete Windows run still trips isIncomplete via an INDEPENDENT marker (:wait_for_result emits "[FAIL] Timeout waiting for <cat> test results", a total wipeout prints "All test processes may have crashed", crashes/dumps print their own markers). Negative-ExitCode crash defense (new -WorkItemCrashed param): to close the residual infra-kill window (a signal-killed work item could flush a partial named TRX before dying), New-DeviceWorkItemFailureRecords now forces the incomplete cap when the Helix work item reports a NEGATIVE ExitCode. The runners only ever exit with a small non-negative code, so a negative code (e.g. -4, observed live on #36161) is the signature of an abnormal Helix/OS kill. Computed at the call site from the authoritative /workitems list ExitCode. Polish: reword the misleading "non-fatal on its own" job-detail comment to state that a failed detail read forces unverified (caps, never false-green); rename the mis-named sawFailCount -> sawWorkItemCount. Tests: +5 Pester cases (clean Windows named-failure run is no longer over-capped; genuine timeout/wipeout still caps; -WorkItemCrashed caps; default opt-out unaffected). 41/41 pass; AST parse clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ivity recreation (#36161) <!-- 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! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details Every time an Android Activity is programmatically recreated (via Activity.Recreate()), Platform.ActivityStateChanged fires more times than expected. After 120 recreations, the event fires 61× more than it should, causing serious performance problems. ### Root Cause ActivityStateManager.Init(Application) was called on every Activity recreation, and each call registered a new listener with Android without removing the old one. Android keeps all registered listeners, so after N recreations, N+1 listeners exist and every event fires N+1 times. ### Description of Change Added a 2-line early-return guard in Init(Application): if (lifecycleListener is not null) return; This ensures the listener is registered only once on the first call. All subsequent calls (from Activity recreations) are safely skipped. Validated the behavior in the following platforms - [x] Android - [ ] Windows - [ ] iOS - [ ] Mac ### Issues Fixed Fixes #36035 ### Output ScreenShot |Before|After| |--|--| | <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/5d8d5679-8285-4a7c-9bf2-5185e6f781b8" />| <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/15a01574-e0c2-4e2b-bcc3-e5e7d0d9ccd5" /> |
…ivity recreation (#36161) <!-- 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! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details Every time an Android Activity is programmatically recreated (via Activity.Recreate()), Platform.ActivityStateChanged fires more times than expected. After 120 recreations, the event fires 61× more than it should, causing serious performance problems. ### Root Cause ActivityStateManager.Init(Application) was called on every Activity recreation, and each call registered a new listener with Android without removing the old one. Android keeps all registered listeners, so after N recreations, N+1 listeners exist and every event fires N+1 times. ### Description of Change Added a 2-line early-return guard in Init(Application): if (lifecycleListener is not null) return; This ensures the listener is registered only once on the first call. All subsequent calls (from Activity recreations) are safely skipped. Validated the behavior in the following platforms - [x] Android - [ ] Windows - [ ] iOS - [ ] Mac ### Issues Fixed Fixes #36035 ### Output ScreenShot |Before|After| |--|--| | <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/5d8d5679-8285-4a7c-9bf2-5185e6f781b8" />| <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/15a01574-e0c2-4e2b-bcc3-e5e7d0d9ccd5" /> |
…ivity recreation (#36161) <!-- 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! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details Every time an Android Activity is programmatically recreated (via Activity.Recreate()), Platform.ActivityStateChanged fires more times than expected. After 120 recreations, the event fires 61× more than it should, causing serious performance problems. ### Root Cause ActivityStateManager.Init(Application) was called on every Activity recreation, and each call registered a new listener with Android without removing the old one. Android keeps all registered listeners, so after N recreations, N+1 listeners exist and every event fires N+1 times. ### Description of Change Added a 2-line early-return guard in Init(Application): if (lifecycleListener is not null) return; This ensures the listener is registered only once on the first call. All subsequent calls (from Activity recreations) are safely skipped. Validated the behavior in the following platforms - [x] Android - [ ] Windows - [ ] iOS - [ ] Mac ### Issues Fixed Fixes #36035 ### Output ScreenShot |Before|After| |--|--| | <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/5d8d5679-8285-4a7c-9bf2-5185e6f781b8" />| <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/15a01574-e0c2-4e2b-bcc3-e5e7d0d9ccd5" /> |
…ivity recreation (#36161) <!-- 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! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details Every time an Android Activity is programmatically recreated (via Activity.Recreate()), Platform.ActivityStateChanged fires more times than expected. After 120 recreations, the event fires 61× more than it should, causing serious performance problems. ### Root Cause ActivityStateManager.Init(Application) was called on every Activity recreation, and each call registered a new listener with Android without removing the old one. Android keeps all registered listeners, so after N recreations, N+1 listeners exist and every event fires N+1 times. ### Description of Change Added a 2-line early-return guard in Init(Application): if (lifecycleListener is not null) return; This ensures the listener is registered only once on the first call. All subsequent calls (from Activity recreations) are safely skipped. Validated the behavior in the following platforms - [x] Android - [ ] Windows - [ ] iOS - [ ] Mac ### Issues Fixed Fixes #36035 ### Output ScreenShot |Before|After| |--|--| | <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/5d8d5679-8285-4a7c-9bf2-5185e6f781b8" />| <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/15a01574-e0c2-4e2b-bcc3-e5e7d0d9ccd5" /> |
…ivity recreation (#36161) <!-- 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! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details Every time an Android Activity is programmatically recreated (via Activity.Recreate()), Platform.ActivityStateChanged fires more times than expected. After 120 recreations, the event fires 61× more than it should, causing serious performance problems. ### Root Cause ActivityStateManager.Init(Application) was called on every Activity recreation, and each call registered a new listener with Android without removing the old one. Android keeps all registered listeners, so after N recreations, N+1 listeners exist and every event fires N+1 times. ### Description of Change Added a 2-line early-return guard in Init(Application): if (lifecycleListener is not null) return; This ensures the listener is registered only once on the first call. All subsequent calls (from Activity recreations) are safely skipped. Validated the behavior in the following platforms - [x] Android - [ ] Windows - [ ] iOS - [ ] Mac ### Issues Fixed Fixes #36035 ### Output ScreenShot |Before|After| |--|--| | <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/5d8d5679-8285-4a7c-9bf2-5185e6f781b8" />| <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/15a01574-e0c2-4e2b-bcc3-e5e7d0d9ccd5" /> |
…ivity recreation (#36161) <!-- 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! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details Every time an Android Activity is programmatically recreated (via Activity.Recreate()), Platform.ActivityStateChanged fires more times than expected. After 120 recreations, the event fires 61× more than it should, causing serious performance problems. ### Root Cause ActivityStateManager.Init(Application) was called on every Activity recreation, and each call registered a new listener with Android without removing the old one. Android keeps all registered listeners, so after N recreations, N+1 listeners exist and every event fires N+1 times. ### Description of Change Added a 2-line early-return guard in Init(Application): if (lifecycleListener is not null) return; This ensures the listener is registered only once on the first call. All subsequent calls (from Activity recreations) are safely skipped. Validated the behavior in the following platforms - [x] Android - [ ] Windows - [ ] iOS - [ ] Mac ### Issues Fixed Fixes #36035 ### Output ScreenShot |Before|After| |--|--| | <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/5d8d5679-8285-4a7c-9bf2-5185e6f781b8" />| <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/15a01574-e0c2-4e2b-bcc3-e5e7d0d9ccd5" /> |
…ivity recreation (#36161) <!-- 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! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details Every time an Android Activity is programmatically recreated (via Activity.Recreate()), Platform.ActivityStateChanged fires more times than expected. After 120 recreations, the event fires 61× more than it should, causing serious performance problems. ### Root Cause ActivityStateManager.Init(Application) was called on every Activity recreation, and each call registered a new listener with Android without removing the old one. Android keeps all registered listeners, so after N recreations, N+1 listeners exist and every event fires N+1 times. ### Description of Change Added a 2-line early-return guard in Init(Application): if (lifecycleListener is not null) return; This ensures the listener is registered only once on the first call. All subsequent calls (from Activity recreations) are safely skipped. Validated the behavior in the following platforms - [x] Android - [ ] Windows - [ ] iOS - [ ] Mac ### Issues Fixed Fixes #36035 ### Output ScreenShot |Before|After| |--|--| | <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/5d8d5679-8285-4a7c-9bf2-5185e6f781b8" />| <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/15a01574-e0c2-4e2b-bcc3-e5e7d0d9ccd5" /> |
…ivity recreation (#36161) <!-- 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! <!-- !!!!!!! MAIN IS THE ONLY ACTIVE BRANCH. MAKE SURE THIS PR IS TARGETING MAIN. !!!!!!! --> ### Issue Details Every time an Android Activity is programmatically recreated (via Activity.Recreate()), Platform.ActivityStateChanged fires more times than expected. After 120 recreations, the event fires 61× more than it should, causing serious performance problems. ### Root Cause ActivityStateManager.Init(Application) was called on every Activity recreation, and each call registered a new listener with Android without removing the old one. Android keeps all registered listeners, so after N recreations, N+1 listeners exist and every event fires N+1 times. ### Description of Change Added a 2-line early-return guard in Init(Application): if (lifecycleListener is not null) return; This ensures the listener is registered only once on the first call. All subsequent calls (from Activity recreations) are safely skipped. Validated the behavior in the following platforms - [x] Android - [ ] Windows - [ ] iOS - [ ] Mac ### Issues Fixed Fixes #36035 ### Output ScreenShot |Before|After| |--|--| | <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/5d8d5679-8285-4a7c-9bf2-5185e6f781b8" />| <img width="367" height="741" alt="image" src="https://github.com/user-attachments/assets/15a01574-e0c2-4e2b-bcc3-e5e7d0d9ccd5" /> |
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
Every time an Android Activity is programmatically recreated (via Activity.Recreate()), Platform.ActivityStateChanged fires more times than expected. After 120 recreations, the event fires 61× more than it should, causing serious performance problems.
Root Cause
ActivityStateManager.Init(Application) was called on every Activity recreation, and each call registered a new listener with Android without removing the old one. Android keeps all registered listeners, so after N recreations, N+1 listeners exist and every event fires N+1 times.
Description of Change
Added a 2-line early-return guard in Init(Application):
if (lifecycleListener is not null)
return;
This ensures the listener is registered only once on the first call. All subsequent calls (from Activity recreations) are safely skipped.
Validated the behavior in the following platforms
Issues Fixed
Fixes #36035
Output ScreenShot