Mudança de privacidade do Android Q: armazenamento com escopo

A partir do Android Q Beta 5, os apps voltados para o Android 9 (nível de API 28) ou versões anteriores terão, por padrão, a mesma forma de armazenamento das versões anteriores do Android. Ao atualizar o app existente para que ele funcione com o armazenamento com escopo, é possível usar o novo atributo de manifest requestLegacyExternalStorage para ativar o novo comportamento no seu app para os dispositivos Android Q, mesmo que o app seja voltado ao Android 9 ou versões anteriores.

No Android Q, a forma como os apps podem acessar arquivos no armazenamento externo do dispositivo, como os arquivos armazenados no caminho /sdcard, é modificada para dar aos usuários mais controle sobre os respectivos arquivos e para limitar a sobrecarga. O Android Q continua a usar as permissões READ_EXTERNAL_STORAGE e WRITE_EXTERNAL_STORAGE, que correspondem à permissão de ambiente de execução do Armazenamento voltada para o usuário. No entanto, os apps voltados para o Android Q por padrão, assim como os apps que optam pela alteração, recebem uma visualização filtrada do armazenamento externo. Esses apps podem ver somente o diretório específico do app e tipos específicos de mídia. Por isso, não precisam solicitar outras permissões de usuário.

Este guia descreve os arquivos incluídos na visualização filtrada, assim como uma forma de atualizar seu app para que ele continue compartilhando, acessando e atualizando arquivos salvos em um dispositivo de armazenamento externo. Este guia também explica várias considerações relacionadas a informações de local em fotografias, acesso a mídia de código nativo e uso de nomes de coluna em consultas de conteúdo.

Para saber mais sobre alterações no armazenamento externo no Android Q, consulte a seção que aborda Melhorias na criação de arquivos no armazenamento externo.

Visualização filtrada no armazenamento externo

Por padrão, se seu app for voltado ao Android Q, ele terá uma visualização filtrada dos arquivos de um dispositivo de armazenamento externo. O app pode armazenar arquivos destinados a si mesmo em um diretório específico dele usando Context.getExternalFilesDir().

Um app que tenha uma visualização filtrada sempre tem acesso de leitura/gravação aos arquivos criados, dentro e fora do diretório específico do app. Seu app não precisa declarar nenhuma permissão de armazenamento para acessar esses arquivos.

O app pode acessar arquivos que outros apps criaram apenas se as duas condições a seguir forem verdadeiras:

  1. Seu app recebeu a permissão READ_EXTERNAL_STORAGE.
  2. Os arquivos residem em um dos seguintes conjuntos de mídia bem-definidos:

Para acessar qualquer outro arquivo criado por outro app, incluindo arquivos em um diretório de downloads, seu app precisará usar a Estrutura de acesso ao armazenamento, que permite que o usuário selecione um arquivo específico.

A visualização filtrada também impõe as seguintes restrições de dados relacionadas à mídia:

  • Os metadados EXIF de arquivos de imagem serão editados, a não ser que o app tenha recebido a permissão ACCESS_MEDIA_LOCATION. Saiba mais na seção sobre como acessar informações de local em fotos.
  • A coluna DATA será editada para cada arquivo no armazenamento de mídia.
  • A tabela MediaStore.Files é filtrada, exibindo apenas fotos, vídeos e arquivos de áudio. Por exemplo, essa tabela não mostra mais arquivos PDF.

Para acessar arquivos de mídia em código nativo, recupere o arquivo usando MediaStore no código baseado em Java ou em Kotlin e, em seguida, passe o descritor de arquivo correspondente para seu código nativo. Para saber mais, consulte a seção sobre como acessar arquivos de mídia em código nativo.

Preservar os arquivos do seu app após a desinstalação

Se um app tiver uma visualização filtrada do armazenamento externo e ele for desinstalado, todos os arquivos no diretório específico do app serão limpos. Para preservar esses arquivos após uma desinstalação, salve-os em um diretório dentro de MediaStore.

