0

I am attempting to write a file on an Android device. I would then like to be able to easily transfer that file to a Windows computer for viewing and analysis.

Needless to say, I am having a lot of issues doing this. I am using .Net MAUI, and my Android device is a Samsung Galaxy Tab A7 Lite running Android 13.

I am running the following code to write a test file. I also have some code inserted as a sanity check to immediately read the file that I wrote:

string target_file = System.IO.Path.Combine(FileSystem.Current.AppDataDirectory, "test_file.txt");
using (FileStream output_stream = System.IO.File.OpenWrite(target_file))
{
    using (StreamWriter stream_writer = new StreamWriter(output_stream))
    {
        stream_writer.Write("hello, world!");
    }
}

using (FileStream input_stream = System.IO.File.OpenRead(target_file))
{
    using (StreamReader stream_reader = new StreamReader(input_stream))
    {
        var input_text = stream_reader.ReadToEnd();
    }
}

This code is working. It is writing a file, and I am am able to read the file. The problem is that I am not able to see this file anywhere outside of the application. I can't see it in the Android "My Files" app on the Samsung tablet, nor can I see it in the Windows File Explorer when I connect my Android tablet to the PC.

The file is supposedly at the following path: "/data/user/0/[com.companyname.appname]/files/test_file.txt"

According to the comments in the following Stack Overflow posts, the "solution" is to save to a "shared" folder, because apparently the private app data folder is not viewable with Windows File Explorer:

  1. Creating and Writing to a text file on Android .NET MAUI
  2. .NET MAUI writing file in android to folder that then can be accessed in windows file explorer

Unfortunately, the answer posted on this post does not work:

.NET MAUI writing file in android to folder that then can be accessed in windows file explorer

It once again just directs the saved file to be in the app's private data folder. Specifically, it tries to save at this path:

"/storage/emulated/0/Android/data/[com.companyname.appname]/files/Documents"

