Save the date! Android Dev Summit is coming to Mountain View, CA on November 7-8, 2018.

Framework de acceso a almacenamiento

Android 4.4 (nivel de API 19) introduce el framework de acceso a almacenamiento (SAF). SAF facilita a los usuarios examinar y abrir documentos, imágenes y otros archivos en todos sus proveedores preferidos de almacenamiento de documentos. Una IU estándar y fácil de usar les permite a los usuarios examinar archivos y accesos recientes de forma consistente entre aplicaciones y proveedores.

Los servicios de almacenamiento local o en la nube pueden participar en este ecosistema al implementar un DocumentsProvider que encapsule sus servicios. Las apps cliente que necesiten acceso a documentos de un proveedor pueden integrarse con el SAF con solo unas pocas líneas de código.

El SAF incluye lo siguiente:

  • Proveedor de documentos; un proveedor de contenido que permite a un servicio de almacenamiento (como Google Drive) revelar los archivos que administra. Un proveedor de documentos se implementa como una subclase de la clase DocumentsProvider. El esquema del proveedor de documentos se basa en una jerarquía de archivos tradicional, aunque tú decides cómo tu proveedor de documentos almacena datos físicamente. La plataforma Android incluye varios proveedores de documentos integrados, como Descargas, Imágenes y Videos.
  • Aplicación cliente; una aplicación personalizada que invoca la intent ACTION_OPEN_DOCUMENT o ACTION_CREATE_DOCUMENT y recibe los archivos devueltos por los proveedores de documentos.
  • Selector; una IU del sistema que les permite a los usuarios acceder a documentos de todos los proveedores de documentos que cumplen los criterios de búsqueda de la aplicación cliente.

Algunas de las funciones que ofrece el SAF son las siguientes:

  • Permite que los usuarios exploren contenido de todos los proveedores de contenido, no solo de una aplicación individual.
  • Permite que tu aplicación tenga acceso persistente y a largo plazo a documentos de un proveedor de documentos. Mediante este acceso, los usuarios pueden agregar, editar, guardar y eliminar archivos en el proveedor.
  • Admite múltiples cuentas de usuario y raíces transitorias, como proveedores de almacenamiento USB, que solo aparecen si la unidad está conectada.

Información general

El SAF se centra en un proveedor de contenido que es una subclase de la clase DocumentsProvider. En un proveedor de documentos, los datos se estructuran como una jerarquía de archivos tradicional:

modelo de datos

Figura 1: Modelo de datos del proveedor de documentos. Una raíz apunta a un solo documento, que luego se distribuye a todo el árbol.

Ten en cuenta lo siguiente:

  • Cada proveedor de documentos informa una o más "raíces" que son puntos de inicio para explorar un árbol de documentos. Cada raíz tiene un COLUMN_ROOT_ID único, y hace referencia a un documento (un directorio) que representa el contenido de esa raíz. Intencionalmente, las raíces son dinámicas para admitir casos de uso como múltiples cuentas, dispositivos de almacenamiento USB transitorios o acceso y salida del usuario.
  • En cada raíz hay un solo documento. Ese documento hace referencia de 1 a N documentos, cada uno de los cuales a la vez puede hacer referencia de 1 a N documentos.
  • Cada backend de almacenamiento presenta archivos y directorios individuales al hacer referencia a ellos con un COLUMN_DOCUMENT_ID único. Los ID de los documentos deben ser únicos y no cambiar una vez que se emiten ya que se usan para asignar URI persistentes en los reinicios del dispositivo.
  • Los documentos pueden ser un archivo que se puede abrir (con un tipo de MIME específico) o un directorio que contiene documentos adicionales (con el tipo de MIME MIME_TYPE_DIR).
  • Cada documento puede tener capacidades diferentes, como lo describe COLUMN_FLAGS. Por ejemplo, FLAG_SUPPORTS_WRITE, FLAG_SUPPORTS_DELETE y FLAG_SUPPORTS_THUMBNAIL. El mismo COLUMN_DOCUMENT_ID se puede incluir en múltiples documentos.

Flujo de control

