java.io.IOException: невозможно внести изменения в файл

Я использую библиотеку JAudioTagger для чтения и записи тегов для аудиофайла. Я могу читать теги, но не могу их написать.

Я получаю путь к аудиофайлу следующим образом:

 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;
    }

Затем, чтобы написать теги, используя 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();

Метод commit() дает следующее исключение:

org.jaudiotagger.audio.exceptions.CannotWriteException: java.io.IOException: невозможно внести изменения в файл xyz.mp3 в org.jaudiotagger.audio.mp3.MP3File.commit(MP3File.java:799) в com.techapps.musicplayerplus.MainActivity$17.onClick(MainActivity.java:2125) на android.support.v7.app.AlertController$ButtonHandler.handleMessage(AlertController.java:157) на android.os.Handler.dispatchMessage(Handler.java:102) на андроиде.os.Looper.loop(Looper.java:148) на android.app.ActivityThread.main(ActivityThread.java:5417) 06-18 10:59:48.134 8802-8802/com.techapps.musicplayerplus W/System.err:
на java.lang.reflect.Method.invoke(собственный метод) на com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) на com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) Причина: java.io.IOException: Невозможно внести изменения в файл Saibo.mp3 в org.jaudiotagger.audio.mp3.MP3File.precheck(MP3File.java:824) в org.jaudiotagger.audio.mp3.MP3File.save(MP3File.java:850) в org.jaudiotagger.audio.mp3.MP3File.save(MP3File.java:783) в org.jaudiotagger.audio.mp3.MP3File.commit(MP3File.java:795)

Я запускаю этот код на Android 6, в то время как мое приложение предназначено для SDK 22. Я также упомянул следующее разрешение в манифесте.

android.permission.WRITE_EXTERNAL_STORAGE

Я все еще не могу записать на SD-карту. Пожалуйста, помогите мне. Заранее спасибо.

4 ответа

Вы должны использовать Storage Access Framework (SAF) для доступа к SD-карте с API 19 (Kitkat) и далее.

  1. Сначала нам нужно попросить пользователя указать URI папки, к которой мы хотим получить доступ. Если мы хотим получить доступ ко всей SD-карте, пользователь должен предоставить URI корневой папки SD-карты. Например, когда пользователь нажимает кнопку "Изменить", нам сначала нужно отобразить диалоговое окно с подсказкой, в котором пользователь должен выбрать нужный каталог на SD-карте, к которому мы хотим получить доступ. Вы можете отобразить следующее изображение в диалоговом окне подсказки, чтобы попросить пользователя выбрать корневой каталог SD-карты: Вы можете использовать это изображение в диалоговом окне подсказки, чтобы попросить пользователя выбрать корневой каталог SD-карты

  2. Когда пользователь закрывает диалоговое окно подсказки, необходимо запустить 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");
    }
    

Как только вы получите Uri выбранного пользователем каталога, вы можете выполнить операцию записи, используя 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);
    }
}

Вы заявили разрешение <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>?

Возможно, вы указываете на несуществующий файл.

Проверьте ваш путь к файлу с помощью журнала.

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

Android-M или API 23 представили Runtime Permissions для уменьшения уязвимостей в устройстве Android.

Чтобы обновить приложения с помощью сервисов Google Play для обработки разрешений Android 6.0, рекомендуется управлять ожиданиями пользователей при настройке разрешений, которые могут потребоваться во время выполнения. Следующая ссылка поможет вам избежать потенциальных проблем.

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

Я видел, что вы уже создали проблему в репозитории JAudioTagger GitHub, что было желательно, но так и не было найдено универсального рабочего решения. Мои выводы на данный момент:

  • Ответ, в котором упоминается SAF, правильный, но он вам не поможет, поскольку SAF предоставит DocumentFile, а не File.
  • Вы можете попробовать изменить JAudioTagger в соответствии с вашими потребностями, заменив File на DocumentFile, но последний имеет не все функции, которые вам понадобятся.
  • Также InputStream и OutputStream вам не помогут, поскольку JAudioTagger нуждается в File и внутренне активно использует RandomAccessFile, который также недоступен.
  • Google "забыл" предоставить некоторую getRandomAccessFileFromUri(), что еще больше усугубляет ситуацию (да, есть хаки, использующие отражение Java, чтобы обойти это ограничение...).
  • Метод "/proc/self/fd" ( как обрабатывать SAF, когда я могу обрабатывать только файл или путь к файлу?) Также не будет работать сразу, поскольку JAudioTagger требует функций копирования и переименования, которые не применимы к файлам такого типа. В частности, JAudioTagger не найдет подходящего расширения имени файла, такого как ".m4a". Конечно, вы можете попробовать изменить JAudioTagger соответствующим образом.
  • Вы можете последовать совету, чтобы сделать копию файла в свое личное хранилище, затем применить к нему JAudioTagger и, наконец, скопировать его обратно на SD-карту, но:
  • Если вы хотите использовать JAudioTagger для чтения с SD-карты, это, как объявил Google, не удастся с Android 10. Начиная с этой версии, у вас даже не будет доступа для чтения к SD-карте через файловый интерфейс.
  • Кроме того, файловый интерфейс дает вам доступ для чтения к SD-картам с Android 9 и ниже, но не к другим устройствам SAF, таким как память USB OTG или общие ресурсы SMB и т. Д.
  • Конечно, вы также можете скопировать каждый файл, чтобы прочитать его метаданные, но это будет очень медленно и не подходит, если у вас больше нескольких файлов.

Итак, мои текущие советы:

  • Попробуйте использовать метод "/proc/self/fd" и соответствующим образом измените JAudioTagger.
  • Если изменения слишком большие, используйте метод fd для чтения тегов и метод копирования для записи.

Кстати: в настоящее время я модифицирую старую версию JAudioTagger для прозрачного использования как File, так и DocumentFile, но изменения огромны, сопряжены с высоким риском, требуются некоторые вспомогательные классы, а работа еще не завершена.

BTSW: Функции DocumentFile мучительно медленны по сравнению с функциями File.

Другие вопросы по тегам