Read/Write files

Maybe it’s time to ping one of the JUCE devs :grimacing:

We’re aware that this is a problem, and we’ll be looking at it shortly.

3 Likes

Great, thanks! Cheerfully cancelled writing a feature request :grin:

As this might screw the whole file approach:
For starters, a wrapper class just opening a dialog and returning the blob besides size and filename could suit in most cases, before changing the big wheels…

Short little bump here :wink:
Is there any progress in sight?
juce_android_Files.cpp still seems two years old on github…

Sorry for the lack of updates.

We’ve had our hands full with other work and so haven’t made any progress here. It’s near the top of our backlog.

2 Likes

After investigating, it looks like certain file locations (particularly shared locations on external storage) must now be accessed via the ContentResolver mechanism. Plain file paths don’t seem to work for these locations. This means that JUCE’s File type isn’t really suitable for interacting with such documents on Android - the document is represented by a URL rather than a filepath, and the supported behaviours don’t match those of standard files.

To allow working with files on Android, I’ve added a new class, AndroidDocument, which integrates properly with the Storage Access Framework, and should allow reading/writing to user-selected files. This class works a bit like File, but has a more limited interface that only includes operations that are supported on Android.

To use it, launch a native file chooser so that the user can pick a file to read or write. Then, pass the URL result of the file chooser to AndroidDocument::fromDocument. You can then use createInputStream and createOutputStream to read/write to the file. It’s also possible to query file information with getInfo, and to delete and rename the file.

If you want to allow the user to read/write collections of files in a shared location (e.g. to export stems or presets from a DAW), you can launch the native file chooser in directory-selection mode, then pass the URL result to AndroidDocument::fromTree. Then, you can create new nested directories with createChildDirectory, and nested files with createChildDocumentWithTypeAndName. You can also use AndroidDocumentIterator::make(Non)Recursive to query the contents of a directory.

To enable writing portable programs, AndroidDocument can be constructed from a File. It’s intended that on Android, a program will use fromDocument or fromTree, and on other platforms, the program will use fromFile instead (you might use #if JUCE_ANDROID to select which version to use). After construction, the same AndroidDocument interface will work consistently on all platforms.

2 Likes

As I’ve started updating my code. I was confused with this commit -

To be honest, I didn’t yet look thoroughly (Android developing targeting multiple versions is the worse imho).

But found this that might be related/helpful for others migrating code:

Did anyone manage to get this working?

Here is a screenshot of my code snippet (I’m using the emulator with API33 image).
I’ve downloaded through chrome on the emulator an mp3 and I try to import it…

But while the AndroidDocument provides the correct name and the correct size. the auto stream is returns 0 length?

It’s not a permission problem as I have READ_EXTERNAL_STORAGE.

I’ve read Access documents and other files from shared storage but don’t see a reason it won’t work.

I’m also able to reproduce this with the AudioPlaybackDemo…
(which needs some Android love to become functional again?)

I’ve added something like this in several places.

        #if JUCE_ANDROID
        auto andoc = AndroidDocument::fromDocument (audioURL);
            reader = formatManager.createReaderFor (andoc.createInputStream());
        #endif

My ugly repro steps:

  • open chrome and download an MP3 file
  • open the desired app, and with native chooser find the new media.

The AndroidDocument does seems to get the correct columns but I have no proper InputStream created.

And you call

RuntimePermissions::request (RuntimePermissions::readExternalStorage...

before?

It’s not permissions and also the AndroidDocument queries seems to give correct details.

Just wanted to stress the need for calling RuntimePermissions::request(...) in addition to setting the READ_EXTERNAL_STORAGE permission in projucer, to actually claim the permission.
For now it works on my side, but think improvement is still possible…

Are you able to open an MP3 from downloads on the Android API33 Emulator?
(permissions requested and approved of course prior to any call)

It’s been two years at least ago that I had an android project, but what I found (in case it’s not already known) that juce::File were only valid inside the block of RuntimePermission::request.

Outside the callback the same juce::File doesn’t work any longer (e.g. when capturing it).
Seems the permission is not stateful, so subsequent calls won’t work without a new request and a callback.

Interesting…
However, we are forced using the URL-based “File” Approach since API30.
Otherwise your app won’t be accepted by the play store…

I’m opening binary files from the Downloads folder, debugging on my Samsung S10e phone.

Actually I planned using a (public?) app folder to load and save Information there.
But for now, to avoid folder accessibility-Issues, I load files from Downloads and make a copy into my app’s (hidden com.xyz…) folder.

For this app I’ve always used the scoped URLs and InputStream. I just import an audio file…

Even when I’m doing the call within the request lambda:

juce::RuntimePermissions::request (
    juce::RuntimePermissions::readExternalStorage
    [this] (bool wasGranted)
    {
        if (wasGranted)
        {
        // new AndroidDocument logic here
        }
    });

The AndroidDocument getInfo() provides the correct details but the InputStream has 0 length.

I did notice that there’s also big headache of branching for different APIs there.
That’s why I’m mentioning the API33 Emulator (I need to buy an Android phone eventually)

Cool, just to rule it out. Passing back to more android-savvy folks.
Good luck

1 Like

Ok,
The request’s callback function just does nothing at my place.
I call the request function during app startup, the heavy stuff sits inside the FileChoosers callback (async).

AndroidDocument::createInputStream returns an instance of AndroidContentUriInputStream, which always returns -1 from getTotalLength(). According to the docs for InputString::getTotalLength(), this is the correct behaviour for streams with an unknown size.

It should still be possible to read from the stream. If you continue after the assertion, does loading the file succeed?

I’ve debugged this a bit now, and I have some improvements staged that will hopefully land in the next few days.

The InputStream created by the AndroidDocument was missing the setPosition function, which meant that it wasn’t very useful for reading from WAV files. This omission will be fixed in the upcoming patch.
I’ve also added an AndroidDocumentInputSource, which should simplify interop with the AudioThumbnail.

I wasn’t seeing any permissions-related issues in the AudioPlaybackDemo in the Android 33 emulator. I did need to manually enable the mp3 audio format config option in order to load mp3 files, though.

1 Like

In my case, I simply pass the InputStream to a juce::AudioFormatReader.

#if JUCE_ANDROID
        auto androidDocument = juce::AndroidDocument::fromDocument (url);
        auto stream = androidDocument.createInputStream();
#else
        std::unique_ptr<juce::InputStream> stream (juce::URLInputSource (url).createInputStream());
#endif
        jassert (stream != nullptr);
        std::unique_ptr<juce::AudioFormatReader> reader (formatManager.createReaderFor (std::move (stream)));

The reader object ends up being nullptr while the file does exists and the AndroidDocument object seems valid.