Problem with writeExternalStorage runtime permissions on Android

Dear (pro)Jucers!

I’m having difficulties with runtime permissions check for writeExternalStorage. When I try to ask for this permission I run into jassertfalse in void RuntimePermissions::request (PermissionID permission, Callback callback) saying that I need to declare this permission also in manifest.

The problem is that it is in manifest already there:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>

I’m using standard boilerplate code:

if (!RuntimePermissions::isGranted(RuntimePermissions::writeExternalStorage))
    {
        SafePointer<EQ> safeThis(this);
        RuntimePermissions::request(RuntimePermissions::writeExternalStorage,
            [safeThis](bool granted) mutable
            {
                if (safeThis != nullptr && granted)
                    safeThis->_saveEqValsToFile();
            });
        return;
    }

The same issue I have with “official” AudioRecordingDemo so it should be that my code or Projucer settings are ok.
Any ideas?

Also it is maybe important to say that other runtime permissions like microphone and readExternalStorage work fine.
I’m using Juce 7.0.2.

Thanks!

Which version of Android are you targeting? Note that for apps targeting SDK 29+, this permission doesn’t do anything:

Instead, you can show a native ‘save’ file dialog to allow the user to select a directory where your app may write. Then, you can use juce::AndroidDocument to write into the selected location.

Hi @reuk! Thank you on your swift response.
I’m running my app on Android 11 (SDK version 30) so your problem description makes sense but how can I make it work if I want to cover Android versions that require this permission and those that don’t?

Should I check for SDK version and if it is lower than 29 ask for permission and if it is higher then don’t?

Do you maybe have an example that I can scavenge? :nerd_face:

I don’t think it should be necessary to check the API level.

The AudioPlaybackDemo shows how files can be read on different Android versions. Writing files should work in a similar way:

Thanks @reuk! Well if I have that boilerplate code for runtime permission check, I get that assertion I mentioned above so some sdk version check is needed I think :thinking: Do you know maybe how to do that?

With this code I managed to write and read from external memory but could you check if you maybe see some problems in it:

inline std::unique_ptr<OutputStream> makeOutputStream (const URL& url)
{
#if JUCE_ANDROID
    if (auto doc = AndroidDocument::fromDocument (url))
        return doc.createOutputStream();
#endif

#if ! JUCE_IOS
    if (url.isLocalFile())
        return url.getLocalFile().createOutputStream();
#endif

    return url.createOutputStream();
}

inline std::unique_ptr<InputSource> makeInputSource (const URL& url)
{
#if JUCE_ANDROID
    if (auto doc = AndroidDocument::fromDocument (url))
        return std::make_unique<AndroidDocumentInputSource> (doc);
#endif

#if ! JUCE_IOS
    if (url.isLocalFile())
        return std::make_unique<FileInputSource> (url.getLocalFile());
#endif

    return std::make_unique<URLInputSource> (url);
}

Function for writing (here is SDK check needed I think):

void EQ::_saveEqValsToFile() {
    if (_fileChooser != nullptr)
        return;
	
	// Here need to check what is current SDK version because this runtime permission
	// check can be done only for Android SDK versions 28 and lower
/*
    if (!RuntimePermissions::isGranted(RuntimePermissions::writeExternalStorage))
    {
        SafePointer<EQ> safeThis(this);
        RuntimePermissions::request(RuntimePermissions::writeExternalStorage,
            [safeThis](bool granted) mutable
            {
                if (safeThis != nullptr && granted)
                    safeThis->_saveEqValsToFile();
            });
        return;
    }
*/
    _fileChooser.reset(new FileChooser("Select a location or a file to save", File(), "*.mycoolformat"));

    _fileChooser->launchAsync(FileBrowserComponent::saveMode | FileBrowserComponent::canSelectFiles | FileBrowserComponent::warnAboutOverwriting,
        [this](const FileChooser& fc) mutable {

            if (fc.getURLResults().size() > 0) {
                auto my_output_file = makeOutputStream(fc.getURLResult());

                std::string my_text = "Writing to files on Android makes me so happy.";
                my_output_file->writeString(my_text);
            }
            _fileChooser = nullptr;
        }, nullptr);
}

