Skip to content

[Android] Fix MediaPicker.PickPhotosAsync UnauthorizedAccessException on API 28 and below#34981

Merged
kubaflo merged 4 commits intodotnet:inflight/currentfrom
HarishwaranVijayakumar:fix-34889
Apr 20, 2026
Merged

[Android] Fix MediaPicker.PickPhotosAsync UnauthorizedAccessException on API 28 and below#34981
kubaflo merged 4 commits intodotnet:inflight/currentfrom
HarishwaranVijayakumar:fix-34889

Conversation

@HarishwaranVijayakumar
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 throws UnauthorizedAccessException on Android 9.0 (API 28).

Root Cause of the issue

  • On Android 9 (API 28) and below, FileSystemUtils.EnsurePhysicalPath() resolves a content:// URI to a raw filesystem path (e.g., /storage/emulated/0/Download/photo.png) via ResolvePhysicalPath(). The existing File.Exists() check passes because stat() only requires execute permission on the parent directory, but when MediaPicker later calls File.OpenRead() on that path, it fails with UnauthorizedAccessException because the app lacks READ_EXTERNAL_STORAGE runtime permission. The picker intent only grants temporary access to the content:// URI, not to the raw filesystem path. On Android 10+, this code path is skipped entirely due to scoped storage, so the fallback CacheContentFile() is always used and the issue doesn't occur.

Description of Change

File access validation improvements:

  • Updated EnsurePhysicalPath in FileSystemUtils.android.cs to check file readability using the new IsFileReadable method, preventing exceptions when a file path exists but is not actually readable due to permission issues.
  • Added the internal method IsFileReadable, which uses Java.IO.File.canRead() to verify read access before returning a file path.

Testing enhancements:

  • Added a new test class Android_FileSystemUtils_Tests with tests for IsFileReadable, covering readable files, non-existent files, and files with revoked read permissions to ensure correct behavior.

Issues Fixed

Fixes #34889

Tested the behaviour in the following platforms

  • - Windows
  • - Android
  • - iOS
  • - Mac
Before After
BeforeFix_34889.mov
AfterFix_34889.mov

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 15, 2026

🚀 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 -- 34981

Or

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

@dotnet-policy-service dotnet-policy-service Bot added the community ✨ Community Contribution label Apr 15, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

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.

@dotnet-policy-service dotnet-policy-service Bot added the partner/syncfusion Issues / PR's with Syncfusion collaboration label Apr 15, 2026
@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 15, 2026
@sheiksyedm sheiksyedm marked this pull request as ready for review April 16, 2026 06:45
Copilot AI review requested due to automatic review settings April 16, 2026 06:45
@sheiksyedm
Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes Android API 28 and below behavior where MediaPicker.PickPhotosAsync could return a raw filesystem path that exists but is not actually readable (leading to UnauthorizedAccessException), by validating readability before using the resolved path and otherwise falling back to caching the content:// URI.

Changes:

  • Update FileSystemUtils.EnsurePhysicalPath (Android) to require a readable resolved path via new IsFileReadable helper.
  • Add FileSystemUtils.IsFileReadable (Android) based on Java.IO.File.CanRead().
  • Add Android device tests covering IsFileReadable for readable, missing, and permission-revoked files.

Reviewed changes

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

File Description
src/Essentials/src/FileSystem/FileSystemUtils.android.cs Adds readability validation to avoid returning inaccessible raw paths on API 28 and below.
src/Essentials/test/DeviceTests/Tests/Android/FileSystemUtils_Tests.cs Adds device tests for the new readability helper.

Comment thread src/Essentials/src/FileSystem/FileSystemUtils.android.cs Outdated
Comment thread src/Essentials/test/DeviceTests/Tests/Android/FileSystemUtils_Tests.cs Outdated
Comment thread src/Essentials/test/DeviceTests/Tests/Android/FileSystemUtils_Tests.cs Outdated
Copy link
Copy Markdown
Contributor

@kubaflo kubaflo left a comment

Choose a reason for hiding this comment

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

Could you please review the AI's and copilot's suggestions?

@HarishwaranVijayakumar
Copy link
Copy Markdown
Contributor Author

Could you please review the AI's and copilot's suggestions?

Addressed the AI and copilot suggestions

@dotnet dotnet deleted a comment from MauiBot Apr 20, 2026
@dotnet dotnet deleted a comment from MauiBot Apr 20, 2026
@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Apr 20, 2026

🤖 AI Summary

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

📊 Review Sessioncb05143 · Address AI concerns · 2026-04-20 16:54 UTC
🚦 Gate — Test Before & After Fix

Gate Result: ✅ PASSED

Platform: ANDROID · Base: main · Merge base: eb0b82fe

