[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. -->
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