Подтвердить что ты не робот

Почему у меня нет разрешения на запись в приложение dir на внешнем хранилище?

Вопрос о TL; DR: мое приложение Android пытается записать в каталог внешнего хранилища на SD-карте. Ошибка с ошибкой разрешений. Но тот же код (метод), извлеченный в минимальное тестовое приложение, преуспевает!

Поскольку наш целевой уровень API включает в себя KitKat и позже (а также JellyBean), а KitKat ограничивает приложения от записи в любом месте на SD-карте, кроме указанного в каталоге внешнего хранилища, приложение пытается записать в указанный каталог, /path/to/sdcard/Android/data/com.example.myapp/files. Я проверяю этот путь каталога, получая список каталогов из Activity.getExternalFilesDirs(null); и нахожу его isRemovable(). То есть мы не жестко кодируем путь к SD-карте, потому что это зависит от производителя и устройства. Вот код, который демонстрирует проблему:

// Attempt to create a test file in dir.
private void testCreateFile(File dir) {
    Log.d(TAG, ">> Testing dir " + dir.getAbsolutePath());

    if (!checkDir(dir)) { return; }

    // Now actually try to create a file in this dir.
    File f = new File(dir, "foo.txt");
    try {
        boolean result = f.createNewFile();
        Log.d(TAG, String.format("Attempted to create file. No errors. Result: %b. Now exists: %b",
                result, f.exists()));
    } catch (Exception e) {
        Log.e(TAG, "Failed to create file " + f.getAbsolutePath(), e);
    }
}

Метод checkDir() не так важен, но я буду включать его здесь для полноты. Он просто гарантирует, что каталог находится на съемном хранилище, которое монтируется, и регистрирует другие свойства каталога (существует, доступен для записи).

private boolean checkDir(File dir) {
    boolean isRemovable = false;
    // Can't tell whether it removable storage?
    boolean cantTell = false;
    String storageState = null;

    // Is this the primary external storage directory?
    boolean isPrimary = false;
    try {
        isPrimary = dir.getCanonicalPath()
                .startsWith(Environment.getExternalStorageDirectory().getCanonicalPath());
    } catch (IOException e) {
        isPrimary = dir.getAbsolutePath()
                .startsWith(Environment.getExternalStorageDirectory().getAbsolutePath());
    }

    if (isPrimary) {
        isRemovable = Environment.isExternalStorageRemovable();
        storageState = Environment.getExternalStorageState();
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        // I actually use a try/catch for IllegalArgumentException here, but 
        // that doesn't affect this example.
        isRemovable = Environment.isExternalStorageRemovable(dir);
        storageState = Environment.getExternalStorageState(dir);
    } else {
        cantTell = true;
    }

    if (cantTell) {
        Log.d(TAG, String.format("  exists: %b  readable: %b  writeable: %b primary: %b  cantTell: %b",
                dir.exists(), dir.canRead(), dir.canWrite(), isPrimary, cantTell));
    } else {
        Log.d(TAG, String.format("  exists: %b  readable: %b  writeable: %b primary: %b  removable: %b  state: %s  cantTell: %b",
                dir.exists(), dir.canRead(), dir.canWrite(), isPrimary, isRemovable, storageState, cantTell));
    }

    return (cantTell || (isRemovable && storageState.equalsIgnoreCase(MEDIA_MOUNTED)));
}

В тестовом приложении (работающем на Android 5.1.1) следующий вывод журнала показывает, что код работает нормально:

10-25 19:56:40 D/MainActivity: >> Testing dir /storage/extSdCard/Android/data/com.example.testapp/files
10-25 19:56:40 D/MainActivity:   exists: true  readable: true  writeable: true primary: false  removable: true  state: mounted  cantTell: false
10-25 19:56:40 D/MainActivity: Attempted to create file. No errors. Result: false. Now exists: true

Итак, файл был создан успешно. Но в моем реальном приложении (также работает на Android 5.1.1) вызов createNewFile() завершается с ошибкой разрешений:

