Cómo acceder a documentos y otros archivos desde el almacenamiento compartido

En dispositivos que ejecutan Android 4.4 (nivel de API 19) o una versión posterior, la app puede interactuar con un proveedor de documentos, incluidos los volúmenes de almacenamiento externo y el almacenamiento basado en la nube, a través del framework de acceso al almacenamiento. El framework permite que los usuarios interactúen con un selector de sistema para elegir un proveedor de documentos y seleccionar documentos específicos y otros archivos a fin de que la app pueda crearlos, abrirlos y modificarlos.

Como el usuario participa en la selección de los archivos o directorios a los que puede acceder la app, este mecanismo no requiere ningún permiso del sistema, y mejora el control y la privacidad del usuario. Además, estos archivos, que se almacenan fuera de un directorio específico de la app y de la tienda de contenido multimedia, permanecen en el dispositivo después de que se desinstala la app.

Para usar el framework, sigue los pasos que se indican a continuación:

  1. Una app invoca un intent que contiene una acción relacionada con el almacenamiento. La acción corresponde a un caso práctico específico que ofrece el framework.
  2. El usuario ve un selector de sistema, lo que le permite explorar un proveedor de documentos y elegir una ubicación o un documento donde se realice la acción relacionada con el almacenamiento.
  3. La app obtiene acceso de lectura y escritura a un URI que representa la ubicación o el documento que eligió el usuario. Con el URI, la app puede realizar operaciones en la ubicación elegida.

Para admitir el acceso a archivos multimedia en dispositivos que ejecutan Android 9 (nivel de API 28) o versiones anteriores, declara el permiso READ_EXTERNAL_STORAGE y establece maxSdkVersion en 28.

En esta guía, se explican los diferentes casos prácticos que admite el framework para trabajar con archivos y otros documentos. También se explica cómo realizar operaciones en la ubicación seleccionada por el usuario.

Casos prácticos para el acceso a documentos y otros archivos

El framework de acceso al almacenamiento admite los siguientes casos prácticos para el acceso a archivos y otros documentos.

Cómo crear un archivo nuevo
La acción de intent ACTION_CREATE_DOCUMENT permite a los usuarios guardar un archivo en una ubicación específica.
Cómo abrir un documento o archivo
La acción de intent ACTION_OPEN_DOCUMENT permite a los usuarios seleccionar un documento o archivo específico para abrir.
Cómo otorgar acceso al contenido de un directorio
La acción de intent ACTION_OPEN_DOCUMENT_TREE, disponible en Android 5.0 (nivel de API 21) y versiones posteriores, permite a los usuarios seleccionar un directorio específico y otorgar a tu app acceso a todos los archivos y subdirectorios dentro de ese directorio.

En las siguientes secciones, se explica cómo configurar cada caso práctico.

Cómo crear un archivo nuevo

Usa la acción de intent ACTION_CREATE_DOCUMENT para cargar el selector de archivos del sistema y permitir que el usuario elija una ubicación donde escribir el contenido de un archivo. El proceso es similar al que se usa en los cuadros de diálogo "Guardar como" de otros sistemas operativos.

Nota: ACTION_CREATE_DOCUMENT no puede reemplazar un archivo existente. Si la app intenta guardar un archivo con el mismo nombre, el sistema agrega un número entre paréntesis al final del nombre de archivo.

Por ejemplo, si la app intenta guardar un archivo llamado confirmation.pdf en un directorio que ya tiene un archivo con ese nombre, el sistema guarda el archivo nuevo con el nombre confirmation(1).pdf.

Cuando configures el intent, especifica el nombre del archivo y el tipo de MIME. De forma opcional, especifica el URI del archivo o directorio que el selector de archivos debe mostrar en la primera carga con el intent adicional EXTRA_INITIAL_URI.

En el siguiente fragmento de código, se muestra cómo crear e invocar el intent para crear un archivo:

Kotlin

// Request code for creating a PDF document.
const val CREATE_FILE = 1

private fun createFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"
        putExtra(Intent.EXTRA_TITLE, "invoice.pdf")

        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker before your app creates the document.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }
    startActivityForResult(intent, CREATE_FILE)
}

