Guardando archivos

Android utiliza un sistema de archivos similar a los basados en disco que podemos encontrar en otras plataformas. Esta lección describe cómo trabajar con el sistema de archivos de Android para leer y escribir archivos con las APIs File.

Un objeto File sirve para leer o escribir cantidades grandes de datos en orden secuencial, sin saltos. Por ejemplo, es apropiado para archivos de imagen o cualquier cosa que se pueda intercambiar por red.

Esta lección muestra cómo llevar a cabo tareas básicas relacionadas con los archivos en tu aplicación. Esta lección asume que estás familiarizado con los conceptos básicos del sistema de ficheros de Linux y las APIs de entrada/salida estándar de java.io.

Elegir entre almacenamiento interno o externo


Todos los dispositivos Android tienen dos espacios de almacenamiento: el almacenamiento "interno" y el "externo". Estos nombres proceden de los primeros tiempos de Android, cuando la mayoría de los dispositivos ofrecían memoria no volatil integrada (almacenamiento interno), y un medio extraíble como una tarjeta micro SD (almacenamiento externo). Algunos dispositivos dividen el espacio de almacenamiento permanente en particiones "interna" y "externa", por lo que incluso sin un medio de almacenamiento extraíble, siempre existen dos espacios de almacenamiento, y el comportamiento de la API es el mismo independientemente de que el almacenamiento externo sea extraíble o no. Las siguientes líneas resumen las características principales de cada tipo de almacenamiento.

Almacenamiento interno:

  • Está disponible siempre.
  • Por defecto los archivos que se guardan aquí sólo puede ser accedidos por tu aplicación.
  • Cuando el usuario desinstala tu aplicación, el sistema borra todos los archivos de tu aplicación del almacenamiento interno.

El almacenamiento interno es ideal cuando quieres asegurarte de que ni el usuario ni otras aplicaciones puedan acceder a tus archivos.

Almacenamiento externo:

  • No siempre está disponible, porque el usuario puede montar el almacenamiento externo como almacenamiento USB y en algunos casos extraerlo del dispositivo.
  • Puede ser leído por todo el mundo, por lo que los archivos que se guarden aquí quedan fuera de tu control.
  • Cuando el usuario desinstala tu aplicación, el sistema borra los archivos de tu aplicación del almacenamiento externo sólo si los guardaste en el directorio devuelto por getExternalFilesDir().

El almacenamiento externo es ideal para almacenar archivos que no requieren restricciones de acceso y para archivos que quieras compartir con otras aplicaciones o que quieras que el usuario pueda acceder a través de un ordenador.

Consejo: Aunque por defecto se instalan en almacenamiento interno, puedes especificar el atributo android:installLocation en tu manifiesto para que tu aplicación pueda ser instalada en almacenamiento externo. Los usuarios aprecian que se les de esta opción cuando el APK ocupa mucho tamaño y tienen un espacio de almacenamiento externo mayor que el interno. Para más información, consulta Emplazamiento de instalación de aplicaciones.

Obtener permisos para el almacenamiento externo


Para escribir en el almacenamiento externo, debes solicitar el permiso WRITE_EXTERNAL_STORAGE en tu archivo de manifiesto:

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

Precaución: Actualmente, todas las aplicaciones tienen la posibilidad de leer del almacenamiento externo sin necesidad de pedir ningún permiso especial. Sin embargo, esto cambiará en futuras versiones. Si tu aplicación necesita leer del almacenamiento externo (pero no escribir en él), necesitarás solicitar el permiso READ_EXTERNAL_STORAGE. Para asegurarte de que tu aplicación continúa funcionando correctamente, deberías solicitar este permiso desde ya, antes de que se produzca el cambio.

<manifest ...>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    ...
</manifest>

Sin embargo, si tu aplicación usa el permiso WRITE_EXTERNAL_STORAGE, de forma implícita, también tendrás permiso para poder leer.

No necesitas ningún permiso para poder guardar archivos en almacenamiento interno. Tu aplicación siempre tiene permiso para leer y escribir en su directorio asociado del almacenamiento interno.

Guardar un archivo en almacenamiento interno


Para guardar un archivo en almacenamiento interno, podemos obtener el directorio adecuado en forma de una instancia de File llamando a uno de estos dos métodos:

getFilesDir()
Devuelve una instancia de File que representa el directorio interno de tu aplicación.
getCacheDir()
Devuelve una instancia de File que representa un directorio interno para los archivos temporales de caché de tu aplicación. Asegúrate de borrar cada archivo cuando no se necesite más y de implementar un límite razonable para la cantidad de memoria que vas a utilizar en un momento dado, por ejemplo 1MB. Si el sistema empieza a quedarse sin espacio de almacenamiento, puede que borre tus archivos de caché sin previo aviso.

Para crear un nuevo archivo en uno de estos directorios, puedes utilizar el constructor File(), pasando como argumento la instancia de File devuelta por uno de los métodos anteriores que representa tu directorio de almacenamiento interno. Por ejemplo:

File file = new File(context.getFilesDir(), filename);

Alternativamente, puedes llamar a openFileOutput() para obtener un FileOutputStream con el que escribir a un archivo en tu almacenamiento interno. Por ejemplo, así es como escribiríamos un texto en un archivo:

String filename = "myfile";
String string = "Hello world!";
FileOutputStream outputStream;

try {
  outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
  outputStream.write(string.getBytes());
  outputStream.close();
} catch (Exception e) {
  e.printStackTrace();
}

O, si necesitas guardar algunos archivos en caché, deberías utilizar createTempFile() en su lugar. Por ejemplo, el siguiente método extrae el nombre de archivo de una URL y crea un archivo con ese nombre en el directorio interno de caché de tu aplicación:

public File getTempFile(Context context, String url) {
    File file;
    try {
        String fileName = Uri.parse(url).getLastPathSegment();
        file = File.createTempFile(fileName, null, context.getCacheDir());
    catch (IOException e) {
        // Error al crear el archivo
    }
    return file;
}

Nota: El directorio de almacenamiento interno de tu aplicación se crea en una localización especial del sistema de archivos de Android con el nombre de paquete de tu aplicación. Técnicamente, otra aplicación podría leer tus archivos internos si estableces el modo del archivo a leíble. Sin embargo, la otra aplicación también necesitaría conocer el nombre del paquete de tu aplicación y los nombres de los archivos. Otras aplicaciones no pueden explorar tus directorios internos y no tienen permisos para leer o escribir en tus archivos a menos que des esos permisos explícitamente. Por lo tanto, siempre que uses MODE_PRIVATE para los archivos de tu almacenamiento interno, estos nunca serán accesibles por otras aplicaciones.

Guardar un archivo en almacenamiento externo


Dado que el almacenamiento externo puede no estar disponible—por ejemplo si el usuario ha montado el almacenamiento externo en el PC o ha extraído la tarjeta SD—siempre deberías verificar que esté disponible antes de acceder. Puedes consultar el estado del almacenamiento externo llamando a getExternalStorageState(). Si el estado devuelto es igual a MEDIA_MOUNTED, entonces puedes leer y escribir. Por ejemplo, los siguientes métodos son de utilidad para determinar la disponibilidad del almacenamiento:

/* Comprueba si el almacenamiento externo está disponible para leer y escribir */
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

/* Comprueba si el almacenamiento externo está disponible al menos para leer */
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

Aunque el almacenamiento externo sea modificable por el usuario y por otras aplicaciones, existen dos categorías de archivos que puede que quieras guardar aquí:

Archivos públicos
Archivos que deberían estar disponibles libremente para otras aplicaciones y para el usuario. Cuando el usuario desinstala tu aplicación, estos archivos deberían seguir estando disponibles para el usuario.

Por ejemplo, fotografías tomadas con tu aplicación o archivos descargados.

Archivos privados
Archivos que pertenecen a tu aplicación y deberían borrarse cuando el usuario desinstala tu aplicación. Aunque estos archivos sean accesibles por otras aplicaciones al estar en almacenamiento externo, son archivos que no proporcionan valor al usuario fuera de tu aplicación. Cuando el usuario desinstala tu aplicación, el sistema borra todos los archivos de tu directorio privado en almacenamiento externo.

Por ejemplo, recursos adicionales descargados por tu aplicación o archivos multimedia temporales.

Si quieres guardar archivos públicos en almacenamiento externo, utiliza el método getExternalStoragePublicDirectory() para obtener una instancia de File que represente el directorio apropiado en almacenamiento externo. Este método toma un argumento que especifica el tipo de archivo que quieres guardar para que se puedan organizar lógicamente con otros archivos públicos, como DIRECTORY_MUSIC (directorio de música) o DIRECTORY_PICTURES (directorio de imágenes). Por ejemplo:

public File getAlbumStorageDir(String albumName) {
    // Obtenemos el directorio público de imágenes del usuario. 
    File file = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES), albumName);
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "Directorio no creado");
    }
    return file;
}