Como se indicó anteriormente, el modelo de datos del proveedor de documentos se basa en una jerarquía de archivos tradicional. No obstante, puedes guardar tus datos físicamente de la forma que quieras, siempre que se pueda acceder a ellos a través de la DocumentsProvider API. Por ejemplo, podrías usar almacenamiento en la nube basado en etiquetas para tus datos.

La figura 2 muestra un ejemplo de cómo una aplicación de fotos podría usar el SAF para acceder a datos almacenados:

app

Figura 2: Flujo del framework de acceso a almacenamiento.

Ten en cuenta lo siguiente:

  • En el SAF, los proveedores y los clientes no interactúan directamente. Un cliente solicita permiso para interactuar con archivos (es decir, para leer, editar, crear o eliminar archivos).
  • La interacción comienza cuando una aplicación (en este ejemplo, una aplicación de fotos) envía la intent ACTION_OPEN_DOCUMENT o ACTION_CREATE_DOCUMENT. La intent puede incluir filtros para refinar aún más los criterios; por ejemplo, “mostrarme todos los archivos que se puedan abrir y que tengan el tipo de MIME 'imagen'”.
  • Una vez que se envía la intent, el selector del sistema se dirige a cada proveedor registrado y le muestra al usuario las raíces de contenido coincidentes.
  • El selector les proporciona a los usuarios una interfaz estándar para acceder a documentos, aunque los proveedores de documentos subyacentes sean muy diferentes. Por ejemplo, la figura 2 muestra un proveedor de Google Drive, un proveedor de USB y un proveedor de nube.

La figura 3 muestra un selector en el que un usuario que busca imágenes seleccionó una cuenta de Google Drive:

selector

Figura 3: Selector.

Cuando el usuario selecciona Google Drive, se muestran las imágenes (como en la figura 4). A partir de este punto, el usuario puede interactuar con ellas de las formas que admitan el proveedor y la aplicación cliente.

selector

Figura 4: Imágenes.

Escritura de una aplicación cliente

En Android 4.3 y versiones anteriores, si quieres que tu aplicación devuelva un archivo de otra app, debe invocar una intent como ACTION_PICK o ACTION_GET_CONTENT. El usuario debe seleccionar una sola aplicación desde la que se pueda seleccionar un archivo, y la aplicación seleccionada debe proporcionar una interfaz de usuario para que el usuario explore y seleccione entre los archivos disponibles.

En Android 4.4 y versiones posteriores, tienes la opción adicional de usar la intent ACTION_OPEN_DOCUMENT, que muestra la IU de un selector controlado por el sistema que le permite al usuario explorar todos los archivos disponibles de otras aplicaciones. Desde esta IU individual el usuario puede seleccionar un archivo de cualquiera de las aplicaciones admitidas.

ACTION_OPEN_DOCUMENT no pretende ser un reemplazo de ACTION_GET_CONTENT. El que debes usar depende de las necesidades de tu aplicación:

  • Usa ACTION_GET_CONTENT si quieres que tu aplicación simplemente lea/importe datos. Con este enfoque, la aplicación importa una copia de los datos, como un archivo de imagen.
  • Usa ACTION_OPEN_DOCUMENT si quieres que tu app tenga acceso persistente y a largo plazo a documentos de un proveedor de documentos. Un ejemplo sería una aplicación de edición de fotos que le permite a los usuarios editar imágenes almacenadas en un proveedor de documentos.

Esta sección describe cómo escribir aplicaciones cliente en función de las intents ACTION_OPEN_DOCUMENT y ACTION_CREATE_DOCUMENT.

El siguiente fragmento de código usa ACTION_OPEN_DOCUMENT para buscar proveedores de documentos que contienen archivos de imagen:

private static final int READ_REQUEST_CODE = 42;
...
/**
 * Fires an intent to spin up the "file chooser" UI and select an image.
 */
public void performFileSearch() {

    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    // browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones)
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only images, using the image MIME data type.
    // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
    // To search for all documents available via installed storage providers,
    // it would be "*/*".
    intent.setType("image/*");

    startActivityForResult(intent, READ_REQUEST_CODE);
}

Ten en cuenta lo siguiente:

  • Cuando la aplicación envía la intent ACTION_OPEN_DOCUMENT, lanza un selector que muestra todos los proveedores de documentos coincidentes.
  • Al agregar la categoría CATEGORY_OPENABLE a la intent filtra los resultados para mostrar solo documentos que se pueden abrir, como archivos de imagen.
  • La instrucción intent.setType("image/*") continúa con el filtrado para mostrar solo documentos que tienen el tipo de MIME imagen.