Java

// Request code for creating a PDF document.
private static final int CREATE_FILE = 1;

private void createFile(Uri pickerInitialUri) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("application/pdf");
    intent.putExtra(Intent.EXTRA_TITLE, "invoice.pdf");

    // Optionally, specify a URI for the directory that should be opened in
    // the system file picker when your app creates the document.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);

    startActivityForResult(intent, CREATE_FILE);
}

Cómo abrir un archivo

La app puede usar documentos como la unidad de almacenamiento donde los usuarios ingresan datos que podrían querer compartir con otras personas o importar a otros documentos. Entre algunos ejemplos se incluye un usuario que abre un documento sobre productividad o un libro guardado como archivo EPUB.

En estos casos, permite que el usuario elija el archivo que desea abrir invocando el intent ACTION_OPEN_DOCUMENT, que abre la app de selector de archivos del sistema. A fin de mostrar solo los tipos de archivos compatibles con la app, especifica un tipo de MIME. También puedes especificar opcionalmente el URI del archivo que el selector de archivos debería mostrar en la primera carga con el intent adicional EXTRA_INITIAL_URI.

En el siguiente fragmento de código, se muestra cómo crear e invocar el intent para abrir un documento PDF:

Kotlin

// Request code for selecting a PDF document.
const val PICK_PDF_FILE = 2

fun openFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"

        // Optionally, specify a URI for the file that should appear in the
        // system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, PICK_PDF_FILE)
}

Java

// Request code for selecting a PDF document.
private static final int PICK_PDF_FILE = 2;

private void openFile(Uri pickerInitialUri) {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("application/pdf");

    // Optionally, specify a URI for the file that should appear in the
    // system file picker when it loads.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);

    startActivityForResult(intent, PICK_PDF_FILE);
}

Restricciones de acceso

En Android 11 (nivel de API 30) y versiones posteriores, no puedes usar la acción de intent ACTION_OPEN_DOCUMENT para solicitar al usuario que seleccione archivos individuales de los siguientes directorios:

  • El directorio Android/data/ y todos los subdirectorios
  • El directorio Android/obb/ y todos los subdirectorios

Cómo otorgar acceso al contenido de un directorio

En general, las apps de administración de archivos y creación de contenido multimedia administran grupos de archivos en una jerarquía de directorios. Para proporcionar esta función en tu app, usa la acción de intent ACTION_OPEN_DOCUMENT_TREE, que permite al usuario otorgar acceso a un árbol de directorios completo, con algunas excepciones a partir de Android 11 (nivel de API 30). La app puede acceder a cualquier archivo en el directorio seleccionado y a cualquiera de sus subdirectorios.

Cuando usas ACTION_OPEN_DOCUMENT_TREE, tu app obtiene acceso solo a los archivos del directorio que selecciona el usuario. No tienes acceso a los archivos de otras apps que se encuentran fuera del directorio que el usuario eligió. Este acceso controlado por los usuarios les permite seleccionar el contenido exacto que prefieren compartir con tu app.

También puedes especificar el URI del directorio que el selector de archivos debería mostrar en la primera carga con el intent adicional EXTRA_INITIAL_URI.

En el siguiente fragmento de código, se muestra cómo crear e invocar el intent para abrir un directorio:

Kotlin

fun openDirectory(pickerInitialUri: Uri) {
    // Choose a directory using the system's file picker.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, your-request-code)
}

Java

public void openDirectory(Uri uriToLoad) {
    // Choose a directory using the system's file picker.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);

    // Optionally, specify a URI for the directory that should be opened in
    // the system file picker when it loads.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uriToLoad);

    startActivityForResult(intent, your-request-code);
}

Restricciones de acceso

En Android 11 (nivel de API 30) y versiones posteriores, no puedes usar la acción de intent ACTION_OPEN_DOCUMENT_TREE para solicitar acceso a los siguientes directorios:

  • El directorio raíz del volumen de almacenamiento interno.
  • El directorio raíz de cada volumen de la tarjeta SD que el fabricante del dispositivo considera confiable, sin importar si la tarjeta está emulada o es extraíble. Un volumen confiable es aquel al que una app puede acceder correctamente la mayor parte del tiempo.
  • El directorio Download.

