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
21.99k stars 1.72k forks source link

MAUI Essentials sample redundantly copies file in MediaPicker sample #6667

Open janseris opened 2 years ago

janseris commented 2 years ago

Description

this sample shows how to select and capture photo (and video).

It copies the selected file into FileSystem.CacheDirectory first (which is /data/user/0/{ApplicationID}/cache/ on Android) and then reads it. This results in this flow on Android:

[0:] FileResult FullPath is /storage/emulated/0/Android/data/{applicationID}/cache/{guid}/{guid}/DSC_0296.JPG
[0:] executing LoadFileThroughApplicationCache 
[0:] Reading file from: /storage/emulated/0/Android/data/{applicationID}/cache/{guid}/{guid}/DSC_0296.JPG
[0:] Copying file to: /data/user/0/{ApplicationID}/cache/DSC_0296.JPG
[0:] file from cache byte[] size: 3145408

Copying file to FileSystem.CacheDirectory can be avoided and is in my opinion redundant. When copy to FileSystem.CacheDirectory from the sample is used, the application cache (from application info in Android) grows by 2 × filesize per every different file selected. When "direct access" is used, the application cache (from application info in Android) grows by 1× filesize per every file selected.

Btw. even when using "direct access", the application copies the original file into the application everytime it is picked. What is the point of the cache then? It generates a new GUID into the second GUID position in the cache/{guid}/{-> here guid}/DSC_0296.JPG path for every subsequent use of MediaPicker.

I can do this:

async Task<byte[]> LoadPhotoDirectlyAsync(FileResult file)
    {
        Trace.WriteLine("LoadPhotoDirectlyAsync");
        Trace.WriteLine($"Reading file from {file.FullPath}");

        using (var stream = await file.OpenReadAsync())
        using (var ms = new MemoryStream((int)stream.Length))
        {
            await stream.CopyToAsync(ms);
            return ms.ToArray();
        }
    }

With the following output:

[0:] FileResult FullPath is /storage/emulated/0/Android/data/{applicationID}/cache/{guid}/{guid}/DSC_0296.JPG
[0:] executing LoadPhotoDirectlyAsync
[0:] Reading file from /storage/emulated/0/Android/data/{applicationID}/cache/{guid}/{guid}/DSC_0296.JPG
[0:] direct file byte[] size: 3145408

Steps to Reproduce

MAUI (I am using MAUI Blazor) app Use MediaPicker example from MAUI Essentials sample code in the maui repo

Version with bug

Release Candidate 2 (current)

Last version that worked well

Unknown/Other

Affected platforms

Android, Windows

Affected platform versions

Android 11, Windows all

Did you find any workaround?

No response

Relevant log output

No response

Sample code (.razor page):

Note: uses Blazorise Span and Button components (they can be replaced with regular span and button tags) and Divider component can be removed

Edit: sample code fixed

@page "/photos"

<Span>Capturing is supported: @(MediaPicker.IsCaptureSupported)</Span> @* what does this mean? *@

<div class="d-flex flex-column" style="gap: 0.5rem;">

<Button Color=Color.Primary Clicked="@(() => DoPhotoAction(CapturePhotoAction, "capture photo", directFileAccess: true))">Take a photo</Button>
<Button Color=Color.Secondary Clicked="@(() => DoPhotoAction(CapturePhotoAction, "capture photo", directFileAccess: false))">Take a photo (cached file)</Button>
<Divider/>
<Button Color=Color.Primary Clicked="@(() => DoPhotoAction(SelectPhotoAction, "select photo", directFileAccess: true))">Select a photo</Button>
<Button Color=Color.Secondary Clicked="@(() => DoPhotoAction(SelectPhotoAction, "select photo", directFileAccess: false))">Select a photo (cached file)</Button>
</div>