Test Without Fix (expect FAIL) With Fix (expect PASS)
📱 FileSystemUtils_Tests (IsFileReadable_Returns_True_For_Readable_File, IsFileReadable_Returns_False_For_NonExistent_File, IsFileReadable_Returns_False_For_Inaccessible_Path) Category=FileSystem ✅ FAIL — 244s ✅ PASS — 454s
🔴 Without fix — 📱 FileSystemUtils_Tests (IsFileReadable_Returns_True_For_Readable_File, IsFileReadable_Returns_False_For_NonExistent_File, IsFileReadable_Returns_False_For_Inaccessible_Path): FAIL ✅ · 244s
  Determining projects to restore...
  Restored /home/vsts/work/1/s/src/Essentials/src/Essentials.csproj (in 4.57 sec).
  Restored /home/vsts/work/1/s/src/Core/src/Core.csproj (in 6.27 sec).
  Restored /home/vsts/work/1/s/src/Controls/src/Xaml/Controls.Xaml.csproj (in 1.12 sec).
  Restored /home/vsts/work/1/s/src/Controls/src/Core/Controls.Core.csproj (in 34 ms).
  Restored /home/vsts/work/1/s/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj (in 29 ms).
  Restored /home/vsts/work/1/s/src/TestUtils/src/DeviceTests/TestUtils.DeviceTests.csproj (in 1.28 sec).
  Restored /home/vsts/work/1/s/src/TestUtils/src/DeviceTests.Runners/TestUtils.DeviceTests.Runners.csproj (in 1.58 sec).
  Restored /home/vsts/work/1/s/src/Graphics/src/Graphics/Graphics.csproj (in 19 ms).
  Restored /home/vsts/work/1/s/src/Essentials/test/DeviceTests/Essentials.DeviceTests.csproj (in 521 ms).
  Restored /home/vsts/work/1/s/src/TestUtils/src/DeviceTests.Runners.SourceGen/TestUtils.DeviceTests.Runners.SourceGen.csproj (in 1.45 sec).
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13884198
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Release/net10.0-android36.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13884198
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Release/net10.0-android36.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13884198
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Release/net10.0-android36.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Release/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13884198
  TestUtils.DeviceTests -> /home/vsts/work/1/s/artifacts/bin/TestUtils.DeviceTests/Release/net10.0-android/Microsoft.Maui.TestUtils.DeviceTests.dll
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Release/net10.0-android36.0/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.70-ci+azdo.13884198
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Release/net10.0-android36.0/Microsoft.Maui.Controls.Xaml.dll
  TestUtils.DeviceTests.Runners -> /home/vsts/work/1/s/artifacts/bin/TestUtils.DeviceTests.Runners/Release/net10.0-android/Microsoft.Maui.TestUtils.DeviceTests.Runners.dll
  TestUtils.DeviceTests.Runners.SourceGen -> /home/vsts/work/1/s/artifacts/bin/TestUtils.DeviceTests.Runners.SourceGen/Release/netstandard2.0/Microsoft.Maui.TestUtils.DeviceTests.Runners.SourceGen.dll
