page.title=Estrutura de acesso ao armazenamento @jd:body

Neste documento mostrar mais

  1. Visão geral
  2. Controlar fluxo
  3. Programação de um aplicativo cliente
    1. Busca de documentos
    2. Processamento de resultados
    3. Examinação de metadados de documentos
    4. Abertura de um documento
    5. Criação de um novo documento
    6. Exclusão de um documento
    7. Edição de um documento
    8. Manutenção de permissões
  4. Criação de um provedor de documentos personalizado
    1. Manifesto
    2. Contratos
    3. Subclasse DocumentsProvider
    4. Segurança

Classes principais

  1. {@link android.provider.DocumentsProvider}
  2. {@link android.provider.DocumentsContract}

Vídeos

  1. DevBytes: estrutura de acesso ao armazenamento do Android 4.4: Provedor
  2. DevBytes: estrutura de acesso ao armazenamento do Android 4.4: Cliente

Amostras de código

  1. Provedor de armazenamento
  2. StorageClient

Veja também

  1. Preceitos do provedor de conteúdo

O Android 4.4 (API de nível 19) introduz a Estrutura de Acesso ao Armazenamento (SAF). A SAF simplifica para os usuários a busca e abertura de documentos, imagens e outros arquivos dentre todos os provedores de armazenamento de documentos de preferência. A interface gráfica padrão é fácil de usar e permite aos usuários buscar arquivos e acessar arquivos recentes de modo coerente em todos os aplicativos e provedores.

Serviços de armazenamento local ou em nuvem podem participar desse ecossistema por meio da implementação de um {@link android.provider.DocumentsProvider} que encapsula os serviços. Aplicativos clientes que precisam acessar documentos de um provedor podem integrar-se com a SAF com apenas algumas linhas de código.

A SAF contém:

A seguir há alguns recursos oferecidos pela SAF:

Visão geral

A SAF consiste em um provedor de conteúdo que é uma subclasse da classe {@link android.provider.DocumentsProvider}. Dentro de um provedor de documentos, os dados são estruturados como uma hierarquia de arquivo tradicional:

data model

Figura 1. Modelo de dados do provedor de documentos Uma Raiz aponta para um único Documento, que então inicia o fan-out de toda a árvore.

Observe o seguinte:

Controle de fluxo

Como indicado anteriormente, o modelo de dados do provedor de documentos se baseia em uma hierarquia de arquivo tradicional. Contudo, é possível armazenar os dados fisicamente como quiser desde que eles possam ser acessados pela API {@link android.provider.DocumentsProvider}. Por exemplo: seria possível usar armazenamento em nuvem com base em tag para os dados.

A figura 2 ilustra um exemplo de um aplicativo de foto que usa a SAF para acessar dados armazenados:

app

Figura 2. Fluxo da estrutura de acesso ao armazenamento

Observe o seguinte:

A figura 3 exibe um seletor em que um usuário em busca de imagens selecionou uma conta do Google Drive:

picker

Figura 3. Seletor

Quando o usuário seleciona o Google Drive, as imagens são exibidas como ilustrado na figura 4. Desse momento em diante, o usuário poderá interagir com eles de todos os modos compatíveis com o provedor e o aplicativo cliente.

picker

Figura 4. Imagens

Programação de um aplicativo cliente