10-25 18:14:56... D/LessonsDB: >> Testing dir /storage/extSdCard/Android/data/com.example.myapp/files
10-25 18:14:56... D/LessonsDB:   exists: true  readable: true  writeable: true primary: false  removable: true  state: mounted  cantTell: false
10-25 18:14:56... E/LessonsDB: Failed to create file /storage/extSdCard/Android/data/com.example.myapp/files/foo.txt
    java.io.IOException: open failed: EACCES (Permission denied)
        at java.io.File.createNewFile(File.java:941)
        at com.example.myapp.dmm.LessonsDB.testCreateFile(LessonsDB.java:169)
    ...
     Caused by: android.system.ErrnoException: open failed: EACCES (Permission denied)
        at libcore.io.Posix.open(Native Method)
        at libcore.io.BlockGuardOs.open(BlockGuardOs.java:186)
        at java.io.File.createNewFile(File.java:934)
    ...

Перед тем, как пометить это как дубликат: я прочитал несколько других вопросов о SO, описывающих ошибки разрешений при записи на SD-карту под KitKat или позже. Но ни одна из причин или решений, приведенных, по-видимому, не применяется к этой ситуации:

  • Устройство не подключено как массовое хранилище. Я дважды проверял. Однако MTP включен. (Я не могу отключить его, не отсоединяя USB-кабель, который я использую для просмотра журналов.)
  • Мой манифест включает <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  • Я нацелен на уровень API 22, поэтому мне не нужно иметь разрешение запрашивать во время выполнения (a la Marshmallow). Build.gradle имеет targetSdkVersion 22buildToolsVersion '21.1.2', compileSdkVersion 23).
  • Я работаю на KitKat и Lollipop; У меня даже нет устройства Marshmallow. Поэтому снова мне не нужно запрашивать разрешения во время выполнения, даже если я планировал уровень API 23.
  • Как уже упоминалось выше, я пишу в указанный внешний каталог хранения, который должен быть доступен для записи моим приложением даже в KitKat.
  • Внешнее хранилище установлено; код проверяет это.

Сводка о том, когда он работает, а когда нет:

  • На моем устройстве pre-KitKat как тестовое приложение, так и реальное приложение работают нормально. Они успешно создают файл в каталоге приложения на SD-карте.
  • На моих устройствах KitKat и Lollipop тестовое приложение работает нормально, но реального приложения нет. Вместо этого он показывает ошибку в приведенном выше журнале.

Какая разница между тестовым приложением и реальным приложением? Ну, очевидно, что в реальном приложении в нем много чего другого. Но я не вижу ничего, что должно иметь значение. Оба имеют идентичные compileSdkVersion, targetSdkVersion, buildToolsVersion и т.д. Оба они также используют compile 'com.android.support:appcompat-v7:23.4.0' как зависимость.

4b9b3361

Ответ 1

Поскольку одно приложение работало, а другое - нет, разница была между приложениями, а не с устройством или картой. В рассматриваемой директории не требуются разрешения Android (например, WRITE_EXTERNAL_STORAGE). Единственная причина, по которой вы не могли бы написать ей, - это если система Android не настроила права на файловую систему правильно.

Возможно, я создал этот каталог самостоятельно и не смог установить разрешения где-то?

Я не уверен, что вы можете создать этот каталог самостоятельно вне приложения и заставить его работать. В идеале это было бы хорошо, но я не пробовал, и я вижу, где это может создать проблемы.

Есть ли еще одна причина не доверять этому?

Учитывая происходящие в системе файловую систему, я очень нервничаю, когда разработчики делают предположения относительно природы путей, что все.

Ответ 2

Я узнал больше об этой проблеме, и она отличается от принятого ответа, что, по моему мнению, стоит новый ответ.

  • Для моего первоначального сценария мне показалось, что SD-карты: если на карте уже была папка /Android/data/com.example.myapp, которая не была создана на этом телефоне, у приложения может возникнуть проблема с получением разрешения на запись в эту папку. Если папка не существует, приложение может ее создать и написать ей. По крайней мере, в KitKat и позже.
  • В более позднем сценарии я обнаружил, что приложение необходимо вызвать context.getExternalFilesDirs(null) для создания папки /Android/data/com.example.myapp на SD-карте. (По крайней мере, на Android 5.1 Lollipop и позже. Необходимо проверить это на KitKat.)