Read function:

void EQ::_loadEqValsFromFile() {
    if (_fileChooser != nullptr)
        return;

    if (!RuntimePermissions::isGranted(RuntimePermissions::readExternalStorage))
    {
        SafePointer<EQ> safeThis(this);
        RuntimePermissions::request(RuntimePermissions::readExternalStorage,
            [safeThis](bool granted) mutable
            {
                if (safeThis != nullptr && granted)
                    safeThis->_loadEqValsFromFile();
            });
        return;
    }

    _fileChooser.reset(new FileChooser("Select a file to read...", File(), "*.mycoolformat"));

    _fileChooser->launchAsync(FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles,
        [this](const FileChooser& fc) mutable {

            if (fc.getURLResults().size() > 0) {

                auto my_input_file = makeInputSource(fc.getURLResult());
                auto my_input_stream = my_input_file->createInputStream();

                std::string my_text = my_input_stream->readString().toStdString();
            }
            _fileChooser = nullptr;
        }, nullptr);
}

It seems so! Have you found a solution for this?

Mine is a simple modification to juce_android_RuntimePermissions.cpp. To the method RuntimePermissions::isRequired (PermissionID permission) I have added :

if (permission == RuntimePermissions::writeExternalStorage && getAndroidSDKVersion() > 28)
        return false;

Now I can check first if (RuntimePermissions::isRequired (RuntimePermissions::writeExternalStorage)) before actually requesting permissions.

Since several people asked me regarding this issue I’m posting here an example function for writing to files (although I removed some parts of the code that were strictly coupled to my use-case, I think function should work):

void EQ::_saveEqValsToFile() {
    if (_fileChooser != nullptr)
        return;

    bool askWritePermission = true;
#if JUCE_ANDROID
    char osVersion[PROP_VALUE_MAX+1];
    int osVersionLength = __system_property_get("ro.build.version.sdk", osVersion);
    int osVersion_int = 0;
    try {
        osVersion_int = std::stoi(osVersion);
    } catch (...) {
        // error management
    }
    if (osVersion_int > 28) askWritePermission = false;
#endif

    if(askWritePermission)
        if (!RuntimePermissions::isGranted(RuntimePermissions::writeExternalStorage))
        {
            SafePointer<EQ> safeThis(this);
            RuntimePermissions::request(RuntimePermissions::writeExternalStorage,
                                        [safeThis](bool granted) mutable
                                        {
                                            if (safeThis != nullptr && granted)
                                                safeThis->_saveEqValsToFile();
                                        });
            return;
        }

    _fileChooser.reset(new FileChooser("Select a location to save", File(), "*.json"));

    _fileChooser->launchAsync(FileBrowserComponent::saveMode | FileBrowserComponent::canSelectFiles | FileBrowserComponent::warnAboutOverwriting,
        [this](const FileChooser& fc) mutable {

            if (fc.getURLResults().size() > 0) {

                try {
                    auto my_output_file = makeOutputStream(fc.getURLResult());
                    auto my_text = _eqValues.toString();
                    my_output_file->writeString(my_text);
                }
                catch (...) {
                    // error management
                }
            }
            _fileChooser = nullptr;
        }, nullptr);
}

And a short side-note: JUCE is a nice framework and JUCE apps run fine on desktop PCs, but there are serious issues when it comes to i.e. Android support. JUCE is not 100% cross platform as advertised, at least when it comes to Android, so some workarounds are often needed and some new and purely Android features are not directly supported like i.e. advanced notifications or controls so just keep that in mind when developing a JUCE app for Android :beers:

Yes, that’s true.
Thanks for sharing!

I just discovered that one can check the API level simply by calling android_get_device_api_level()

#if JUCE_ANDROID
 if (android_get_device_api_level() <= 28)
#endif
 if (! RuntimePermissions::isGranted (RuntimePermissions::writeExternalStorage))
...
1 Like