[Android] Fix PickPhotosAsync remains pending when its activity is recreated while Photo Picker is open - #36767
Conversation
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
|
Hey there @@HarishwaranVijayakumar! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@HarishwaranVijayakumar — new AI review results are available based on this last commit:
4c4ce7c.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ❌ FAILED
Platform: ANDROID · Base: inflight/current · Merge base: 448250f1
🩺 Test does not reproduce the bug — ran the same in both states (PASS without fix, PASS with fix). The repro test is not exercising the issue. Strengthen the test before reviewing the fix.
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🖥️ Issue36523 Issue36523 |
❌ PASS — 3909s | ✅ PASS — 621s |
🔴 Without fix — 🖥️ Issue36523: PASS ❌ · 3909s
Error-relevant lines (filtered from the build log):
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: Mono.AndroidTools.InstallFailedException: Unexpected install output: cmd: Failure calling service package: Broken pipe (32) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: at Mono.AndroidTools.Internal.AdbOutputParsing.CheckInstallSuccess(String output, String packageName) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: at Mono.AndroidTools.AndroidDevice.<>c__DisplayClass105_0.<InstallPackage>b__0(Task`1 t) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: --- End of stack trace from previous location --- [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: at AndroidDeviceExtensions.PushAndInstallPackageAsync(AndroidDevice device, PushAndInstallCommand command, CancellationToken token) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: at Xamarin.Android.Tasks.FastDeploy.InstallPackage(Boolean installed) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: at Xamarin.Android.Tasks.FastDeploy.RunInstall() [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
Build FAILED.
🟢 With fix — 🖥️ Issue36523: PASS ✅ · 621s
(no coded error found; showing last 1200 chars)
:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.18] Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.50] 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
>>>>> 07/26/2026 23:56:18 FixtureSetup for Issue36523(Android)
>>>>> 07/26/2026 23:56:21 PickPhotosAsyncShouldReturnAfterRotation Start
>>>>> 07/26/2026 23:56:21 PickPhotosAsyncShouldReturnAfterRotation Stop
PickPhotosAsyncShouldReturnAfterRotation: Issue #36523 only manifests on Android API 33+. Current device API: 30.
Skipped PickPhotosAsyncShouldReturnAfterRotation [1 s]
NUnit Adapter 4.5.0.0: Test execution complete
Results File: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue36523.trx
Test Run Successful.
Total tests: 1
Skipped: 1
Total time: 17.4108 Seconds
>>> TRX_RESULT_FILE: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue36523.trx
⚠️ Failure Details
- ❌ Issue36523 PASSED without fix (should fail) — tests don't catch the bug
📁 Fix files reverted (1 files)
src/Essentials/src/Platform/ActivityForResultRequest.android.cs
📋 Pre-Flight — Context & Validation
Issue: #36523 - [inflight regression] Android PickPhotosAsync remains pending when its activity is recreated while Photo Picker is open
PR: #36767 - [WIP] [Android] Fix PickPhotosAsync remains pending when its activity is recreated while Photo Picker is open
Platforms Affected: Android
Files Changed: 1 implementation, 2 test
Key Findings
- The issue is an Android API 33+ Photo Picker regression introduced by PR #35944: pending requests are keyed by the original
ComponentActivity, but after rotation the result is delivered through a recreated activity, so the original task can remain pending. - PR #36767 adds migration from the old activity to the recreated one using a single
_inFlightActivitystrong reference, plus Android UI coverage for the rotation/cancel path. - The PR's single
_inFlightActivityfield conflicts with the existing per-activity concurrency contract: a second activity can overwrite the strong reference while the first still has a pending request. - GitHub CLI auth is unavailable in this environment; public API context and local diff were used. Required-check status is undetermined/red from public check-run data and cannot be treated as passing.
Code Review Summary
Verdict: NEEDS_CHANGES
Confidence: low
Errors: 1 | Warnings: 1 | Suggestions: 1
Key code review findings:
- ✗
src/Essentials/src/Platform/ActivityForResultRequest.android.cs:53— single_inFlightActivitybreaks concurrent activity request isolation and can leave an earlier activity's pending request weakly protected again. - ⚠
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36523.cs:41— fixed sleeps can pass without deterministically proving rotation happened while the picker was open. - ℹ
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36523.cs:51—returnedwait result is assigned but not asserted.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #36767 | Track one _inFlightActivity, migrate its pending request during Register(newActivity), and clear the reference on completion/cancel/failure. |
❌ FAILED (Gate) | src/Essentials/src/Platform/ActivityForResultRequest.android.cs, src/Controls/tests/TestCases.HostApp/Issues/Issue36523.cs, src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36523.cs |
Original PR; code review found the single strong reference is not safe for concurrent activities. |
🔬 Code Review — Deep Analysis
Code Review — PR #36767
Independent Assessment
What this changes: Android ActivityForResultRequest now keeps one strong _inFlightActivity reference and migrates one pending picker TaskCompletionSource from an old ComponentActivity to a recreated one during configuration changes. It also adds an Android UI regression test for rotating while Photo Picker is open.
Inferred motivation: Prevent PickPhotosAsync from hanging when Android recreates the launching activity while the system Photo Picker is active.
Reconciliation with PR Narrative
Author claims: Fixes #36523 by keeping the launching activity alive and migrating pending requests to the recreated activity; adds UI coverage.
Agreement/disagreement: The root cause matches the issue, and the single-activity rotation path is addressed. However, the implementation narrows a class that explicitly supports concurrent requests from multiple activities to one strong in-flight activity, so one activity can still lose GC protection when another activity launches.
Prior Review Reconciliation
No prior ❌ Error findings found. GitHub public API returned 0 reviews, 0 inline comments, and only two non-review issue comments.
Blast Radius Assessment
- Runs for all instances: Yes — affects singleton Android Photo Picker request helpers.
- Startup impact: Low — registration occurs during Android activity init, but behavior changes only when Photo Picker requests are in flight.
- Static/shared state: Yes —
_inFlightActivityis shared per singleton request type (PickVisualMediaForResult,PickMultipleVisualMediaForResult).
CI Status
- Required-check result:
gh pr checks --requiredunavailable (gh auth loginrequired). - Public API result: PR head has failing
maui-pr/ Build Analysis check runs. - Classification: undetermined; failures are broad build/integration failures and not fully attributable from available unauthenticated data.
- Action taken: invoked
azdo-build-investigator; used public GitHub/AzDO APIs; confidence capped low.
Findings
❌ Error — Single _inFlightActivity breaks concurrent activity request isolation
src/Essentials/src/Platform/ActivityForResultRequest.android.cs:53
The existing type is documented to support multiple activities with independent pending requests, but the new strong reference stores only one activity. If Activity A launches a picker, then Activity B launches before A completes, line 158 overwrites _inFlightActivity. Activity A’s pending CWT entry is again weakly keyed with no strong owner, so a later recreation can still lose/miss migration and hang. This regresses the concurrency guarantee introduced by the per-activity CWT design. The fix should track in-flight activities per ComponentActivity, not as a single field.
⚠️ Warning — UI test can pass without exercising rotation-during-picker
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36523.cs:41
The test uses fixed Task.Delay(2000).Wait() sleeps before and after rotation. On a slow emulator, rotation may happen before Photo Picker is actually open, or Back may occur before the recreated activity has registered. In that case the test can pass through a normal cancel path without validating the migration. Prefer deterministic waits for picker/activity state.
💡 Suggestion — Check the wait result explicitly
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36523.cs:51
returned is assigned but never asserted. Asserting it would produce clearer timeout diagnostics.
Failure-Mode Probing
- Two activities launch requests concurrently: the later launch overwrites
_inFlightActivity, leaving the earlier pending request weakly protected. - Activity rotates after being overwritten:
MigratePendingRequestssees the wrong/null activity and skips the needed migration. - CI unavailable/red: cannot claim readiness; verdict cannot be LGTM.
Verdict: NEEDS_CHANGES
Confidence: low
Summary: The core single-activity fix is plausible, but the new single shared strong reference regresses the existing multi-activity isolation contract. CI status is also red/undetermined under unauthenticated tooling.
🛠️ Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | maui-expert-reviewer + try-fix | Replace the PR's single _inFlightActivity with per-request activity aliases: each launch creates a strong PendingRequest, recreated activities can be added as aliases to the same request, and completion/cancel/failure removes all aliases. |
✅ PASSED | 1 file | Preserves per-activity isolation while fixing rotation/recreation result delivery. |
| PR | PR #36767 | Track one _inFlightActivity, migrate that one pending request during Register(newActivity), and clear it on result/cancel/failure. |
❌ FAILED (Gate) | 3 files | Original PR; code review found the single strong reference can be overwritten by another activity and regress concurrent activity isolation. |
Candidate 1 Details
Approach: Per-request activity aliases.
Why different from the PR fix: The PR stores one global strong activity reference. Candidate 1 replaces that with a strong pending-request map keyed by activity, allowing each in-flight activity to remain isolated while a recreated activity is added as another lookup key for the same pending request.
Test command: pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform android -TestFilter 'Issue36523|Issue35826'
Test result: ✅ PASS — Android device emulator-5554; NUnit discovered 2/2 tests; Test Run Successful; Total tests: 2; script result SUCCESS.
Self-review: Clean (reviewer-findings.json contains []). The candidate removes all aliases on result/cancel/failure and handles the review-identified concurrent-activity failure mode.
Artifacts:
CustomAgentLogsTmp/PRState/36767/PRAgent/try-fix-1/content.mdCustomAgentLogsTmp/PRState/36767/PRAgent/try-fix/attempt-1/fix.diffCustomAgentLogsTmp/PRState/36767/PRAgent/try-fix/attempt-1/test-output.log
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| maui-expert-reviewer | 1 | Yes | Per-request alias map / strong pending request object rather than a single _inFlightActivity. |
Exhausted: No — stopped because Candidate 1 passed the targeted regression tests and is demonstrably better than the PR's current fix for the code-review failure mode.
Selected Fix: Candidate #1 — it passes Issue36523 and Issue35826 on Android and preserves the per-activity concurrency contract that the PR's single-reference approach regresses.
📝 Recommended PR Title & Description
Assessment: ✏️ Recommend updating — the current title still has [WIP], and the current description describes the raw single-_inFlightActivity implementation rather than the winning per-request alias fix.
Recommended title
[Android] MediaPicker: Fix PickPhotosAsync after Photo Picker activity recreation
Recommended description
### Issue Details::
- MediaPicker.PickPhotosAsync() hangs forever when the device is rotated while the Android Photo Picker is open.
### Root Cause of the issue
**Regression:**
- Introduced by PR #35944, which changed ActivityForResultRequest from a single shared TaskCompletionSource to per-activity storage.
- After rotation, Android destroys Activity-1 and creates Activity-2. The pending request is stored under Activity-1's key, but Activity-2's callback/register path needs to resolve the same in-flight request. If the request remains associated only with the destroyed activity, the result can be dropped and the task can hang permanently.
- The fix must preserve ActivityForResultRequest's per-activity concurrency contract: child activities and main activities can have independent in-flight requests without clobbering each other.
### Description of Change
**Bug fix for activity recreation and pending requests:**
* Replaced the raw single-activity tracking model with per-request activity aliasing in ActivityForResultRequest.
* Each launch creates a strong pending request object keyed by the launching ComponentActivity.
* When a recreated activity registers during a configuration change, it is added as another alias to the same pending request so either activity key can complete the task.
* Completion, cancellation, and launch failure remove all aliases for the completed pending request, preventing stale references while preserving concurrent activity isolation.
**Test coverage:**
* Added a new shared test case and UI test (`Issue36523`) that exercises the rotation scenario and verifies that `PickPhotosAsync` completes rather than hanging after device rotation and picker cancellation.
### Issues Fixed
Fixes #36523
### Tested the behavior in the following platforms
- [ ] Windows
- [x] Android
- [ ] iOS
- [ ] Mac
### Output
| Before | After |
|----------|----------|
| <img src="https://github.com/user-attachments/assets/b054d308-c65e-4cb0-ba84-0a06cf5268e7"> | <img src="https://github.com/user-attachments/assets/c819e6e5-3b14-4a6d-b7e1-b5230c694d71"> |
🏁 Report — Final Recommendation
Comparative Analysis — PR #36767
Candidates compared
| Rank | Candidate | Regression result | Assessment |
|---|---|---|---|
| 1 | try-fix-1 |
✅ PASSED | Best candidate. Replaces the raw PR's single global _inFlightActivity with per-request activity aliases, preserving concurrent activity isolation while allowing recreated activities to resolve the same pending request. Stored self-review is clean and the targeted Android run reported success. |
| 2 | pr-plus-reviewer |
Not separately run | Applies the expert reviewer's actionable production feedback and converges on the same per-request aliasing model as try-fix-1, plus identifies test-hardening improvements. It ranks below try-fix-1 because it is a sandbox evaluation artifact rather than the empirically validated STEP 5a candidate. |
| 3 | pr |
❌ FAILED gate / test did not prove repro | Raw PR identifies the right root cause but uses one shared _inFlightActivity, which regresses the class's per-activity concurrency contract. It also has fragile UI-test timing and an unused wait result. Candidates that failed regression verification rank below passing candidates. |
Key comparison
The raw PR fixes the simple rotation path by keeping one launching activity strongly referenced and migrating its TCS during Register(newActivity). That is insufficient for a type whose _pendingRequests storage and XML documentation are explicitly per-activity: another activity can overwrite or clear the single _inFlightActivity, leaving the first pending request weakly protected again.
try-fix-1 solves the same root cause without collapsing the request model to one global activity. It stores strong PendingRequest objects in a reference-equality dictionary keyed by activity, aliases a matching recreated activity to the same request during configuration changes, and removes every alias when the request completes, is canceled, or fails. That directly addresses the expert reviewer's major finding.
Winner
Winner: try-fix-1
try-fix-1 is the only candidate that both addresses the major correctness issue and has a stored passing regression result. The raw PR must not win because it failed gate expectations and retains a concrete concurrent-activity failure mode; pr-plus-reviewer is directionally correct but is not the separately validated STEP 5a artifact.
🧭 Next Steps — alternative fix proposed (try-fix-1)
Automated review — alternative fix proposed
The expert-reviewer evaluation compared the PR fix against automatically generated candidates and selected try-fix-1 as the strongest fix.
Why: try-fix-1 preserves per-activity request isolation by using per-request activity aliases instead of the raw PR's single shared _inFlightActivity, directly fixing the expert reviewer's major correctness finding. It also has the stored passing STEP 5a regression result, while the raw PR failed gate expectations.
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-1)
--- a/src/Essentials/src/Platform/ActivityForResultRequest.android.cs
+++ b/src/Essentials/src/Platform/ActivityForResultRequest.android.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
@@ -44,9 +45,19 @@
// eligible for collection when the activity is no longer referenced.
readonly ConditionalWeakTable<ComponentActivity, ActivityResultLauncher> _activityLaunchers = new();
- // Tracks pending TaskCompletionSource per ComponentActivity to prevent race conditions.
+ // Tracks pending requests per ComponentActivity to prevent race conditions.
// This prevents Activity B from overwriting Activity A's pending request.
- readonly ConditionalWeakTable<ComponentActivity, TaskCompletionSource<TResult>> _pendingRequests = new();
+ readonly Dictionary<ComponentActivity, PendingRequest> _pendingRequests = new(ReferenceEqualityComparer.Instance);
+
+ sealed class PendingRequest
+ {
+ public PendingRequest()
+ {
+ CompletionSource = new TaskCompletionSource<TResult>();
+ }
+
+ public TaskCompletionSource<TResult> CompletionSource { get; }
+ }
/// <summary>
/// Gets a value indicating whether the request has a launcher registered for the
@@ -71,6 +82,9 @@
if (_activityLaunchers.TryGetValue(componentActivity, out _))
return;
+ // Alias pending requests from the old activity to the new one on config change (e.g. rotation).
+ MigratePendingRequests(componentActivity);
+
var contract = new TContract();
// CRITICAL: capture the same `componentActivity` instance the launcher is being
@@ -83,10 +97,10 @@
var registeredActivity = componentActivity;
var callback = new ActivityResultCallback<TResult>(result =>
{
- if (_pendingRequests.TryGetValue(registeredActivity, out var tcs))
+ if (_pendingRequests.TryGetValue(registeredActivity, out var pendingRequest))
{
- _pendingRequests.Remove(registeredActivity);
- tcs?.TrySetResult(result);
+ RemovePendingRequest(pendingRequest);
+ pendingRequest.CompletionSource.TrySetResult(result);
}
});
@@ -135,18 +149,19 @@
if (launchingActivity is null)
throw new ArgumentNullException(nameof(launchingActivity));
- if (_pendingRequests.TryGetValue(launchingActivity, out var existingTcs))
+ if (_pendingRequests.TryGetValue(launchingActivity, out var existingRequest))
{
// Instead of rejecting the new launch, cancel the orphaned previous request and replace it.
// This prevents permanent deadlock if a picker result never arrives due to process death or OEM edge cases.
// Rejection semantics would block all future launches from this activity forever.
Trace.WriteLine("ActivityForResultRequest: canceling overlapping pending request and launching new request.");
- _pendingRequests.Remove(launchingActivity);
- existingTcs?.TrySetCanceled();
+ RemovePendingRequest(existingRequest);
+ existingRequest.CompletionSource.TrySetCanceled();
}
- var tcs = new TaskCompletionSource<TResult>();
- _pendingRequests.Add(launchingActivity, tcs);
+ var pendingRequest = new PendingRequest();
+ var tcs = pendingRequest.CompletionSource;
+ _pendingRequests.Add(launchingActivity, pendingRequest);
// Get the launcher for this specific activity
if (!_activityLaunchers.TryGetValue(launchingActivity, out var launcher))
@@ -181,10 +196,10 @@
/// <param name="componentActivity">The activity whose pending request should be cancelled.</param>
internal void CancelPendingRequest(ComponentActivity componentActivity)
{
- if (_pendingRequests.TryGetValue(componentActivity, out var tcs))
+ if (_pendingRequests.TryGetValue(componentActivity, out var pendingRequest))
{
- _pendingRequests.Remove(componentActivity);
- tcs?.TrySetCanceled();
+ RemovePendingRequest(pendingRequest);
+ pendingRequest.CompletionSource.TrySetCanceled();
}
}
@@ -198,4 +213,66 @@
return null;
}
+
+ /// <summary>
+ /// Aliases a pending request from the old (destroyed) activity to the new activity
+ /// during a configuration change, so the result callback can find the request under
+ /// either activity key.
+ /// </summary>
+ void MigratePendingRequests(ComponentActivity newActivity)
+ {
+ if (_pendingRequests.ContainsKey(newActivity))
+ {
+ return;
+ }
+
+ PendingRequest matchingRequest = null;
+ foreach (var pendingRequest in _pendingRequests)
+ {
+ var oldActivity = pendingRequest.Key;
+ if (ReferenceEquals(oldActivity, newActivity) ||
+ !oldActivity.IsChangingConfigurations ||
+ oldActivity.TaskId != newActivity.TaskId ||
+ oldActivity.GetType() != newActivity.GetType())
+ {
+ continue;
+ }
+
+ if (matchingRequest is not null && !ReferenceEquals(matchingRequest, pendingRequest.Value))
+ {
+ Trace.WriteLine("ActivityForResultRequest: multiple pending requests matched recreated activity; skipping migration.");
+ return;
+ }
+
+ matchingRequest = pendingRequest.Value;
+ }
+
+ if (matchingRequest is not null)
+ {
+ _pendingRequests.Add(newActivity, matchingRequest);
+ }
+ }
+
+ void RemovePendingRequest(PendingRequest pendingRequest)
+ {
+ List<ComponentActivity> keysToRemove = null;
+ foreach (var request in _pendingRequests)
+ {
+ if (ReferenceEquals(request.Value, pendingRequest))
+ {
+ keysToRemove ??= new List<ComponentActivity>();
+ keysToRemove.Add(request.Key);
+ }
+ }
+
+ if (keysToRemove is null)
+ {
+ return;
+ }
+
+ foreach (var activity in keysToRemove)
+ {
+ _pendingRequests.Remove(activity);
+ }
+ }
}
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue36523.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue36523.cs
new file mode 100644
index 0000000000..1ce043119f
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue36523.cs
@@ -0,0 +1,149 @@
+using Microsoft.Maui.Media;
+
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 36523, "MediaPicker.PickPhotosAsync hangs after device rotation on API 33+", PlatformAffected.Android)]
+public class Issue36523 : ContentPage
+{
+ public Issue36523()
+ {
+ var openButton = new Button { AutomationId = "OpenRotationActivityButton", Text = "Open Rotation Activity" };
+ openButton.Clicked += (_, _) =>
+ {
+#if ANDROID
+ Issue36523State.Reset();
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity!;
+ activity.StartActivity(new Android.Content.Intent(activity, typeof(Issue36523RotationActivity)));
+#endif
+ };
+ Content = new VerticalStackLayout { Padding = 30, Children = { openButton } };
+ }
+}
+
+#if ANDROID
+static class Issue36523State
+{
+ static readonly object s_gate = new();
+ static Task s_pickerTask;
+ static int s_launchActivityId;
+ static string s_outcome = "READY";
+
+ public static void Reset() { lock (s_gate) { s_pickerTask = null; s_launchActivityId = 0; s_outcome = "READY"; } }
+ public static string GetOutcome() { lock (s_gate) return s_outcome; }
+ public static void Complete(string outcome) { lock (s_gate) s_outcome = outcome; }
+
+ public static void BeginPick(int activityId, Task task)
+ {
+ lock (s_gate)
+ { s_launchActivityId = activityId; s_pickerTask = task; s_outcome = "WAITING"; }
+ }
+
+ public static bool CheckForHang(int currentActivityId)
+ {
+ lock (s_gate)
+ {
+ if (s_pickerTask is { IsCompleted: false } && s_launchActivityId != currentActivityId)
+ {
+ s_outcome = "FAIL: picker task hung after activity recreation";
+ return true;
+ }
+ return false;
+ }
+ }
+}
+
+[Android.App.Activity(Label = "Issue36523", Theme = "@style/Maui.SplashTheme")]
+public class Issue36523RotationActivity : AndroidX.AppCompat.App.AppCompatActivity
+{
+ static int s_nextId;
+ readonly int _id = Interlocked.Increment(ref s_nextId);
+ Android.Widget.TextView _status = null!;
+ CancellationTokenSource _cts;
+
+ protected override void OnCreate(Android.OS.Bundle savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+ Microsoft.Maui.ApplicationModel.Platform.Init(this, savedInstanceState);
+
+ var layout = new Android.Widget.LinearLayout(this) { Orientation = Android.Widget.Orientation.Vertical };
+ var statusBarHeight = 0;
+ var resourceId = Resources.GetIdentifier("status_bar_height", "dimen", "android");
+ if (resourceId > 0)
+ {
+ statusBarHeight = Resources.GetDimensionPixelSize(resourceId);
+ }
+ layout.SetPadding(50, statusBarHeight + 50, 50, 50);
+
+ _status = new Android.Widget.TextView(this) { Text = $"Status: {Issue36523State.GetOutcome()}" };
+ SetAutomationId(_status, "RotationActivityStatusLabel");
+
+ var btn = new Android.Widget.Button(this) { Text = "Pick Photos" };
+ SetAutomationId(btn, "RotationActivityPickButton");
+ btn.Click += OnPick;
+
+ layout.AddView(_status);
+ layout.AddView(btn);
+ SetContentView(layout);
+ }
+
+ public override void OnWindowFocusChanged(bool hasFocus)
+ {
+ base.OnWindowFocusChanged(hasFocus);
+ _cts?.Cancel();
+ _cts?.Dispose();
+ _cts = null;
+ if (!hasFocus)
+ {
+ return;
+ }
+ _cts = new CancellationTokenSource();
+ _ = DelayedHangCheck(_cts.Token);
+ }
+
+ async Task DelayedHangCheck(CancellationToken ct)
+ {
+ try
+ {
+ await Task.Delay(3000, ct);
+ if (!IsDestroyed)
+ {
+ RunOnUiThread(() => { Issue36523State.CheckForHang(_id); _status.Text = $"Status: {Issue36523State.GetOutcome()}"; });
+ }
+ }
+ catch (OperationCanceledException) { }
+ }
+
+ async void OnPick(object s, EventArgs e)
+ {
+ _status.Text = "Status: WAITING";
+ try
+ {
+ var task = MediaPicker.PickPhotosAsync();
+ Issue36523State.BeginPick(_id, task);
+ var r = await task;
+ Issue36523State.Complete(r?.Count > 0 ? $"PASS: got {r.Count} photo(s)" : "PASS: cancelled");
+ }
+ catch (OperationCanceledException) { Issue36523State.Complete("PASS: cancelled"); }
+ catch (Exception ex) { Issue36523State.Complete($"ERROR: {ex.Message}"); }
+ if (!IsDestroyed)
+ {
+ _status.Text = $"Status: {Issue36523State.GetOutcome()}";
+ }
+ }
+
+ protected override void OnDestroy() { _cts?.Cancel(); _cts?.Dispose(); _cts = null; base.OnDestroy(); }
+
+ void SetAutomationId(Android.Views.View view, string id)
+ {
+ AndroidX.Core.View.ViewCompat.SetAccessibilityDelegate(view,
+ new IdDelegate($"{PackageName}:id/{id}"));
+ }
+
+ class IdDelegate(string name) : AndroidX.Core.View.AccessibilityDelegateCompat
+ {
+ public override void OnInitializeAccessibilityNodeInfo(Android.Views.View host,
+ AndroidX.Core.View.Accessibility.AccessibilityNodeInfoCompat info)
+ { base.OnInitializeAccessibilityNodeInfo(host, info); info.ViewIdResourceName = name; }
+ }
+}
+#endif
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36523.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36523.cs
new file mode 100644
index 0000000000..15b34b3bc5
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36523.cs
@@ -0,0 +1,66 @@
+#if ANDROID
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+public class Issue36523 : _IssuesUITest
+{
+ public Issue36523(TestDevice device) : base(device) { }
+
+ public override string Issue => "MediaPicker.PickPhotosAsync hangs after device rotation on API 33+";
+
+ [TearDown]
+ public void TearDown()
+ {
+ App.SetOrientationPortrait();
+ }
+
+ [Test]
+ [Category(UITestCategories.Essentials)]
+ public void PickPhotosAsyncShouldReturnAfterRotation()
+ {
+ // Bug only manifests on API 33+ where the native Photo Picker is used.
+ if (App is AppiumApp appiumApp)
+ {
+ var apiLevel = (long?)appiumApp.Driver.Capabilities.GetCapability("deviceApiLevel") ?? 0;
+ if (apiLevel < 33)
+ {
+ Assert.Ignore($"Issue #36523 only manifests on Android API 33+. Current device API: {apiLevel}.");
+ }
+ }
+
+ App.WaitForElement("OpenRotationActivityButton");
+ App.Tap("OpenRotationActivityButton");
+
+ App.WaitForElement("RotationActivityPickButton");
+ App.WaitForElement("RotationActivityStatusLabel");
+
+ App.Tap("RotationActivityPickButton");
+ Task.Delay(2000).Wait();
+
+ // Rotate while picker is open — triggers activity destroy/recreate
+ App.SetOrientationLandscape();
+ Task.Delay(2000).Wait();
+
+ // Cancel the picker
+ App.Back();
+
+ // With fix: task completes → "PASS". Without fix: task hangs → "FAIL".
+ var returned = App.WaitForTextToBePresentInElement("RotationActivityStatusLabel", "PASS",
+ timeout: TimeSpan.FromSeconds(30));
+
+ var resultText = App.FindElement("RotationActivityStatusLabel").GetText();
+
+ Assert.That(resultText, Does.Contain("PASS"),
+ $"PickPhotosAsync must complete after device rotation. Actual: '{resultText}'.");
+
+ Assert.That(resultText, Does.Not.Contain("FAIL"),
+ $"Picker task hung after activity recreation: '{resultText}'.");
+
+ App.Back();
+ App.WaitForElement("OpenRotationActivityButton");
+ }
+}
+#endif
kubaflo
left a comment
There was a problem hiding this comment.
Could you please check the ai's suggestions?
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
ℹ️ The review agent did not produce a full summary on this run (an infrastructure issue on the CI agent), but the deep UI tests completed — their results are below. Re-comment
/reviewfor a fresh full review.
⏭️ Deep UI tests — 0 passed, 0 failed across 1 category on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Essentials |
0/2 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
ℹ️ The review agent did not produce a full summary on this run (an infrastructure issue on the CI agent), but the deep UI tests completed — their results are below. Re-comment
/reviewfor a fresh full review.
⏭️ Deep UI tests — 0 passed, 0 failed across 1 category on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Essentials |
0/2 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
cd474ce to
0312161
Compare
5da0ae9 to
b1f3aba
Compare
Skill Validation Results
❌ Skill Validation Results —
|
b1f3aba to
19fe700
Compare
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
ℹ️ The review agent did not produce a full summary on this run (an infrastructure issue on the CI agent), but the deep UI tests completed — their results are below. Re-comment
/reviewfor a fresh full review.
⏭️ Deep UI tests — 0 passed, 0 failed across 1 category on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Essentials |
0/2 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
9942f14 to
a6e2d41
Compare
kubaflo
left a comment
There was a problem hiding this comment.
Could you please resolve conflicts?
19fe700 to
ffeb80f
Compare
ffeb80f to
f9a8772
Compare
I've resolved the conflicts. |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
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: Not ready - click to expandOverall verdict: Not ready. Of 216 distinct failures, 18 are deterministic regressions vs the base branch (
Coverage: 96 checks · 51 passing · 45 failing · 0 pending · 0 inaccessible · 1 unmapped · 65 unexplained build legs · 0 unaccounted failing checks · 1 aborted failing checks · 0 canceled-build checks · 3 device-test unverified · 198 unattributed · 18 regressed-vs-base. Deterministic ceiling: Not ready — 18 legs regressed vs base, 65 unexplained build legs, 198 unattributed failures, 1 aborted check, 3 device-test checks unverified, 1 unmapped check (Build Analysis). Builds (this PR): maui-pr 1532761, maui-pr-devicetests 1532783, maui-pr-uitests 1532782. Base sampling ( Recommended actionA human should investigate the 18 Visual failure comparisonsFull-resolution CI baseline, actual, and diff images are embedded below. They supplement the failure classification and do not change the deterministic verdict ceiling.
|
| CI baseline | Fresh PR actual | CI diff |
|---|---|---|
![]() |
![]() |
![]() |
VerifyShellSearch_CharacterSpacing - windows - Needs human investigation - visual comparison
CI reported 1.85% difference in build 1532782.
Relationship to PR: Needs human investigation - No decisive exact test-and-platform baseline attribution was available.
| CI baseline | Fresh PR actual | CI diff |
|---|---|---|
![]() |
![]() |
![]() |
VerifyShellSearch_CancelButtonColor - windows - Needs human investigation - visual comparison
CI reported 1.79% difference in build 1532782.
Relationship to PR: Needs human investigation - No decisive exact test-and-platform baseline attribution was available.
| CI baseline | Fresh PR actual | CI diff |
|---|---|---|
![]() |
![]() |
![]() |
VerifyShellSearch_TextColor - windows - Needs human investigation - visual comparison
CI reported 1.67% difference in build 1532782.
Relationship to PR: Needs human investigation - No decisive exact test-and-platform baseline attribution was available.
| CI baseline | Fresh PR actual | CI diff |
|---|---|---|
![]() |
![]() |
![]() |
VerifyShellSearch_TextTransformLowercase - windows - Needs human investigation - visual comparison
CI reported 1.79% difference in build 1532782.
Relationship to PR: Needs human investigation - No decisive exact test-and-platform baseline attribution was available.
| CI baseline | Fresh PR actual | CI diff |
|---|---|---|
![]() |
![]() |
![]() |
VerifyShellSearch_SearchBoxVisibilityExpanded - windows - Needs human investigation - visual comparison
CI reported 1.46% difference in build 1532782.
Relationship to PR: Needs human investigation - No decisive exact test-and-platform baseline attribution was available.
| CI baseline | Fresh PR actual | CI diff |
|---|---|---|
![]() |
![]() |
![]() |
VerifyShellSearch_SearchBoxVisibilityCollapsible - windows - Needs human investigation - visual comparison
CI reported 1.48% difference in build 1532782.
Relationship to PR: Needs human investigation - No decisive exact test-and-platform baseline attribution was available.
| CI baseline | Fresh PR actual | CI diff |
|---|---|---|
![]() |
![]() |
![]() |
VerifyShellSearch_HorizontalTextAlignmentStart - windows - Needs human investigation - visual comparison
CI reported 1.35% difference in build 1532782.
Relationship to PR: Needs human investigation - No decisive exact test-and-platform baseline attribution was available.
| CI baseline | Fresh PR actual | CI diff |
|---|---|---|
![]() |
![]() |
![]() |
VerifyShellSearch_HorizontalTextAlignmentEnd - windows - Needs human investigation - visual comparison
CI reported 1.19% difference in build 1532782.
Relationship to PR: Needs human investigation - No decisive exact test-and-platform baseline attribution was available.
| CI baseline | Fresh PR actual | CI diff |
|---|---|---|
![]() |
![]() |
![]() |
VerifyShellSearch_HorizontalTextAlignmentCenter - windows - Needs human investigation - visual comparison
CI reported 1.18% difference in build 1532782.
Relationship to PR: Needs human investigation - No decisive exact test-and-platform baseline attribution was available.
| CI baseline | Fresh PR actual | CI diff |
|---|---|---|
![]() |
![]() |
![]() |
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
ℹ️ The review agent did not produce a full summary on this run (an infrastructure issue on the CI agent), but the deep UI tests completed — their results are below. Re-comment
/reviewfor a fresh full review.
⏭️ Deep UI tests — 0 passed, 0 failed across 1 category on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Essentials |
0/2 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
…o space left on device' Build 14843877 (#36767 android) red-failed the ReviewPR/gate stage at 'Build MSBuild Tasks' with 'No space left on device' — pure agent infrastructure, not the PR. The existing 'Free Disk Space for Emulator' cleanup reclaims the same hosted-tool caches but runs LATER (after this build, right before the emulator), so it could not prevent the build from exhausting the disk. Add an android-only pre-build cleanup step (mirrors the proven emulator cleanup) right after 'Verify .dotnet' and before 'Build MSBuild Tasks', reclaiming the unused Linux hosted-image caches (/usr/share/dotnet, hostedtoolcache CodeQL/go/ Python, swift, powershell, chromium) up front. Best-effort + non-blocking so cleanup can never itself fail the review. The build uses ./.dotnet, never the system /usr/share/dotnet (already proven safe by the later cleanup). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
…stop hitting NETSDK1147 Root-caused the systemic android NETSDK1147 gate-infra flake (build 14843829 / #36572, and many prior). Sequence: 'Install .NET and workloads' installs the android/ios/maccatalyst workload into ./.dotnet; then 'Build MSBuild Tasks' hits the corrupt-SDK failure (unloadable System.IO.Pipes.dll, exit 134) and, on its retry, WIPES ./.dotnet to reinstall a clean SDK. That wipe also removes the platform WORKLOAD, and the buildtasks reinstall (dotnet-buildtasks) restores only the SDK — so the gate's later test build fails with NETSDK1147 ('the following workloads must be installed: android'). That is pure GATE INFRASTRUCTURE, not the PR, but it surfaced as an ENV/INCONCLUSIVE gate. Fix (targeted, band-correct, idempotent): - When the buildtasks step wipes ./.dotnet, drop a PERSISTENT marker (dotnet-wiped-needs-workloads) that — unlike $wipeMarker — is not cleared on success. - Add a 'Restore .NET workloads if .dotnet was wiped' step right after Build MSBuild Tasks that, ONLY when the marker is present, re-runs the same proven './build.ps1 --target=dotnet' workload install (repo's pinned manifest, never a mismatched band) and clears the marker. The common no-wipe path pays nothing. Also commit 9cd0ebe: free disk space BEFORE the build (android) so the gate build stops hitting 'No space left on device' (#36767) — the other our-infra ENV gate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 15d2af20-e4ab-4e88-9011-cfbd83513bc0
…created while Photo Picker is open (#36767) <!-- 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: - MediaPicker.PickPhotosAsync() hangs forever when the device is rotated while the photo picker is open. ### Root Cause of the issue **Regression:** - Introduced by PR #35944, which changed ActivityForResultRequest from a single shared TaskCompletionSource to per-activity storage using ConditionalWeakTable<ComponentActivity, TCS> . Reproduced on API 33 and 36, not on API 30 (different code path). - After rotation, Android destroys Activity-1 and creates Activity-2. The TCS is stored under Activity-1's key, but Activity-2's callback looks up Activity-2's key → miss → result silently dropped → task hangs permanently. Additionally, ConditionalWeakTable uses weak keys, so the GC can silently collect Activity-1's entry (including the TCS) with no error. ### Description of Change **Bug fix for activity recreation and pending requests:** * Added a strong reference (`_inFlightActivity`) to the launching `ComponentActivity` within `ActivityForResultRequest` to prevent garbage collection and ensure that pending requests survive configuration changes such as device rotation. * Implemented `MigratePendingRequests`, which transfers any pending `TaskCompletionSource` from the old activity to the new one during a configuration change, preventing hangs when the activity is recreated. This migration is triggered in `Register` and ensures the result callback can still complete the task. * Updated code paths in `Launch` and the activity result callback to clear `_inFlightActivity` when the request completes, is canceled, or fails, ensuring proper cleanup and preventing memory leaks. **Test coverage:** * Added a new shared test case and UI test (`Issue36523`) that reproduces the rotation scenario and verifies that `PickPhotosAsync` completes (rather than hanging) after device rotation and picker cancellation. ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #36523 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <img src="https://github.com/user-attachments/assets/b054d308-c65e-4cb0-ba84-0a06cf5268e7"> | <img src="https://github.com/user-attachments/assets/c819e6e5-3b14-4a6d-b7e1-b5230c694d71"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. -->
…created while Photo Picker is open (#36767) <!-- 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: - MediaPicker.PickPhotosAsync() hangs forever when the device is rotated while the photo picker is open. ### Root Cause of the issue **Regression:** - Introduced by PR #35944, which changed ActivityForResultRequest from a single shared TaskCompletionSource to per-activity storage using ConditionalWeakTable<ComponentActivity, TCS> . Reproduced on API 33 and 36, not on API 30 (different code path). - After rotation, Android destroys Activity-1 and creates Activity-2. The TCS is stored under Activity-1's key, but Activity-2's callback looks up Activity-2's key → miss → result silently dropped → task hangs permanently. Additionally, ConditionalWeakTable uses weak keys, so the GC can silently collect Activity-1's entry (including the TCS) with no error. ### Description of Change **Bug fix for activity recreation and pending requests:** * Added a strong reference (`_inFlightActivity`) to the launching `ComponentActivity` within `ActivityForResultRequest` to prevent garbage collection and ensure that pending requests survive configuration changes such as device rotation. * Implemented `MigratePendingRequests`, which transfers any pending `TaskCompletionSource` from the old activity to the new one during a configuration change, preventing hangs when the activity is recreated. This migration is triggered in `Register` and ensures the result callback can still complete the task. * Updated code paths in `Launch` and the activity result callback to clear `_inFlightActivity` when the request completes, is canceled, or fails, ensuring proper cleanup and preventing memory leaks. **Test coverage:** * Added a new shared test case and UI test (`Issue36523`) that reproduces the rotation scenario and verifies that `PickPhotosAsync` completes (rather than hanging) after device rotation and picker cancellation. ### Issues Fixed <!-- Please make sure that there is a bug logged for the issue being fixed. The bug should describe the problem and how to reproduce it. --> Fixes #36523 ### Tested the behavior in the following platforms - [ ] Windows - [x] Android - [ ] iOS - [ ] Mac ### Output | Before | After | |----------|----------| | <img src="https://github.com/user-attachments/assets/b054d308-c65e-4cb0-ba84-0a06cf5268e7"> | <img src="https://github.com/user-attachments/assets/c819e6e5-3b14-4a6d-b7e1-b5230c694d71"> | <!-- Are you targeting main? All PRs should target the main branch unless otherwise noted. -->

































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:
Root Cause of the issue
Regression:
Description of Change
Bug fix for activity recreation and pending requests:
_inFlightActivity) to the launchingComponentActivitywithinActivityForResultRequestto prevent garbage collection and ensure that pending requests survive configuration changes such as device rotation.MigratePendingRequests, which transfers any pendingTaskCompletionSourcefrom the old activity to the new one during a configuration change, preventing hangs when the activity is recreated. This migration is triggered inRegisterand ensures the result callback can still complete the task.Launchand the activity result callback to clear_inFlightActivitywhen the request completes, is canceled, or fails, ensuring proper cleanup and preventing memory leaks.Test coverage:
Issue36523) that reproduces the rotation scenario and verifies thatPickPhotosAsynccompletes (rather than hanging) after device rotation and picker cancellation.Issues Fixed
Fixes #36523
Tested the behavior in the following platforms
Output