@code {

    //How application cache on Android works:
    //For some or maybe all Android versions, files are not accessed directly from filesystem but are first automatically copied into Application Cache and this file is server.
    //This cache is located in /data/user/0/{Android ApplicationID}/cache/
    //This cache is never automatically cleared.
    //On Windows, files are accessed directly.

    private Func<MediaPickerOptions, Task<FileResult>> SelectPhotoAction => MediaPicker.PickPhotoAsync;

    //when using photo capture on Android, filename is a random GUID
    private Func<MediaPickerOptions, Task<FileResult>> CapturePhotoAction => MediaPicker.CapturePhotoAsync;

    async void DoPhotoAction(Func<MediaPickerOptions, Task<FileResult>> action, string actionName, bool directFileAccess)
    {
        try
        {
            //a part of this code also requests StorageWrite permission
            FileResult file = await action(null); //null options
                                                  //file is automatically put into local application cache (can be checked via Application Info menu on Android)
                                                  //this cache is located at: /data/user/0/{Android ApplicationID}/cache/{{random string[32]}/{random string[32] regenerated for every MediaPicker usage}
                                                  //every file is automatically read from this path (because it is internally copied to this part first)

            if(file is null)
            {
                //always null on Windows for CapturePhotoAsync without distinguishing cancelled/no camera
                Trace.WriteLine($"User cancelled the file operation.");
                return;
            }

            Trace.WriteLine($"FileResult is {file.FullPath}");

            if (directFileAccess)
            {
                byte[] directData = await LoadPhotoDirectlyAsync(file);
                Trace.WriteLine($"direct file byte[] size: {directData.Length}");
            } else
            {
                string filePath = await LoadFileThroughApplicationCache(file);
                byte[] dataThroughCache = File.ReadAllBytes(filePath); //loaded through application cache
                Trace.WriteLine($"file from cache byte[] size: {dataThroughCache.Length}");
            }
        }
        catch (Exception ex)
        {
            //e.g. user denied permission -> PermissionException or capture action missing in Android manifest -> FeatureNotSupportedException
            Trace.WriteLine($"{actionName} THREW: {ExceptionHelper.GetExAndInnerExInfo(ex)}");
        }
    }

    /// <summary>
    /// This is sample code from MAUI Essentials sample (https://github.com/dotnet/maui/blob/main/src/Essentials/samples/Samples/ViewModel/MediaPickerViewModel.cs).
    /// <br>Loads file from <see cref="FileResult"/>, copies it into <see cref="FileSystem.CacheDirectory"/> for some reason and returns new path in <see cref="FileSystem.CacheDirectory"/>.</br>
    /// <br>Application cache size grows 2 × <paramref name="file"/> size if this file was not present in <see cref="FileSystem.CacheDirectory"/> yet.</br>
    /// <br>Application cache size grows 1 × <paramref name="file"/> size if this file was already present in <see cref="FileSystem.CacheDirectory"/>.</br>
    /// <br>On Android, the <see cref="FileSystem.CacheDirectory"/> folder path is /data/user/0/{Android ApplicationID}/cache/</br>
    /// <br>On Windows, the <see cref="FileSystem.CacheDirectory"/> folder path is .../AppData/Local/Packages/{GUID}_{random string[13]}/LocalCache/</br>
    /// </summary>
    /// <param name="file"></param>
    /// <returns>path of copied file</returns>
    async Task<string> LoadFileThroughApplicationCache(FileResult file)
    {
        Trace.WriteLine("LoadFileThroughApplicationCache");
        Trace.WriteLine($"Reading file from: {file.FullPath}");
        // save the file into application local storage
        var newFilePath = Path.Combine(FileSystem.CacheDirectory, file.FileName);
        Trace.WriteLine($"Copying file to: {newFilePath}");
        using (var stream = await file.OpenReadAsync())
        using (var newStream = File.OpenWrite(newFilePath))
        {
            await stream.CopyToAsync(newStream);
            return newFilePath;
        }
    }

    /// <summary>
    /// Reads selected file without copying it into <see cref="FileSystem.CacheDirectory"/> which is redundant.
    /// <br>On Android, the file from MediaPicker is still automatically copied into application cache at /data/user/0/{Android ApplicationID}/cache/</br>
    /// <br>On Windows, the file is served directly</br>
    /// </summary>
    /// <param name="file"></param>
    /// <returns></returns>
    async Task<byte[]> LoadPhotoDirectlyAsync(FileResult file)
    {
        Trace.WriteLine("LoadPhotoDirectlyAsync");
        Trace.WriteLine($"Reading file from {file.FullPath}");

        using (var stream = await file.OpenReadAsync())
        using (var ms = new MemoryStream((int)stream.Length))
        {
            await stream.CopyToAsync(ms);
            return ms.ToArray();
        }
    }
}
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.