Acessar documentos e outros arquivos do armazenamento compartilhado

Em dispositivos com o Android 4.4 (nível 19 da API) e versões mais recentes, seu app pode interagir com um provedor de documentos, incluindo volumes de armazenamento externo e baseado na nuvem, usando o framework de acesso ao armazenamento. Esse framework permite que os usuários interajam com um seletor de sistema para escolher um provedor de documentos e selecionar documentos específicos e outros arquivos para o app criar, abrir ou modificar.

Como o usuário está envolvido na seleção dos arquivos ou diretórios que o app pode acessar, esse mecanismo não exige nenhuma permissão do sistema, e o controle e a privacidade do usuário foram aprimorados. Além disso, esses arquivos, armazenados fora de um diretório específico do app e fora do armazenamento de mídia, permanecem no dispositivo após a desinstalação do app.

O uso do framework envolve estas etapas:

  1. Um app invoca uma intent que contém uma ação relacionada ao armazenamento. Essa ação corresponde a um caso de uso específico que o framework disponibiliza.
  2. O usuário vê um seletor de sistema que permite que ele procure um provedor de documentos e escolha um local ou documento em que a ação relacionada ao armazenamento ocorra.
  3. O app recebe acesso de leitura e gravação a um URI que representa o local ou documento escolhido pelo usuário. Com esse URI, o app pode executar operações no local escolhido.

Para oferecer suporte ao acesso a arquivos de mídia em dispositivos com o Android 9 (nível 28 da API) ou versões anteriores, declare a permissão READ_EXTERNAL_STORAGE e defina maxSdkVersion como 28.

Este guia explica os diferentes casos de uso com suporte do framework para trabalhar com arquivos e outros documentos. Além disso, também explica como executar operações no local selecionado pelo usuário.

Casos de uso para acessar documentos e outros arquivos

O framework de acesso ao armazenamento oferece suporte aos casos de uso abaixo para acessar arquivos e outros documentos.

Criar um novo arquivo
A ação da intent ACTION_CREATE_DOCUMENT permite que os usuários salvem um arquivo em um local específico.
Abrir um documento ou arquivo
A ação da intent ACTION_OPEN_DOCUMENT permite que os usuários selecionem um documento ou arquivo específico para abrir.
Conceder acesso ao conteúdo de um diretório
A ação da intent ACTION_OPEN_DOCUMENT_TREE, disponível no Android 5.0 (nível 21 da API) e em versões mais recentes, permite que os usuários selecionem um diretório específico, concedendo ao app acesso a todos os arquivos e subdiretórios nesse diretório.

As seções a seguir dão orientações sobre como configurar cada caso de uso.

Criar um novo arquivo

Use a ação da intent ACTION_CREATE_DOCUMENT para carregar o seletor de arquivos do sistema e permitir que o usuário escolha um local para gravar o conteúdo de um arquivo. Esse processo é semelhante ao usado nas caixas de diálogo "Salvar como" usadas por outros sistemas operacionais.

Observação:ACTION_CREATE_DOCUMENT não é possível substituir um arquivo existente. Se o app tentar salvar um arquivo com o mesmo nome, o sistema acrescentará um número entre parênteses no final do nome do arquivo.

Por exemplo, se o app tentar salvar um arquivo com o nome confirmation.pdf em um diretório que já tenha um arquivo com o mesmo nome, o sistema vai salvar o novo arquivo com o nome confirmation(1).pdf.

Ao configurar a intent, especifique o nome do arquivo e o tipo MIME e, como opção, especifique o URI do arquivo ou diretório que o seletor de arquivos precisa exibir ao carregar pela primeira vez usando o extra da intent EXTRA_INITIAL_URI.

O snippet de código a seguir mostra como criar e invocar a intent de criação de arquivo:

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

Abrir um arquivo

Seu app pode usar documentos como a unidade de armazenamento em que os usuários inserem dados que eles podem querer compartilhar com amigos ou importar para outros documentos. Vários exemplos incluem um usuário abrindo um documento de produtividade ou um livro salvo como um arquivo EPUB.

Nesses casos, permita que o usuário escolha o arquivo a ser aberto invocando a intent ACTION_OPEN_DOCUMENT, que abre o app seletor de arquivos do sistema. Para mostrar apenas os tipos de arquivos a que o app oferece suporte, especifique um tipo MIME. Como opção, você pode especificar o URI do arquivo que o seletor de arquivos exibirá ao carregar pela primeira vez usando o extra da intent EXTRA_INITIAL_URI.

O snippet de código a seguir mostra como criar e invocar a intent para abrir um 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);
}

Restrições de acesso

No Android 11 (nível 30 da API) e versões mais recentes, não é possível usar a ação da intent ACTION_OPEN_DOCUMENT para solicitar que o usuário selecione arquivos específicos destes diretórios:

  • O diretório Android/data/ e todos os subdiretórios
  • O diretório Android/obb/ e todos os subdiretórios

Conceder acesso ao conteúdo de um diretório

Em geral, apps de gerenciamento de arquivos e criação de mídia gerenciam grupos de arquivos em uma hierarquia de diretórios. Para oferecer esse recurso no app, use a ação da intent ACTION_OPEN_DOCUMENT_TREE, que permite que o usuário conceda acesso a uma árvore de diretórios inteira, com algumas exceções a partir do Android 11 (nível 30 da API). O app pode acessar qualquer arquivo no diretório selecionado e em qualquer subdiretório.

Ao usar ACTION_OPEN_DOCUMENT_TREE, o app recebe acesso somente aos arquivos no diretório selecionado pelo usuário. Você não tem acesso aos arquivos de outros apps que residem fora desse diretório selecionado pelo usuário. Esse acesso controlado permite que os usuários escolham exatamente qual conteúdo eles se sentem à vontade para compartilhar com o app.