Resultados del proceso

Una vez que el usuario selecciona un documento en el selector, se llama a onActivityResult(). El URI que apunta al documento seleccionado se encuentra en el parámetro resultData. Extrae el URI usando getData(). Una vez que lo tengas, podrás usarlo para recuperar el documento que quiere el usuario. Por ejemplo:

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {

    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
    // response to some other intent, and the code below shouldn't run at all.

    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        // The document selected by the user won't be returned in the intent.
        // Instead, a URI to that document will be contained in the return intent
        // provided to this method as a parameter.
        // Pull that URI using resultData.getData().
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            Log.i(TAG, "Uri: " + uri.toString());
            showImage(uri);
        }
    }
}

Explorar metadatos de documentos

Una vez que tienes el URI para un documento, obtienes acceso a sus metadatos. Este fragmento de código captura los metadatos para un documento especificado por el URI y los registra:

public void dumpImageMetaData(Uri uri) {

    // The query, since it only applies to a single document, will only return
    // one row. There's no need to filter, sort, or select fields, since 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 since an
            // int can't be null in Java, the behavior is implementation-specific,
            // which is just a fancy term for "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();
    }
}

Abrir un documento

Una vez que tienes el URI para un documento, puedes abrirlo o hacer lo que quieras con él.

Mapa de bits

Aquí te mostramos un ejemplo de cómo podrías abrir un Bitmap:

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

Ten en cuenta que no debes hacer esto en el subproceso de la IU. Hazlo en segundo plano usando AsyncTask. Una vez que abres el mapa de bits, puedes mostrarlo en una ImageView.

Obtener un InputStream

Aquí te mostramos un ejemplo de cómo obtener un InputStream del URI. En este fragmento de código, las líneas del archivo se leen en una string:

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

Crear un documento nuevo

Tu aplicación puede crear un documento nuevo en un proveedor de documentos usando la intent ACTION_CREATE_DOCUMENT. Para crear un archivo, le das a la intent un tipo de MIME y un nombre de archivo, y la lanzas con un código de solicitud único. No necesitas hacer nada más:

// Here are some examples of how you might call this method.
// The first parameter is the MIME type, and the second parameter is the name
// of the file you are creating:
//
// createFile("text/plain", "foobar.txt");
// createFile("image/png", "mypicture.png");

// Unique request code.
private static final int WRITE_REQUEST_CODE = 43;
...
private void createFile(String mimeType, String fileName) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);

    // Filter to only show results that can be "opened", such as
    // a file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Create a file with the requested MIME type.
    intent.setType(mimeType);
    intent.putExtra(Intent.EXTRA_TITLE, fileName);
    startActivityForResult(intent, WRITE_REQUEST_CODE);
}

Una vez que creas un documento nuevo, puedes obtener su URI en onActivityResult(), de modo que puedas continuar escribiendo en él.

Eliminar un documento

Si tienes el URI para un documento y el Document.COLUMN_FLAGS del documento contiene SUPPORTS_DELETE, puedes eliminar el documento. Por ejemplo:

DocumentsContract.deleteDocument(getContentResolver(), uri);

Editar un documento

Puedes usar el SAF para editar un documento de texto existente. Este fragmento de código envía la intent ACTION_OPEN_DOCUMENT y usa la categoría CATEGORY_OPENABLE para mostrar solo documentos que puedan abrirse. Continúa filtrando para mostrar únicamente archivos de texto:

private static final int EDIT_REQUEST_CODE = 44;
/**
 * Open a file for writing and append some text to it.
 */
 private void editDocument() {
    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
    // file browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only text files.
    intent.setType("text/plain");

    startActivityForResult(intent, EDIT_REQUEST_CODE);
}

A continuación, desde onActivityResult() (ver Resultados del proceso) puedes llamar a código para realizar la edición. El siguiente fragmento de código obtiene un FileOutputStream del ContentResolver. De forma predeterminada, usa el modo de escritura. Se recomienda solicitar la menor cantidad de acceso que necesites de modo que no solicites acceso de lectura/escritura cuando lo único que necesitas sea escritura:

private void alterDocument(Uri uri) {
    try {
        ParcelFileDescriptor pfd = getActivity().getContentResolver().
                openFileDescriptor(uri, "w");
        FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        fileOutputStream.write(("Overwritten by MyCloud 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();
    }
}

Permisos persistentes

Cuando tu aplicación abre un archivo para leer o escribir, el sistema le otorga a tu aplicación un permiso de URI para ese archivo. El permiso dura hasta que se reinicia el dispositivo del usuario. Pero supón que tu aplicación es una aplicación de edición de imágenes y quieres que los usuarios puedan acceder a las últimas 5 imágenes que editaron directamente desde tu aplicación. Si se reinició el dispositivo del usuario, tendrías que enviar de regreso al usuario al selector del sistema para buscar los archivos, y esto, por supuesto, no es la acción ideal.

Para evitar que esto ocurra, puedes conservar los permisos que el sistema le concede a tu aplicación. De hecho, tu aplicación "toma" el permiso de URI persistente que el sistema ofrece. Esto le permite al usuario acceder de forma continua a los archivos a través de tu aplicación, aunque se haya reiniciado el dispositivo:

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

Hay un paso final. Es posible que hayas guardado los URI a los que tu aplicación accedió recientemente, pero quizá ya no sean válidos; otra aplicación podría haber eliminado o modificado un documento. Por eso, siempre debes llamar a getContentResolver().takePersistableUriPermission() para comprobar los datos más actualizados.

Escritura de un proveedor de documentos personalizado

Si estás desarrollando una app que proporciona servicios de almacenamiento para archivos (como un servicio de almacenamiento en la nube), puedes hacer que tus archivos estén disponibles a través del SAF al escribir un proveedor de documentos personalizado. Esta sección describe cómo hacerlo.

Manifiesto

Para implementar un proveedor de documentos personalizado, agrega lo siguiente al manifiesto de tu aplicación:

  • Un objetivo con el nivel de API 19 o un nivel superior.
  • Un elemento <provider> que declara tu proveedor de almacenamiento personalizado.
  • El nombre de tu proveedor, que es su nombre de clase, incluido el nombre de paquete. Por ejemplo: com.example.android.storageprovider.MyCloudProvider.
  • El nombre de tu autoridad, que es tu nombre de paquete (en este ejemplo, com.example.android.storageprovider) más el tipo de proveedor de contenido (documents). Por ejemplo, com.example.android.storageprovider.documents.
  • El atributo android:exported configurado en "true". Debes exportar tu proveedor para que otras apps puedan verlo.
  • El atributo android:grantUriPermissions configurado en "true". Esta configuración le permite al sistema permitir que otras app accedan a contenido en tu proveedor. Para acceder a una discusión acerca de cómo conservar un permiso para un documento específico, consulta Permisos persistentes.
  • El permiso MANAGE_DOCUMENTS. De forma predeterminada, un proveedor está disponible para todos. Agregar este permiso restringe tu proveedor para el sistema. Esta restricción es importante por motivos de seguridad.
  • El atributo android:enabled fijado en un valor booleano definido en un archivo de recurso. El objetivo de este atributo es inhabilitar el proveedor en dispositivos con Android 4.3 o versiones anteriores. Por ejemplo, android:enabled="@bool/atLeastKitKat". Además de incluir este atributo en el manifiesto, debes realizar lo siguiente:
    • En tu archivo de recursos bool.xml, en res/values/, agrega esta línea:
      <bool name="atLeastKitKat">false</bool>
    • En tu archivo de recursos bool.xml, en res/values-v19/, agrega esta línea:
      <bool name="atLeastKitKat">true</bool>
  • Un filtro de intents que incluya la acción android.content.action.DOCUMENTS_PROVIDER, para que tu proveedor aparezca en el selector cuando el sistema busque proveedores.

Aquí te mostramos extractos de un manifiesto de ejemplo que incluye un proveedor:

<manifest... >
    ...
    <uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />
        ....
        <provider
            android:name="com.example.android.storageprovider.MyCloudProvider"
            android:authorities="com.example.android.storageprovider.documents"
            android:grantUriPermissions="true"
            android:exported="true"
            android:permission="android.permission.MANAGE_DOCUMENTS"
            android:enabled="@bool/atLeastKitKat">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

Dispositivos admitidos con Android 4.3 o versiones anteriores

La intent ACTION_OPEN_DOCUMENT solo está disponible en dispositivos que ejecutan Android 4.4 y versiones posteriores. Si quieres que tu aplicación sea compatible con ACTION_GET_CONTENT para admitir dispositivos que ejecutan Android 43 y versiones anteriores, debes inhabilitar el filtro de intentACTION_GET_CONTENT en tu manifiesto para dispositivos que ejecutan Android 4.4 o versiones posteriores. Un proveedor de documentos y ACTION_GET_CONTENT deben considerarse mutuamente exclusivos. Si admites ambos simultáneamente, tu app aparecerá dos veces en la IU del selector del sistema y ofrecerá dos formas diferentes de acceder a tus datos almacenados. Esto podría ser confuso para los usuarios.

Esta es la forma recomendada de inhabilitar el filtro de intent ACTION_GET_CONTENT para dispositivos que ejecutan Android 4.4 o versiones posteriores:

  1. En tu archivo de recursos bool.xml, en res/values/, agrega esta línea:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. En tu archivo de recursos bool.xml, en res/values-v19/, agrega esta línea:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Agrega un alias de actividad para inhabilitar el filtro de intent ACTION_GET_CONTENT para las versiones 4.4 (nivel de API 19) y posteriores. Por ejemplo:
    <!-- This activity alias is added so that GET_CONTENT intent-filter
         can be disabled for builds on API level 19 and higher. -->
    <activity-alias android:name="com.android.example.app.MyPicker"
            android:targetActivity="com.android.example.app.MyActivity"
            ...
            android:enabled="@bool/atMostJellyBeanMR2">
        <intent-filter>
            <action android:name="android.intent.action.GET_CONTENT" />
            <category android:name="android.intent.category.OPENABLE" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="image/*" />
            <data android:mimeType="video/*" />
        </intent-filter>
    </activity-alias>
    

Contratos

Generalmente, cuando escribes un proveedor de contenido personalizado, una de las tareas implementa clases de contrato, como se describe en la guía para desarrolladores Proveedores de contenido. Una clase Contract es una clase public final que contiene definiciones de constantes para los URI, nombres de columnas, tipos de MIME y otros metadatos pertenecientes al proveedor. El SAF proporciona esas clases de contrato por ti, para que no tengas que escribir las tuyas:

Por ejemplo, aquí te mostramos las columnas que deberías devolver en un cursor cuando se consulta tu proveedor de documentos para acceder a documentos o a la raíz:

private static final String[] DEFAULT_ROOT_PROJECTION =
        new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
        Root.COLUMN_AVAILABLE_BYTES,};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
        String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
        Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};

