Вопрос о 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 22
(иbuildToolsVersion '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'
как зависимость.