Desativar a visualização filtrada

A maioria dos apps que já seguem as práticas recomendadas de armazenamento precisa funcionar com armazenamento com escopo definido após fazer alterações mínimas. Antes de o app ser totalmente compatível ou testado, é possível desativar temporariamente o comportamento de armazenamento com escopo definido com base no nível do SDK de destino do app ou em um novo atributo de manifest chamado requestLegacyExternalStorage:

  • Segmentar o Android 9 (nível de API 28) ou anterior.

  • Se você destinar ao Android Q, defina o valor de requestLegacyExternalStorage como true no arquivo de manifest do app:

        <manifest ... >
          <!-- This attribute is "false" by default on apps targeting Android Q. -->
          <application android:requestLegacyExternalStorage="true" ... >
            ...
          </application>
        </manifest>
        

Se um app for instalado com o armazenamento externo de legado ativado, o app permanecerá nesse modo até ser desinstalado. Esse comportamento de compatibilidade se aplica independentemente de o dispositivo ser atualizado posteriormente para executar o Android Q ou o app ser atualizado posteriormente para ser voltado ao Android Q.

Configurar um dispositivo virtual de armazenamento externo

Para fins de teste, em dispositivos sem armazenamento externo removível, utilize o comando a seguir para habilitar um disco virtual:

    adb shell sm set-virtual-disk true
    

Resumo do acesso ao arquivo com visualização filtrada

A tabela a seguir resume como um app que tem uma visualização filtrada no armazenamento externo pode acessar arquivos:

Localização do arquivo Permissões necessárias Método de acesso (*) Arquivos removidos quando o app é desinstalado?
Diretório específico do app Nenhuma getExternalFilesDir() Sim
Coleções de mídia
(fotos, vídeos, áudio)
READ_EXTERNAL_STORAGE
apenas ao
acessar arquivos de outros apps
MediaStore Não
Downloads
(documentos e
e-books)
Nenhuma Estrutura de acesso ao armazenamento
(carrega o seletor de arquivos do sistema)
Não

*Você pode usar a Estrutura de acesso ao armazenamento para acessar cada um dos locais mostrados na tabela anterior sem solicitar nenhuma permissão.

Adaptar à mudança tipos específicos de padrões de uso

Esta seção oferece recomendações para vários tipos específicos de apps baseados em mídia para que se adaptem à alteração no comportamento de armazenamento em apps voltados ao Android Q.

É uma prática recomendada usar a visualização filtrada, a menos que o app precise de acesso a um arquivo que não resida no próprio diretório ou em MediaStore.

Compartilhar arquivos de mídia

Alguns apps permitem que os usuários compartilhem arquivos de mídia entre si. Por exemplo, apps de mídia social dão aos usuários a habilidade de compartilhar fotos e vídeos com amigos.

Para acessar os arquivos de mídia que os usuários querem compartilhar, use a API MediaStore. Você pode usar essa mesma API para armazenar os arquivos que o usuário recebe por meio do app, aproveitando as melhorias introduzidas no Android Q.

Nos casos em que você fornece um conjunto de apps complementares, como um app de mensagens e um de perfil, configure o compartilhamento de arquivos usando os URIs content://. Já recomendamos esse fluxo de trabalho como uma prática recomendada de segurança.

Utilizar documentos

Alguns apps usam 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 empresarial ou um livro salvo como um arquivo EPUB.

Nesses casos, permita que o usuário escolha o arquivo a ser aberto invocando o intent ACTION_OPEN_DOCUMENT, que abre o app seletor de arquivos do sistema. Para mostrar apenas os tipos de arquivos compatíveis com seu app, inclua o extra Intent.EXTRA_MIME_TYPES no seu intent.

O exemplo ActionOpenDocument no GitHub (link em inglês) mostra como usar ACTION_OPEN_DOCUMENT para abrir um arquivo depois de receber o consentimento do usuário.