One suggestions that has been made is to use the .Net MAUI FileSaver (https://learn.microsoft.com/en-us/dotnet/communitytoolkit/maui/essentials/file-saver?tabs=android). While this may seemingly allow saving to a shared folder (I haven't yet tested it to fully find out), it would require user interaction to actually select the save location of the file, which is unacceptable in this scenario. My app needs to be able to save log files to a very specific folder at any time (without user interaction), and then periodically the user needs to be able to transfer those files to a computer for viewing/analysis.

Any suggestions on how to solve this issue?

David
  • 1,847
  • 4
  • 26
  • 35
  • this has been [discussed](https://www.google.com/search?q=android+writing+to+public+folder+without+user+prompt+site:stackoverflow.com) many times – Jason Feb 28 '23 at 01:15
  • Your comment is unhelpful. I already referenced several previous questions and other links in my post, and demonstrated that they don't solve the problem. – David Feb 28 '23 at 04:24

2 Answers2

2

For those wondering how to write files to a public folder, especially using .Net MAUI, I figured out how to do it. Here is how:

First off, my main resources were these two posts:

  1. Storage Access Framework persist permissions not working
  2. Android Studio, Kiosk mode, Single-Purpose Devices, Lock Task mode

The following documentation was also helpful:

  1. https://developer.android.com/reference/android/content/Intent#FLAG_GRANT_PREFIX_URI_PERMISSION
  2. https://developer.android.com/reference/android/content/ContentResolver#takePersistableUriPermission(android.net.Uri,%20int)
  3. https://developer.android.com/reference/android/content/ContentResolver#getPersistedUriPermissions()

Anyways, step 1 is to define a partial class that can then be re-defined on a per-platform basis. Here is my partial class:

namespace MyProjectName.CrossPlatformComponents
{
    public partial class ChooseLogFolder_CrossPlatform
    {
        public static int REQUEST_TREE = 85;

        public static partial void RequestSelectLogFolder();

        public static partial void OpenLogFileForWriting(string file_name, string file_contents);

        public static partial bool HasFolderBeenSelectedAndPermissionsGiven ();
    }
}

As you see, I'm basically wanting to implement 3 functions:

(1) a function to let the user select a folder - this folder will have persistable permissions so we can always read/write from/to it. It will be a public folder, not a folder a folder private to the app. (2) a function to actually write data to a file (3) a function to check and see if the folder location has been selected and permissions have been given.

Now, for .Net MAUI, assuming you want to actually deploy your app to multiple platforms, you would need to define these functions for each platform. For the sake of this post, we are only concerned about Android.

So here is my Android implementation of these functions:

namespace MyProjectName.CrossPlatformComponents
{
    public partial class ChooseLogFolder_CrossPlatform
    {
        public static partial void RequestSelectLogFolder()
        {
            var current_activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;   
            var intent = new Android.Content.Intent(Android.Content.Intent.ActionOpenDocumentTree);
            intent.AddFlags(Android.Content.ActivityFlags.GrantReadUriPermission | 
                            Android.Content.ActivityFlags.GrantWriteUriPermission | 
                            Android.Content.ActivityFlags.GrantPersistableUriPermission | 
                            Android.Content.ActivityFlags.GrantPrefixUriPermission);
            current_activity.StartActivityForResult(intent, REQUEST_TREE);
        }

        public static partial void OpenLogFileForWriting(string file_name, string file_contents)
        {
            var current_activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;

            List<UriPermission> permissions = current_activity.ContentResolver.PersistedUriPermissions.ToList();
            if (permissions != null && permissions.Count > 0 )
            {
                DocumentFile log_folder = DocumentFile.FromTreeUri(current_activity, permissions[0].Uri);
                DocumentFile log_file = log_folder.CreateFile(MimeTypeMap.Singleton.GetMimeTypeFromExtension("txt"), file_name);
                ParcelFileDescriptor pfd = current_activity.ContentResolver.OpenFileDescriptor(log_file.Uri, "w");
                FileOutputStream file_output_stream = new FileOutputStream(pfd.FileDescriptor);
                file_output_stream.Write(Encoding.UTF8.GetBytes(file_contents));
                file_output_stream.Close();
            }
        }

        public static partial bool HasFolderBeenSelectedAndPermissionsGiven()
        {
            var current_activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;

            List<UriPermission> permissions = current_activity.ContentResolver.PersistedUriPermissions.ToList();
            return (permissions != null && permissions.Count > 0);
        }
    }
}

Next, on the actual Android activity class, we will need to handle the result of the RequestSelectLogFolder function - because it calls StartActivityForResult. So in our Android activity, we need to override OnActivityResult and then do a bit of work to handle that result. Here is my implementation:

public class MainActivity : MauiAppCompatActivity
{
    protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
    {
        base.OnActivityResult(requestCode, resultCode, data);

        if (resultCode == Result.Ok)
        {
            if (requestCode == ChooseLogFolder_CrossPlatform.REQUEST_TREE)
            {
                // The result data contains a URI for the document or directory that the user selected.
                if (data != null)
                {
                    Android.Net.Uri uri = data.Data;
                    var flags = data.Flags & (Android.Content.ActivityFlags.GrantReadUriPermission | Android.Content.ActivityFlags.GrantWriteUriPermission);

                    //Take the persistable URI permissions (so that they actually persist)
                    this.ContentResolver.TakePersistableUriPermission(uri, flags);
                }
            }
        }
    }
}

Finally, having done all of this, we should be good to go. We will be able to read/write files to the user-specified public directory on the Android device, and these permissions will be persisted across app restarts and across device reboots.

So, to actually use this code, I could do something like the following. Let's assume the user presses the button in the UI, and I want to write data to a file when the user presses that button. This could be my code inside of my button event-handler:

if (ChooseLogFolder_CrossPlatform.HasFolderBeenSelectedAndPermissionsGiven())
{
    ChooseLogFolder_CrossPlatform.OpenLogFileForWriting("test_file.txt", "hello, world!");
}
else
{
    ChooseLogFolder_CrossPlatform.RequestSelectLogFolder();
}

There we go. That's all of it. Hope this helps someone in the future.

David
  • 1,847
  • 4
  • 26
  • 35
-1

Actually, you can not change the download folder due to the Access restrictions. In addition, you can not get the data from The Android/data/ directory and all subdirectories.

More information you can refer to Storage updates in Android 11.

Guangyu Bai - MSFT
  • 2,555
  • 1
  • 2
  • 8
  • Hi Guangyu, I wasn't really asking about changing the download folder. Rather, I was asking about being able to write files to a public folder so that a connected computer could see those files and access them. Anyway, I was able to figure out the answer, and I have posted the answer on this thread. – David Mar 14 '23 at 21:19