Search code examples
androidxamarinxamarin.androidandroid-contentproviderfilesavepicker

Whats the correct way to save a file when using the Content sheme?


I use the following intent to allow the user to choose where a file should be saved:

// https://developer.android.com/guide/topics/providers/document-provider
var intent = new Intent(Intent.ActionCreateDocument);
intent.AddCategory(Intent.CategoryOpenable);
intent.SetType("image/png");
intent.PutExtra(Intent.ExtraTitle, "myfile");
StartActivityForResult(Intent.CreateChooser(intent, "Select Save Location"), 43);

It creates the file and returns the file's uri:

content://com.android.providers.downloads.documents/document/436

But now I am left hanging because that section of the documentation ends with

After you create a new document you can get its URI in onActivityResult() so that you can continue to write to it.

And I don't know how to do that. Since my result is using the content scheme I can't just treat it like a regular Java.IO.File and write it to the disk. So how do I save a file to the content uri that I have?


Solution

  • When you get the content Uri back in the OnActivityResult you have temporary permission (and thus write access) to that Uri, so you can open a parcel-based file descriptor in write-mode, create a output stream from that parcel's file descriptor and write whatever you need to it.

    Example writing a Asset stream to the file that the user choose:

    protected async override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
    {
        if (resultCode == Result.Ok && requestCode == 43)
        {
            var buffer = new byte[1024];
            using (var pFD = ContentResolver.OpenFileDescriptor(data.Data, "w"))
            using (var outputSteam = new FileOutputStream(pFD.FileDescriptor))
            using (var inputStream = Assets.Open("somePicture.png"))
            {
                while (inputStream.CanRead && inputStream.IsDataAvailable())
                {
                    var readCount = await inputStream.ReadAsync(buffer, 0, buffer.Length);
                    await outputSteam.WriteAsync(buffer, 0, readCount);
                }
            }
    
        }
        base.OnActivityResult(requestCode, resultCode, data);
    }
    

    Update (performance):

    Just an FYI, if you are saving/streaming large files avoid the async versions of the Read and Write on the streams and just spin up a single thread (or use one from the threadpool via Task.Run).

    Note: This will always be faster due all the async/await overhead and is kind-of like normally how I would do it (typically faster by 2x(+) based upon file size).

    if (resultCode == Result.Ok && requestCode == 43)
    {
        await Task.Run(() =>
        {
            // Buffer size can be "tuned" to enhance read/write performance
            var buffer = new byte[1024]; 
            using (var pFD = ContentResolver.OpenFileDescriptor(data.Data, "w"))
            using (var outputSteam = new FileOutputStream(pFD.FileDescriptor))
            using (var inputStream = Assets.Open("her.png"))
            {
                while (inputStream.CanRead && inputStream.IsDataAvailable())
                {
                    var readCount = inputStream.Read(buffer, 0, buffer.Length);
                    outputSteam.Write(buffer, 0, readCount);
                }
            }
        });
    }