¿Por qué no tengo permiso para escribir en el directorio de aplicaciones en almacenamiento externo?

El resumen de preguntas de TL; DR: Mi aplicación de Android intenta escribir en el directorio de almacenamiento externo de la aplicación en una tarjeta SD. Se produce un error con un error de permisos . Pero el mismo código (método), extraído en una aplicación de prueba mínima, tiene éxito!

Dado que nuestro nivel de API de destino incluye KitKat y posterior (así como JellyBean), y KitKat restringe aplicaciones de escribir en cualquier lugar de la tarjeta SD, excepto el directorio de almacenamiento externo designado de la aplicación, la aplicación intenta escribir en ese directorio /path/to/sdcard/Android/data/com.example.myapp/files . Comprobar la ruta de este directorio obteniendo una lista de directorios de Activity.getExternalFilesDirs(null); Y encontrar uno que isRemovable() . Es decir, no estamos codificando el camino hacia la tarjeta SD, ya que varía según el fabricante y el dispositivo. Aquí está el código que demuestra el problema:

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

El método checkDir () no es tan relevante, pero lo incluiré aquí para ver si está completo. Sólo se asegura de que el directorio está en almacenamiento extraíble que está montado, y registra otras propiedades del directorio (existe, escribible).

 private boolean checkDir(File dir) { boolean isRemovable = false; // Can't tell whether it's 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))); } 

En la aplicación de prueba (que se ejecuta en Android 5.1.1), la salida de registro siguiente muestra que el código funciona correctamente:

 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 

Así que el archivo se creó correctamente. Pero en mi aplicación real (también en ejecución en Android 5.1.1), la llamada a createNewFile() falla con un error de permisos:

 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) ... 

Antes de marcar esto como un duplicado: he leído varias otras preguntas sobre SO que describen fallos de permisos al escribir en una tarjeta SD en KitKat o posterior. Pero ninguna de las causas o soluciones dadas parece aplicarse a esta situación:

  • El dispositivo no está conectado como almacenamiento masivo . He comprobado dos veces. Sin embargo, MTP está encendido. (No puedo desactivarla sin desconectar el cable USB que utilizo para ver los registros).
  • Mi manifiesto incluye <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  • Estoy apuntando API nivel 22, por lo que no debería tener que pedir permiso en tiempo de ejecución (a la Marshmallow ). El build.gradle tiene targetSdkVersion 22 (y buildToolsVersion '21.1.2' , compileSdkVersion 23 ).
  • Estoy corriendo en KitKat y Lollipop; Ni siquiera tengo un dispositivo Marshmallow. Por lo tanto, de nuevo, no debería tener que pedir permiso en tiempo de ejecución, incluso si estuviera apuntando a API nivel 23.
  • Como se mencionó anteriormente, estoy escribiendo en el directorio de almacenamiento externo designado que se supone debe ser escribible por mi aplicación, incluso en KitKat.
  • El almacenamiento externo está montado ; El código lo verifica.

Resumen de cuándo funciona y cuándo no funciona:

  • En mi dispositivo pre-KitKat, tanto la aplicación de prueba como la aplicación real funcionan bien. Crean con éxito un archivo en el directorio de la aplicación en la tarjeta SD.
  • En mis dispositivos KitKat y Lollipop, la aplicación de prueba funciona bien, pero la aplicación real no. En su lugar, muestra el error en el registro anterior.

¿Qué diferencia hay entre la aplicación de prueba y la aplicación real? Bueno, obviamente la aplicación real tiene mucho más cosas en ella. Pero no puedo ver nada que deba importar. Ambos tienen compileSdkVersion idéntico, targetSdkVersion , buildToolsVersion , etc. Ambos también están utilizando compile 'com.android.support:appcompat-v7:23.4.0' como una dependencia.

Dado que una aplicación funcionaba y otra no, la diferencia era entre las aplicaciones, no con el dispositivo o la tarjeta. El directorio en cuestión no requiere ningún permiso de Android (por ejemplo, WRITE_EXTERNAL_STORAGE ). La única razón por la que no sería capaz de escribir a ella sería si el sistema Android no había configurado los permisos de sistema de archivos correctamente.

¿Puedo haber creado ese directorio yo mismo, y no haber fijado permisos en alguna parte?

No estoy seguro de que pueda crear ese directorio usted mismo desde fuera de la aplicación y que funcione. Lo ideal sería que estaría bien, pero no lo he probado y puedo ver dónde podría plantear problemas.

¿Hay otra razón para no confiar en ella?

Dado los shenanigans del sistema de archivos que se están encendiendo, consigo muy nervioso cuando los desarrolladores hacen asunciones con respecto a la naturaleza de caminos, eso es todo.

He aprendido más sobre este tema, y ​​es bastante diferente de la respuesta aceptada que creo que vale la pena una nueva respuesta.

  • Lo que hizo la diferencia para mi escenario original fueron las tarjetas SD: Si una tarjeta ya tenía una carpeta /Android/data/com.example.myapp en ella que no se creó en este teléfono , es posible que la aplicación tenga problemas para obtener permiso para escribir A esa carpeta. Mientras que si la carpeta no existe, la aplicación podría crearla y escribirla. Al menos en KitKat y más tarde.
  • En un escenario posterior, encontré que es necesario que la aplicación llame a context.getExternalFilesDirs(null) para que la carpeta /Android/data/com.example.myapp se cree en la tarjeta SD. (Por lo menos, en Android 5.1 Lollipop y más tarde. Necesita probar eso en KitKat.)
  • Android getExternalCacheDir () devuelve null
  • OutputStreamWriter no agrega
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.