Skip to content

[Android] MediaPicker: Fix photo picker completion from child activities - #35944

Merged
kubaflo merged 10 commits into
dotnet:inflight/currentfrom
KarthikRajaKalaimani:fix-35826
Jul 9, 2026
Merged

[Android] MediaPicker: Fix photo picker completion from child activities#35944
kubaflo merged 10 commits into
dotnet:inflight/currentfrom
KarthikRajaKalaimani:fix-35826

Conversation

@KarthikRajaKalaimani

Copy link
Copy Markdown
Contributor

Note

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

Issue Details:

MediaPicker - PickPhotosAsync doesn't return on Android when using multiple Activities

Root Cause:

Commit 0c25c62 introduced a guard in ActivityForResultRequest.Register() that checked whether an existing ActivityResultLauncher was already registered for a still-alive activity. If one existed, the method returned early without registering a new launcher. This guard was designed to prevent duplicate registrations (fixing issue #32845), but it had an unintended side effect: when a child AppCompatActivity called Platform.Init(), the guard saw the main activity's launcher as still alive and silently skipped registration entirely. As a result, the child activity had no ActivityResultLauncher of its own. On Android API 36, launcher ownership is strictly enforced — the system only delivers an ActivityResult to the launcher that belongs to the activity that launched the intent. Since the child activity had no launcher, PickPhotosAsync() launched the photo picker but its TaskCompletionSource was never resolved, causing the await to hang indefinitely.

Description of Change:

The fix replaces the single global launcher slot and its guard with a ConditionalWeakTable<ComponentActivity, ActivityResultLauncher>. This table stores one launcher entry per activity instance. Because ConditionalWeakTable uses weak keys, entries are automatically eligible for garbage collection when the activity is destroyed, preventing memory leaks. The Register() method now checks whether the specific calling activity already has an entry (idempotent per-instance), and if not, creates and stores a new launcher for it. At call time, GetLauncherForCurrentActivity() resolves the current foreground activity and retrieves its entry from the table. This ensures every activity — whether the main activity or any child — gets its own launcher, fixing the hang while also preserving the original #32845 fix since no activity can overwrite another's entry.

Tested the behavior in the following platforms:

  • Android
  • Windows
  • iOS
  • Mac

Reference:

N/A

Issues Fixed:

Fixes #35826

Screenshots

Before After
Before_35826.mov
After_35826.mov

KarthikRajaKalaimani and others added 6 commits June 11, 2026 11:14
…ity launcher registry

Commit 0c25c62 introduced a guard in ActivityForResultRequest.Register() that
returns early when any still-alive activity already holds a registration. This
prevented child activities from ever receiving their own ActivityResultLauncher,
so MediaPicker.PickPhotosAsync() called from a child activity hung indefinitely
on Android API 36 (task completion source is never resolved).

Fix: Replace the single shared launcher + WeakReference guard with a
ConditionalWeakTable<ComponentActivity, ActivityResultLauncher> that maps one
launcher per activity instance. Register() is now idempotent per-activity
(safe to call again after a config change) but never blocks a different
activity from registering. Launch() resolves the launcher for the current
foreground activity at call time via ActivityStateManager.GetCurrentActivity().

This also preserves the fix for dotnet#32845: each activity owns its entry in the
table so a temporary ComponentActivity can no longer overwrite the main
activity's launcher.

Adds:
- UI test (Issue35826) that opens a child AppCompatActivity, triggers
  PickPhotosAsync, cancels, and asserts the result is delivered (not hung).
  Test skips on Android API < 36 where the hang is not reproducible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 35944

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 35944"

@dotnet-policy-service dotnet-policy-service Bot added the community ✨ Community Contribution label Jun 16, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey there @@KarthikRajaKalaimani! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@dotnet-policy-service dotnet-policy-service Bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Jun 16, 2026
@github-actions github-actions Bot added area-essentials Essentials: Device, Display, Connectivity, Secure Storage, Sensors, App Info platform/android labels Jun 16, 2026
@kubaflo

kubaflo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

/review -b feature/enhanced-reviewer -p android

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jun 16, 2026
@MauiBot MauiBot added s/agent-fix-win AI found a better alternative fix than the PR s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Jun 16, 2026
MauiBot

This comment was marked as outdated.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jun 16, 2026
@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jun 21, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 3 findings

See inline comments for details.

Comment thread src/Essentials/src/Platform/ActivityForResultRequest.android.cs Outdated
Comment thread src/Essentials/src/Platform/ActivityStateManager.android.cs Outdated
// TaskCompletionSource is never resolved. With the fix it updates promptly.
// Also accept "Error" as a valid outcome: what matters is that the call returns
// (doesn't hang), not the specific result — emulators may not have a photo picker.
var returned = App.WaitForTextToBePresentInElement(ChildActivityResultLabel, "Cancelled",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[moderate] Regression Prevention — The regression test accepts any Error result as success and skips below API 36, so it does not prove the PR's photo-picker lifecycle path returns the expected cancellation result. A broken launcher path that immediately throws/cancels changes the label to Error and still passes. Assert the expected Cancelled outcome on a device/API where the picker is available, and use a separate assumption/skip for environments without a photo picker.

@MauiBot MauiBot added s/agent-gate-failed AI could not verify tests catch the bug s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR labels Jun 21, 2026
MauiBot

This comment was marked as outdated.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jun 22, 2026
@KarthikRajaKalaimani KarthikRajaKalaimani changed the title Fix MediaPicker - PickPhotosAsync doesn't return on Android when using multiple Activities [Android] MediaPicker: Fix photo picker completion from child activities Jun 22, 2026
@KarthikRajaKalaimani

Copy link
Copy Markdown
Contributor Author

Addressed Ai summary concerns.

@kubaflo
kubaflo marked this pull request as ready for review June 22, 2026 13:33

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 4 findings

See inline comments for details.

var registeredActivity = componentActivity;
var callback = new ActivityResultCallback<TResult>(result =>
{
if (_pendingRequests.TryGetValue(registeredActivity, out var tcs))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Android Platform Specifics / Async and Threading Safety — The pending request is keyed by the ComponentActivity instance captured when the launcher was registered. On a configuration change while the photo picker is open, AndroidX restores the ActivityResultRegistry state into the recreated activity and delivers the result to the new registration callback, not to the destroyed activity instance. That callback will look up _pendingRequests with the new registeredActivity, while the awaiting TCS was stored under the old activity in Launch(launchingActivity, ...), so the result is dropped and the original PickPhotosAsync can hang again. The pending operation needs to survive recreation by ActivityResultRegistry key/request identity rather than by activity object identity, or otherwise explicitly cancel/complete the old request during recreation.

// 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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[major] Async and Threading Safety — Replacing an existing pending request for the same activity cancels the first caller while its Android picker launch is still in flight, then reuses the same ActivityResultLauncher for the second caller. If two PickPhotosAsync calls overlap (for example a double tap/programmatic retry), the first result can complete the second TCS or be dropped after the first TCS was already canceled. This branch also does not address process death or rotation: those create a different activity/process and therefore will not find the old entry by the same ComponentActivity key. Prefer rejecting/serializing overlapping launches before starting another picker, and handle orphan cleanup through lifecycle/result ownership instead of cancel-and-replace on the same launcher.

if (App is AppiumApp appiumApp)
{
var apiLevel = (long?)appiumApp.Driver.Capabilities.GetCapability("deviceApiLevel") ?? 0;
if (apiLevel < 36)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Regression Prevention — Test Coverage — The regression test is skipped on every Android device below API 36. Because this PR changes shared ActivityResultLauncher/TCS ownership for all Android photo-picker calls, and the available PR environment does not reproduce/run this API 36-only path, the test will not catch regressions in the gate that validates the PR. Please either add coverage that exercises the ownership bug on a CI-supported API level, add an API 36 lane for this test, or document the remaining coverage gap explicitly.


[Test]
[Category(UITestCategories.Essentials)]
public void PickPhotosAsyncShouldReturnFromChildActivity()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Regression Prevention — Test Coverage — This test only covers the child-activity cancel path. The fix also changes adjacent high-risk behavior for activity recreation while a picker is open and overlapping launches from the same activity, but neither scenario is exercised here. Those are the places where the new per-activity pending-request table is most likely to strand or misroute a result, so they need regression coverage or an explicit acknowledged limitation before this plumbing change is safe to merge.

@MauiBot MauiBot added s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-win AI found a better alternative fix than the PR labels Jul 2, 2026
MauiBot

This comment was marked as outdated.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jul 2, 2026

@kubaflo kubaflo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you please check the ai's suggestions?

@kubaflo

This comment has been minimized.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Jul 9, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Expert Review — 3 findings

See inline comments for details.

var registeredActivity = componentActivity;
var callback = new ActivityResultCallback<TResult>(result =>
{
if (_pendingRequests.TryGetValue(registeredActivity, out var tcs))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[critical] Layout/Lifecycle Logic and Correctness — Async and Threading Safety — The rotation invariant claimed in the type doc comment (lines 30-36: "delivery therefore does NOT depend on whichever activity is current at delivery time") does not hold, and this reintroduces the exact hang this PR is fixing, triggered by device rotation instead of a child activity.

Concrete scenario: Activity A calls PickPhotosAsync(), which adds a pending TCS keyed by A (_pendingRequests.Add(launchingActivity, tcs) at line 149) and calls launcher.Launch(input) on A's launcher. The user rotates the device while the system photo picker is open. Android destroys A with IsFinishing == false (config change), so ActivityStateManager.android.cs correctly skips CancelPendingRequest. Android recreates the activity as instance B, whose OnCreate calls Platform.Init(B, bundle)Register(B), which registers a new ActivityResultLauncher/callback closing over B (this method, registeredActivity = B).

AndroidX's ActivityResultRegistry delivers the eventual result to whichever callback is registered for the same restored key at delivery time — i.e., the newly registered callback for B, not the original callback captured for A (this is documented AndroidX behavior: registerForActivityResult must be re-invoked identically every onCreate, and the framework rebinds delivery to the latest registration). When that fires, this code does _pendingRequests.TryGetValue(registeredActivity=B, out var tcs), but the actual pending entry is stored under key A (never migrated/re-keyed to B). The lookup fails silently, tcs is never resolved, and MediaPicker.PickPhotosAsync() hangs forever after any rotation while the picker is open.

This needs either: (1) transferring/re-keying the pending request from the old activity instance to the newly-registered one in Register(), or (2) resolving/cancelling the pending request on the old activity in OnActivityDestroyed even for non-finishing (config-change) destroys and having the caller re-launch after recreation, or (3) tracking pending requests by a stable identifier (e.g., a saved-instance-state token) instead of the transient ComponentActivity instance.

[Test]
[Category(UITestCategories.Essentials)]
public void PickPhotosAsyncShouldReturnFromChildActivity()
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[moderate] Regression Prevention and Test Coverage — This is the only regression test added for this fix, and it covers just the child-activity launch scenario; it does not cover the config-change/rotation-while-picker-is-open scenario (see the critical finding on ActivityForResultRequest.android.cs:86), nor concurrent picker requests from two activities, which the PR's own doc comments explicitly claim to have solved. In addition, the test unconditionally Assert.Ignores (lines 28-30) on any device below API 36, so on CI lanes running API 30 emulator images (referenced elsewhere in this test suite, e.g. UITest.cs), this test provides zero signal and will always report as ignored rather than passed or failed — masking the true regression-coverage gap. Please add a rotation-during-picker regression test (or explicitly document why it cannot be automated) and consider whether the API-gate can be narrowed rather than skipping the whole scenario.

/// currently focused activity.
/// </summary>
protected bool IsRegistered => launcher is not null;
protected bool HasLauncherForCurrentActivity => GetLauncherForCurrentActivity() is not null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 AI-Generated Review (multi-model)

[minor] Complexity Reduction — Dead CodeHasLauncherForCurrentActivity and its backing GetLauncherForCurrentActivity() (line 191) are no longer referenced anywhere after this change; all call sites now use the explicit Launch(ComponentActivity, T) overload and _activityLaunchers/_pendingRequests lookups keyed by an explicit activity. Consider removing both members, or if they're kept intentionally for external/future use, note why since they add unused surface area to an internal type.

@MauiBot MauiBot added s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) and removed s/agent-gate-failed AI could not verify tests catch the bug labels Jul 9, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI Review Summary

@KarthikRajaKalaimani — new AI review results are available based on this last commit: d5a1ae0. To request a fresh review after new comments or commits, comment /review rerun.

Gate Passed Confidence Low Platform Android


🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix

Gate Result: ✅ PASSED

Platform: ANDROID · Base: main · Merge base: 5535b43c

Test Without Fix (expect FAIL) With Fix (expect PASS)
🖥️ Issue35826 Issue35826 ✅ FAIL — 1065s ✅ PASS — 792s
🔴 Without fix — 🖥️ Issue35826: FAIL ✅ · 1065s

(truncated to last 15,000 chars)

in/Controls.Core/Debug/net10.0-android36.0/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Maps.dll
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Foldable.dll
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Xaml.dll
  Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-android36.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Controls.TestCases.HostApp -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Controls.TestCases.HostApp.dll
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Maps.dll
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Foldable.dll
  Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Xaml.dll
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Maps.dll

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

Time Elapsed 00:09:52.64
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Starting: Intent { act=android.settings.SETTINGS }
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
  Determining projects to restore...
  Restored /home/vsts/work/1/s/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj (in 1.26 sec).
  Restored /home/vsts/work/1/s/src/TestUtils/src/VisualTestUtils/VisualTestUtils.csproj (in 7 ms).
  Restored /home/vsts/work/1/s/src/TestUtils/src/VisualTestUtils.MagickNet/VisualTestUtils.MagickNet.csproj (in 7.76 sec).
  Restored /home/vsts/work/1/s/src/Controls/tests/TestCases.Android.Tests/Controls.TestCases.Android.Tests.csproj (in 9.25 sec).
  Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.Core/UITest.Core.csproj (in 2 ms).
  Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.Appium/UITest.Appium.csproj (in 2 ms).
  Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.NUnit/UITest.NUnit.csproj (in 347 ms).
  Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.Analyzers/UITest.Analyzers.csproj (in 2.69 sec).
  5 of 13 projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  Controls.CustomAttributes -> /home/vsts/work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.90-ci+azdo.14611549
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
  UITest.Core -> /home/vsts/work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
  VisualTestUtils -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
  UITest.NUnit -> /home/vsts/work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
  VisualTestUtils.MagickNet -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
  UITest.Appium -> /home/vsts/work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
  UITest.Analyzers -> /home/vsts/work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
  Controls.TestCases.Android.Tests -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
Test run for /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
�[38;5;207m[d0c36728]�[0m�[38;5;160m[Logcat]�[0m Logcat terminated with code 1, signal null
�[38;5;118m[8f7946c5]�[0m�[38;5;160m[Logcat]�[0m Logcat terminated with code 1, signal null
�[38;5;50m[29b753b0]�[0m�[38;5;160m[Logcat]�[0m Logcat terminated with code 1, signal null
�[38;5;18m[9ca2050d]�[0m�[38;5;160m[Logcat]�[0m Logcat terminated with code 1, signal null
�[38;5;211m[6390a802]�[0m�[38;5;160m[Logcat]�[0m Logcat terminated with code 1, signal null
�[38;5;123m[df4cc863]�[0m�[38;5;160m[Logcat]�[0m Logcat terminated with code 1, signal null
�[38;5;58m[c7512fb8]�[0m�[38;5;160m[Logcat]�[0m Logcat terminated with code 1, signal null
�[38;5;76m[46130486]�[0m�[38;5;160m[Logcat]�[0m Logcat terminated with code 1, signal null
�[38;5;55m[e8034ea5]�[0m�[38;5;160m[Logcat]�[0m Logcat terminated with code 1, signal null
�[38;5;23m[ea5092a2]�[0m�[38;5;160m[Logcat]�[0m Logcat terminated with code 1, signal null
>>>>> 07/09/2026 15:20:30 The SaveDeviceDiagnosticInfo threw an exception during Issue35826(Android).
Exception details: System.InvalidOperationException: Call InitialSetup before accessing the App property.
   at UITest.Appium.NUnit.UITestContextBase.get_App() in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 32
   at UITest.Appium.NUnit.UITestBase.SaveDeviceDiagnosticInfo(String note, Boolean storeForReattachment) in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 255
TearDown failed for test fixture Microsoft.Maui.TestCases.Tests.Issues.Issue35826(Android)
OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 install -r --no-incremental /home/vsts/work/1/s/.appium/node_modules/appium-uiautomator2-driver/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-v7.4.1.apk' timed out after 20000ms'. Try to increase the 20000ms adb execution timeout represented by 'uiautomator2ServerInstallTimeout' capability
TearDown : System.InvalidOperationException : Call InitialSetup before accessing the App property.
StackTrace:    at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
   at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.WebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
   at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions)
   at OpenQA.Selenium.Appium.Android.AndroidDriver..ctor(Uri remoteAddress, DriverOptions driverOptions)
   at UITest.Appium.AppiumAndroidApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 11
   at UITest.Appium.AppiumAndroidApp.CreateAndroidApp(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 41
   at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 42
   at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
   at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
   at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
--TearDown
   at UITest.Appium.NUnit.UITestContextBase.get_App() in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 32
   at UITest.Appium.NUnit.UITestBase.OneTimeTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 244
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
NUnit Adapter 4.5.0.0: Test execution complete
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.12]   Discovering: Controls.TestCases.Android.Tests
  Failed PickPhotosAsyncShouldReturnFromChildActivity [4 m 28 s]
  Error Message:
   OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: Error executing adbExec. Original error: 'Command '/usr/local/lib/android/sdk/platform-tools/adb -P 5037 -s emulator-5554 install -r --no-incremental /home/vsts/work/1/s/.appium/node_modules/appium-uiautomator2-driver/node_modules/appium-uiautomator2-server/apks/appium-uiautomator2-server-v7.4.1.apk' timed out after 20000ms'. Try to increase the 20000ms adb execution timeout represented by 'uiautomator2ServerInstallTimeout' capability
  Stack Trace:
     at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
   at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.WebDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.Appium.AppiumDriver.Execute(String driverCommandToExecute, Dictionary`2 parameters)
   at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
   at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout, AppiumClientConfig clientConfig)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions, TimeSpan commandTimeout)
   at OpenQA.Selenium.Appium.AppiumDriver..ctor(Uri remoteAddress, ICapabilities appiumOptions)
   at OpenQA.Selenium.Appium.Android.AndroidDriver..ctor(Uri remoteAddress, DriverOptions driverOptions)
   at UITest.Appium.AppiumAndroidApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 11
   at UITest.Appium.AppiumAndroidApp.CreateAndroidApp(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs:line 41
   at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 42
   at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
   at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
   at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

[xUnit.net 00:00:00.65]   Discovered:  Controls.TestCases.Android.Tests
Results File: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue35826.trx

Total tests: 1
     Failed: 1
Test Run Failed.
 Total time: 4.6461 Minutes
>>> TRX_RESULT_FILE: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue35826.trx

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

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

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

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
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/09/2026 15:33:31 FixtureSetup for Issue35826(Android)
>>>>> 07/09/2026 15:33:34 PickPhotosAsyncShouldReturnFromChildActivity Start
>>>>> 07/09/2026 15:33:34 PickPhotosAsyncShouldReturnFromChildActivity Stop
PickPhotosAsyncShouldReturnFromChildActivity: Issue #35826 only manifests on Android API 36+. Current device API: 30.
  Skipped PickPhotosAsyncShouldReturnFromChildActivity [1 s]
NUnit Adapter 4.5.0.0: Test execution complete
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.16]   Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.65]   Discovered:  Controls.TestCases.Android.Tests
Results File: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue35826.trx

Test Run Successful.
Total tests: 1
    Skipped: 1
 Total time: 24.9861 Seconds
>>> TRX_RESULT_FILE: /home/vsts/work/1/s/CustomAgentLogsTmp/UITests/TestResults/Issue35826.trx

📁 Fix files reverted (3 files)
  • src/Essentials/src/MediaPicker/MediaPicker.android.cs
  • src/Essentials/src/Platform/ActivityForResultRequest.android.cs
  • 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).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
Essentials 0/1 ✓
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)

📋 Pre-Flight — Context & Validation

Issue: #35826 - PickPhotosAsync hangs when called from a child activity
PR: #35944 - Android MediaPicker child-activity ActivityResult fix
Platforms Affected: Android
Files Changed: 3 implementation, 2 test

Key Findings

  • GitHub metadata/comments/reviews were unavailable because gh is unauthenticated; local branch pr-review-35944 has squashed PR commit 4c4f58aa7e on base 5535b43c.
  • PR implementation changes Android Essentials ActivityForResultRequest, ActivityStateManager, and MediaPicker to register and launch photo-picker contracts per ComponentActivity.
  • Regression coverage is Android UI test Issue35826 in category Essentials; prior gate says without-fix failed and with-fix passed, so gate was not re-run.
  • Existing PR fix approach: ConditionalWeakTable<ComponentActivity, ActivityResultLauncher> plus per-activity pending TCS, explicit Launch(ComponentActivity, input), and finish-time cancellation.

Code Review Summary

Verdict: NEEDS_CHANGES
Confidence: low
Errors: 1 | Warnings: 0 | Suggestions: 0

Key code review findings:

  • ActivityForResultRequest.android.cs:86 / :149: pending requests are keyed by the launching activity instance, but AndroidX may deliver a result to the recreated activity callback after configuration change. Because OnActivityDestroyed intentionally does not cancel non-finishing destroys, rotation during picker can leave the original task pending.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #35944 Per-activity launcher and pending-request maps keyed by ComponentActivity, with explicit launching activity passed from MediaPicker. ✅ PASSED (Gate) MediaPicker.android.cs, ActivityForResultRequest.android.cs, ActivityStateManager.android.cs Original PR; code review found a configuration-change gap.

🔬 Code Review — Deep Analysis

Code Review — PR #35944

Independent Assessment

What this changes: Reworks Android photo-picker ActivityResultLauncher storage from one global launcher/TCS to per-ComponentActivity launchers and pending requests, updates MediaPicker to launch against the current activity, cancels pending picker requests when an activity is finishing, and adds an Android UI regression test for child activities.

Inferred motivation: Fix PickPhotosAsync hanging when invoked from a child ComponentActivity because the existing registration guard kept only the main activity’s launcher.

Reconciliation with PR Narrative

Author claims: PR metadata was unavailable because gh is unauthenticated. Local code/test comments claim this fixes child-activity photo picker hangs on Android API 36+.
Agreement/disagreement: The child-activity motivation matches the code. However, the implementation introduces an unresolved configuration-change result-routing hole.

Prior Review Reconciliation

Prior ❌ Error Finding Source Status Evidence
Prior review data unavailable gh reviews / inline comments / issue comments Unknown All GitHub CLI metadata calls failed with gh auth login requirement.

Blast Radius Assessment

  • Runs for all instances: Partly. Registration runs for every Android ComponentActivity passed through Platform.Init when photo picker is available; launch behavior affects MediaPicker photo/video picker calls.
  • Startup impact: Yes, launcher registration happens during activity initialization.
  • Static/shared state: Yes, singleton picker request objects now hold per-activity activity-result state.

CI Status

  • Required-check result: unavailable
  • Classification: undetermined
  • Action taken: GitHub CLI unavailable; continued local-only per instruction and capped overall confidence.

Findings

❌ Error — Photo picker can still hang across configuration changes

src/Essentials/src/Platform/ActivityForResultRequest.android.cs:86

Pending requests are keyed by the specific ComponentActivity instance used at launch (_pendingRequests.Add(launchingActivity, tcs) at line 149), and the result callback resolves using the activity instance captured during Register (line 86).

Failure scenario:

  1. PickPhotosAsync() launches from Activity A.
  2. Device rotates while Android’s photo picker is open.
  3. Activity A is destroyed with IsFinishing == false, so ActivityStateManager.android.cs:217 intentionally does not cancel the pending request.
  4. Activity B is recreated and registers a new launcher/callback.
  5. AndroidX activity-result delivery after recreation is delivered to the newly registered callback.
  6. That callback looks up _pendingRequests using Activity B, but the pending TCS is stored under Activity A, so the result is dropped and the original task remains pending.

This contradicts the new comments claiming rotation/configuration changes are preserved and reintroduces a picker hang.

Failure-Mode Probing

  • Child activity without rotation: likely fixed because each child activity gets its own launcher and pending request.
  • Activity destroyed by Back/Finish: pending request is canceled when IsFinishing is true.
  • Rotation/config change during picker: unresolved; pending request remains under the old activity while result delivery can occur through the recreated activity.
  • Concurrent requests from different activities: improved versus prior global TCS, assuming no recreation.

Verdict: NEEDS_CHANGES

Confidence: low overall due unavailable CI/prior-review metadata; the code finding itself is high-confidence.
Summary: The PR addresses the child-activity launcher issue but leaves a concrete Android lifecycle hang during configuration changes. The pending request must be keyed/transferred across recreation or canceled instead of left pending.


🛠️ Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix Activity-scoped launcher list with one shared pending TCS per request type. ❌ Rejected after review; build PASS, UI runner PASS/SKIPPED on API 30 1 file Expert found cross-activity TCS clobbering, missing finish cancellation, stale retention.
2 try-fix Per-registration launcher/TCS with same-type config-change pending transfer and finish cancellation. ✅ Available tests PASS; API 36 body not exercised locally 2 files Final expert review found no high-confidence blockers; better by lifecycle reasoning than PR fix for rotation.
PR PR #35944 ConditionalWeakTable per-activity launcher/TCS maps, explicit Launch(ComponentActivity, input), finish cancellation. ✅ PASSED (Gate) 3 files Original PR; pre-flight code review found config-change result-routing risk.

Cross-Pollination

Model Round New Ideas? Details
maui-expert-reviewer 1 Yes Suggested activity-scoped registration objects, direct ActivityResultRegistry lifecycle registration, and proxy/intermediate activity ownership.
maui-expert-reviewer 2 Yes Rejected try-fix-1 shared-TCS approach; recommended preserving per-activity pending ownership and finish cancellation.
maui-expert-reviewer 3 Yes Rejected initial try-fix-2 retention/transfer gaps; revised to remove transferred registrations and scope transfer to same activity type.
maui-expert-reviewer 4 No Final review found no high-confidence blockers in revised try-fix-2. Remaining alternatives (raw registry/proxy activity) are more invasive and not clearly better.

Exhausted: Yes — meaningfully different lower-risk approaches were explored; remaining alternatives are materially more invasive and not justified without API 36 empirical evidence.
Selected Fix: Candidate #2 — structurally better than the PR fix for configuration-change result delivery while preserving per-activity pending ownership. Selection is conditional because local Android emulator was API 30, so the API 36+ regression body was skipped.


📝 Recommended PR Title & Description

Assessment: ✏️ Recommend updating — the current title is good, but the description specifically describes the raw ConditionalWeakTable<ComponentActivity, ActivityResultLauncher> implementation and would be stale if the winning pr-plus-reviewer lifecycle fix is adopted.

Recommended title

[Android] MediaPicker: Fix photo picker completion from child activities

Recommended description

### Issue Details:

MediaPicker - PickPhotosAsync doesn't return on Android when using multiple Activities.

### Root Cause:

Commit [0c25c62](https://github.com/dotnet/maui/commit/0c25c624659182fb3e14ef4693a3c5c3ae43cc08) introduced a guard in ActivityForResultRequest.Register() that checked whether an existing ActivityResultLauncher was already registered for a still-alive activity. If one existed, the method returned early without registering a new launcher. This guard was designed to prevent duplicate registrations (fixing issue #32845), but it had an unintended side effect: when a child AppCompatActivity called Platform.Init(), the guard saw the main activity's launcher as still alive and silently skipped registration entirely. As a result, the child activity had no ActivityResultLauncher of its own. On Android API 36, launcher ownership is strictly enforced — the system only delivers an ActivityResult to the launcher that belongs to the activity that launched the intent. Since the child activity had no launcher, PickPhotosAsync() launched the photo picker but its TaskCompletionSource was never resolved, causing the await to hang indefinitely.

### Description of Change:

The fix replaces the single global launcher slot and its guard with activity-scoped ActivityResult registrations. Each ComponentActivity that calls Platform.Init() gets its own ActivityResultLauncher, so the main activity and child AppCompatActivity instances cannot overwrite or block each other's launcher registration. Each registration owns its own pending TaskCompletionSource, preventing concurrent picker requests from different activities from clobbering one another.

The lifecycle handling also distinguishes finishing destroys from configuration-change destroys. Pending picker requests are canceled when the owning activity is truly finishing, but a pending request from a destroyed non-finishing activity can be transferred to the recreated same-type activity during registration. This avoids leaving a pending request keyed to a transient old activity instance when AndroidX delivers the result to the recreated activity's callback after rotation/configuration change.

MediaPicker's Android photo-picker paths continue to launch through the ActivityResult APIs for both single and multiple photo/video selection, and ActivityStateManager registers the picker launchers for each ComponentActivity during Platform.Init().

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

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

### Reference:

N/A

### Issues Fixed:

Fixes #35826

### Screenshots
| Before  | After  |
|---------|--------|
|   <Video src="https://github.com/user-attachments/assets/79983dcf-bc04-4536-af48-2b1ff2215611" Width="600" Height="300"/>   |   <Video src="https://github.com/user-attachments/assets/8775de80-9200-496d-ac8d-33d049502425" Width="600" Height="300"/>  |

🏁 Report — Final Recommendation

Comparative Analysis — PR #35944

Candidate ranking

Rank Candidate Regression result Assessment
1 pr-plus-reviewer Not independently re-run; based on PR gate plus reviewer-applied sandbox design Best overall. Preserves the PR's per-activity launcher ownership and applies the expert reviewer's lifecycle fix by transferring pending state across same-type configuration-change recreation. This removes the raw PR's concrete rotation hang while retaining child-activity behavior.
2 try-fix-2 ✅ Build PASS; UI runner PASS/SKIPPED on API 30; expert found no high-confidence blockers Strongest standalone try-fix. It uses per-registration launcher/TCS ownership, same-type config-change transfer, and finish cancellation. It is effectively the same lifecycle model as pr-plus-reviewer, but was not the PR-derived candidate.
3 pr ✅ Gate PASSED for Issue35826 child-activity scenario Fixes the reported no-rotation child-activity hang, but expert review found a critical configuration-change hole: pending requests remain keyed to the old activity instance while AndroidX may deliver results to the recreated activity's callback.
4 try-fix-1 ❌ Rejected after review; build PASS, UI runner PASS/SKIPPED on API 30 Must rank below passing candidates. Its single shared pending TCS can be clobbered by concurrent same-request launches from different activities, callbacks can complete the wrong activity's task, and finishing activity cleanup is incomplete.

Key comparison points

Child activity launcher ownership: pr, pr-plus-reviewer, and try-fix-2 all solve the root child-activity issue by ensuring a child ComponentActivity has a launcher of its own. try-fix-1 has launchers per activity, but shares pending state globally and is unsafe under overlap.

Configuration changes: The raw pr candidate fails this lifecycle probe because pending requests are stored under the original activity instance and are not transferred. pr-plus-reviewer and try-fix-2 handle the scenario by moving pending state from a destroyed non-finishing predecessor to the recreated same-type activity. try-fix-1 does not safely preserve ownership.

Cancellation semantics: pr, pr-plus-reviewer, and try-fix-2 cancel pending work when the activity is finishing. pr-plus-reviewer/try-fix-2 additionally avoid leaving a non-finishing destroyed activity as the only owner of an unresolved TCS.

Test evidence: The gate proves the submitted PR catches and fixes Issue35826's child-activity hang. try-fix-2 passed available build/UI-runner checks, but the local runner skipped the API 36 body. No candidate has an empirical rotation-during-picker regression run; the ranking therefore relies on lifecycle correctness for that edge.

Winner

Winner: pr-plus-reviewer

pr-plus-reviewer is the best candidate because it preserves the PR's gate-proven child-activity fix while applying the expert reviewer's actionable lifecycle correction. It is structurally equivalent to the successful revised try-fix-2 model for configuration changes and avoids the raw PR's activity-instance-keyed pending request hang.


🧭 Next Steps — review latest findings

No alternative fix was selected for this run. Review the session findings and CI results before merging.

@MauiBot MauiBot removed the s/agent-review-in-progress AI review is currently running for this PR label Jul 9, 2026
@kubaflo
kubaflo changed the base branch from main to inflight/current July 9, 2026 21:00
@kubaflo
kubaflo merged commit b28d9c1 into dotnet:inflight/current Jul 9, 2026
29 of 38 checks passed
@github-actions github-actions Bot added this to the .NET 10 SR9 milestone Jul 9, 2026
kubaflo pushed a commit that referenced this pull request Jul 10, 2026
…ies (#35944)

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

### Issue Details:

MediaPicker - PickPhotosAsync doesn't return on Android when using
multiple Activities
       
### Root Cause:

Commit
[0c25c62](0c25c62)
introduced a guard in ActivityForResultRequest.Register() that checked
whether an existing ActivityResultLauncher was already registered for a
still-alive activity. If one existed, the method returned early without
registering a new launcher. This guard was designed to prevent duplicate
registrations (fixing issue #32845), but it had an unintended side
effect: when a child AppCompatActivity called Platform.Init(), the guard
saw the main activity's launcher as still alive and silently skipped
registration entirely. As a result, the child activity had no
ActivityResultLauncher of its own. On Android API 36, launcher ownership
is strictly enforced — the system only delivers an ActivityResult to the
launcher that belongs to the activity that launched the intent. Since
the child activity had no launcher, PickPhotosAsync() launched the photo
picker but its TaskCompletionSource was never resolved, causing the
await to hang indefinitely.

### Description of Change:

The fix replaces the single global launcher slot and its guard with a
ConditionalWeakTable<ComponentActivity, ActivityResultLauncher>. This
table stores one launcher entry per activity instance. Because
ConditionalWeakTable uses weak keys, entries are automatically eligible
for garbage collection when the activity is destroyed, preventing memory
leaks. The Register() method now checks whether the specific calling
activity already has an entry (idempotent per-instance), and if not,
creates and stores a new launcher for it. At call time,
GetLauncherForCurrentActivity() resolves the current foreground activity
and retrieves its entry from the table. This ensures every activity —
whether the main activity or any child — gets its own launcher, fixing
the hang while also preserving the original #32845 fix since no activity
can overwrite another's entry.

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

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

### Reference:

N/A

### Issues Fixed:

Fixes  #35826          

### Screenshots
| Before  | After  |
|---------|--------|
| <Video
src="https://github.com/user-attachments/assets/79983dcf-bc04-4536-af48-2b1ff2215611"
Width="600" Height="300"/> | <Video
src="https://github.com/user-attachments/assets/8775de80-9200-496d-ac8d-33d049502425"
Width="600" Height="300"/> |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Jul 15, 2026
…ies (#35944)

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

### Issue Details:

MediaPicker - PickPhotosAsync doesn't return on Android when using
multiple Activities
       
### Root Cause:

Commit
[0c25c62](0c25c62)
introduced a guard in ActivityForResultRequest.Register() that checked
whether an existing ActivityResultLauncher was already registered for a
still-alive activity. If one existed, the method returned early without
registering a new launcher. This guard was designed to prevent duplicate
registrations (fixing issue #32845), but it had an unintended side
effect: when a child AppCompatActivity called Platform.Init(), the guard
saw the main activity's launcher as still alive and silently skipped
registration entirely. As a result, the child activity had no
ActivityResultLauncher of its own. On Android API 36, launcher ownership
is strictly enforced — the system only delivers an ActivityResult to the
launcher that belongs to the activity that launched the intent. Since
the child activity had no launcher, PickPhotosAsync() launched the photo
picker but its TaskCompletionSource was never resolved, causing the
await to hang indefinitely.

### Description of Change:

The fix replaces the single global launcher slot and its guard with a
ConditionalWeakTable<ComponentActivity, ActivityResultLauncher>. This
table stores one launcher entry per activity instance. Because
ConditionalWeakTable uses weak keys, entries are automatically eligible
for garbage collection when the activity is destroyed, preventing memory
leaks. The Register() method now checks whether the specific calling
activity already has an entry (idempotent per-instance), and if not,
creates and stores a new launcher for it. At call time,
GetLauncherForCurrentActivity() resolves the current foreground activity
and retrieves its entry from the table. This ensures every activity —
whether the main activity or any child — gets its own launcher, fixing
the hang while also preserving the original #32845 fix since no activity
can overwrite another's entry.

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

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

### Reference:

N/A

### Issues Fixed:

Fixes  #35826          

### Screenshots
| Before  | After  |
|---------|--------|
| <Video
src="https://github.com/user-attachments/assets/79983dcf-bc04-4536-af48-2b1ff2215611"
Width="600" Height="300"/> | <Video
src="https://github.com/user-attachments/assets/8775de80-9200-496d-ac8d-33d049502425"
Width="600" Height="300"/> |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Jul 22, 2026
…ies (#35944)

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

### Issue Details:

MediaPicker - PickPhotosAsync doesn't return on Android when using
multiple Activities
       
### Root Cause:

Commit
[0c25c62](0c25c62)
introduced a guard in ActivityForResultRequest.Register() that checked
whether an existing ActivityResultLauncher was already registered for a
still-alive activity. If one existed, the method returned early without
registering a new launcher. This guard was designed to prevent duplicate
registrations (fixing issue #32845), but it had an unintended side
effect: when a child AppCompatActivity called Platform.Init(), the guard
saw the main activity's launcher as still alive and silently skipped
registration entirely. As a result, the child activity had no
ActivityResultLauncher of its own. On Android API 36, launcher ownership
is strictly enforced — the system only delivers an ActivityResult to the
launcher that belongs to the activity that launched the intent. Since
the child activity had no launcher, PickPhotosAsync() launched the photo
picker but its TaskCompletionSource was never resolved, causing the
await to hang indefinitely.

### Description of Change:

The fix replaces the single global launcher slot and its guard with a
ConditionalWeakTable<ComponentActivity, ActivityResultLauncher>. This
table stores one launcher entry per activity instance. Because
ConditionalWeakTable uses weak keys, entries are automatically eligible
for garbage collection when the activity is destroyed, preventing memory
leaks. The Register() method now checks whether the specific calling
activity already has an entry (idempotent per-instance), and if not,
creates and stores a new launcher for it. At call time,
GetLauncherForCurrentActivity() resolves the current foreground activity
and retrieves its entry from the table. This ensures every activity —
whether the main activity or any child — gets its own launcher, fixing
the hang while also preserving the original #32845 fix since no activity
can overwrite another's entry.

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

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

### Reference:

N/A

### Issues Fixed:

Fixes  #35826          

### Screenshots
| Before  | After  |
|---------|--------|
| <Video
src="https://github.com/user-attachments/assets/79983dcf-bc04-4536-af48-2b1ff2215611"
Width="600" Height="300"/> | <Video
src="https://github.com/user-attachments/assets/8775de80-9200-496d-ac8d-33d049502425"
Width="600" Height="300"/> |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Jul 28, 2026
…ies (#35944)

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

### Issue Details:

MediaPicker - PickPhotosAsync doesn't return on Android when using
multiple Activities
       
### Root Cause:

Commit
[0c25c62](0c25c62)
introduced a guard in ActivityForResultRequest.Register() that checked
whether an existing ActivityResultLauncher was already registered for a
still-alive activity. If one existed, the method returned early without
registering a new launcher. This guard was designed to prevent duplicate
registrations (fixing issue #32845), but it had an unintended side
effect: when a child AppCompatActivity called Platform.Init(), the guard
saw the main activity's launcher as still alive and silently skipped
registration entirely. As a result, the child activity had no
ActivityResultLauncher of its own. On Android API 36, launcher ownership
is strictly enforced — the system only delivers an ActivityResult to the
launcher that belongs to the activity that launched the intent. Since
the child activity had no launcher, PickPhotosAsync() launched the photo
picker but its TaskCompletionSource was never resolved, causing the
await to hang indefinitely.

### Description of Change:

The fix replaces the single global launcher slot and its guard with a
ConditionalWeakTable<ComponentActivity, ActivityResultLauncher>. This
table stores one launcher entry per activity instance. Because
ConditionalWeakTable uses weak keys, entries are automatically eligible
for garbage collection when the activity is destroyed, preventing memory
leaks. The Register() method now checks whether the specific calling
activity already has an entry (idempotent per-instance), and if not,
creates and stores a new launcher for it. At call time,
GetLauncherForCurrentActivity() resolves the current foreground activity
and retrieves its entry from the table. This ensures every activity —
whether the main activity or any child — gets its own launcher, fixing
the hang while also preserving the original #32845 fix since no activity
can overwrite another's entry.

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

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

### Reference:

N/A

### Issues Fixed:

Fixes  #35826          

### Screenshots
| Before  | After  |
|---------|--------|
| <Video
src="https://github.com/user-attachments/assets/79983dcf-bc04-4536-af48-2b1ff2215611"
Width="600" Height="300"/> | <Video
src="https://github.com/user-attachments/assets/8775de80-9200-496d-ac8d-33d049502425"
Width="600" Height="300"/> |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Jul 29, 2026
…ies (#35944)

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

### Issue Details:

MediaPicker - PickPhotosAsync doesn't return on Android when using
multiple Activities
       
### Root Cause:

Commit
[0c25c62](0c25c62)
introduced a guard in ActivityForResultRequest.Register() that checked
whether an existing ActivityResultLauncher was already registered for a
still-alive activity. If one existed, the method returned early without
registering a new launcher. This guard was designed to prevent duplicate
registrations (fixing issue #32845), but it had an unintended side
effect: when a child AppCompatActivity called Platform.Init(), the guard
saw the main activity's launcher as still alive and silently skipped
registration entirely. As a result, the child activity had no
ActivityResultLauncher of its own. On Android API 36, launcher ownership
is strictly enforced — the system only delivers an ActivityResult to the
launcher that belongs to the activity that launched the intent. Since
the child activity had no launcher, PickPhotosAsync() launched the photo
picker but its TaskCompletionSource was never resolved, causing the
await to hang indefinitely.

### Description of Change:

The fix replaces the single global launcher slot and its guard with a
ConditionalWeakTable<ComponentActivity, ActivityResultLauncher>. This
table stores one launcher entry per activity instance. Because
ConditionalWeakTable uses weak keys, entries are automatically eligible
for garbage collection when the activity is destroyed, preventing memory
leaks. The Register() method now checks whether the specific calling
activity already has an entry (idempotent per-instance), and if not,
creates and stores a new launcher for it. At call time,
GetLauncherForCurrentActivity() resolves the current foreground activity
and retrieves its entry from the table. This ensures every activity —
whether the main activity or any child — gets its own launcher, fixing
the hang while also preserving the original #32845 fix since no activity
can overwrite another's entry.

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

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

### Reference:

N/A

### Issues Fixed:

Fixes  #35826          

### Screenshots
| Before  | After  |
|---------|--------|
| <Video
src="https://github.com/user-attachments/assets/79983dcf-bc04-4536-af48-2b1ff2215611"
Width="600" Height="300"/> | <Video
src="https://github.com/user-attachments/assets/8775de80-9200-496d-ac8d-33d049502425"
Width="600" Height="300"/> |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Aug 1, 2026
…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.
-->
kubaflo pushed a commit that referenced this pull request Aug 7, 2026
…ies (#35944)

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

### Issue Details:

MediaPicker - PickPhotosAsync doesn't return on Android when using
multiple Activities
       
### Root Cause:

Commit
[0c25c62](0c25c62)
introduced a guard in ActivityForResultRequest.Register() that checked
whether an existing ActivityResultLauncher was already registered for a
still-alive activity. If one existed, the method returned early without
registering a new launcher. This guard was designed to prevent duplicate
registrations (fixing issue #32845), but it had an unintended side
effect: when a child AppCompatActivity called Platform.Init(), the guard
saw the main activity's launcher as still alive and silently skipped
registration entirely. As a result, the child activity had no
ActivityResultLauncher of its own. On Android API 36, launcher ownership
is strictly enforced — the system only delivers an ActivityResult to the
launcher that belongs to the activity that launched the intent. Since
the child activity had no launcher, PickPhotosAsync() launched the photo
picker but its TaskCompletionSource was never resolved, causing the
await to hang indefinitely.

### Description of Change:

The fix replaces the single global launcher slot and its guard with a
ConditionalWeakTable<ComponentActivity, ActivityResultLauncher>. This
table stores one launcher entry per activity instance. Because
ConditionalWeakTable uses weak keys, entries are automatically eligible
for garbage collection when the activity is destroyed, preventing memory
leaks. The Register() method now checks whether the specific calling
activity already has an entry (idempotent per-instance), and if not,
creates and stores a new launcher for it. At call time,
GetLauncherForCurrentActivity() resolves the current foreground activity
and retrieves its entry from the table. This ensures every activity —
whether the main activity or any child — gets its own launcher, fixing
the hang while also preserving the original #32845 fix since no activity
can overwrite another's entry.

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

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

### Reference:

N/A

### Issues Fixed:

Fixes  #35826          

### Screenshots
| Before  | After  |
|---------|--------|
| <Video
src="https://github.com/user-attachments/assets/79983dcf-bc04-4536-af48-2b1ff2215611"
Width="600" Height="300"/> | <Video
src="https://github.com/user-attachments/assets/8775de80-9200-496d-ac8d-33d049502425"
Width="600" Height="300"/> |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
kubaflo pushed a commit that referenced this pull request Aug 7, 2026
…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.
-->
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 9, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-essentials Essentials: Device, Display, Connectivity, Secure Storage, Sensors, App Info community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/android s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-gate-passed AI verified tests catch the bug (fail without fix, pass with fix) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MediaPicker - PickPhotosAsync doesn't return on Android when using multiple Activities

5 participants