Skip to content

Most visited

Recently visited

navigation

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:

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

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:

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:

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:

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:

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:

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);
}
This site uses cookies to store your preferences for site-specific language and display options.

Get the latest Android developer news and tips that will help you find success on Google Play.

* Required Fields

Hooray!

Browse this site in ?

You requested a page in , but your language preference for this site is .

Would you like to change your language preference and browse this site in ? If you want to change your language preference later, use the language menu at the bottom of each page.

This class requires API level or higher

This doc is hidden because your selected API level for the documentation is . You can change the documentation API level with the selector above the left navigation.

For more information about specifying the API level your app requires, read Supporting Different Platform Versions.

Take a one-minute survey?
Help us improve Android tools and documentation.