Gerenciar grupos de arquivos

Apps de gerenciamento de arquivos e criação de mídia geralmente gerenciam grupos de arquivos em uma hierarquia de diretórios. Esses apps podem invocar o intent ACTION_OPEN_DOCUMENT_TREE para permitir que o usuário conceda acesso a uma árvore de diretórios inteira. Tal app conseguiria editar qualquer arquivo no diretório selecionado, bem como qualquer um dos subdiretórios dele.

Utilizando essa interface, os usuários podem acessar arquivos de qualquer instância instalada do DocumentsProvider, compatível com qualquer solução baseada em nuvem ou localmente.

O exemplo ActionOpenDocumentTree no GitHub (link em inglês) mostra como usar ACTION_OPEN_DOCUMENT_TREE para abrir uma árvore de diretórios depois de receber o consentimento do usuário.

Acessar e editar conteúdo de mídia

Esta seção fornece as práticas recomendadas para carregar e armazenar arquivos de mídia em armazenamento externo para que seu app continue a proporcionar uma boa experiência do usuário no Android Q.

Observação: se um app tiver uma visualização filtrada no armazenamento externo e solicitar a permissão de ambiente de execução Armazenamento, ele poderá ver um determinado arquivo apenas se o arquivo residir no diretório específico do app ou em uma das seguintes coleções de mídia:

Mesmo com a permissão Armazenamento, esse app que acessa a visualização bruta do sistema de arquivos de um dispositivo de armazenamento externo tem acesso somente ao caminho bruto do app específico do pacote. Se um app tentar abrir arquivos fora do caminho específico do pacote usando uma visualização bruta de sistema de arquivos, ocorrerá um erro:

Acessar arquivos

Não carregue arquivos de mídia usando as colunas DATA obsoletas. Em vez disso, chame um dos seguintes métodos do ContentResolver:

  • Para a miniatura de um único arquivo de mídia, use loadThumbnail(), informando o tamanho da miniatura que você quer carregar.
  • Para um único arquivo de mídia, use openFileDescriptor().
  • Para uma coleção de arquivos de mídia, use query().

O snippet de código a seguir mostra como acessar arquivos de mídia:

    // Load thumbnail of a specific media item.
    val mediaThumbnail = resolver.loadThumbnail(item, Size(640, 480), null)

    // Open a specific media item.
    resolver.openFileDescriptor(item, mode).use { pfd ->
        // ...
    }

    // Find all videos on a given storage device, including pending files.
    val collection = MediaStore.Video.Media.getContentUri(volumeName)
    val collectionWithPending = MediaStore.setIncludePending(collection)
    resolver.query(collectionWithPending, null, null, null).use { c ->
        // ...
    }
    

Acesso a partir do código nativo

Você pode se deparar com situações em que seu app precise trabalhar com um arquivo de mídia específico em código nativo, como um arquivo que outro app tenha compartilhado com seu app ou um arquivo da coleção de mídia do usuário. Nesses casos, inicie a descoberta do arquivo de mídia no seu código baseado em Java ou em Kotlin e, em seguida, passe o descritor de arquivo associado para seu código nativo.

O snippet de código a seguir mostra como passar o descritor de arquivo de um objeto de mídia para o código nativo do seu app:

Kotlin

    val contentUri: Uri =
            ContentUris.withAppendedId(
            android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            cursor.getLong(BaseColumns._ID))
    val fileOpenMode = "r"
    val parcelFd = resolver.openFileDescriptor(uri, fileOpenMode)
    val fd = parcelFd?.detachFd()
    // Pass the integer value "fd" into your native code. Remember to call
    // close(2) on the file descriptor when you're done using it.
    