Además, en Android 11 (nivel de API 30) y versiones posteriores, no puedes usar la acción de intent ACTION_OPEN_DOCUMENT_TREE para solicitar al usuario que seleccione archivos individuales de los siguientes directorios:

  • El directorio Android/data/ y todos los subdirectorios
  • El directorio Android/obb/ y todos los subdirectorios

Cómo realizar operaciones en la ubicación seleccionada

Cuando el usuario haya seleccionado un archivo o directorio con el selector de archivos del sistema, puedes recuperar el URI del elemento seleccionado con el siguiente código en onActivityResult():

Kotlin

override fun onActivityResult(
        requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == your-request-code
            && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        resultData?.data?.also { uri ->
            // Perform operations on the document using its URI.
        }
    }
}

Java

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {
    if (requestCode == your-request-code
            && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            // Perform operations on the document using its URI.
        }
    }
}

Si obtienes una referencia al URI del elemento seleccionado, la app puede realizar varias operaciones en este. Por ejemplo, puedes acceder a los metadatos del elemento, editar el elemento en su lugar o borrarlo.

En las siguientes secciones, se muestra cómo realizar acciones en los archivos que selecciona el usuario.

Cómo determinar las operaciones que admite un proveedor

Los diferentes proveedores de contenido permiten realizar diferentes operaciones en los documentos, como copiarlos o ver su contenido. Para determinar qué operaciones admite un proveedor determinado, verifica el valor de Document.COLUMN_FLAGS. Así, la IU de la app solo puede mostrar las opciones que admite el proveedor.

Permisos persistentes

Cuando la app abre un archivo para leer o escribir, el sistema le otorga un permiso de URI para ese archivo, que dura hasta que se reinicia el dispositivo del usuario. Sin embargo, supongamos que tienes una app de edición de imágenes y quieres que los usuarios puedan acceder a las 5 imágenes que editaron recientemente desde la app. Si el dispositivo del usuario se reinicia, deberías enviar al usuario nuevamente al selector del sistema para que encuentre los archivos.

Para conservar el acceso a los archivos después de reiniciar el dispositivo y crear una mejor experiencia del usuario, la app puede "tomar" el permiso de URI persistente que ofrece el sistema, como se muestra en el siguiente fragmento de código:

Kotlin

val contentResolver = applicationContext.contentResolver

val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
// Check for the freshest data.
contentResolver.takePersistableUriPermission(uri, takeFlags)

Java

final int takeFlags = intent.getFlags()
            & (Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);

Cómo examinar metadatos de documentos

Una vez que tienes el URI de un documento, puedes acceder a sus metadatos. Este fragmento captura y registra los metadatos de un documento que especifica el URI:

Kotlin

val contentResolver = applicationContext.contentResolver

fun dumpImageMetaData(uri: Uri) {

    // The query, because it only applies to a single document, returns only
    // one row. There's no need to filter, sort, or select fields,
    // because we want all fields for one document.
    val cursor: Cursor? = contentResolver.query(
            uri, null, null, null, null, null)

    cursor?.use {
        // moveToFirst() returns false if the cursor has 0 rows. Very handy for
        // "if there's anything to look at, look at it" conditionals.
        if (it.moveToFirst()) {

            // Note it's called "Display Name". This is
            // provider-specific, and might not necessarily be the file name.
            val displayName: String =
                    it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
            Log.i(TAG, "Display Name: $displayName")

            val sizeIndex: Int = it.getColumnIndex(OpenableColumns.SIZE)
            // If the size is unknown, the value stored is null. But because an
            // int can't be null, the behavior is implementation-specific,
            // and unpredictable. So as
            // a rule, check if it's null before assigning to an int. This will
            // happen often: The storage API allows for remote files, whose
            // size might not be locally known.
            val size: String = if (!it.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                it.getString(sizeIndex)
            } else {
                "Unknown"
            }
            Log.i(TAG, "Size: $size")
        }
    }
}

Java