No Android 4.3 e em versões anteriores, para o aplicativo recuperar um arquivo de outro aplicativo, ele precisa chamar uma intenção como {@link android.content.Intent#ACTION_PICK} ou {@link android.content.Intent#ACTION_GET_CONTENT}. Em seguida, o usuário deve selecionar um único aplicativo do qual deseja retirar um arquivo e o aplicativo selecionado deve fornecer uma interface do usuário para a busca e a seleção dos arquivos disponíveis.

No Android 4.4 e em versões posteriores, existe a opção adicional de usar a intenção {@link android.content.Intent#ACTION_OPEN_DOCUMENT}, que exibe uma IU do seletor controlada pelo sistema que permite ao usuário buscar todos os arquivos que outros aplicativos disponibilizaram. Nessa IU exclusiva, o usuário pode selecionar um arquivo de qualquer aplicativo compatível.

{@link android.content.Intent#ACTION_OPEN_DOCUMENT} não substitui {@link android.content.Intent#ACTION_GET_CONTENT}. O uso de cada um deles depende da necessidade do aplicativo:

Esta seção descreve como programar aplicativos clientes com base nas intenções {@link android.content.Intent#ACTION_OPEN_DOCUMENT} e {@link android.content.Intent#ACTION_CREATE_DOCUMENT}.

O fragmento a seguir usa {@link android.content.Intent#ACTION_OPEN_DOCUMENT} para buscar provedores de documentos que contenham arquivos de imagem:

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

Observe o seguinte:

Processamento de resultados

Quando o usuário seleciona um documento no seletor, {@link android.app.Activity#onActivityResult onActivityResult()} é chamado. A URI direcionada ao documento selecionado está presente no parâmetro {@code resultData}. Extraia a URI usando {@link android.content.Intent#getData getData()}. Depois, será possível usá-la para recuperar o documento que o usuário deseja. Por exemplo:

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

Examinação de metadados de documentos

Quando tiver a URI de um documento, você terá acesso aos seus metadados. Este fragmento apanha os metadados de um documento especificado pela URI e registra-os:

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

Abertura de um documento

Assim que tiver a URI de um documento, você poderá abri-lo ou fazer o que quiser com ele.

Bitmap

A seguir há um exemplo de como abrir um {@link android.graphics.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;
}

Observe que não se deve realizar essa operação no encadeamento da IU. Faça isso em segundo plano usando {@link android.os.AsyncTask}. Assim que abrir o bitmap, você poderá exibi-lo em uma {@link android.widget.ImageView}.

Obter uma InputStream

Abaixo há um exemplo de como obter uma {@link java.io.InputStream} dessa URI. Neste fragmento, as linhas do arquivo estão sendo lidas em uma 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();
}

Criação de um novo documento

O aplicativo pode criar um novo documento em um provedor de documentos usando a intenção {@link android.content.Intent#ACTION_CREATE_DOCUMENT}. Para criar um arquivo, deve-se fornecer um tipo MIME e um nome para o arquivo à intenção e ativá-la com um código de solicitação exclusivo. O resto você não precisa fazer:

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

Ao criar um novo documento, é possível obter sua URI em {@link android.app.Activity#onActivityResult onActivityResult()} para que seja possível continuar a gravar nele.

Exclusão de um documento

Se você tem uma URI de um documento e os {@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS} do documento contêm {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE}, você pode excluir o documento. Por exemplo:

DocumentsContract.deleteDocument(getContentResolver(), uri);

Edição de um documento

Você pode usar a SAF para editar o documento de texto no local em que está armazenado. Esse fragmento dispara a intenção {@link android.content.Intent#ACTION_OPEN_DOCUMENT} e usa a categoria {@link android.content.Intent#CATEGORY_OPENABLE} para exibir somente documentos que possam ser abertos. Ela filtra ainda mais para exibir somente arquivos 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);
}

Em seguida, de {@link android.app.Activity#onActivityResult onActivityResult()} (consulte Processar resultados), você pode chamar o código para realizar a edição. O fragmento a seguir obtém um {@link java.io.FileOutputStream} do {@link android.content.ContentResolver}. Por padrão, ele usa o modo de "gravação". Recomenda-se solicitar a menor quantidade de acesso necessária, portanto não solicite acesso de leitura e programação se só for necessária a programação:

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

Manutenção de permissões

Quando o aplicativo abre um arquivo para leitura ou programação, o sistema lhe fornece uma concessão da permissão da URI para tal arquivo. Ela vigora até a reinicialização do dispositivo do usuário. Contudo, suponhamos que seu aplicativo seja de edição de imagens e você queira que os usuários acessem as últimas 5 imagens que editaram diretamente do aplicativo. Se o dispositivo do usuário reiniciou, você teria que enviar o usuário de volta ao seletor do sistema para encontrar os arquivos, o que, obviamente, não é o ideal.

Para evitar que isso aconteça, você pode manter as permissões que o sistema forneceu ao aplicativo. Efetivamente, o aplicativo "toma" a concessão de permissão da URI persistente que o sistema está oferecendo. Isso concede ao usuário um acesso contínuo aos arquivos por meio do aplicativo mesmo se o dispositivo for reiniciado:

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

Há uma etapa final. Você pode ter salvo as URIs mais recentes acessadas pelo seu aplicativo, mas elas não serão mais válidas — outro aplicativo pode ter excluído ou modificado um documento. Portanto, deve-se sempre chamar {@code getContentResolver().takePersistableUriPermission()} para verificar se há dados mais recentes.

Criação de um provedor de documentos personalizado

Se você está desenvolvimento um aplicativo que fornece serviços de armazenamento para arquivos (como um serviço de armazenamento em nuvem), é possível disponibilizar os arquivos pela SAF criando um provedor de documentos personalizado. Esta seção mostra como fazê-lo.

Manifesto

Para implementar um provedor de documentos personalizado, adicione ao manifesto do aplicativo:

Abaixo há alguns excertos de amostra de um manifesto que contém um provedor:

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

Compatibilidade com dispositivos que executam Android 4.3 ou anterior

A intenção {@link android.content.Intent#ACTION_OPEN_DOCUMENT} está disponível somente em dispositivos que executam o Android 4.4 ou posteriores. Se você deseja que o aplicativo seja compatível com {@link android.content.Intent#ACTION_GET_CONTENT} para adaptar-se a dispositivos que executam o Android 4.3 ou versões anteriores, é necessário desativar o filtro de intenção {@link android.content.Intent#ACTION_GET_CONTENT} no manifesto para dispositivos que executam Android 4.4 ou versões posteriores. Um provedor de documentos e {@link android.content.Intent#ACTION_GET_CONTENT} devem ser avaliados de forma mutuamente exclusiva. Se houver compatibilidade com ambos simultaneamente, o aplicativo aparecerá duas vezes na IU do seletor do sistema, oferecendo dois meios de acesso diferentes aos dados armazenados. Isso pode confundir os usuários.

A seguir apresenta-se a forma recomendada de desativar o filtro de intenções {@link android.content.Intent#ACTION_GET_CONTENT} para dispositivos que executam o Android 4.4 ou versões posteriores:

  1. No arquivo de recursos {@code bool.xml} em {@code res/values/}, adicione esta linha:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. No arquivo de recursos {@code bool.xml} em {@code res/values-v19/}, adicione esta linha:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Adicione um alias de atividade para desativar o filtro de intenções {@link android.content.Intent#ACTION_GET_CONTENT} para versões 4.4 (API de nível 19) e posteriores. Por exemplo:
    <!-- 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

Normalmente, ao criar um provedor de conteúdo personalizado, uma das tarefas é implementar classes de contrato, como descrito no guia dos desenvolvedores de Provedores de conteúdo. Classe de contrato é uma classe {@code public final} que contém definições constantes das URIs, nomes de coluna, tipos MIME e outros metadados que pertencem ao provedor. A SAF fornece essas classes de contrato, portanto não é necessário programá-las:

Por exemplo, eis as colunas que podem retornar em um cursor ao consultar documentos ou a raiz do provedor de documentos:

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

Subclasse DocumentsProvider

A próxima etapa na criação de um provedor de documentos personalizado é atribuir uma subclasse à classe {@link android.provider.DocumentsProvider} abstrata. No mínimo, é necessário implementar os seguintes métodos:

Esses são os únicos métodos que obrigatoriamente devem ser implementados, mas há muitos outros que podem ser usados. Consulte {@link android.provider.DocumentsProvider} para ver mais detalhes.

Implementação de queryRoots

A implementação de {@link android.provider.DocumentsProvider#queryRoots queryRoots()} deve retornar um {@link android.database.Cursor} que aponta para todos os diretórios raiz dos provedores de documentos, usando colunas definidas em {@link android.provider.DocumentsContract.Root}.

No fragmento a seguir, o parâmetro {@code projection} representa os campos específicos que o autor da chamada quer receber de volta. O fragmento cria um novo cursor e adiciona-lhe uma linha — uma raiz, um diretório de nível superior, como Downloads ou Imagens. A maioria dos provedores tem somente uma raiz. É possível ter mais de uma, por exemplo, no caso de diversas contas de usuário. Nesse caso, adicione somente uma segunda linha ao 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;
}

Implementação de queryChildDocuments

A implementação de {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()} deve retornar um {@link android.database.Cursor} que aponta para todos os arquivos no diretório especificado com colunas definidas em {@link android.provider.DocumentsContract.Document}.

Esse método é chamado quando uma raiz do aplicativo é escolhida na IU do seletor. Ele coleta os documentos filhos de um diretório na raiz. Ele pode ser chamado em qualquer nível na hierarquia de arquivos, não somente na raiz. Esse fragmento cria um novo cursor com as colunas solicitadas e, em seguida, adiciona informações ao cursor sobre cada filho imediato no diretório pai. O filho pode ser uma imagem, outro diretório — qualquer arquivo:

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

Implementação de queryDocument

A implementação de {@link android.provider.DocumentsProvider#queryDocument queryDocument()} deve retornar um {@link android.database.Cursor} que aponta para o arquivo especificado com colunas definidas em {@link android.provider.DocumentsContract.Document}.

O método {@link android.provider.DocumentsProvider#queryDocument queryDocument()} retorna as mesmas informações passadas em {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}, mas para um arquivo 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;
}

Implementação de openDocument

Deve-se implementar {@link android.provider.DocumentsProvider#openDocument openDocument()} para retornar um {@link android.os.ParcelFileDescriptor} que represente o arquivo especificado. Outros aplicativos podem usar o {@link android.os.ParcelFileDescriptor} retornado para transmitir dados. O sistema chama esse método quando o usuário seleciona um arquivo e o aplicativo cliente solicita acesso a ele chamando {@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()}. Por exemplo:

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

Segurança

Suponha que o provedor de documentos seja um serviço de armazenamento em nuvem protegido por senha e que você queira certificar-se de que os usuários estejam conectados antes de iniciar o compartilhamento dos arquivos. O que o aplicativo deve fazer se o usuário não estiver conectado? A solução é retornar zero raiz na implementação de {@link android.provider.DocumentsProvider#queryRoots queryRoots()}, ou seja, um cursor de raiz vazio:

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

A outra etapa é chamar {@code getContentResolver().notifyChange()}. Lembra-se do {@link android.provider.DocumentsContract}? Estamos usando-o para criar esta URI. O fragmento a seguir pede ao sistema que consulte as raízes do provedor de documentos sempre que o status de login do usuário mudar. Se o usuário não estiver conectado, uma chamada de {@link android.provider.DocumentsProvider#queryRoots queryRoots()} retornará um cursor vazio, como exibido anteriormente. Isso garante que os documentos do provedor estejam disponíveis somente se o usuário tiver acesso ao provedor.

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