yasirkula / UnitySimpleFileBrowser

A uGUI based runtime file browser for Unity 3D (draggable and resizable)
MIT License
817 stars 111 forks source link

Filepath working on Windows but failing on Android #49

Closed designtology closed 3 years ago

designtology commented 3 years ago

The FileBrowser was working fine until a few days, but since a few commits the android filepath seems to be wrong: content://com.android.externalstorage.documents/tree/primary%3ADCIM/document/primary%3ADCIM%2FScreenshots%2Fphoto.jpg

When I test in Unity 2019 and upload files via Windows everything seems to be fine. The path comes from the ShowLoadDialogCoroutine directly. It doubles the path for some reason. I did not change anything in the filebrowser i believe. Anyone had some similar issues?

designtology commented 3 years ago

Working on Android 11 btw

yasirkula commented 3 years ago

That path comes from Storage Access Framework which is the native framework that is used on Android 11+ (because there is no alternative). While working with paths returned by file browser, you need to use FileBrowserHelpers functions which also support Storage Access Framework. Just be sure to pass only the paths returned by FileBrowser to FileBrowserHelpers (except for CopyFile/MoveFile functions that also accept raw file paths so that a file can be copied from/to persistentDataPath or temporaryCachePath).

designtology commented 3 years ago

I am using your testclass from the git page. When I try to add this line in the coroutine to get the path with the helper:

Debug.Log("Filepath in FileBrowser with Helper: " + FileBrowserHelpers.GetDirectoryName(FileBrowser.Result[0]));

I get the same result for the path.

yasirkula commented 3 years ago

Example code doesn't do anything special in particular. Can I see your code and we continue from there?

designtology commented 3 years ago

Sure:

using UnityEngine;
using System.Collections;
using System.IO;
using SimpleFileBrowser;
using System.Collections.Generic;
using UnityEngine.UI;
using TMPro;
using UnityEngine.Events;
using System;

public class FileBrowserTest : MonoBehaviour
{
    // Warning: paths returned by FileBrowser dialogs do not contain a trailing '\' character
    // Warning: FileBrowser can only show 1 dialog at a time

    [Serializable]
    public class FilePathEvent : UnityEvent<string> { };

    public MenuManager mm;
    public InputField InputField;
    public Button btn;
    public Button delete;   
    [SerializeField] public FilePathEvent OnFilepathChanged = new FilePathEvent();
    [HideInInspector]
    public string absoluteFilePath;

    protected virtual void Start()
    {
        GameObject MenuSystem = GameObject.FindGameObjectWithTag("MenuManager");
        mm = MenuSystem.GetComponent<MenuManager>();

        btn.onClick.AddListener(openBrowserMenu);
    }

    public void openBrowserMenu()
    {
        // Set filters (optional)
        // It is sufficient to set the filters just once (instead of each time before showing the file browser dialog), 
        // if all the dialogs will be using the same filters
        FileBrowser.SetFilters(true, new FileBrowser.Filter("Bilder", ".jpg", ".png"), new FileBrowser.Filter("PDF Dateien", ".pdf"));

        // Set default filter that is selected when the dialog is shown (optional)
        // Returns true if the default filter is set successfully
        // In this case, set Images filter as the default filter
        FileBrowser.SetDefaultFilter(".jpg");

        // Set excluded file extensions (optional) (by default, .lnk and .tmp extensions are excluded)
        // Note that when you use this function, .lnk and .tmp extensions will no longer be
        // excluded unless you explicitly add them as parameters to the function
        FileBrowser.SetExcludedExtensions(".lnk", ".tmp", ".zip", ".rar", ".exe");

        // Add a new quick link to the browser (optional) (returns true if quick link is added successfully)
        // It is sufficient to add a quick link just once
        // Name: Users
        // Path: C:\Users
        // Icon: default (folder icon)
        //FileBrowser.AddQuickLink("Dokumente", "content://com.android.externalstorage.documents", null);       

        // Show a select folder dialog 
        // onSuccess event: print the selected folder's path
        // onCancel event: print "Canceled"
        // Load file/folder: folder, Allow multiple selection: false
        // Initial path: default (Documents), Initial filename: empty
        // Title: "Select Folder", Submit button text: "Select"

        StartCoroutine(ShowLoadDialogCoroutine());

    }