Si quieres guardar archivos privados para tu aplicación, puedes obtener el directorio apropiado llamando a getExternalFilesDir() y pasándole un nombre que indique el tipo de directorio que deseas. Cada directorio creado de esta forma se añade a un directorio padre que encapsula todos los archivos de almacenamiento externo de tu aplicación, los cuales borrará el sistema cuando el usuario desinstale tu aplicación.

Por ejemplo, a continuación se muestra un método que puedes utilizar para crear un directorio para un álbum de fotos:

public File getAlbumStorageDir(Context context, String albumName) {
    // Obtenemos el directorio de imágenes privadas para la aplicación. 
    File file = new File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName);
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "Directorio no creado");
    }
    return file;
}

Si ninguno de los subdirectorios predefinidos se adapta a tus necesidades, puedes llamar a getExternalFilesDir() en su lugar y pasarle null como parámetro. Esto devolverá el directorio privado raíz para tu aplicación en almacenamiento externo.

Recuerda que getExternalFilesDir() crea un directorio dentro de un directorio que se borra cuando el usuario desinstala tu aplicación. Si los archivos que estás guardando deberían seguir estando disponibles después de que el usuario desinstale tu aplicación—por ejemplo si la aplicación sirve para tomar fotografías el usuario querrá conservarlas—en su lugar deberías usar getExternalStoragePublicDirectory().

Independientemente de si usas getExternalStoragePublicDirectory() para archivos compartidos o getExternalFilesDir() para archivos privados, es importante que uses nombres de directorios proporcionados por las constantes de la API como DIRECTORY_PICTURES. Estos nombres de directorios aseguran que los archivos son tratados de manera apropiada por el sistema. Por ejemplo, los archivos que se guardan en DIRECTORY_RINGTONES se categorizan como tonos de llamada en lugar de canciones por el escáner multimedia del sistema.

Consultar el espacio libre


Si conoces el tamaño de los datos que vas a guardar con anterioridad, puedes comprobar si hay suficiente espacio sin provocar una IOException llamando a getFreeSpace() o getTotalSpace(). Estos métodos devuelven el espacio de almacenamiento disponible y el espacio de almacenamiento total en el volumen, respectivamente. Esta información también es de utilidad para evitar llenar el volumen por encima de un cierto límite.

Sin embargo, el sistema no garantiza que puedas escribir tantos bytes como indica getFreeSpace(). Si el número devuelto es unos pocos MB mayor que el tamaño de los datos que quieres guardar, o si el sistema de archivos no está más del 90% ocupado, entonces es probable que sea seguro continuar. En caso contrario, probablemente no deberías escribir en el almacenamiento.

Nota: No es obligatorio comprobar el espacio disponible antes de guardar el archivo. En su lugar puedes intentar escribir el archivo de todas formas, y capturar la excepción IOException si se lanza. Puede que necesites hacer esto si no sabes exactamente el espacio que necesitas. Por ejemplo, si cambias la codificación de caracteres del archivo antes de guardarlo convirtiendo una imagen PNG a JPEG, no sabrás el tamaño con antelación.

Borrar un archivo


Siempre deberías borrar todos los archivos que ya no necesites. La forma más sencilla de borrar un archivo es hacer que el archivo abierto llame a delete() sobre sí mismo.

myFile.delete();

Si el archivo está guardado en almacenamiento interno, puedes preguntar al Context para localizar y borrar un archivo llamando a deleteFile():

myContext.deleteFile(fileName);

Nota: Cuando el usuario desinstala tu aplicación, el sistema Android borra lo siguiente:

  • Todos los archivos que guardaste en almacenamiento interno
  • Todos los archivos que guardaste en almacenamiento externo usando getExternalFilesDir().

Sin embargo, deberías borrar manualmente todos los archivos de caché con getCacheDir() y otros archivos que necesites de forma regular.