How can I grant permission to store in DIRECTORY_DOWNLOADS when running on Android 10?

Viewed 639

My problem

I have an Android app targetted at API 30 which stores a data file in the public directory DIRECTORY_DOWNLOADS for users to access and copy to their own directory elsewhere.

This works for API 28 and below, and for API 30, but not API 29 which has access denied.

What I have done so far

I have studied various posts on here, including answers by experienced users who have replied to topics on API 29 / Android 10, and relevant topics on Android Developers but have been unable to find a specific answer to my problem (or one that I can understand). Some examples I've seen are in Kotlin - I am familiar with Java, but not Kotlin.

What I have in my code

In my manifest:

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

(First and third are for other functions of my app.)

In build.gradle:

    compileSdkVersion 30
    defaultConfig {
        applicationId "com.enborne.spine"
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 29
        versionName "3.0"
        signingConfig signingConfigs.config
    }

In my Java code:

My main class Spine includes the following global variables:

    public static File      dataDir;
    public static File      fileSaveDir;

In other modules I have:

            Spine.dataDir = getFilesDir();
            Spine.fileSaveDir =
                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);

and the code that saves the file:

                    // Default toast text
                    toastText = "Failed to save file";

                    // [code to build fileName snipped]

                    // Create the output file
                    outFile = new File(Spine.fileSaveDir, fileName);
                    
                    try
                    {
                        out = new BufferedWriter(new FileWriter(outFile), 1024);
                    
                        // Output the formatted body
                        // [code snipped]

                        // Flush and close file
                        out.flush();
                        out.close();

                        // File has been saved
                        toastText = "File saved as " + formatDesc;
                    }
                    catch (IOException e)
                    {
                        toastText += "\n" + e.getMessage();
                    }

When running on API 29, the exception is thrown when the attempt is made to create the BufferedWriter. Otherwise on other API levels everything works. I've searched everywhere for a solution to this problem, but have been unable to find one.

The actual toast text output when running on an API 29 emulator in Android Studio is:

Failed to save file
/storage/emulated/0/Download/
20210520.txt: open failed : EACCES
(Permission denied)
3 Answers
<application
        android:requestLegacyExternalStorage="true"
    ....

Add this in your android manifest.

Apps that run on Android 11 but target Android 10 (API level 29) can still request the requestLegacyExternalStorage attribute. This flag allows apps to temporarily opt out of the changes associated with scoped storage, such as granting access to different directories and different types of media files. After you update your app to target Android 11, the system ignores the requestLegacyExternalStorage flag.

If your app opts out of scoped storage when running on Android 10 devices, it's recommended that you continue to set requestLegacyExternalStorage to true in your app's manifest file. That way, your app can continue to behave as expected on devices that run Android 10.

Source: https://developer.android.com/about/versions/11/privacy/storage

The solution (thanks to those who responded) is to add android:requestLegacyExternalStorage="true" in the application section of the manifest, while still targeting API 30.

In all my testing I had this in the wrong section of the manifest which is why I couldn't get it to work.

Some time you need runtime permission to write external storage.

if (ContextCompat.checkSelfPermission(Main_activity.this),
                Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            if (!ActivityCompat.shouldShowRequestPermissionRationale(OMain_activity.this,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                ActivityCompat.requestPermissions(getActivity(),
                        new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                        1);
            }
    }
Related