[Android] Fix MediaPicker.PickPhotosAsync UnauthorizedAccessException on API 28 and below#34981
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34981Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34981" |
|
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. |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
Fixes 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 newIsFileReadablehelper. - Add
FileSystemUtils.IsFileReadable(Android) based onJava.IO.File.CanRead(). - Add Android device tests covering
IsFileReadablefor 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. |
kubaflo
left a comment
There was a problem hiding this comment.
Could you please review the AI's and copilot's suggestions?
Addressed the AI and copilot suggestions |
🤖 AI Summary
📊 Review Session —
|
| 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,
ResolvePhysicalPathreturns a raw filesystem path (e.g./storage/emulated/0/Download/photo.png) by querying the content resolver's_datacolumn.File.Exists()passes (only needsstat(2)which only requires execute permission on parent dir), butFile.OpenRead()fails withUnauthorizedAccessExceptionbecause the app lacksREAD_EXTERNAL_STORAGEand only has a temporary grant for the originalcontent://URI. - The fix adds
IsFileReadable()usingJava.IO.File.IsFile && CanRead()to gate the resolved path, causing unreadable paths to fall through toCacheContentFile()which copies via the content resolver (respecting the temporary URI grant). - On API 29+ (scoped storage),
ResolvePhysicalPathtypically returns null for content URIs anyway, so that code path was already usingCacheContentFile(). 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_Pathsilently no-ops on rooted CI emulators viareturn— the critical permission test case never runs on CI (FileSystemUtils_Tests.cs:41-53) - 💡
IsFilecheck inIsFileReadableis redundant at the current call site (informational; keeps method self-contained for future callers) (FileSystemUtils.android.cs:80-82) - 💡
IsFileReadablehas no null guard (safe at call site today due to!string.IsNullOrWhiteSpaceguard 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)usingJava.IO.File.IsFile && CanRead()— bridging to Android'saccess(path, R_OK)syscall - Uses
usingfor 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.
… 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. -->
… 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. -->
… 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. -->
… 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. -->
… 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. -->
… on API 28 and below (dotnet#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 dotnet#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. -->
## What's Coming .NET MAUI inflight/candidate introduces significant improvements across all platforms with focus on quality, performance, and developer experience. This release includes 85 commits with various improvements, bug fixes, and enhancements. ## Button - [Android, iOS] Button: Fix VisualState properties not restored when leaving custom state by @BagavathiPerumal in #33346 <details> <summary>🔧 Fixes</summary> - [Button VisualStates do not work](#19690) </details> ## CollectionView - Fix CollectionView grid spacing updates for first row and column by @KarthikRajaKalaimani in #34527 <details> <summary>🔧 Fixes</summary> - [[MAUI] I2_Vertical grid for horizontal Item Spacing and Vertical Item Spacing - horizontally updating the spacing only applies to the second column](#34257) </details> - CarouselView: Fix cascading PositionChanged/CurrentItemChanged events on collection update by @praveenkumarkarunanithi in #31275 <details> <summary>🔧 Fixes</summary> - [[Windows] CurrentItemChangedEventArgs and PositionChangedEventArgs Not Working Properly in CarouselView](#29529) </details> - [Windows] Fixed ItemSpacing doesn't work in Carousel View by @SubhikshaSf4851 in #30014 <details> <summary>🔧 Fixes</summary> - [ItemSpacing on CarouselView is not applied on Windows.](#29772) </details> - Fix CollectionView not scrolling to top on iOS status bar tap by @jfversluis in #34687 <details> <summary>🔧 Fixes</summary> - [[iOS] UICollectionView ScrollToTop does not work](#19866) </details> - [iOS] Fixed CollectionView Scroll Jitter for TextType HTML Labels by @SubhikshaSf4851 in #34383 <details> <summary>🔧 Fixes</summary> - [CollectionView scrolling is jittery when ItemTemplate contains Label with TextType="Html" in .NET 10](#33065) </details> - Fix CollectionView Header is not visible when ItemsSource is not set and an EmptyView is set in iOS, Mac platform by @KarthikRajaKalaimani in #34989 <details> <summary>🔧 Fixes</summary> - [CollectionView Header is not visible when ItemsSource is not set and EmptyView is set in iOS, Mac platform](#34897) </details> - [Android] Fix CollectionView EmptyView not displayed correctly by @KarthikRajaKalaimani in #34956 <details> <summary>🔧 Fixes</summary> - [[Android] CollectionView - EmptyView not displayed correctly](#34861) </details> - [iOS] Fix CollectionView ScrollOffset not resetting when ItemsSource changes by @SyedAbdulAzeemSF4852 in #34488 <details> <summary>🔧 Fixes</summary> - [[IOS] CollectionView ScrollOffset does not reset when the ItemSource is changed in iOS.](#26366) - [Re-enable Issue7993 test on iOS/Catalyst - CollectionView scroll position not reset when updating ItemsSource](#33500) </details> - [Revert] [iOS] Fixed CollectionView Scroll Jitter for TextType HTML Labels by @SubhikshaSf4851 in #35341 ## Core Lifecycle - [Android] Fix NRE in ContainerView when Android Context is null during lifecycle transition by @rmarinho in #34901 <details> <summary>🔧 Fixes</summary> - [[Android] NullReferenceException in NavigationRootManager.Connect when mapping Window content](#34900) </details> ## DateTimePicker - [Android] Fix for TimePicker Dialog doesn't update the layout when rotating the device with dialog open by @HarishwaranVijayakumar in #31910 <details> <summary>🔧 Fixes</summary> - [[Android] TimePicker Dialog doesn't update the layout when rotating the device with dialog open](#31658) </details> - [Android, iOS] Fixed TimePicker FlowDirection Not Applied Across Platforms by @Dhivya-SF4094 in #30369 <details> <summary>🔧 Fixes</summary> - [TimePicker FlowDirection Not Working on All Platforms](#30192) </details> - [Windows] Fixed TimePicker CharacterSpacing issue by @SubhikshaSf4851 in #30533 <details> <summary>🔧 Fixes</summary> - [[Windows] TimePicker CharacterSpacing Property Not Working on Windows](#30199) </details> - [MacCatalyst] Fix DatePicker Opened/Closed events not being raised by @SubhikshaSf4851 in #34970 <details> <summary>🔧 Fixes</summary> - [[MacCatalyst] DatePicker Opened and Closed events are not raised on Mac platform](#34848) </details> ## Dialogalert - [Android] Fix AlertDialog, ActionSheet, and Prompt render with Material 2 styles when Material 3 is enabled by @HarishwaranVijayakumar in #35121 <details> <summary>🔧 Fixes</summary> - [[Android] AlertDialog, ActionSheet, and Prompt render with Material 2 styles when Material 3 is enabled](#35119) </details> ## Docs - docs: Add UITesting-Guide, ReleasePlanning, and ReleaseProcess to docs/README.md index by @PureWeen in #35195 - docs: Fix hardcoded path and add library overview in Essentials.AI README by @PureWeen in #35194 - docs: Update branch reference from net10.0 to net11.0 in DEVELOPMENT.md by @PureWeen in #35193 ## Drawing - Fix Path Rendering Issue Inside StackLayout When Margin Is Set by @Shalini-Ashokan in #28071 <details> <summary>🔧 Fixes</summary> - [Path does not render if it has Margin](#13801) </details> - Fixed FlowDirection property not working on Drawable control and GraphicsView by @Dhivya-SF4094 in #34557 <details> <summary>🔧 Fixes</summary> - [[Android, Windows, iOS, macOS] FlowDirection property not working on BoxView Control](#34402) </details> - [iOS & Mac] Fix image tile misalignment in GraphicsView ImagePaint by @SubhikshaSf4851 in #34935 <details> <summary>🔧 Fixes</summary> - [[iOS] Image resized with ResizeMode.Fit is not rendered correctly in GraphicsView](#34755) </details> - Fix Shadow does not honour Styles by @KarthikRajaKalaimani in #35081 <details> <summary>🔧 Fixes</summary> - [Shadow does not honour Styles](#19560) </details> ## Entry - [iOS/macCatalyst] Fix Entry and Editor BackgroundColor reset when set to null by @Shalini-Ashokan in #34741 <details> <summary>🔧 Fixes</summary> - [[iOS, Maccatalyst] Entry & Editor BackgroundColor not reset to Null](#34611) </details> - [Windows] Fix password Entry crash when setting text on empty field by @praveenkumarkarunanithi in #33891 <details> <summary>🔧 Fixes</summary> - [[WinUI] Password Obfuscation causes unhandled crash](#33334) </details> ## Essentials - [Essentials] Use mean sea level altitude on Android API 34+ by @KitKeen in #35097 <details> <summary>🔧 Fixes</summary> - [Add support for MslAltitudeMeters in Essentials Geolocation on Android](#27554) </details> ## Flyout - Fixed Flyout Not Displayed on Android When FlyoutWidth Is Set Only for Desktop via OnIdiom by @NanthiniMahalingam in #29028 <details> <summary>🔧 Fixes</summary> - [[Android] FlyoutWidth with OnIdiom shows no flyout](#13243) </details> - Revert "[Windows] Fix Flyout/Locked mode header collapse regression causing UI test failures on candidate branch" by @kubaflo in #35339 - Revert "Revert "[Windows] Fix Flyout/Locked mode header collapse regression causing UI test failures on candidate branch"" by @kubaflo in #35342 ## Flyoutpage - Fix [Android] Title of FlyOutPage is not updating anymore after showing a NonFlyOutPage by @KarthikRajaKalaimani in #34839 <details> <summary>🔧 Fixes</summary> - [[Android] Title of FlyOutPage is not updating anymore after showing a NonFlyOutPage](#33615) </details> ## Label - [iOS] Fix span Tap gesture on wrapped Label lines in iOS 26+ by @SubhikshaSf4851 in #34640 <details> <summary>🔧 Fixes</summary> - [[iOS]Span TapGestureRecognizer does not work on the second line of the span, if the span is wrapped to the next line](#34504) </details> ## Layout - Fixed Stacklayout is not rendered when clip is applied and StackLayout placed child to the Border control in iOS/ Mac platform by @KarthikRajaKalaimani in #33330 <details> <summary>🔧 Fixes</summary> - [[Mac/iOS] StackLayout fails to render content while applying Clip, and the layout is placed inside a Border with Background in .NET MAUI](#33241) </details> ## Map - Fix Changing Location on a Pin does nothing by @NirmalKumarYuvaraj in #30201 <details> <summary>🔧 Fixes</summary> - [[Maps] [Regression from Xamarin.Forms.Maps] Changing Location on a Pin does nothing](#12916) </details> ## Mediapicker - [iOS] Fix HEIC images picked via PickPhotosAsync not displayed by @HarishwaranVijayakumar in #34954 <details> <summary>🔧 Fixes</summary> - [[iOS] [Regression] HEIC images picked via PickPhotosAsync not displayed](#34953) </details> - [Android] Fix MediaPicker.PickPhotosAsync UnauthorizedAccessException on API 28 and below by @HarishwaranVijayakumar in #34981 <details> <summary>🔧 Fixes</summary> - [MediaPicker.PickPhotos fails to modify image, tries to load original source, fails to load source on Android 9.0](#34889) </details> ## Pages - [iOS] Fix ContentPage with ToolbarItem Clicked event leaks when presented as modal page by @devanathan-vaithiyanathan in #35009 <details> <summary>🔧 Fixes</summary> - [ContentPage with ToolbarItem Clicked event leaks when presented as modal page](#34892) </details> ## Platform - [Android] Fix OnBackButtonPressed not invoked for Shell by @Dhivya-SF4094 in #35150 <details> <summary>🔧 Fixes</summary> - [On Screen Back Button Does Not Fire OnBackButtonPressed in Android](#9095) </details> ## RadioButton - Fix RadioButtonGroup not working with ContentView by @Dhivya-SF4094 in #34781 <details> <summary>🔧 Fixes</summary> - [RadioButtonGroup not working with ContentView](#34759) </details> - [Windows] Fix for RadioButton BorderColor and BorderWidth not updated at runtime by @SyedAbdulAzeemSF4852 in #28335 <details> <summary>🔧 Fixes</summary> - [RadioButton Border color not working for focused visual state](#15806) </details> - [iOS] Fix RadioButton BackgroundColor bleeding outside CornerRadius by @SyedAbdulAzeemSF4852 in #34844 <details> <summary>🔧 Fixes</summary> - [[iOS] RadioButton BackgroundColor bleeds outside CornerRadius](#34842) </details> ## SafeArea - [iOS] Fix stale bottom safe area after changing SafeAreaEdges with keyboard open by @praveenkumarkarunanithi in #35083 <details> <summary>🔧 Fixes</summary> - [[iOS] ContentPage bottom has white space after changing SafeAreaEdges while keyboard is open](#34846) </details> ## ScrollView - [Windows] Fix Preserve ScrollView offsets when Orientation changes to Neither by @SubhikshaSf4851 in #34827 <details> <summary>🔧 Fixes</summary> - [[Windows] ScrollView offsets do not preserve when Orientation changes to Neither](#34671) </details> ## Searchbar - [iOS] Fix SearchBar unexpected left margin in iPad windowed mode on 26 Version by @SubhikshaSf4851 in #34704 <details> <summary>🔧 Fixes</summary> - [in iPad windowed mode SearchBar adds left margin equivaltent to SafeAreaInsets when placed inside grid](#34551) </details> ## Shell - [Windows] Fix for Shell.FlyoutBehavior="Flyout" forces the title height space above the tab bar even if the page title is empty by @BagavathiPerumal in #30382 <details> <summary>🔧 Fixes</summary> - [(Windows) Shell.FlyoutBehavior="Flyout" forces the title height space above the tab bar even if the page title is empty](#30254) </details> - Fix Shell flyout items scrolling behind FlyoutHeader on iOS by @Qythyx in #34936 <details> <summary>🔧 Fixes</summary> - [Shell flyout items scroll behind FlyoutHeader on iOS](#34925) </details> - [iOS, Mac] Fix Shell.CurrentState.Location stale in OnNavigated after GoToAsync by @Vignesh-SF3580 in #34880 <details> <summary>🔧 Fixes</summary> - [Shell.OnNavigated not called for route navigation](#34662) </details> - [iOS26]Fix BackButtonBehavior_IsEnabled_False_BackButtonDoesNotNavigate UITest fails by @devanathan-vaithiyanathan in #34890 <details> <summary>🔧 Fixes</summary> - [[iOS 26] BackButtonBehavior_IsEnabled_False_BackButtonDoesNotNavigate test fails with TimeoutException](#34771) </details> - [iOS] Fix Shell page memory leak when using TitleView with x:Name by @Shalini-Ashokan in #35082 <details> <summary>🔧 Fixes</summary> - [[iOS] Title view memory leak](#34975) </details> - [Material 3] Fix Material 2 color flash in AppBar when switching tabs for the first time by @Dhivya-SF4094 in #35117 <details> <summary>🔧 Fixes</summary> - [Material 3: AppBar briefly displays Material 2 colors when switching tabs for the first time](#35116) </details> - [Android] Fix Shell/TabbedPage "More" BottomSheet uses hard-coded M2 colors when Material3 is enabled by @HarishwaranVijayakumar in #35129 <details> <summary>🔧 Fixes</summary> - [[Android] Shell/TabbedPage "More" BottomSheet uses hard-coded M2 colors when Material3 is enabled](#35127) </details> - [Android] Shell: Fix top-tab unselected text visibility in Material 3 light theme by @SyedAbdulAzeemSF4852 in #35128 <details> <summary>🔧 Fixes</summary> - [[Android] Shell top-tab unselected text appears too faint in Material 3 light theme](#35125) </details> - Fix Shell.Items.Clear() memory leak by disconnecting child handlers on removal (#34898) by @Shalini-Ashokan in #35031 <details> <summary>🔧 Fixes</summary> - [Shell.Items.Clear() does not disconnect handlers correctly](#34898) </details> - [iOS&Mac] Fix Shell SearchHandler Query update on Initial load by @SubhikshaSf4851 in #35008 <details> <summary>🔧 Fixes</summary> - [[iOS&Mac] Shell SearchHandler Query not shown in search bar on initial load](#35005) </details> ## SwipeView - [iOS,MacCatalyst] Fix for SwipeView.Open() throwing an ArgumentException on the second programmatic call by @BagavathiPerumal in #34982 <details> <summary>🔧 Fixes</summary> - [[net 11.0][iOS,MacCatalyst] SwipeView.Open() throws ArgumentException on second programmatic call](#34917) </details> - [Android/iOS] Fix SwipeItem visibility change causing double command execution in Execute mode by @praveenkumarkarunanithi in #35087 <details> <summary>🔧 Fixes</summary> - [Changing visibility on an SwipeItem causes multiple items to be executed](#7580) </details> ## Switch - [iOS] Fix Switch ThumbColor reset on iOS 26+ theme changes. by @Shalini-Ashokan in #33953 <details> <summary>🔧 Fixes</summary> - [Switch ThumbColor not Initialized Using VisualStateManager on iOS Device](#33783) - [I9-On macOS 26.2, the "Animate scroll" button is white by default on iOS and Maccatalyst platforms.](#33767) </details> ## TabbedPage - [Windows] TabbedPage: Refresh layout when NavigationView size changes by @BagavathiPerumal in #26217 <details> <summary>🔧 Fixes</summary> - [TabbedPage - ScrollView not allowing scrolling when it should](#26103) - [TabbedPage App on resize hides page bottom content](#11402) - [Grid overflows child ContentPage of parent TabbedPage on initial load and when resizing on Windows](#20028) </details> - [Android] Material 3 Fixed BottomNavigationView overflowing in Tabbed page by @NirmalKumarYuvaraj in #35064 <details> <summary>🔧 Fixes</summary> - [[Android] Material3 - TabbedPage bottom tabs overflowing the contents](#35063) </details> - [Windows] Fix for Multiple Tabs Being Selected in WinUI TabbedPage by @SyedAbdulAzeemSF4852 in #33312 <details> <summary>🔧 Fixes</summary> - [WinUI TabbedPage can have multiple tabs selected](#31799) </details> ## Theming - [iOS] Fix StaticResource Hot Reload crash on iOS by @StephaneDelcroix in #35020 <details> <summary>🔧 Fixes</summary> - [The maui app quit and no errors in error list after editing ResourceDictionary XAML file on iOS Simulator with MAUI SR6 10.0.60](#35018) </details> ## Toolbar - [Windows] Fix for CS1061 build error caused by missing HasMenuBarContent property in MauiToolbar by @BagavathiPerumal in #35040 ## Tooling - Fix VisualStateGroups duplicate name crash with implicit styles (#34716) by @StephaneDelcroix in #34719 <details> <summary>🔧 Fixes</summary> - [SourceGen: VisualStateManager.VisualStateGroups causes 'Names must be unique' at startup](#34716) </details> ## WebView - Refactor the HybridWebView and properly support complex parameters by @mattleibow in #32491 - [Android] Fix WebView scrolling inside ScrollView by @Shalini-Ashokan in #33133 <details> <summary>🔧 Fixes</summary> - [[Android] WebView's content does not scroll when placed inside a ScrollView](#32971) </details> <details> <summary>🔧 Infrastructure (1)</summary> - [Windows] Fix Narrator announcing ContentView children twice when Description is set by @praveenkumarkarunanithi in #33979 <details> <summary>🔧 Fixes</summary> - [[Windows] SemanticProperties.Description announced twice when set on focusable container cell (Label inside)](#33373) </details> </details> <details> <summary>🧪 Testing (14)</summary> - [Testing] SafeArea Feature Matrix Test Cases for ContentPage by @TamilarasanSF4853 in #34877 - [Windows] Fix CollectionView ScrollTo related test cases failed in CI by @HarishwaranVijayakumar in #34907 <details> <summary>🔧 Fixes</summary> - [[Testing][Windows]CollectionView ScrollTo related test cases failed in CI](#34772) </details> - [Testing] Fixed Build error on inflight/ candidate PR 35234 by @HarishKumarSF4517 in #35241 - Fix CI for ValidateKeyboardRuntime_SwitchContainerToSoftInput_WhileKeyboardOpen test failure in May 4th Candidate by @devanathan-vaithiyanathan in #35307 - [Windows] Fix Flyout/Locked mode header collapse regression causing UI test failures on candidate branch by @BagavathiPerumal in #35312 - [iOS/macCatalyst] [Candidate Fix] Editor shadow and theme regression caused by BackgroundColor reset on initial handler connection by @Shalini-Ashokan in #35343 - [Testing] Fixed UI test image failure in PR 35234 - [30/03/2026] Candidate - 1 by @HarishKumarSF4517 in #35325 - [iOS] Fix ShellFeatureMatrix test failures on candidate branch by @Vignesh-SF3580 in #35346 - [Windows] Fix Issue29529VerifyPreviousPositionOnInsert test failure on candidate branch by @praveenkumarkarunanithi in #35398 - [Android] [Candidate Fix] Shell: Fix handler disconnect timing to preserve WebView navigation and memory leak fix by @Shalini-Ashokan in #35417 - [Testing]Revert 'Fix Preserve ScrollView offsets when Orientation changes to Neither' by @TamilarasanSF4853 in #35412 - [Windows] Fix VerifyAllIndicatorDotsShowShadowsWhenIndicatorSize test failure on candidate branch by @praveenkumarkarunanithi in #35458 - [Testing] Fixed test failure in PR 35234 - [05/08/2026] Candidate by @TamilarasanSF4853 in #35362 - [Testing] Fixed test failure in PR 35234 - [05/04/2026] Candidate - 3 by @TamilarasanSF4853 in #35639 </details> <details> <summary>📦 Other (6)</summary> - [UIKit] Avoid useless measure invalidation propagation cycles by @albyrock87 in #33459 - BindableObject property access micro-optimizations by @albyrock87 in #33584 - Extract filename from DisplayName and add extension if missing by @mattleibow in #35050 - [core] Add keyed-DI screenshot extensibility for 3rd-party platform backends by @Redth in #35096 <details> <summary>🔧 Fixes</summary> - [`ViewExtensions.CaptureAsync(IView)` and `IPlatformScreenshot` need extensibility for third-party platform backends](#34266) </details> - Fix MainThread throwing on custom platform backends by @Redth in #35070 <details> <summary>🔧 Fixes</summary> - [`MainThread.BeginInvokeOnMainThread` throws on custom platform backends - Common UI-thread marshaling pattern crashes; `Dispatcher` works but isn't the documented/recommended path](#34101) </details> - Tests: Add 11 missing UnitConverters unit tests by @PureWeen in #35191 </details> <details> <summary>📝 Issue References</summary> Fixes #7580, Fixes #9095, Fixes #11402, Fixes #12916, Fixes #13243, Fixes #13801, Fixes #15806, Fixes #19560, Fixes #19690, Fixes #19866, Fixes #20028, Fixes #26103, Fixes #26366, Fixes #27554, Fixes #29529, Fixes #29772, Fixes #30192, Fixes #30199, Fixes #30254, Fixes #31658, Fixes #31799, Fixes #32971, Fixes #33065, Fixes #33241, Fixes #33334, Fixes #33373, Fixes #33500, Fixes #33615, Fixes #33767, Fixes #33783, Fixes #34101, Fixes #34257, Fixes #34266, Fixes #34402, Fixes #34504, Fixes #34551, Fixes #34611, Fixes #34662, Fixes #34671, Fixes #34716, Fixes #34755, Fixes #34759, Fixes #34771, Fixes #34772, Fixes #34842, Fixes #34846, Fixes #34848, Fixes #34861, Fixes #34889, Fixes #34892, Fixes #34897, Fixes #34898, Fixes #34900, Fixes #34917, Fixes #34925, Fixes #34953, Fixes #34975, Fixes #35005, Fixes #35018, Fixes #35063, Fixes #35116, Fixes #35119, Fixes #35125, Fixes #35127 </details> **Full Changelog**: main...inflight/candidate
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Issue Details
Root Cause of the issue
FileSystemUtils.EnsurePhysicalPath()resolves acontent://URI to a raw filesystem path (e.g.,/storage/emulated/0/Download/photo.png) viaResolvePhysicalPath(). The existingFile.Exists()check passes becausestat()only requires execute permission on the parent directory, but when MediaPicker later callsFile.OpenRead()on that path, it fails withUnauthorizedAccessExceptionbecause the app lacksREAD_EXTERNAL_STORAGEruntime permission. The picker intent only grants temporary access to thecontent://URI, not to the raw filesystem path. On Android 10+, this code path is skipped entirely due to scoped storage, so the fallbackCacheContentFile()is always used and the issue doesn't occur.Description of Change
File access validation improvements:
EnsurePhysicalPathinFileSystemUtils.android.csto check file readability using the newIsFileReadablemethod, preventing exceptions when a file path exists but is not actually readable due to permission issues.IsFileReadable, which usesJava.IO.File.canRead()to verify read access before returning a file path.Testing enhancements:
Android_FileSystemUtils_Testswith tests forIsFileReadable, 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
BeforeFix_34889.mov
AfterFix_34889.mov