java.io.IOException: Cannot make changes to file

1.2k views Asked by At

I am using JAudioTagger library for reading and writing tags for an audio file. I am able to read the tags but unable to write them.

I am retrieving audio file path like this :

 private String getSongPath(long songId) {
        String path = null;
        ContentResolver contentResolver = getContentResolver();
        Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
        String[] projection = {MediaStore.Audio.Media.DATA};
        String selection = MediaStore.Audio.Media._ID + " == ?";
        String[] selectionArgs = {String.valueOf(songId)};

        Cursor cursor = contentResolver.query(uri, projection, selection, selectionArgs, null);

        if (cursor != null) {
            int pathCol = cursor.getColumnIndexOrThrow(projection[0]);
            cursor.moveToFirst();
            path = cursor.getString(pathCol);
            cursor.close();
        }

        return path;
    }

Then to write tags using JAudioTagger :

File songFile = new File(path); // path looks like /storage/3932-3434/Music/xyz.mp3
AudioFile audiofile = = AudioFileIO.read(songFile);
Tag tag = = audiofile.getTag();
tag.setField(FieldKey.TITLE, title);
// some more setField calls for different feilds
audiofile.commit();

The commit() method is giving following Exception :

org.jaudiotagger.audio.exceptions.CannotWriteException: java.io.IOException: Cannot make changes to file xyz.mp3 at org.jaudiotagger.audio.mp3.MP3File.commit(MP3File.java:799) at com.techapps.musicplayerplus.MainActivity$17.onClick(MainActivity.java:2125) at android.support.v7.app.AlertController$ButtonHandler.handleMessage(AlertController.java:157) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5417) 06-18 10:59:48.134 8802-8802/com.techapps.musicplayerplus W/System.err:
at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) Caused by: java.io.IOException: Cannot make changes to file Saibo.mp3 at org.jaudiotagger.audio.mp3.MP3File.precheck(MP3File.java:824) at org.jaudiotagger.audio.mp3.MP3File.save(MP3File.java:850) at org.jaudiotagger.audio.mp3.MP3File.save(MP3File.java:783) at org.jaudiotagger.audio.mp3.MP3File.commit(MP3File.java:795)

I am running this code on Android 6 while my app is targeted at SDK 22. I have also mentioned following permission in manifest.

android.permission.WRITE_EXTERNAL_STORAGE

Still I am unable to write to SD card. Please help me. Thanks in advance.

5

There are 5 answers

0
akshay7692 On

You have to use Storage Access Framework (SAF) to access SD Card from API 19 (Kitkat) onward.

  1. First we need to ask user to provide a URI of the folder we want to access. If we want access to entire SD card, user needs to provide URI of SD card's root folder. For example, when user hits Edit button, we have to first show hint dialog box, asking user to select required directory in SD Card which we want to access. You can display following image in hint dialog box to ask user to select root directory of SD Card :You can use this image in hint dialog box to ask user to select SD Card root directory

  2. When user dismisses hint dialog box, you need to trigger Storage Access Framework :

    private void triggerStorageAccessFramework() {
     Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
     startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
     }
    
    public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
     if (resultCode == Activity.RESULT_OK) {
            if (requestCode == REQUEST_CODE_STORAGE_ACCESS) {
                    Uri treeUri = null;
                    // Get Uri from Storage Access Framework.
                    treeUri = resultData.getData();
                    pickedDir= DocumentFile.fromTreeUri(this, treeUri);
    
                    if (!isSDCardRootDirectoryUri(treeUri)) {
                        Toast.makeText(this, "Wrong directory selected. Please select SD Card root directory.", Toast.LENGTH_LONG).show();
                        createSDCardHintDialog().show();
                        return;
                    }        
    
                    // Persist URI in shared preference so that you can use it later.                   
                    SharedPreferences sharedPreferences = getSharedPreferences(App.PREFERENCE_FILENAME, Context.MODE_PRIVATE);
                    SharedPreferences.Editor editor = sharedPreferences.edit();
                    editor.putString(App.SDCARD_URI_KEY, treeUri.toString());
                    editor.apply();  
    
                    // Persist access permissions, so you dont have to ask again
                    final int takeFlags = resultData.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                    getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    
                }
       } 
    
    private boolean isSDCardRootDirectoryUri(Uri treeUri) {
        String uriString = treeUri.toString();
        return uriString.endsWith("%3A");
    }
    

Once you get Uri of user picked directory, you can perform write operation using SAF : (creadit : this answer )

public void writeFile(DocumentFile pickedDir) {
    try {
        DocumentFile file = pickedDir.createFile("image/jpeg", "try2.jpg");
        OutputStream out = getContentResolver().openOutputStream(file.getUri());
        try {

            // write the image content

        } finally {
            out.close();
        }

    } catch (IOException e) {
        throw new RuntimeException("Something went wrong : " + e.getMessage(), e);
    }
}
2
ישו אוהב אותך On

It could be that you pointing to non existing file.

Check your path file by using Log.

Log.d("Activity", "path = " + path);
1
Niroj On

have you declared the permission <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> ?

1
Elango On

Android-M or API 23 introduced Runtime Permissions for reducing security flaws in android device.

To update your apps using Google Play services to handle Android 6.0 permissions, it’s good practice to manage the user’s expectations in setting permissions that the runtime may require. The following link will help you avoid potential issues.

https://developer.android.com/training/permissions/requesting.html

0
Andreas K. aus M. On

I saw that you already created an issue in the JAudioTagger GitHub repository which was advisable, but never got a universally working solution. My findings so far:

  • The answer mentioning SAF is correct, but it won't help you as SAF will provide a DocumentFile, not a File.
  • You might try to modify JAudioTagger to your needs, replacing File with DocumentFile, but the latter one has not all functions you will need.
  • Also InputStream and OutputStream will not help you, as JAudioTagger needs File and internally heavily uses RandomAccessFile which is not available either.
  • Google "forgot" to provide some getRandomAccessFileFromUri() which makes things even worse (Yes, there are hacks using Java reflection to work around this limitation...).
  • The "/proc/self/fd" method (How to handle SAF when I can only handle File or file-path?) will also not work immediately, as JAudioTagger needs copy and renaming functions that are not applicable to this kind of files. Particularly JAudioTagger will not find a suitable file name extension like ".m4a". Of course you could try to change JAudioTagger accordingly.
  • You might follow the advice to make a copy of the file to your personal storage, then apply JAudioTagger to it and finally copy it back to SD card, but:
  • If you want to use JAudioTagger to read from SD card, this will, as announced by Google, fail with Android 10. Starting with that version, you will not even have read access to the SD card via the File interface.
  • Further, the File interface gives you read access to SD cards with Android 9 and below, but not to other SAF devices, like USB OTG memory or SMB shares etc.
  • Of course you could also copy each file in order to read its metadata, but this will be awfully slow and is not suitable if you have more than a few files.

So my current advices are:

  • Try the "/proc/self/fd" method and modify JAudioTagger accordingly.
  • If the changes are too heavy, use the fd method for reading the tags and the copy method for writing.

BTW: I am currently modifying an older version of JAudioTagger for using both File and DocumentFile transparently, but the changes are tremendous, bear a high risk, need some help classes, and the work is unfinished, yet.

BTSW: The DocumentFile functions are painfully slow, compared to the File functions.