    IEnumerator ShowLoadDialogCoroutine() {
        yield return FileBrowser.WaitForLoadDialog(FileBrowser.PickMode.Files, false, null, null, "Wähle eine Datei", "Auswählen");

        if (FileBrowser.Success)
        {
            // Applies only Filename to UI Textfield to hide unix pathname
            // FileBrowser.Result[0] // Complete Path to selected file
            // Please do not change order otherwise shopFileBrowser will not work anymore!
            absoluteFilePath = FileBrowser.Result[0];

            Debug.Log("Filepath in FileBrowser with Helper: " + FileBrowserHelpers.GetDirectoryName(FileBrowser.Result[0]));
            Debug.Log("Filename in FileBrowser with Helper: " + FileBrowserHelpers.GetFilename(FileBrowser.Result[0]));

            absoluteFilePath = FileBrowserHelpers.GetDirectoryName(absoluteFilePath) + "\\" + FileBrowserHelpers.GetFilename(absoluteFilePath);

            Debug.Log("Filepath in FileBrowser: " + absoluteFilePath);
            InputField.text = FileBrowserHelpers.GetFilename(FileBrowser.Result[0]);

            OnFilepathChanged.Invoke(absoluteFilePath);
            mm.CreatedNewForm = false;
        }
        else
        {
            Debug.Log("Failed to load file!");
        }

    }

}
yasirkula commented 3 years ago

You can't modify Storage Access Framework (SAF) paths, it just won't work. So, the line absoluteFilePath = blabla + "\\" + blabla; isn't something you can do in the SAF world. You MUST pass the returned path AS IS to FileBrowserHelpers functions. If you are trying to get a readable path from SAF path, you can't. SAF is a native abstraction layer that doesn't allow such things. While working with SAF, you really need to forget all about how you could modify raw file paths, these don't apply to SAF.

The following code, however, is valid because it uses FileBrowserHelpers functions and doesn't attempt to modify the original path with + "\\" + (FYI, paths returned by FileBrowserHelpers are also SAF paths so you can't modify them with + "\\" + either):

// This code works on all platforms because it uses FileBrowserHelpers

// On Android 11+, returned path will be a SAF path
string returnedPath = FileBrowser.Result[0];

// Get the path of the selected file's parent directory (returned path will be a SAF path)
string parentDirectory = FileBrowserHelpers.GetDirectoryName(returnedPath);

// Create another file inside this folder (returned path will, again, be a SAF path)
string anotherFile = FileBrowserHelpers.CreateFileInDirectory(parentDirectory, "SomeFile.txt");

// Write some text to that file
FileBrowserHelpers.WriteTextToFile(anotherFile, "Hello world");

In summary, this code creates a second file in the selected file's directory and fills it with text "Hello world". As you can see, you can achieve almost anything by using FileBrowserHelpers function without modifying the returned paths with + "\\" +.

designtology commented 3 years ago

But Path.combine would work to get the full path including filename? I dont need to create a file. I need to pass the whole path for firebase to upload. I wonder why it was working before. Maybe there has been an Android update? I was using Android 11 before I think.

designtology commented 3 years ago

Alright, if I use this:

absoluteFilePath = FileBrowser.Result[0]; string parentDirectory = FileBrowserHelpers.GetDirectoryName(absoluteFilePath); string filename = FileBrowserHelpers.GetFilename(absoluteFilePath); Debug.Log("SAF Path" + parentDirectory); Debug.Log("SAF Filename" + filename);

It gets me this:

AndroidPlayer(ADB@127.0.0.1:34999) SAF 
Pathcontent://com.android.externalstorage.documents/tree/primary%3ADCIM/document/primary%3ADCIM%2FScreenshots
AndroidPlayer(ADB@127.0.0.1:34999) SAF FilenameScreenshot_20210505-152535_Brave.jpg
yasirkula commented 3 years ago

Path.Combine also wouldn't work with SAF paths. You must use FileBrowserHelpers. FileBrowserHelpers.CreateFileInDirectory is the closest thing to Path.Combine in SAF world, there is also FileBrowserHelpers.CreateFolderInDirectory.

In your case, you want to upload the file to some server but the service you are using only accepts raw file paths. The only solution to this problem is to copy the selected file to temporaryCachePath and upload it from there:

if( !File.Exists( filePath ) ) // This will return true on non-SAF platforms. No need to copy the file to temporaryCachePath since 'filePath' is already a raw file path
{
    string tempFilePath = Path.Combine( Application.temporaryCachePath, FileBrowserHelpers.GetFilename( filePath ) );
    FileBrowserHelpers.CopyFile( filePath, tempFilePath );
    filePath = tempFilePath;
}

// Upload file at filePath to Firebase here
...
designtology commented 3 years ago

Thank you.

Firebase now throws an error because there is no "file://" or similar so it cant find it. But by putting it infront as a string works for now. I wonder why it doesnt return a full path. You got any easy explained docs for SAF by any chance?

Thanks for now

yasirkula commented 3 years ago

For security reasons, Android team decided to prevent full file system access using raw file paths on Android 10 and later. You can google "android 10 scoped storage" to learn more about this change.