dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.11k stars 1.73k forks source link

Scoped Storage (Android) Not Allowing Files To Be Saved, But Saves To Internal Storage Work Fine #5295

Open ctmobiledev opened 2 years ago

ctmobiledev commented 2 years ago

Description

NOTE: This issue occurs with Visual Studio Version 17.2.0 Preview 1.0. There's no item for that in the drop-down below, so I wanted to report that first.

This is an issue I've only observed writing an app using .NET MAUI for the Android platform, and it pertains solely to a feature unique to Android: ScopedStorage.

Simply put, an app can create a folder successfully using the Android folder picker, but when processing attempts to create a new file in that selected folder - such as with WriteAllLines - processing chokes with a System.UnauthorizedAccessException.

I tested by attempting a write to the Android app's internal "sandbox" space, which normally has a folder named similar to this:

/storage/emulated/0/Android/data/com.companyname.mauiinputoutputtest1/files

The exception above doesn't happen, and the file is visible when an Android device is attached to a workstation via USB and the File Manager.

Here's the project I used (with bin and obj folders removed to reduce space):

maui-io-scoped-storage-create-file-bug.zip

The file in question is Platforms/Android/FileHelper.cs.

The routines of interest are:

public static int BackupAllData() - starting at line 126 public static Boolean CreateBackupFolder(String bfolder) - starting at line 217 public static void MakeFile(String fileName) - starting at line 329

To see the exception occur, comment out line 346:

346 var outputFile = Path.Combine(PrivateExternalFolder, outname);

And comment in line 350:

350 var outputFile = Path.Combine(deviceStoragePath, fileName); If your test results are consistent with mine, your output should resemble the following:

[0:] >>>*****************************************
[0:] >>> BackupAllData() fired
[0:] >>>*****************************************
[0:] >>>************************************
[0:] >>> ANDROID PLATFORM DETECTED
[0:] >>>************************************
[0:] >>> Key selectedStorageUri value is: content://com.android.externalstorage.documents/tree/primary%3ADummy1
[0:] >>>********************************************************************
[0:] >>> deviceStorageUri  = content://com.android.externalstorage.documents/tree/primary%3ADummy1
[0:] >>> deviceStoragePath = /storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4
[0:] >>> appStorageRootPath = /storage/emulated/0/Dummy1
[0:] >>> CreateBackupFolder called for path: /storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4
[0:] >>> Attempting to create path: /storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4
[0:] >>> SUCCESSFUL.
[0:] >>> BackupAllData: createSuccess = True
[0:] >>> BackupAllData: calling other backup routines...
[0:] >>> MakeFile fired
[0:] >>> outputFile = /storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4/dummy_output.txt
[0:] >>> Filling listOutputLines
[0:] >>> listOutputLines.Count = 20
[0:] >>> Performing WriteAllLines to outputFile: /storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4/dummy_output.txt
[0:] >>> MakeFile exception: System.UnauthorizedAccessException: Access to the path '/storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4/dummy_output.txt' is denied.
 ---> System.IO.IOException: Operation not permitted
   --- End of inner exception stack trace ---
   at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirectory, Func`2 errorRewriter)
   at Interop.CheckIo(Error error, String path, Boolean isDirectory, Func`2 errorRewriter)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String path, OpenFlags flags, Int32 mode)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
   at System.IO.Strategies.UnixFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategy(FileStream fileStream, String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, Int64 preallocationSize)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, Int64 preallocationSize)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean useAsync)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize)
   at System.IO.File.Create(String path, Int32 bufferSize)
   at System.IO.File.Create(String path)
   at MauiInputOutputTest1.FileHelper.MakeFile(String fileName) in C:\Maui_Projects\MauiInputOutputTest1\MauiInputOutputTest1\Platforms\Android\FileHelper.cs:line 333
[0:] >>> BackupAllData: finished

I can get along with internal storage for the time being, but it would be nice to have scoped storage working, since that's a big deal with Android 11 now. Users wanting to create new files where they choose needs to work.

Thanks for your help!

Steps to Reproduce

  1. Attach an Android device (preferably a phone) to a workstation via USB), workstation should have .NET MAUI and VS installed (of course). Device should already be set to developer mode and have USB debugging option set to on (RSA message should already have been approved).
  2. Unzip the project from above to a work space.
  3. Compile the project.
  4. Run the project.
  5. On the Android device, tap the MauiInputOutputTest1 icon (purple .NET icon). After the splash page clears, tap the "Choose Scoped Storage Ext Folder" button and create or select a folder using the Android system dialog. Tap "Use This Folder" and the confirmation message "Allow MauiInputOutputTest1 to access files in [your folder]?" appears. On that popup, tap "Allow" and that returns you to the app.
  6. Tap the "Write File To Scoped Storage Folder" button:
  7. If internal storage is being used, generation of a new file on the Android device should occur without incident. (A popup dialog will appear saying "File in scoped storage created successfully.")
  8. If external storage is being used, generation of a new file on the Android device will produce a message similar to the following: System.UnauthorizedAccessException: Access to the path '/storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4/dummy_output.txt' is denied. You will need to view the debug console to see the output, however.

Version with bug

Unknown/Other (please specify)

Last version that worked well

Unknown/Other

Affected platforms