public void dumpImageMetaData(Uri uri) {

    // The query, because it only applies to a single document, returns only
    // one row. There's no need to filter, sort, or select fields,
    // because we want all fields for one document.
    Cursor cursor = getActivity().getContentResolver()
            .query(uri, null, null, null, null, null);

    try {
        // moveToFirst() returns false if the cursor has 0 rows. Very handy for
        // "if there's anything to look at, look at it" conditionals.
        if (cursor != null && cursor.moveToFirst()) {

            // Note it's called "Display Name". This is
            // provider-specific, and might not necessarily be the file name.
            String displayName = cursor.getString(
                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            Log.i(TAG, "Display Name: " + displayName);

            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            // If the size is unknown, the value stored is null. But because an
            // int can't be null, the behavior is implementation-specific,
            // and unpredictable. So as
            // a rule, check if it's null before assigning to an int. This will
            // happen often: The storage API allows for remote files, whose
            // size might not be locally known.
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                size = cursor.getString(sizeIndex);
            } else {
                size = "Unknown";
            }
            Log.i(TAG, "Size: " + size);
        }
    } finally {
        cursor.close();
    }
}

Cómo abrir un documento

Con una referencia al URI de un documento, puedes abrir un documento para procesarlo. En esta sección, se muestran ejemplos para abrir un mapa de bits y un flujo de entrada.

Mapa de bits

En el siguiente fragmento de código, se muestra cómo abrir un archivo Bitmap según el URI:

Kotlin

val contentResolver = applicationContext.contentResolver

@Throws(IOException::class)
private fun getBitmapFromUri(uri: Uri): Bitmap {
    val parcelFileDescriptor: ParcelFileDescriptor =
            contentResolver.openFileDescriptor(uri, "r")
    val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor
    val image: Bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor)
    parcelFileDescriptor.close()
    return image
}

Java

private Bitmap getBitmapFromUri(Uri uri) throws IOException {
    ParcelFileDescriptor parcelFileDescriptor =
            getContentResolver().openFileDescriptor(uri, "r");
    FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
    Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
    parcelFileDescriptor.close();
    return image;
}

Después de abrir el mapa de bits, puedes mostrarlo en una ImageView.

Flujo de entrada

En el siguiente fragmento de código, se muestra cómo abrir un objeto InputStream teniendo en cuenta su URI. En este fragmento, las líneas del archivo se leen en una string:

Kotlin

val contentResolver = applicationContext.contentResolver

@Throws(IOException::class)
private fun readTextFromUri(uri: Uri): String {
    val stringBuilder = StringBuilder()
    contentResolver.openInputStream(uri)?.use { inputStream ->
        BufferedReader(InputStreamReader(inputStream)).use { reader ->
            var line: String? = reader.readLine()
            while (line != null) {
                stringBuilder.append(line)
                line = reader.readLine()
            }
        }
    }
    return stringBuilder.toString()
}

Java

private String readTextFromUri(Uri uri) throws IOException {
    StringBuilder stringBuilder = new StringBuilder();
    try (InputStream inputStream =
            getContentResolver().openInputStream(uri);
            BufferedReader reader = new BufferedReader(
            new InputStreamReader(Objects.requireNonNull(inputStream)))) {
        String line;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line);
        }
    }
    return stringBuilder.toString();
}

Cómo editar un documento

Puedes usar el framework de acceso al almacenamiento para editar un documento de texto.

El siguiente fragmento de código reemplaza el contenido del documento representado por el URI dado:

Kotlin

val contentResolver = applicationContext.contentResolver