Além disso, como opção, você pode especificar o URI do arquivo que o seletor de arquivos exibe ao carregar pela primeira vez usando o extra da intent EXTRA_INITIAL_URI.

O snippet de código a seguir mostra como criar e invocar a intent para abrir um diretório:

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

Restrições de acesso

No Android 11 (nível 30 da API) e versões mais recentes, não é possível usar a ação da intent ACTION_OPEN_DOCUMENT_TREE para solicitar acesso a estes diretórios:

  • O diretório raiz do volume de armazenamento interno.
  • O diretório raiz de cada volume do cartão SD que o fabricante do dispositivo considera confiável, independentemente do cartão ser emulado ou removível. Um volume confiável é aquele que um app pode acessar na maioria das vezes.
  • O diretório Download

Além disso, no Android 11 (nível 30 da API) e versões mais recentes, não é possível usar a ação da intent ACTION_OPEN_DOCUMENT_TREE para solicitar que o usuário selecione arquivos específicos destes diretórios:

  • O diretório Android/data/ e todos os subdiretórios
  • O diretório Android/obb/ e todos os subdiretórios

Realizar operações no local escolhido

Depois que o usuário selecionar um arquivo ou diretório usando o seletor de arquivos do sistema, você pode extrair o URI do item selecionado usando o código abaixo em 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.
        }
    }
}

Ao receber uma referência ao URI do item selecionado, o app pode realizar várias operações nele. Por exemplo, você pode tanto acessar os metadados do item quanto editá-lo e excluí-lo.

As seções a seguir mostram como concluir ações nos arquivos selecionados pelo usuário.

Determinar as operações com suporte de um provedor

Diferentes provedores de conteúdo permitem que operações distintas sejam realizadas em documentos, como os copiar ou visualizar a miniatura deles. Para determinar quais operações são compatíveis com um provedor, verifique o valor de Document.COLUMN_FLAGS. A IU do seu app pode mostrar apenas as opções que são compatíveis com o provedor.

Manutenção de permissões

Quando o app abre um arquivo para leitura ou gravação, o sistema fornece uma concessão de permissão do URI para aquele arquivo, que dura até o dispositivo do usuário ser reiniciado. Suponha, no entanto, que seu app seja de edição de imagens e você queira que os usuários acessem as cinco imagens com edições mais recentes, feitas diretamente do app. Se o dispositivo do usuário tiver sido reiniciado, você precisará enviá-lo de volta ao seletor do sistema para encontrar os arquivos.

Para preservar o acesso a arquivos em reinicializações de dispositivos e criar uma experiência do usuário melhor, o app pode receber a concessão de permissão de URI persistente oferecida pelo sistema, como mostrado no snippet de código a seguir:

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

Verificação de metadados de documentos

Quando tiver o URI de um documento, você terá acesso aos seus metadados. Esse snippet identifica os metadados de um documento especificado pelo URI e os registra:

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

Abrir um documento

Com uma referência ao URI de um documento, você pode abrir um documento para processamento adicional. Esta seção mostra exemplos de como abrir um bitmap e um fluxo de entrada.

Bitmap

O snippet de código abaixo mostra como abrir um arquivo Bitmap de acordo com o URI dele:

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

Depois de abrir o bitmap, você pode o exibir em uma ImageView.

Fluxo de entrada

O snippet de código a seguir mostra como abrir um objeto InputStream de acordo com seu URI. Neste snippet, as linhas do arquivo estão sendo lidas em uma 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();
}

Edição de um documento

Você pode usar o framework de acesso ao armazenamento para editar um documento de texto.

O snippet de código a seguir substitui o conteúdo do documento representado pelo URI fornecido:

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

Excluir de um documento

Se você tiver o URI de um documento e as Document.COLUMN_FLAGS do documento contiverem SUPPORTS_DELETE, é possível o excluir. Exemplo:

Kotlin

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)

Java

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);

Extrair um URI de mídia equivalente

O método getMediaUri() fornece um URI de armazenamento de mídia equivalente a um URI do provedor de documentos. Os dois URIs se referem ao mesmo item. Com o URI de armazenamento de mídia, é mais fácil acessar arquivos de mídia do armazenamento compartilhado.

O método getMediaUri() oferece suporte a URIs ExternalStorageProvider. No Android 12 (nível 31 da API) e versões mais recentes, o método também oferece suporte a URIs MediaDocumentsProvider.

Abrir um arquivo virtual

No Android 7.0 (nível 25 da API) e versões mais recentes, o app pode usar arquivos virtuais disponibilizados pelo framework de acesso ao armazenamento. Mesmo que os arquivos virtuais não tenham uma representação binária, seu app pode abrir o conteúdo forçando a conversão deles em um tipo de arquivo diferente ou exibindo esses arquivos usando uma ação da intent ACTION_VIEW.

Para abrir arquivos virtuais, seu app cliente precisa incluir uma lógica especial para gerenciá-los. Se você quiser acessar uma representação em bytes do arquivo (para visualizar o arquivo, por exemplo), é necessário solicitar um tipo MIME alternativo do provedor de documentos.

Depois que o usuário fizer uma seleção, use o URI nos dados dos resultados para determinar se o arquivo é virtual, conforme mostrado no snippet de código a seguir:

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

Depois de verificar se o documento é um arquivo virtual, você poderá forçar o arquivo a um tipo MIME alternativo, como "image/png". O snippet de código a seguir mostra como verificar se um arquivo virtual pode ser representado como uma imagem e, em caso afirmativo, recebe um stream de entrada a partir do arquivo 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();
}

Outros recursos

Para saber mais sobre como armazenar e acessar documentos e outros arquivos, consulte os seguintes recursos.

Exemplos

Vídeos