Subclase DocumentsProvider

El próximo paso en la escritura de un proveedor de documentos personalizado es crear la subclase de la clase abstracta DocumentsProvider. Como mínimo, debes implementar los siguientes métodos:

Estos son los únicos métodos que debes implementar, pero hay muchos más que podrías considerar. Consulta DocumentsProvider para obtener información detallada.

Implementar queryRoots

Tu implementación de queryRoots() debe devolver un Cursor que apunte a todos los directorios raíz de tus proveedores de documentos usando las columnas que se definen en DocumentsContract.Root.

En el siguiente fragmento de código, el parámetro projection representa los campos específicos que el emisor desea recuperar. El fragmento de código crea un nuevo cursor y le agrega una fila; una raíz, un directorio de nivel superior, como Descargas o Imágenes. La mayoría de los proveedores solo tienen una raíz. Deberías tener más de uso, por ejemplo, en caso de múltiples cuentas de usuario. En ese caso, simplemente agrega una segunda fila al cursor.

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Create a cursor with either the requested fields, or the default
    // projection if "projection" is null.
    final MatrixCursor result =
            new MatrixCursor(resolveRootProjection(projection));

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    // Construct one row for a root called "MyCloud".
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);
    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));

    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
    // recently used documents will show up in the "Recents" category.
    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
    // shares.
    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
            Root.FLAG_SUPPORTS_RECENTS |
            Root.FLAG_SUPPORTS_SEARCH);

    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));

    // This document id cannot change once it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));

    // The child MIME types are used to filter the roots and only present to the
    //  user roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