private fun alterDocument(uri: Uri) {
    try {
        contentResolver.openFileDescriptor(uri, "w")?.use {
            FileOutputStream(it.fileDescriptor).use {
                it.write(
                    ("Overwritten at ${System.currentTimeMillis()}\n")
                        .toByteArray()
                )
            }
        }
    } catch (e: FileNotFoundException) {
        e.printStackTrace()
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

Java

private void alterDocument(Uri uri) {
    try {
        ParcelFileDescriptor pfd = getActivity().getContentResolver().
                openFileDescriptor(uri, "w");
        FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        fileOutputStream.write(("Overwritten at " + System.currentTimeMillis() +
                "\n").getBytes());
        // Let the document provider know you're done by closing the stream.
        fileOutputStream.close();
        pfd.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Cómo borrar un documento

Si tienes el URI de un documento y el objeto Document.COLUMN_FLAGS del documento contiene SUPPORTS_DELETE, puedes borrar el documento. Por ejemplo:

Kotlin

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)

Java

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);

Cómo recuperar un URI de contenido multimedia equivalente

El método getMediaUri() proporciona un URI de almacenamiento multimedia equivalente al URI de proveedor de documentos determinado. Los dos URI se refieren al mismo elemento subyacente. Con el URI de la tienda de contenido multimedia, puedes acceder con mayor facilidad a los archivos multimedia desde el almacenamiento compartido.

El método getMediaUri() admite URI de ExternalStorageProvider. En Android 12 (nivel de API 31) y versiones posteriores, el método también admite URI de MediaDocumentsProvider.

Cómo abrir un archivo virtual

En Android 7.0 (nivel de API 25) y versiones posteriores, la app puede usar archivos virtuales que ofrece el framework de acceso de almacenamiento. Aunque los archivos virtuales no tienen una representación binaria, tu app puede abrir su contenido forzando la conversión a un tipo de archivo diferente o viéndolos mediante la acción de intent ACTION_VIEW.

A fin de abrir archivos virtuales, la app cliente necesita incluir una lógica especial para controlarlos. Si quieres obtener una representación en bytes del archivo (por ejemplo, para acceder a una vista previa del archivo), debes solicitar un tipo de MIME alternativo al proveedor de documentos.

Después de que el usuario haga una selección, usa el URI en los datos de los resultados para determinar si el archivo es virtual, como se muestra en el siguiente fragmento de código:

Kotlin

private fun isVirtualFile(uri: Uri): Boolean {
    if (!DocumentsContract.isDocumentUri(this, uri)) {
        return false
    }

    val cursor: Cursor? = contentResolver.query(
            uri,
            arrayOf(DocumentsContract.Document.COLUMN_FLAGS),
            null,
            null,
            null
    )

    val flags: Int = cursor?.use {
        if (cursor.moveToFirst()) {
            cursor.getInt(0)
        } else {
            0
        }
    } ?: 0

    return flags and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0
}

Java

private boolean isVirtualFile(Uri uri) {
    if (!DocumentsContract.isDocumentUri(this, uri)) {
        return false;
    }

    Cursor cursor = getContentResolver().query(
        uri,
        new String[] { DocumentsContract.Document.COLUMN_FLAGS },
        null, null, null);

    int flags = 0;
    if (cursor.moveToFirst()) {
        flags = cursor.getInt(0);
    }
    cursor.close();

    return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0;
}

Tras verificar si el documento es un archivo virtual, puedes forzar su conversión a un tipo de MIME alternativo, como "image/png". El siguiente fragmento de código muestra cómo comprobar si se puede representar un archivo virtual como imagen y, en caso afirmativo, obtiene un flujo de entrada del archivo virtual:

Kotlin

@Throws(IOException::class)
private fun getInputStreamForVirtualFile(
        uri: Uri, mimeTypeFilter: String): InputStream {

    val openableMimeTypes: Array<String>? =
            contentResolver.getStreamTypes(uri, mimeTypeFilter)

    return if (openableMimeTypes?.isNotEmpty() == true) {
        contentResolver
                .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
                .createInputStream()
    } else {
        throw FileNotFoundException()
    }
}

Java

private InputStream getInputStreamForVirtualFile(Uri uri, String mimeTypeFilter)
    throws IOException {

    ContentResolver resolver = getContentResolver();

    String[] openableMimeTypes = resolver.getStreamTypes(uri, mimeTypeFilter);

    if (openableMimeTypes == null ||
        openableMimeTypes.length < 1) {
        throw new FileNotFoundException();
    }

    return resolver
        .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
        .createInputStream();
}

Recursos adicionales

Para obtener más información sobre cómo almacenar documentos y otros archivos, y acceder a ellos, consulta los siguientes recursos.

Ejemplos

Videos