Android

Affected platform versions

Android 11

Did you find any workaround?

For the time being, I just use internal storage rather than user-selected scoped storage.

Relevant log output

[0:] >>>*****************************************
[0:] >>> BackupAllData() fired
[0:] >>>*****************************************
[0:] >>>************************************
[0:] >>> ANDROID PLATFORM DETECTED
[0:] >>>************************************
[0:] >>> Key selectedStorageUri value is: content://com.android.externalstorage.documents/tree/primary%3ADummy1
[0:] >>>********************************************************************
[0:] >>> deviceStorageUri  = content://com.android.externalstorage.documents/tree/primary%3ADummy1
[0:] >>> deviceStoragePath = /storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4
[0:] >>> appStorageRootPath = /storage/emulated/0/Dummy1
[0:] >>> CreateBackupFolder called for path: /storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4
[0:] >>> Attempting to create path: /storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4
[0:] >>> SUCCESSFUL.
[0:] >>> BackupAllData: createSuccess = True
[0:] >>> BackupAllData: calling other backup routines...
[0:] >>> MakeFile fired
[0:] >>> outputFile = /storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4/dummy_output.txt
[0:] >>> Filling listOutputLines
[0:] >>> listOutputLines.Count = 20
[0:] >>> Performing WriteAllLines to outputFile: /storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4/dummy_output.txt
[0:] >>> MakeFile exception: System.UnauthorizedAccessException: Access to the path '/storage/emulated/0/Dummy1/Backup 2022-03-09 at 01.21.33 v4/dummy_output.txt' is denied.
 ---> System.IO.IOException: Operation not permitted
   --- End of inner exception stack trace ---
   at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirectory, Func`2 errorRewriter)
   at Interop.CheckIo(Error error, String path, Boolean isDirectory, Func`2 errorRewriter)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String path, OpenFlags flags, Int32 mode)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
   at System.IO.Strategies.UnixFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategy(FileStream fileStream, String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, Int64 preallocationSize)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, Int64 preallocationSize)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean useAsync)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize)
   at System.IO.File.Create(String path, Int32 bufferSize)
   at System.IO.File.Create(String path)
   at MauiInputOutputTest1.FileHelper.MakeFile(String fileName) in C:\Maui_Projects\MauiInputOutputTest1\MauiInputOutputTest1\Platforms\Android\FileHelper.cs:line 333
[0:] >>> BackupAllData: finished
drasticactions commented 2 years ago

@ctmobiledev Judging by the fact that this is failing inside of Platform specific code (In this, run in Android) I don't think MAUI is influencing this and it's just an Android issue with the platform (Although I could be missing something here). @jonathanpeppers Do you have any ideas?

jonathanpeppers commented 2 years ago

@drasticactions it is failing inside the BCL:

> System.IO.IOException: Operation not permitted
   --- End of inner exception stack trace ---
   at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirectory, Func`2 errorRewriter)
   at Interop.CheckIo(Error error, String path, Boolean isDirectory, Func`2 errorRewriter)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String path, OpenFlags flags, Int32 mode)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
   at System.IO.Strategies.UnixFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)

https://github.com/dotnet/runtime/blob/c3dbdea91835d67cc461b22125e652ad5063d746/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs#L94-L98

Which means the OS gave an error code that indicates the file wasn't writeable.

If you tried to write to this same file in a Java/Kotlin Android app it would probably fail the same way? @ctmobiledev can you test that?

In either case, I don't see anything to be fixed in MAUI for this. If there is a bug, it looks like some change is needed in dotnet/runtime.

ctmobiledev commented 2 years ago

Ahhhh, but the same code when run under Xamarin.Forms completes without issue. The code was, in fact, developed originally using Xamarin.Forms, and I ported it over to .NET MAUI, hence my reason for suspicion.

I could create a completely new "dummy" Xamarin.Forms project with the ScopedStorage et al modules for comparison, but I've used this code in other applications without problems.

Get Outlook for Androidhttps://aka.ms/AAb9ysg


From: Tim Miller @.> Sent: Tuesday, March 15, 2022 7:17:37 PM To: dotnet/maui @.> Cc: CTMobileDev @.>; Mention @.> Subject: Re: [dotnet/maui] Scoped Storage (Android) Not Allowing Files To Be Saved, But Saves To Internal Storage Work Fine (Issue #5295)

@ctmobiledevhttps://github.com/ctmobiledev Judging by the fact that this is failing inside of Platform specific code (In this, run in Android) I don't think MAUI is influencing this and it's just an Android issue with the platform (Although I could be missing something here). @jonathanpeppershttps://github.com/jonathanpeppers Do you have any ideas?

— Reply to this email directly, view it on GitHubhttps://github.com/dotnet/maui/issues/5295#issuecomment-1068593315, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AGN4XN6C27NJNPOXJ2T6RUDVAESCDANCNFSM5QXUGSHQ. Triage notifications on the go with GitHub Mobile for iOShttps://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Androidhttps://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub. You are receiving this because you were mentioned.Message ID: @.***>

jonathanpeppers commented 2 years ago

@steveisok would someone on the Mono team be able to check if there is a new problem here in .NET 6?

steveisok commented 2 years ago

@mkhamoyan Please take a look.

ghost commented 2 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.