Search code examples
androidfilepermissionsandroid-permissions

Why does MediaProvider intermittently refuse external file access permission?


On a Pixel 6a running Android 13, when creating/accessing/deleting files I sometimes get in the Logcat log MediaProvider: Permission to access file storage/emulated/0/Documents/com.myappname/myfilename is denied

To give some context, the Java code below is being called inside a custom Cordova plugin. Earlier testing did not reveal this issue. Even now this only occurs in certain hard-to-define circumstances. Whenever it fails it logs the above message, which I assume means the issue is not with the higher-level Javascript and Cordova intermediate layer that functions to invoke this.

Sometimes Java is successful in creating the file, and reading it, but then fails when trying to delete it, and I even could not trash that file using Android's own files app, but then later I could.

I thought maybe the file was still being buffered, and tried introducing a delay between successive requests, though that did not fix it.

I also wondered if it could be a scoped storage issue, but why would it work sometimes and not others?

AndroidManifest.xml:

...
    <uses-sdk
        android:minSdkVersion="24"
        android:targetSdkVersion="33" />
...
    <uses-permission android:name="ANDROID.PERMISSION.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="ANDROID.PERMISSION.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="ANDROID.PERMISSION.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
...

OfflineDataFile.java:

public class OfflineDataFile {  
    
    private final String filePath;

    public OfflineDataFile(Context context) {
        String pkg = context.getPackageName();
        String fileDir = pkg+".OfflineData";
        filePath = "/Documents/" + fileDir;
    }

    private File getAppDir() throws IOException {
        String state = Environment.getExternalStorageState();
        if ( Environment.MEDIA_MOUNTED.equals( state ) ) {
                return new File( Environment.getExternalStorageDirectory() + filePath );
        } else {
            throw new IOException("Could not mount storage");
        }
    }

    public void write(String filename, byte[] data) throws IOException {
        File appDirectory = getAppDir();

        if ( !appDirectory.exists() ) {
            appDirectory.mkdirs();
        }

        File dataFile = new File( appDirectory, filename );
        dataFile.createNewFile();

        FileOutputStream fos = new FileOutputStream(dataFile);
        fos.write(data);
        fos.close();
    }

    public byte[] read(String filename, Integer size) throws IOException {
            
        File appDirectory = getAppDir();

        if ( !appDirectory.exists() ) {
            throw new IOException("Data directory does not exist");
        }

        File dataFile = new File( appDirectory, filename );
        InputStream input = new FileInputStream(dataFile);
        byte [] data = OfflineDataUtil.readInputStream(input, size);
        input.close();
        return data;
    }

    protected void remove(String filename) {
        File appDirectory;

        try {
            appDirectory = getAppDir();
        } catch (IOException e) {           
            return;
        }

        if ( !appDirectory.exists() ) {
            return;
        }

        File dataFile = new File( appDirectory, filename );

        if ( !dataFile.exists() ) {
            return;
        }

        dataFile.delete();
    }

}

Solution

  • To steer around this issue I've decided to use private external storage rather than shared external storage, since these files are only used internally by the app, so have re-written this as follows (now with edits based on comments).

    OfflineDataFile.java:

    public class OfflineDataFile {  
        
        private final File appDir;
    
        public OfflineDataFile(Context context) throws IOException {
            appDir = context.getExternalFilesDir("OfflineData");
        }
    
        public void write(String filename, byte[] data) throws IOException {
    
            File dataFile = new File( appDir, filename );
    
            FileOutputStream fos = new FileOutputStream(dataFile);
            fos.write(data);
            fos.close();
        }
    
        public byte[] read(String filename, Integer size) throws IOException {
    
            File dataFile = new File( appDir, filename );
            
            InputStream input = new FileInputStream(dataFile);
            byte [] data = OfflineDataUtil.readInputStream(input, size);
            input.close();
            
            return data;
        }
    
        protected void remove(String filename) throws IOException {
    
            File dataFile = new File( appDir, filename );
            
            if (!dataFile.delete()) {
                throw new IOException("Unable to delete file");
            }
        
        }
    
    }