Java

    Uri contentUri = ContentUris.withAppendedId(
            android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            cursor.getLong(Integer.parseInt(BaseColumns._ID)));
    String fileOpenMode = "r";
    ParcelFileDescriptor parcelFd = resolver.openFileDescriptor(uri, fileOpenMode);
    if (parcelFd != null) {
        int fd = parcelFd.detachFd();
        // Pass the integer value "fd" into your native code. Remember to call
        // close(2) on the file descriptor when you're done using it.
    }
    

Para saber mais sobre como acessar arquivos em código nativo, veja a palestra Files for Miles (em inglês), da Conferência de Desenvolvedores Android 2018, a partir de 15:20.

Atualizar arquivos de mídia de outros apps

Para modificar um determinado arquivo de mídia que outro app salvou originalmente em um dispositivo de armazenamento externo, capture a RecoverableSecurityException que a plataforma gera. Você pode então solicitar que o usuário conceda ao app acesso de gravação para esse item específico, conforme mostrado no snippet de código a seguir:

Kotlin

    try {
        // ...
    } catch (rse: RecoverableSecurityException) {
        val requestAccessIntentSender = rse.userAction.actionIntent.intentSender

        // In your code, handle IntentSender.SendIntentException.
        startIntentSenderForResult(requestAccessIntentSender, your-request-code,
                null, 0, 0, 0, null)
    }
    

Java

    try {
        // ...
    } catch (RecoverableSecurityException rse) {
        IntentSender requestAccessIntentSender = rse.getUserAction()
                .getActionIntent().getIntentSender();

        // In your code, handle IntentSender.SendIntentException.
        startIntentSenderForResult(requestAccessIntentSender, your-request-code,
                null, 0, 0, 0, null);
    }
    

Informações de localização em fotos

Algumas fotos contêm informações de local nos metadados EXIF, que permitem que os usuários vejam o lugar em que uma foto foi tirada. Como as informações de local são confidenciais, por padrão, o Android Q as esconde do seu app se ele tiver uma visualização filtrada no armazenamento externo. Essa restrição às informações é diferente da que se aplica às características da câmera.

Caso seu app precise de acesso às informações de local de uma foto, siga as etapas a seguir:

  1. Adicione a nova permissão ACCESS_MEDIA_LOCATION ao manifest do seu app.
  2. A partir do objeto MediaStore, chame setRequireOriginal(), passando o URI da foto.

Um exemplo desse processo é mostrado no seguinte snippet de código:

Kotlin

    // Get location data from the ExifInterface class.
    val photoUri = MediaStore.setRequireOriginal(photoUri)
    contentResolver.openInputStream(photoUri).use { stream ->
        ExifInterface(stream).run {
            // If lat/long is null, fall back to the coordinates (0, 0).
            val latLong = ?: doubleArrayOf(0.0, 0.0)
        }
    }
    

Java

    Uri photoUri = Uri.withAppendedPath(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            cursor.getString(idColumnIndex));

    final double[] latLong;

    // Get location data from the ExifInterface class.
    photoUri = MediaStore.setRequireOriginal(photoUri);
    InputStream stream = getContentResolver().openInputStream(photoUri);
    if (stream != null) {
        ExifInterface exifInterface = new ExifInterface(stream);
        double[] returnedLatLong = exifInterface.getLatLong();

        // If lat/long is null, fall back to the coordinates (0, 0).
        latLong = returnedLatLong != null ? returnedLatLong : new double[2];

        // Don't reuse the stream associated with the instance of "ExifInterface".
        stream.close();
    } else {
        // Failed to load the stream, so return the coordinates (0, 0).
        latLong = new double[2];
    }
    

Nomes de colunas em consultas de conteúdo

Se o código do app usar uma projeção de nome de coluna, como mime_type AS MimeType, lembre-se de que o Android Q exige nomes de colunas definidos pela API MediaStore.

Se seu código depender de uma biblioteca que espera um nome de coluna indefinido na API Android, como MimeType, use CursorWrapper para traduzir dinamicamente o nome da coluna no processo do seu app.