Implementar queryChildDocuments

Tu implementación de queryChildDocuments() debe devolver un Cursor que apunte a todos los archivos en el directorio especificado usando las columnas que se definen en DocumentsContract.Document.

Se llama a este método cuando seleccionas la raíz de una aplicación en la IU del selector. El método obtiene los documentos secundarios de un directorio en esa raíz. Se lo puede llamar en cualquier nivel de la jerarquía de archivos, no solo en la raíz. Este fragmento de código crea un cursos nuevo con las columnas solicitadas, luego agrega información al cursor acerca de cada elemento secundario inmediato en el directorio principal. Un elemento secundario puede ser una imagen, otro directorio o cualquier archivo:

@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
                              String sortOrder) throws FileNotFoundException {

    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    final File parent = getFileForDocId(parentDocumentId);
    for (File file : parent.listFiles()) {
        // Adds the file's display name, MIME type, size, and so on.
        includeFile(result, null, file);
    }
    return result;
}

Implementar queryDocument

Tu implementación de queryDocument() debe devolver un Cursor que apunte al archivo especificado usando las columnas que se definen en DocumentsContract.Document.

El método queryDocument() devuelve la misma información que se pasó en queryChildDocuments(), pero para un archivo específico:

@Override
public Cursor queryDocument(String documentId, String[] projection) throws
        FileNotFoundException {

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    includeFile(result, documentId, null);
    return result;
}

Implementar openDocument

Debes implementar openDocument() para devolver un ParcelFileDescriptor que represente el archivo especificado. Otras apps pueden usar el ParcelFileDescriptor devuelto para transmitir datos. El sistema llama a este método una vez que el usuario selecciona un archivo y la app cliente solicita acceso a él llamando a openFileDescriptor(). Por ejemplo:

@Override
public ParcelFileDescriptor openDocument(final String documentId,
                                         final String mode,
                                         CancellationSignal signal) throws
        FileNotFoundException {
    Log.v(TAG, "openDocument, mode: " + mode);
    // It's OK to do network operations in this method to download the document,
    // as long as you periodically check the CancellationSignal. If you have an
    // extremely large file to transfer from the network, a better solution may
    // be pipes or sockets (see ParcelFileDescriptor for helper methods).

    final File file = getFileForDocId(documentId);

    final boolean isWrite = (mode.indexOf('w') != -1);
    if(isWrite) {
        // Attach a close listener if the document is opened in write mode.
        try {
            Handler handler = new Handler(getContext().getMainLooper());
            return ParcelFileDescriptor.open(file, accessMode, handler,
                        new ParcelFileDescriptor.OnCloseListener() {
                @Override
                public void onClose(IOException e) {

                    // Update the file with the cloud server. The client is done
                    // writing.
                    Log.i(TAG, "A file with id " +
                    documentId + " has been closed!
                    Time to " +
                    "update the server.");
                }

            });
        } catch (IOException e) {
            throw new FileNotFoundException("Failed to open document with id "
            + documentId + " and mode " + mode);
        }
    } else {
        return ParcelFileDescriptor.open(file, accessMode);
    }
}

Seguridad

Supón que tu proveedor de documentos es un servicio de almacenamiento en la nube protegido con contraseña y quieres asegurarte de que los usuarios hayan iniciado sesión antes de comenzar a compartir sus archivos. ¿Qué debería hacer tu app si el usuario no inició sesión? La solución es no devolver raíces en tu implementación de queryRoots(). Es decir, un cursor de raíz vacío:

public Cursor queryRoots(String[] projection) throws FileNotFoundException {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
}

El otro paso es llamar a getContentResolver().notifyChange(). ¿Recuerdas DocumentsContract? Lo usamos para crear este URI. El siguiente fragmento de código le indica al sistema que consulte las raíces de tu proveedor de documentos siempre que cambie el estado de acceso del usuario. Si el usuario no inició sesión, una llamada a queryRoots() devuelve un cursor vacío, como se mostró anteriormente. Esto garantiza que los documentos de un proveedor solo estén disponibles si el usuario accedió al proveedor.

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}