Se você estiver desenvolvendo um app que oferece serviços de armazenamento de arquivos (por exemplo, um serviço de salvamento na nuvem), é possível disponibilizar seus arquivos com o framework de acesso ao armazenamento (SAF, na sigla em inglês) gravando um provedor de documentos personalizado. Esta página descreve como criar um provedor de documentos personalizado.
Para saber mais sobre como funciona o Framework de acesso ao armazenamento, consulte a Visão geral do Framework de acesso ao armazenamento.
Manifest
Para implementar um provedor de documentos personalizado, adicione o seguinte ao manifesto do aplicativo:
- Uma segmentação de API nível 19 ou posterior.
- Um elemento
<provider>
que declara seu provedor de armazenamento personalizado. -
O atributo
android:name
definido como o nome da subclasseDocumentsProvider
, que é o nome da classe, incluindo o nome do pacote:com.example.android.storageprovider.MyCloudProvider
. -
O atributo
android:authority
, que é o nome do pacote (neste exemplo,com.example.android.storageprovider
) mais o tipo de provedor de conteúdo (documents
). - O atributo
android:exported
definido como"true"
. É necessário exportar seu provedor para que outros apps possam vê-lo. - O atributo
android:grantUriPermissions
definido como"true"
. Essa configuração permite que o sistema conceda a outros apps acesso ao conteúdo do seu provedor. Para conferir uma discussão sobre como esses outros apps podem manter o acesso ao conteúdo do provedor, consulte Persistir permissões. - a permissão
MANAGE_DOCUMENTS
; Por padrão, um provedor está disponível para todos. O acréscimo dessa permissão restringe seu provedor ao sistema. Essa restrição é importante para a segurança. - Um filtro de intent que inclua a ação
android.content.action.DOCUMENTS_PROVIDER
para que seu provedor apareça no seletor quando o sistema procurar por provedores.
Veja alguns trechos de uma amostra de manifesto que inclui 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"> <intent-filter> <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> </intent-filter> </provider> </application> </manifest>
Oferecer compatibilidade com dispositivos com o Android 4.3 e versões anteriores
A intent
ACTION_OPEN_DOCUMENT
só está disponível
em dispositivos com o Android 4.4 e versões mais recentes.
Se você quer que seu aplicativo ofereça suporte a ACTION_GET_CONTENT
para acomodar dispositivos que executam o Android 4.3 e versões anteriores, desative o filtro de intent ACTION_GET_CONTENT
no
seu manifesto para dispositivos com o Android 4.4 ou mais recente. Um
provedor de documentos e ACTION_GET_CONTENT
precisam ser considerados
mutuamente exclusivos. Se o app tiver suporte para os dois simultaneamente, ele
vai aparecer duas vezes na interface do seletor do sistema, oferecendo duas maneiras diferentes de acessar
os dados armazenados. Isso é confuso para os usuários.
Esta é a maneira recomendada de desativar o filtro de intent
ACTION_GET_CONTENT
para dispositivos
com o Android 4.4 ou versão mais recente:
- No seu arquivo de recursos
bool.xml
emres/values/
, adicione esta linha:<bool name="atMostJellyBeanMR2">true</bool>
- No seu arquivo de recursos
bool.xml
emres/values-v19/
, adicione esta linha:<bool name="atMostJellyBeanMR2">false</bool>
- Adicione um
alias
de atividade para desativar o filtro de intent
ACTION_GET_CONTENT
para as versões 4.4 (nível 19 da API) e mais recentes. 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, conforme descrito no
guia para desenvolvedores sobre
Provedores de conteúdo. Uma classe de contrato é uma classe public final
que contém definições de constantes para os URIs, nomes de coluna, tipos MIME e
outros metadados pertencentes ao provedor. A SAF
fornece essas classes de contrato, para que você não precise escrever
as próprias classes:
Por exemplo, veja as colunas que você pode retornar em um cursor quando o provedor de documentos for consultado em busca de documentos ou da raiz:
Kotlin
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf( DocumentsContract.Root.COLUMN_ROOT_ID, DocumentsContract.Root.COLUMN_MIME_TYPES, DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.COLUMN_ICON, DocumentsContract.Root.COLUMN_TITLE, DocumentsContract.Root.COLUMN_SUMMARY, DocumentsContract.Root.COLUMN_DOCUMENT_ID, DocumentsContract.Root.COLUMN_AVAILABLE_BYTES ) private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf( DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_FLAGS, DocumentsContract.Document.COLUMN_SIZE )
Java
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,};
Seu cursor para a raiz precisa incluir algumas colunas obrigatórias. As colunas são:
O cursor para documentos precisa incluir as seguintes colunas obrigatórias:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
Criar uma subclasse de DocumentsProvider
A próxima etapa na gravação de um provedor de documentos personalizado é criar uma subclasse para a
classe abstrata DocumentsProvider
. No mínimo, é necessário
implementar os métodos abaixo:
Esses são os únicos métodos que você precisa implementar, mas há
muitos outros que podem ser usados. Consulte DocumentsProvider
para ver mais detalhes.
Definir uma raiz
A implementação de queryRoots()
precisa retornar um Cursor
que aponta para todos
os diretórios raiz do provedor de documentos, usando colunas definidas em
DocumentsContract.Root
.
No snippet a seguir, o parâmetro projection
representa os
campos específicos que o autor da chamada quer recuperar. O snippet cria um novo cursor
e adiciona uma linha a ele: uma raiz, um diretório de nível superior, como
Downloads ou Imagens. A maior parte dos provedores só tem uma raiz. É possível ter mais de uma,
por exemplo, no caso de várias contas de usuário. Nesse caso, basta adicionar
uma segunda linha ao cursor.
Kotlin
override fun queryRoots(projection: Array<out String>?): Cursor { // Use a MatrixCursor to build a cursor // with either the requested fields, or the default // projection if "projection" is null. val result = 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. result.newRow().apply { add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT) // You can provide an optional summary, which helps distinguish roots // with the same title. You can also use this field for displaying an // user account name. add(DocumentsContract.Root.COLUMN_SUMMARY, context.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. add( DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or DocumentsContract.Root.FLAG_SUPPORTS_SEARCH ) // COLUMN_TITLE is the root title (e.g. Gallery, Drive). add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title)) // This document id cannot change after it's shared. add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir)) // The child MIME types are used to filter the roots and only present to the // user those roots that contain the desired type somewhere in their file hierarchy. add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir)) add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace) add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher) } return result }
Java
@Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { // Use a MatrixCursor to build 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. final MatrixCursor.RowBuilder row = result.newRow(); row.add(Root.COLUMN_ROOT_ID, ROOT); // You can provide an optional summary, which helps distinguish roots // with the same title. You can also use this field for displaying an // user account name. 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 after it's shared. row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir)); // The child MIME types are used to filter the roots and only present to the // user those roots that contain the desired type somewhere in their file hierarchy. row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir)); row.add(Root.COLUMN_AVAILABLE_BYTES, baseDir.getFreeSpace()); row.add(Root.COLUMN_ICON, R.drawable.ic_launcher); return result; }
Se o provedor de documentos se conectar a um conjunto dinâmico de raízes (por exemplo, a um dispositivo USB que possa ser desconectado ou a uma conta de onde o usuário possa sair), será possível atualizar a IU do documento para manter a sincronia com essas alterações usando o método ContentResolver.notifyChange()
, conforme mostrado no snippet de código a seguir.
Kotlin
val rootsUri: Uri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY) context.contentResolver.notifyChange(rootsUri, null)
Java
Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY); context.getContentResolver().notifyChange(rootsUri, null);
Listar documentos no provedor
Sua implementação de
queryChildDocuments()
precisa retornar um Cursor
que aponte para todos os arquivos no
diretório especificado, usando colunas definidas em
DocumentsContract.Document
.
Esse método é chamado quando o usuário escolhe sua raiz na IU do seletor.
O método recupera os filhos do ID do documento especificado por COLUMN_DOCUMENT_ID
.
Em seguida, o sistema chamará esse método sempre que o usuário selecionar um
subdiretório no seu provedor de documentos.
Esse snippet cria um novo cursor com as colunas solicitadas e, em seguida, adiciona ao cursor informações sobre cada filho imediato no diretório pai. Um filho pode ser uma imagem, outro diretório, qualquer arquivo:
Kotlin
override fun queryChildDocuments( parentDocumentId: String?, projection: Array<out String>?, sortOrder: String? ): Cursor { return MatrixCursor(resolveDocumentProjection(projection)).apply { val parent: File = getFileForDocId(parentDocumentId) parent.listFiles() .forEach { file -> includeFile(this, null, file) } } }
Java
@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; }
Receber informações de documentos
Sua implementação de queryDocument()
precisa retornar um Cursor
que aponte para o arquivo especificado, usando colunas definidas em DocumentsContract.Document
.
O método queryDocument()
retorna as mesmas informações que foram transmitidas em
queryChildDocuments()
,
mas para um arquivo específico:
Kotlin
override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor { // Create a cursor with the requested projection, or the default projection. return MatrixCursor(resolveDocumentProjection(projection)).apply { includeFile(this, documentId, null) } }
Java
@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; }
Seu provedor de documentos também pode fornecer miniaturas de um documento
modificando o método
DocumentsProvider.openDocumentThumbnail()
e adicionando a sinalização
FLAG_SUPPORTS_THUMBNAIL
aos arquivos compatíveis.
O snippet de código abaixo oferece um exemplo de como implementar o
DocumentsProvider.openDocumentThumbnail()
.
Kotlin
override fun openDocumentThumbnail( documentId: String?, sizeHint: Point?, signal: CancellationSignal? ): AssetFileDescriptor { val file = getThumbnailFileForDocId(documentId) val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) return AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH) }
Java
@Override public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { final File file = getThumbnailFileForDocId(documentId); final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); }
Cuidado: um provedor de documentos não deve retornar imagens em miniatura com mais do que o dobro do tamanho especificado pelo parâmetro sizeHint
.
Abrir um documento
Implemente openDocument()
para retornar um ParcelFileDescriptor
que represente
o arquivo especificado. Outros apps podem usar o ParcelFileDescriptor
retornado
para fazer streaming de dados. O sistema chama esse método depois que o usuário seleciona um arquivo
e o app cliente solicita acesso chamando
openFileDescriptor()
.
Por exemplo:
Kotlin
override fun openDocument( documentId: String, mode: String, signal: CancellationSignal ): ParcelFileDescriptor { 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). val file: File = getFileForDocId(documentId) val accessMode: Int = ParcelFileDescriptor.parseMode(mode) val isWrite: Boolean = mode.contains("w") return if (isWrite) { val handler = Handler(context.mainLooper) // Attach a close listener if the document is opened in write mode. try { ParcelFileDescriptor.open(file, accessMode, handler) { // 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 (e: IOException) { throw FileNotFoundException( "Failed to open document with id $documentId and mode $mode" ) } } else { ParcelFileDescriptor.open(file, accessMode) } }
Java
@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 int accessMode = ParcelFileDescriptor.parseMode(mode); 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); } }
Se o provedor de documentos transmitir arquivos ou processar estruturas
de dados complicadas, implemente os métodos
createReliablePipe()
ou
createReliableSocketPair()
.
Esses métodos permitem criar um par de objetos
ParcelFileDescriptor
, em que você pode retornar um
e enviar o outro por meio de um
ParcelFileDescriptor.AutoCloseOutputStream
ou
ParcelFileDescriptor.AutoCloseInputStream
.
Oferecer compatibilidade com documentos recentes e pesquisa
Você pode fornecer uma lista de documentos modificados recentemente na raiz do seu provedor de documentos substituindo o método queryRecentDocuments()
e retornando FLAG_SUPPORTS_RECENTS
. O snippet de código a seguir mostra um exemplo de como implementar os métodos queryRecentDocuments()
.
Kotlin
override fun queryRecentDocuments(rootId: String?, projection: Array<out String>?): Cursor { // This example implementation walks a // local file structure to find the most recently // modified files. Other implementations might // include making a network call to query a // server. // Create a cursor with the requested projection, or the default projection. val result = MatrixCursor(resolveDocumentProjection(projection)) val parent: File = getFileForDocId(rootId) // Create a queue to store the most recent documents, // which orders by last modified. val lastModifiedFiles = PriorityQueue( 5, Comparator<File> { i, j -> Long.compare(i.lastModified(), j.lastModified()) } ) // Iterate through all files and directories // in the file structure under the root. If // the file is more recent than the least // recently modified, add it to the queue, // limiting the number of results. val pending : MutableList<File> = mutableListOf() // Start by adding the parent to the list of files to be processed pending.add(parent) // Do while we still have unexamined files while (pending.isNotEmpty()) { // Take a file from the list of unprocessed files val file: File = pending.removeAt(0) if (file.isDirectory) { // If it's a directory, add all its children to the unprocessed list pending += file.listFiles() } else { // If it's a file, add it to the ordered queue. lastModifiedFiles.add(file) } } // Add the most recent files to the cursor, // not exceeding the max number of results. for (i in 0 until Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size)) { val file: File = lastModifiedFiles.remove() includeFile(result, null, file) } return result }
Java
@Override public Cursor queryRecentDocuments(String rootId, String[] projection) throws FileNotFoundException { // This example implementation walks a // local file structure to find the most recently // modified files. Other implementations might // include making a network call to query a // server. // Create a cursor with the requested projection, or the default projection. final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); final File parent = getFileForDocId(rootId); // Create a queue to store the most recent documents, // which orders by last modified. PriorityQueue lastModifiedFiles = new PriorityQueue(5, new Comparator() { public int compare(File i, File j) { return Long.compare(i.lastModified(), j.lastModified()); } }); // Iterate through all files and directories // in the file structure under the root. If // the file is more recent than the least // recently modified, add it to the queue, // limiting the number of results. final LinkedList pending = new LinkedList(); // Start by adding the parent to the list of files to be processed pending.add(parent); // Do while we still have unexamined files while (!pending.isEmpty()) { // Take a file from the list of unprocessed files final File file = pending.removeFirst(); if (file.isDirectory()) { // If it's a directory, add all its children to the unprocessed list Collections.addAll(pending, file.listFiles()); } else { // If it's a file, add it to the ordered queue. lastModifiedFiles.add(file); } } // Add the most recent files to the cursor, // not exceeding the max number of results. for (int i = 0; i < Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size()); i++) { final File file = lastModifiedFiles.remove(); includeFile(result, null, file); } return result; }
Você pode ver o código completo do snippet acima fazendo o download do exemplo de código StorageProvider (link em inglês).
Oferecer compatibilidade com a criação de documentos
Você pode permitir que apps clientes criem arquivos no seu provedor de documentos.
Se um app cliente enviar um intent ACTION_CREATE_DOCUMENT
, seu provedor de documentos poderá permitir que esse app cliente crie
novos documentos no provedor.
Para oferecer suporte à criação de documentos, sua raiz precisa ter a sinalização FLAG_SUPPORTS_CREATE
.
Os diretórios que permitem a criação de novos arquivos precisam ter a
flag
FLAG_DIR_SUPPORTS_CREATE
.
Seu provedor de documentos também precisa implementar o método createDocument()
. Quando um usuário seleciona um diretório no seu
provedor de documentos para salvar um novo arquivo, o provedor recebe uma chamada para
createDocument()
. Dentro da implementação do método
createDocument()
, um novo
COLUMN_DOCUMENT_ID
é retornado para o
arquivo. O app cliente pode usar esse ID para conseguir um identificador para o arquivo
e, por fim, chamar
openDocument()
para gravar no novo arquivo.
O snippet de código a seguir demonstra como criar um novo arquivo em um provedor de documentos.
Kotlin
override fun createDocument(documentId: String?, mimeType: String?, displayName: String?): String { val parent: File = getFileForDocId(documentId) val file: File = try { File(parent.path, displayName).apply { createNewFile() setWritable(true) setReadable(true) } } catch (e: IOException) { throw FileNotFoundException( "Failed to create document with name $displayName and documentId $documentId" ) } return getDocIdForFile(file) }
Java
@Override public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException { File parent = getFileForDocId(documentId); File file = new File(parent.getPath(), displayName); try { file.createNewFile(); file.setWritable(true); file.setReadable(true); } catch (IOException e) { throw new FileNotFoundException("Failed to create document with name " + displayName +" and documentId " + documentId); } return getDocIdForFile(file); }
Você pode ver o código completo do snippet acima fazendo o download do exemplo de código StorageProvider (link em inglês).
Oferecer compatibilidade com recursos de gerenciamento de documentos
Além de abrir, criar e visualizar arquivos, o provedor de documentos
também pode permitir que apps clientes renomeiem, copiem, movam e excluam
arquivos. Para adicionar a funcionalidade de gerenciamento de documentos
ao seu provedor, adicione uma sinalização à coluna
COLUMN_FLAGS
do documento
para indicar a funcionalidade compatível. Também é necessário implementar
o método correspondente da classe
DocumentsProvider
.
A tabela a seguir fornece a
flag COLUMN_FLAGS
e o método DocumentsProvider
que um provedor de
documentos precisa implementar para expor recursos específicos.
Recurso | Sinalização | Método |
---|---|---|
Excluir um arquivo |
FLAG_SUPPORTS_DELETE
|
deleteDocument()
|
Renomear um arquivo |
FLAG_SUPPORTS_RENAME
|
renameDocument()
|
Copiar um arquivo para um novo diretório pai no provedor de documentos |
FLAG_SUPPORTS_COPY
|
copyDocument()
|
Mover um arquivo de um diretório para outro no provedor de documentos |
FLAG_SUPPORTS_MOVE
|
moveDocument()
|
Remover um arquivo do diretório pai |
FLAG_SUPPORTS_REMOVE
|
removeDocument()
|
Oferecer compatibilidade com arquivos virtuais e formatos de arquivos alternativos
Os arquivos virtuais, um recurso introduzido no Android 7.0 (API de nível 24), permitem que os provedores de documentos ofereçam acesso de visualização a arquivos que não têm uma representação direta do bytecode. Para permitir que outros apps vejam arquivos virtuais, seu provedor de documentos precisa produzir uma representação alternativa de arquivo que possa ser aberta para os arquivos virtuais.
Por exemplo, imagine que um provedor de documentos contém um formato de arquivo que outros apps não podem abrir diretamente, basicamente um arquivo virtual.
Quando um app cliente envia um intent ACTION_VIEW
sem a categoria CATEGORY_OPENABLE
, os usuários podem selecionar esses arquivos virtuais no provedor de documentos para visualização. Em seguida, o provedor de documentos retorna o arquivo virtual em um formato de arquivo diferente, mas que pode ser aberto, como uma imagem.
Assim, o app cliente pode abrir o arquivo virtual para que o usuário o veja.
Para declarar que um documento no provedor é virtual, adicione a sinalização
FLAG_VIRTUAL_DOCUMENT
ao arquivo retornado pelo
método
queryDocument()
. Essa sinalização alerta os apps clientes de que o arquivo não tem uma representação direta de bytecode
e não pode ser aberto diretamente.
Se você declarar que um arquivo no seu provedor de documentos é virtual, é altamente recomendável disponibilizá-lo em outro tipo MIME, como uma imagem ou um PDF. O provedor de documentos
declara os tipos MIME alternativos com
suporte para a visualização de um arquivo virtual modificando o
método
getDocumentStreamTypes()
. Quando os apps clientes chamam o método
getStreamTypes(android.net.Uri, java.lang.String)
, o sistema chama o método
getDocumentStreamTypes()
do provedor de documentos. Depois, o método getDocumentStreamTypes()
retorna uma matriz de tipos MIME alternativos que são compatíveis com o provedor de documentos para o arquivo.
Depois que o cliente determina
que o provedor de documentos pode produzir o documento em um formato de arquivo
visível, o app cliente chama o método
openTypedAssetFileDescriptor()
, que chama internamente o método
openTypedDocument()
do provedor de documentos. O provedor de documentos retorna o arquivo ao app cliente no
formato de arquivo solicitado.
O snippet de código a seguir demonstra uma implementação simples dos métodos getDocumentStreamTypes()
e openTypedDocument()
.
Kotlin
var SUPPORTED_MIME_TYPES : Array<String> = arrayOf("image/png", "image/jpg") override fun openTypedDocument( documentId: String?, mimeTypeFilter: String, opts: Bundle?, signal: CancellationSignal? ): AssetFileDescriptor? { return try { // Determine which supported MIME type the client app requested. when(mimeTypeFilter) { "image/jpg" -> openJpgDocument(documentId) "image/png", "image/*", "*/*" -> openPngDocument(documentId) else -> throw IllegalArgumentException("Invalid mimeTypeFilter $mimeTypeFilter") } } catch (ex: Exception) { Log.e(TAG, ex.message) null } } override fun getDocumentStreamTypes(documentId: String, mimeTypeFilter: String): Array<String> { return when (mimeTypeFilter) { "*/*", "image/*" -> { // Return all supported MIME types if the client app // passes in '*/*' or 'image/*'. SUPPORTED_MIME_TYPES } else -> { // Filter the list of supported mime types to find a match. SUPPORTED_MIME_TYPES.filter { it == mimeTypeFilter }.toTypedArray() } } }
Java
public static String[] SUPPORTED_MIME_TYPES = {"image/png", "image/jpg"}; @Override public AssetFileDescriptor openTypedDocument(String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) { try { // Determine which supported MIME type the client app requested. if ("image/png".equals(mimeTypeFilter) || "image/*".equals(mimeTypeFilter) || "*/*".equals(mimeTypeFilter)) { // Return the file in the specified format. return openPngDocument(documentId); } else if ("image/jpg".equals(mimeTypeFilter)) { return openJpgDocument(documentId); } else { throw new IllegalArgumentException("Invalid mimeTypeFilter " + mimeTypeFilter); } } catch (Exception ex) { Log.e(TAG, ex.getMessage()); } finally { return null; } } @Override public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) { // Return all supported MIME tyupes if the client app // passes in '*/*' or 'image/*'. if ("*/*".equals(mimeTypeFilter) || "image/*".equals(mimeTypeFilter)) { return SUPPORTED_MIME_TYPES; } ArrayList requestedMimeTypes = new ArrayList<>(); // Iterate over the list of supported mime types to find a match. for (int i=0; i < SUPPORTED_MIME_TYPES.length; i++) { if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) { requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]); } } return (String[])requestedMimeTypes.toArray(); }
Segurança
Suponha que seu provedor de documentos seja um serviço de armazenamento em nuvem protegido por senha
e você queira garantir que os usuários estejam conectados antes de começar a compartilhar os arquivos.
O que seu app deverá fazer se o usuário não estiver conectado? A solução é retornar
zero raízes na implementação de queryRoots()
. Ou seja, um cursor de raiz vazio:
Kotlin
override fun queryRoots(projection: Array<out String>): Cursor { ... // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result }
Java
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 getContentResolver().notifyChange()
.
Você se lembra do DocumentsContract
? Nós o usamos para criar esse URI. O snippet a seguir instrui o sistema a consultar 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 para queryRoots()
retornará um
cursor vazio, como mostrado acima. Isso garante que os documentos de um provedor só estejam
disponíveis se o usuário estiver conectado a ele.
Kotlin
private fun onLoginButtonClick() { loginOrLogout() getContentResolver().notifyChange( DocumentsContract.buildRootsUri(AUTHORITY), null ) }
Java
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }
Para ver a amostra de código relacionada a esta página, consulte:
- StorageProvider (link em inglês)
- StorageClient (link em inglês)
Para ver vídeos relacionados a esta página, consulte:
- DevBytes: Framework de acesso ao armazenamento para o Android 4.4 (provedor)
- Estrutura de acesso ao armazenamento: como criar um DocumentsProvider (link em inglês)
- Arquivos virtuais no Framework de acesso ao armazenamento
Para ver mais informações relacionadas, consulte:
- Como criar um DocumentsProvider (link em inglês)
- Abrir arquivos com o framework de acesso ao armazenamento
- Noções básicas sobre o provedor de conteúdo