/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Tests/Android/FileSystemUtils_Tests.cs(17,33): error CS0117: 'FileSystemUtils' does not contain a definition for 'IsFileReadable' [/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Essentials.DeviceTests.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Tests/Android/FileSystemUtils_Tests.cs(32,33): error CS0117: 'FileSystemUtils' does not contain a definition for 'IsFileReadable' [/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Essentials.DeviceTests.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Tests/Android/FileSystemUtils_Tests.cs(49,34): error CS0117: 'FileSystemUtils' does not contain a definition for 'IsFileReadable' [/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Essentials.DeviceTests.csproj::TargetFramework=net10.0-android]

Build FAILED.

/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Tests/Android/FileSystemUtils_Tests.cs(17,33): error CS0117: 'FileSystemUtils' does not contain a definition for 'IsFileReadable' [/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Essentials.DeviceTests.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Tests/Android/FileSystemUtils_Tests.cs(32,33): error CS0117: 'FileSystemUtils' does not contain a definition for 'IsFileReadable' [/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Essentials.DeviceTests.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Tests/Android/FileSystemUtils_Tests.cs(49,34): error CS0117: 'FileSystemUtils' does not contain a definition for 'IsFileReadable' [/home/vsts/work/1/s/src/Essentials/test/DeviceTests/Essentials.DeviceTests.csproj::TargetFramework=net10.0-android]
    0 Warning(s)
    3 Error(s)

Time Elapsed 00:03:48.58

🟢 With fix — 📱 FileSystemUtils_Tests (IsFileReadable_Returns_True_For_Readable_File, IsFileReadable_Returns_False_For_NonExistent_File, IsFileReadable_Returns_False_For_Inaccessible_Path): PASS ✅ · 454s

(truncated to last 15,000 chars)

0 08:56:29.557 10100 10341 I DOTNET  : 	[PASS] OpenAppPackageFileAsync_Can_Load_File
      04-20 08:56:29.328 10100 10100 W com.microsoft.maui.essentials.devicetests: type=1400 audit(0.0:339): avc: denied { ioctl } for comm=2E4E4554204C6F6E672052756E6E69 path="/data/data/com.microsoft.maui.essentials.devicetests/cache/2203693cc04e0be7f4f024d5f9499e13/528e7e8645f04bd98690cc7885fd7775/the-file.txt" dev="dm-5" ino=133057 ioctlcmd=0x9409 scontext=u:r:untrusted_app:s0:c154,c256,c512,c768 tcontext=u:object_r:app_data_file:s0:c154,c256,c512,c768 tclass=file permissive=0 app=com.microsoft.maui.essentials.devicetests
      04-20 08:56:29.561 10100 10341 I DOTNET  : 	[PASS] OpenAppPackageFileAsync_Throws_If_File_Is_Not_Found
      04-20 08:56:29.561 10100 10341 I DOTNET  : Microsoft.Maui.Essentials.DeviceTests.FileSystem_Tests 0.0406112 ms
      04-20 08:56:29.561 10100 10341 I DOTNET  : Test collection for Microsoft.Maui.Essentials.DeviceTests.DeviceInfo_Tests
      04-20 08:56:29.561 10100 10341 I DOTNET  : 	[PASS] Versions_Are_Correct
      04-20 08:56:29.565 10100 10341 I DOTNET  : 	[PASS] Platform_Is_Correct
      04-20 08:56:29.565 10100 10341 I DOTNET  : Microsoft.Maui.Essentials.DeviceTests.DeviceInfo_Tests 0.0041277 ms
      04-20 08:56:29.619 10100 10132 I DOTNET  : Xml file was written to the provided writer.
      04-20 08:56:29.620 10100 10132 I DOTNET  : Tests run: 298 Passed: 268 Inconclusive: 0 Failed: 0 Ignored: 30
      04-20 08:56:29.637   476   915 D CompatibilityChangeReporter: Compat change id reported: 149924527; UID 10154; state: ENABLED
      04-20 08:56:29.638   476   915 D CompatibilityChangeReporter: Compat change id reported: 132649864; UID 10154; state: DISABLED
      04-20 08:56:29.671  1449  7489 I MediaProvider: Using lower FS for /storage/emulated/0/Download/com.microsoft.maui.essentials.devicetests/48b1ebe5deeb49d7a4ff4d02ee9cd914/testResults.xml
      04-20 08:56:29.683   476   915 I ActivityManager: Force stopping com.microsoft.maui.essentials.devicetests appid=10154 user=0: finished inst
      04-20 08:56:29.683   476   915 I ActivityManager: Killing 10100:com.microsoft.maui.essentials.devicetests/u0a154 (adj 0): stop com.microsoft.maui.essentials.devicetests due to finished inst
      04-20 08:56:29.691   476   915 W ActivityTaskManager: Force removing ActivityRecord{d7d7cbd u0 com.microsoft.maui.essentials.devicetests/.TestActivity t12 f}}: app died, no saved state
      04-20 08:56:29.708   476   915 W InputReader: Device virtio_input_multi_touch_10 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.708   476   915 W InputReader: Device virtio_input_multi_touch_4 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.708   476   915 W InputReader: Device virtio_input_multi_touch_11 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.708   476   915 W InputReader: Device virtio_input_multi_touch_8 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.708   476   915 W InputReader: Device virtio_input_multi_touch_5 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.708   476   915 W InputReader: Device virtio_input_multi_touch_2 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.708   476   915 W InputReader: Device virtio_input_multi_touch_6 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.708   476   915 W InputReader: Device virtio_input_multi_touch_3 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.708   476   915 W InputReader: Device virtio_input_multi_touch_9 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.708   476   915 W InputReader: Device virtio_input_multi_touch_7 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.709   476   915 W InputReader: Device virtio_input_multi_touch_10 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.709   476   915 W InputReader: Device virtio_input_multi_touch_4 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.709   476   915 W InputReader: Device virtio_input_multi_touch_11 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.709   476   915 W InputReader: Device virtio_input_multi_touch_8 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.709   476   915 W InputReader: Device virtio_input_multi_touch_5 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.709   476   915 W InputReader: Device virtio_input_multi_touch_2 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.709   476   915 W InputReader: Device virtio_input_multi_touch_6 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.709   476   915 W InputReader: Device virtio_input_multi_touch_3 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.709   476   915 W InputReader: Device virtio_input_multi_touch_9 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.709   476   915 W InputReader: Device virtio_input_multi_touch_7 is associated with display ADISPLAY_ID_NONE.
      04-20 08:56:29.720 10087 10087 D AndroidRuntime: Shutting down VM
      04-20 08:56:29.738   298  6553 D goldfish-address-space: claimShared: Ask to claim region [0x3fa223000 0x3faa0c000]
      04-20 08:56:29.741   298  6553 D goldfish-address-space: claimShared: Ask to claim region [0x3f8a68000 0x3f9251000]
      04-20 08:56:29.742   298  6553 D goldfish-address-space: claimShared: Ask to claim region [0x3f9251000 0x3f9a3a000]
      04-20 08:56:29.817  9396  9396 I bvtw    : onStart
      04-20 08:56:29.819   298  6553 D goldfish-address-space: claimShared: Ask to claim region [0x3ff6ad000 0x3ffe96000]
      04-20 08:56:29.820   298  6553 D goldfish-address-space: claimShared: Ask to claim region [0x3fc7e9000 0x3fcfd2000]
      04-20 08:56:29.821   298  6553 D goldfish-address-space: claimShared: Ask to claim region [0x3fcfd2000 0x3fd7bb000]
      04-20 08:56:30.337   476   915 D ConnectivityService: requestNetwork for uid/pid:10109/9396 NetworkRequest [ TRACK_DEFAULT id=76, [ Capabilities: INTERNET&NOT_RESTRICTED&TRUSTED Uid: 10109 AdministratorUids: [] RequestorUid: 10109 RequestorPackageName: com.google.android.googlequicksearchbox] ]
      04-20 08:56:30.337   476   620 D ConnectivityService: NetReassign [76 : null → 100]
      04-20 08:56:30.337   476   616 D WifiNetworkFactory: got request NetworkRequest [ TRACK_DEFAULT id=76, [ Capabilities: INTERNET&NOT_RESTRICTED&TRUSTED Uid: 10109 AdministratorUids: [] RequestorUid: 10109 RequestorPackageName: com.google.android.googlequicksearchbox] ] with score 60 and providerId 3
      04-20 08:56:30.338  1004  1004 D PhoneSwitcherNetworkRequstListener: got request NetworkRequest [ TRACK_DEFAULT id=76, [ Capabilities: INTERNET&NOT_RESTRICTED&TRUSTED Uid: 10109 AdministratorUids: [] RequestorUid: 10109 RequestorPackageName: com.google.android.googlequicksearchbox] ] with score 60 and providerId 3
      04-20 08:56:30.341   476   616 D UntrustedWifiNetworkFactory: got request NetworkRequest [ TRACK_DEFAULT id=76, [ Capabilities: INTERNET&NOT_RESTRICTED&TRUSTED Uid: 10109 AdministratorUids: [] RequestorUid: 10109 RequestorPackageName: com.google.android.googlequicksearchbox] ] with score 60 and providerId 3
      04-20 08:56:30.356  6518  6518 V KeyguardUpdateMonitor: onSubscriptionInfoChanged()
      04-20 08:56:30.357  6518  6518 V KeyguardUpdateMonitor: SubInfo:{id=1 iccId=890141032[****] simSlotIndex=0 carrierId=1 displayName=T-Mobile carrierName=T-Mobile nameSource=3 iconTint=-16746133 number=[****] dataRoaming=0 iconBitmap=android.graphics.Bitmap@3f0b79c mcc=310 mnc=260 countryIso=us isEmbedded=false nativeAccessRules=null cardString=890141032[****] cardId=-1 isOpportunistic=false groupUUID=null isGroupDisabled=false profileClass=-1 ehplmns=null hplmns=null subscriptionType=0 groupOwner=null carrierConfigAccessRules=null areUiccApplicationsEnabled=true}
      04-20 08:56:30.359  9396  9396 I btpr    : (REDACTED) [%s] onStart()
      04-20 08:56:30.360  9396  9396 I bvtw    : onResume
      04-20 08:56:30.371  1004  1004 D Telephony: isEmergencyPreferredAccount: subId=1, activeData=1
      04-20 08:56:30.371  1004  1004 D Telephony: isEmergencyPreferredAccount: Device does not require preference.
      04-20 08:56:30.385  9396  9511 W ServiceBindIntentUtils: Dynamic lookup for intent failed for action: com.google.android.gms.inappreach.service.START
      04-20 08:56:30.386   476   915 W ActivityManager: Unable to start service Intent { act=com.google.android.gms.inappreach.service.START pkg=com.google.android.gms } U=0: not found
      04-20 08:56:30.401   476   915 W ActivityManager: Unbind failed: could not find connection for android.os.BinderProxy@f92d3c4
      04-20 08:56:30.431  9396  9511 W GmsClient: unable to connect to service: com.google.android.gms.inappreach.service.START on com.google.android.gms
      04-20 08:56:30.431  9396  9511 W GoogleApiManager: Not showing notification since connectionResult is not user-facing: ConnectionResult{statusCode=API_UNAVAILABLE, resolution=null, message=null, clientMethodKey=null}
      04-20 08:56:29.332 10100 10100 W com.microsoft.maui.essentials.devicetests: type=1400 audit(0.0:340): avc: denied { ioctl } for comm=2E4E4554204C6F6E672052756E6E69 path=2F73746F726167652F656D756C617465642F302F416E64726F69642F646174612F636F6D2E6D6963726F736F66742E6D6175692E657373656E7469616C732E64657669636574657374732F63616368652F32323033363933636330346530626537663466303234643566393439396531332F36653034333938613365326434616536626530383933373565653662333035382F7468652D66696C652E747874202864656C6574656429 dev="dm-5" ino=270404 ioctlcmd=0x9409 scontext=u:r:untrusted_app:s0:c154,c256,c512,c768 tcontext=u:object_r:media_rw_data_file:s0:c154,c256,c512,c768 tclass=file permissive=0 app=com.microsoft.maui.essentials.devicetests
      04-20 08:56:30.484   476   504 W ActivityManager: setHasOverlayUi called on unknown pid: 10100
      04-20 08:56:30.487   268   268 I Zygote  : Process 10100 exited due to signal 9 (Killed)
      04-20 08:56:30.512 10090 10090 W dex2oat32: Verification of byte[] rzm.A(rzb, java.lang.String) took 830.758ms (184.17 bytecodes/s) (2576B approximate peak alloc)
      04-20 08:56:30.520   476   512 I libprocessgroup: Successfully killed process cgroup uid 10154 pid 10100 in 831ms
      04-20 08:56:29.356 10100 10100 W com.microsoft.maui.essentials.devicetests: type=1400 audit(0.0:341): avc: denied { ioctl } for comm=2E4E4554204C6F6E672052756E6E69 path=2F73746F726167652F656D756C617465642F302F416E64726F69642F646174612F636F6D2E6D6963726F736F66742E6D6175692E657373656E7469616C732E64657669636574657374732F63616368652F32323033363933636330346530626537663466303234643566393439396531332F38643662636162343830386234393164383962623939313066316232346630612F7468652D66696C652E747874202864656C6574656429 dev="dm-5" ino=270408 ioctlcmd=0x9409 scontext=u:r:untrusted_app:s0:c154,c256,c512,c768 tcontext=u:object_r:media_rw_data_file:s0:c154,c256,c512,c768 tclass=file permissive=0 app=com.microsoft.maui.essentials.devicetests
      04-20 08:56:30.732   476 10342 I ActivityManager: Failure reporting to instrumentation watcher: comp=ComponentInfo{com.microsoft.maui.essentials.devicetests/com.microsoft.maui.essentials.devicetests.TestInstrumentation} results=Bundle[mParcelledData.dataSize=520]
      04-20 08:56:31.596 10090 10090 W dex2oat32: Accessing hidden method Landroid/app/job/JobParameters;->getStopReason()I (greylist-max-o, linking, denied)
      04-20 08:56:31.663  8696  8696 I Finsky  : [2] SCH: job service start with id 9237.
      04-20 08:56:31.677  8696  9393 I Finsky  : [752] SCH: Satisfied jobs for 9237 are: 12-1
      04-20 08:56:31.681   476  1814 D WifiNl80211Manager: Scan result ready event
      04-20 08:56:31.681   476  1814 D WifiNative: Scan result ready event
      04-20 08:56:31.700  8696  9757 I Finsky  : [765] SCH: Job 12-1 starting
      04-20 08:56:31.701  8696  8696 I Finsky  : [2] WM::SCH: Logging work start for 12-1
      04-20 08:56:31.702  8696  8696 I Finsky  : [2] [ContentSync] job started
      04-20 08:56:31.881  8696 10344 I Finsky  : [780] App states replicator found 4 unowned apps
      04-20 08:56:31.907  8696  8770 I Finsky  : [627] Completed 0 account content syncs with 0 successful.
      04-20 08:56:31.908  8696  8696 I Finsky  : [2] [ContentSync] Installation state replication succeeded.
      04-20 08:56:31.908  8696  8696 I Finsky  : [2] SCH: jobFinished: 12-1. TimeElapsed: 207ms.
      04-20 08:56:31.908  8696  8696 I Finsky  : [2] WM::SCH: Logging work end for 12-1
      04-20 08:56:31.926  8696  8761 I Finsky  : [618] SCH: Scheduling phonesky job Id: 1-1337, CT: 1776700372213, Constraints: [{ L: 28867911, D: 72067911, C: CHARGING_NONE, I: IDLE_NONE, N: NET_ANY, B: BATTERY_ANY }]
      04-20 08:56:31.926  8696  8761 I Finsky  : [618] SCH: Scheduling phonesky job Id: 10-4, CT: 1776700371217, Constraints: [{ L: 1200000, D: 86400000, C: CHARGING_NONE, I: IDLE_NONE, N: NET_ANY, B: BATTERY_ANY }]
      04-20 08:56:31.926  8696  8761 I Finsky  : [618] SCH: Scheduling phonesky job Id: 10-11, CT: 1776700371217, Constraints: [{ L: 600000, D: 86400000, C: CHARGING_NONE, I: IDLE_NONE, N: NET_ANY, B: BATTERY_ANY }]
      04-20 08:56:31.926  8696  8761 I Finsky  : [618] SCH: Scheduling phonesky job Id: 10-26, CT: 1776700371218, Constraints: [{ L: 600000, D: 86400000, C: CHARGING_NONE, I: IDLE_NONE, N: NET_ANY, B: BATTERY_ANY }]
      04-20 08:56:31.926  8696  8761 I Finsky  : [618] SCH: Scheduling phonesky job Id: 10-123, CT: 1776700371222, Constraints: [{ L: 600000, D: 86400000, C: CHARGING_NONE, I: IDLE_NONE, N: NET_ANY, B: BATTERY_ANY }]
      04-20 08:56:31.930  8696  8761 I Finsky  : [618] SCH: Scheduling 1 system job(s)
      04-20 08:56:31.931  8696  8761 I Finsky  : [618] SCH: Scheduling system job Id: 9247, L: 379287, D: 71848194, C: false, I: false, N: 1
      04-20 08:56:31.933  8696  9757 I Finsky  : [765] SCH: job service finished with id 9237.
      04-20 08:56:32.263 10090 10090 W dex2oat32: Accessing hidden method Landroid/content/Context;->isUiContext()Z (blacklist, linking, denied)
      04-20 08:56:32.276 10090 10090 W dex2oat32: Accessing hidden method Landroid/media/MediaRoute2Info;->getType()I (blacklist, linking, denied)
      04-20 08:56:32.276 10090 10090 W dex2oat32: Accessing hidden method Landroid/media/MediaRouter2$RoutingController;->getRoutingSessionInfo()Landroid/media/RoutingSessionInfo; (blacklist, linking, denied)
      
�[40m�[32minfo�[39m�[22m�[49m: Attempting to remove apk 'com.microsoft.maui.essentials.devicetests'..
�[40m�[37mdbug�[39m�[22m�[49m: Executing command: '/home/vsts/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26107.1/runtimes/any/native/adb/linux/adb -s emulator-5554 uninstall com.microsoft.maui.essentials.devicetests'
�[40m�[32minfo�[39m�[22m�[49m: Successfully uninstalled com.microsoft.maui.essentials.devicetests
XHarness exit code: 0
  Tests completed successfully

📁 Fix files reverted (1 files)
  • src/Essentials/src/FileSystem/FileSystemUtils.android.cs

🔍 Pre-Flight — Context & Validation

Issue: #34889 - MediaPicker.PickPhotos fails to modify image, tries to load original source, fails to load source on Android 9.0
PR: #34981 - [Android] Fix MediaPicker.PickPhotosAsync UnauthorizedAccessException on API 28 and below
Platforms Affected: Android (API 28 and below)
Files Changed: 1 implementation, 1 test

Key Findings

  • On Android API 28 and below, ResolvePhysicalPath returns a raw filesystem path (e.g. /storage/emulated/0/Download/photo.png) by querying the content resolver's _data column. File.Exists() passes (only needs stat(2) which only requires execute permission on parent dir), but File.OpenRead() fails with UnauthorizedAccessException because the app lacks READ_EXTERNAL_STORAGE and only has a temporary grant for the original content:// URI.
  • The fix adds IsFileReadable() using Java.IO.File.IsFile && CanRead() to gate the resolved path, causing unreadable paths to fall through to CacheContentFile() which copies via the content resolver (respecting the temporary URI grant).
  • On API 29+ (scoped storage), ResolvePhysicalPath typically returns null for content URIs anyway, so that code path was already using CacheContentFile(). This fix aligns API 28- behavior with API 29+.
  • Prior agent review: 3 inline comment threads from copilot-pull-request-reviewer, all marked resolved by the author (IsFile check, SetReadable assertion, comment wording).
  • All CI checks pass (maui-pr + all sub-jobs). ✅

Code Review Summary

Verdict: LGTM
Confidence: high
Errors: 0 | Warnings: 0 | Suggestions: 3

Key code review findings:

  • 💡 IsFileReadable_Returns_False_For_Inaccessible_Path silently no-ops on rooted CI emulators via return — the critical permission test case never runs on CI (FileSystemUtils_Tests.cs:41-53)
  • 💡 IsFile check in IsFileReadable is redundant at the current call site (informational; keeps method self-contained for future callers) (FileSystemUtils.android.cs:80-82)
  • 💡 IsFileReadable has no null guard (safe at call site today due to !string.IsNullOrWhiteSpace guard upstream; low priority) (FileSystemUtils.android.cs:78-82)

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34981 Add IsFileReadable using Java.IO.File.IsFile && CanRead() to gate resolved path in EnsurePhysicalPath; falls through to CacheContentFile if not readable ✅ PASSED (Gate) FileSystemUtils.android.cs, FileSystemUtils_Tests.cs Original PR

🔬 Code Review — Deep Analysis

Code Review — PR #34981

Independent Assessment

What this changes: EnsurePhysicalPath in FileSystemUtils.android.cs now gates on a new IsFileReadable helper before returning a resolved physical path. The helper wraps Java.IO.File.IsFile && Java.IO.File.CanRead(), which bridges to Android's access(path, R_OK) syscall. Three device tests for IsFileReadable are added.

Inferred motivation: On Android API 28 and below, ResolvePhysicalPath can return a raw path like /storage/emulated/0/Download/photo.png after querying the content resolver's _data column. File.Exists() (used inside ResolvePhysicalPath) passes because stat(2) only requires execute permission on the parent directory — but File.OpenRead() fails with UnauthorizedAccessException because the app lacks READ_EXTERNAL_STORAGE and only holds a temporary grant for the original content:// URI. The fix routes unreadable paths to the CacheContentFile() fallback, which copies via the content resolver and respects the temporary URI grant.


Reconciliation with PR Narrative

Author claims: MediaPicker.PickPhotosAsync throws UnauthorizedAccessException on Android 9 (API 28) because EnsurePhysicalPath returns a raw path that exists but isn't readable, then the caller tries to open it. On API 29+, scoped storage means ResolvePhysicalPath returns null for content URIs and the code already uses CacheContentFile.

Agreement: Independent assessment matches this root cause exactly. The author's description of why File.Exists() passes while File.OpenRead() fails is technically accurate. The fix is well-scoped.


Findings

💡 Suggestion — Silent test skip on rooted emulators masks the critical test case

IsFileReadable_Returns_False_For_Inaccessible_Path calls javaFile.SetReadable(false) and silently returns if it fails:

if (!javaFile.SetReadable(false))
{
    return; // Permission change not supported on this device/config — skip test
}

Standard Android CI emulators run as root. On rooted devices, SetReadable(false) typically fails (or succeeds but canRead() still returns true because root bypasses permission checks). Either way, the test exits silently via return and records a pass. This is the most important test case — it directly exercises the permission scenario the fix addresses — but it effectively never runs on CI.

Consider either replacing return with an explicit xUnit skip (if Assert.Skip / Skip.If is available in this test project), or add a Console.WriteLine diagnostic so the skip is at least visible in test logs. Alternatively, document clearly in the test comment that this case is expected to be a no-op on rooted devices.

💡 Suggestion — IsFile check in IsFileReadable is redundant at the call site (minor)

internal static bool IsFileReadable(string path)
{
    using var file = new Java.IO.File(path);
    return file.IsFile && file.CanRead();
}

ResolvePhysicalPath already verifies File.Exists() before returning a path. So any absolute value that reaches IsFileReadable via EnsurePhysicalPath is guaranteed to be an existing regular file — IsFile will always be true there. That said, keeping IsFile makes the method self-contained and safe for future callers. Informational — no change required.

💡 Suggestion — IsFileReadable has no null guard

The method doesn't check for null or empty path:

internal static bool IsFileReadable(string path)
{
    using var file = new Java.IO.File(path);   // NullPointerException if path is null
    return file.IsFile && file.CanRead();
}

At the single call site in EnsurePhysicalPath, path is already guarded by !string.IsNullOrWhiteSpace(absolute), so this is safe today. But a null/empty guard (returning false) would make the method more robust if ever called directly.


Devil's Advocate

Could IsFileReadable return false for a file the app can actually read? Only if canRead() returns false incorrectly — not possible under normal Android security semantics. If canRead() returns true, the code path is unchanged (returns the resolved path as before).

Could routing to CacheContentFile() cause regressions? CacheContentFile() copies the file to the local cache. The old code was already broken for the unreadable-path case (it returned an inaccessible path, causing an exception at the caller). So routing to the fallback is strictly better. The only trade-off is copy overhead, which is appropriate because the direct path was unusable anyway.

Is the file:// URI case still correct? Yes — EnsurePhysicalPath returns early for file:// URIs before any readability check. This is correct and unchanged.

CI status: All required checks pass (maui-pr and all sub-jobs). ✅


Verdict: LGTM

Confidence: high
Summary: The fix is technically correct, precisely targeted at the API 28 permission gap, and well-explained in the inline comment. The IsFileReadable helper is clean, uses using for proper JNI peer disposal, and is correctly gated by existing null/whitespace checks at its call site. The only substantive concern is that the permission-revocation device test silently no-ops on rooted CI emulators — the core scenario the fix addresses never gets exercised in automated testing. The fix can merge; the test gap is worth a follow-up comment to the author.


🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (claude-opus-4.6) Try-Open validation: attempt System.IO.File.OpenRead(path) in try-catch; catch UnauthorizedAccessException/IOException and fall through to CacheContentFile ✅ PASS FileSystemUtils.android.cs, FileSystemUtils_Tests.cs No Java interop; catches exact exception callers would see; slight overhead on happy path
2 try-fix (claude-sonnet-4.6) Skip physical path resolution entirely on API ≤ 28 when requireExtendedAccess=true; ShouldSkipPhysicalPathResolution() helper gates before ResolvePhysicalPath ✅ PASS FileSystemUtils.android.cs, FileSystemUtils_Tests.cs Clean architecture; may skip optimistic path even when app has READ_EXTERNAL_STORAGE
3 try-fix (gpt-5.3-codex) Permission-gate _data path in GetDataFilePath: check READ_EXTERNAL_STORAGE permission status on API ≤ 28 ✅ PASS FileSystemUtils.android.cs, FileSystemUtils_Tests.cs Fixes at lowest layer (point of path production)
4 try-fix (gpt-5.4) Gate _data paths from content:// URIs: only use resolved paths inside app-owned storage ✅ PASS FileSystemUtils.android.cs, FileSystemUtils_Tests.cs Similar to Attempt 2 at a different layer
PR PR #34981 Add IsFileReadable using Java.IO.File.IsFile && CanRead() to gate resolved path in EnsurePhysicalPath ✅ PASSED (Gate) FileSystemUtils.android.cs, FileSystemUtils_Tests.cs Most targeted: validates actual runtime readability, not API version

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 2 No NO NEW IDEAS
claude-sonnet-4.6 2 Yes (not pursued) ContentResolver.openFileDescriptor() — converges on CacheContentFile mechanism
gpt-5.3-codex 2 Yes (not pursued) Always materialize via ContentResolver.OpenInputStream — same as CacheContentFile
gpt-5.4 2 Yes (not pursued) Return URI-backed FileResult with lazy cache copy — requires API signature changes, out of scope

Exhausted: Yes
Selected Fix: PR's fix — most targeted (validates actual runtime readability, not API version); preserves optimal path for apps that legitimately have READ_EXTERNAL_STORAGE; minimal footprint (1-line gate + small helper with proper JNI disposal)


📋 Report — Final Recommendation

✅ Final Recommendation: APPROVE

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE Issue #34889 + PR #34981 context gathered; 2 changed files classified
Code Review LGTM (high) 0 errors, 0 warnings, 3 suggestions
Gate ✅ PASSED Android
Try-Fix ✅ COMPLETE 4 attempts, 4 passing; PR's fix selected
Report ✅ COMPLETE

Code Review Impact on Try-Fix

Code review found no errors or warnings (LGTM, high confidence). The 3 suggestions were advisory: silent test skip on rooted emulators, redundant IsFile check, and missing null guard. These were passed as background context to try-fix models. Attempt 2 (skip on API ≤ 28) and Attempt 3 (permission gate in GetDataFilePath) were partly inspired by the suggestion that the rooted-emulator test case always no-ops — those approaches avoid the readability-check problem entirely. However, the PR's approach turned out to be the most targeted because it validates actual runtime readability rather than relying on API version or permission status, which can diverge in practice.

Summary

PR #34981 fixes UnauthorizedAccessException in MediaPicker.PickPhotosAsync on Android API 28 and below. The fix is technically correct, well-scoped, passes all CI checks, and was validated against 4 independent alternative approaches — all of which also pass but are less precise than the PR's solution.

Selected Fix: PR's fix (IsFileReadable using Java.IO.File.IsFile && CanRead())

Root Cause

On Android API 28 and below, ResolvePhysicalPath queries the content resolver's _data column and returns a raw filesystem path. File.Exists() passes (only needs execute permission on the parent directory), but File.OpenRead() fails with UnauthorizedAccessException because the app has only a temporary content:// URI grant from the picker — not READ_EXTERNAL_STORAGE. On API 29+ (scoped storage), ResolvePhysicalPath returns null for content URIs, so CacheContentFile is always used and the issue doesn't occur.

Fix Quality

The fix is minimal and surgical:

  • Adds IsFileReadable(string path) using Java.IO.File.IsFile && CanRead() — bridging to Android's access(path, R_OK) syscall
  • Uses using for correct JNI peer disposal
  • Call site already guards against null/empty via !string.IsNullOrWhiteSpace(absolute)
  • Correctly handles both cases: apps with READ_EXTERNAL_STORAGE get the direct path (optimal); apps without it fall through to CacheContentFile (correct)
  • Compared to alternatives: Attempt 2 (skip on API ≤ 28) is architecturally cleaner but too conservative — it always copies to cache even when the app legitimately has the permission; the PR's approach validates actual runtime readability

One follow-up worth noting to the author: IsFileReadable_Returns_False_For_Inaccessible_Path silently exits via return on rooted CI emulators (where SetReadable(false) has no effect), so the critical permission-check scenario is never exercised on CI. The fix is still correct; this is a test coverage gap that could be improved in a follow-up.


@MauiBot MauiBot added s/agent-approved AI agent recommends approval - PR fix is correct and optimal and removed s/agent-changes-requested AI agent recommends changes - found a better alternative or issues labels Apr 20, 2026
@kubaflo kubaflo changed the base branch from main to inflight/current April 20, 2026 21:41
@kubaflo kubaflo merged commit 84e5120 into dotnet:inflight/current Apr 20, 2026
34 checks passed
@github-actions github-actions Bot added this to the .NET 10 SR7 milestone Apr 20, 2026
PureWeen pushed a commit that referenced this pull request Apr 22, 2026
… on API 28 and below (#34981)

<!-- 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 throws UnauthorizedAccessException on
Android 9.0 (API 28).

### Root Cause of the issue

- On Android 9 (API 28) and below,
`FileSystemUtils.EnsurePhysicalPath()` resolves a `content://` URI to a
raw filesystem path (e.g., `/storage/emulated/0/Download/photo.png`) via
`ResolvePhysicalPath()`. The existing `File.Exists()` check passes
because `stat()` only requires execute permission on the parent
directory, but when MediaPicker later calls `File.OpenRead()` on that
path, it fails with `UnauthorizedAccessException` because the app lacks
`READ_EXTERNAL_STORAGE` runtime permission. The picker intent only
grants temporary access to the `content://` URI, not to the raw
filesystem path. On Android 10+, this code path is skipped entirely due
to scoped storage, so the fallback `CacheContentFile()` is always used
and the issue doesn't occur.

### Description of Change
**File access validation improvements:**

* Updated `EnsurePhysicalPath` in `FileSystemUtils.android.cs` to check
file readability using the new `IsFileReadable` method, preventing
exceptions when a file path exists but is not actually readable due to
permission issues.
* Added the internal method `IsFileReadable`, which uses
`Java.IO.File.canRead()` to verify read access before returning a file
path.

**Testing enhancements:**

* Added a new test class `Android_FileSystemUtils_Tests` with tests for
`IsFileReadable`, covering readable files, non-existent files, and files
with revoked read permissions to ensure correct behavior.

<!-- Enter description of the fix in this section -->

### 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 #34889

### Tested the behaviour in the following platforms

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

| Before | After |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/cd24e7f3-d090-47b3-9214-39a2a519590d">
| <video
src="https://github.com/user-attachments/assets/c3add72f-c235-46eb-bda7-dcb5ea0950c4">
|








<!--
Are you targeting main? All PRs should target the main branch unless
otherwise noted.
-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates 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.PickPhotos fails to modify image, tries to load original source, fails to load source on Android 9.0

6 participants