Se você está desenvolvendo um app que oferece serviços de armazenamento de arquivos, como o recurso "Salvar na nuvem", é possível disponibilizar seus arquivos por meio do 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 mais informações sobre o funcionamento desse framework, consulte a Visão geral do framework de acesso ao armazenamento.
Manifesto
Para implementar um provedor de documentos personalizado, adicione o seguinte ao manifesto do app:
- Uma segmentação de API nível 19 ou posterior.
- Um elemento
<provider>
que declare seu provedor de armazenamento personalizado. -
O atributo
android:name
definido para o nome da sua subclasseDocumentsProvider
, que é o nome da classe, incluindo o nome do pacote:com.example.android.storageprovider.MyCloudProvider
. -
O atributo
android:authority
, que é o nome do seu pacote (neste exemplo,com.example.android.storageprovider
) e 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 ver uma discussão sobre como persistir uma autorização para um documento específico, consulte Persistir permissões. - A permissão
MANAGE_DOCUMENTS
. Por padrão, um provedor é disponibilizado 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 inclui a ação
android.content.action.DOCUMENTS_PROVIDER
, para que seu provedor apareça no seletor quando o sistema procurar 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
O intent ACTION_OPEN_DOCUMENT
só está disponível em dispositivos com o Android 4.4 e versões posteriores.
Se você quiser que seu app seja compatível com ACTION_GET_CONTENT
para acomodar dispositivos com o Android 4.3 e versões anteriores, desative o filtro de intent ACTION_GET_CONTENT
no manifesto para dispositivos com o Android 4.4 ou posterior. Um provedor de documentos e ACTION_GET_CONTENT
devem ser considerados mutuamente exclusivos. Se seu app for compatível com ambos simultaneamente, ele aparecerá duas vezes na IU do seletor do sistema, oferecendo duas formas diferentes de acessar seus dados armazenados. Isso é confuso para os usuários.
Esta é a maneira recomendada de desativar o filtro de intent ACTION_GET_CONTENT
para dispositivos com a versão 4.4 ou posterior do Android:
- No seu arquivo de recursos
bool.xml
, emres/values/
, adicione a seguinte linha:<bool name="atMostJellyBeanMR2">true</bool>
- No seu arquivo de recursos
bool.xml
, emres/values-v19/
, adicione a seguinte 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 (API 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
Geralmente, ao gravar um provedor de conteúdo personalizado, uma das tarefas implementa classes de contrato, conforme descrito no guia para desenvolvedores Provedores de conteúdo. Uma classe de contrato é uma classe public final
que contém definições de constante para URIs, nomes de coluna, tipos MIME e outros metadados relacionados ao provedor. O SAF oferece essas classes de contratos. Assim, não é necessário gravar a própria classe:
Por exemplo, estas são as colunas que você pode retornar em um cursor quando o provedor de documentos é 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 da gravação de um provedor de documentos personalizado é criar uma subclasse para a classe DocumentsProvider
abstrata. É preciso implementar pelo menos os seguintes métodos:
Esses são os únicos métodos que precisam, obrigatoriamente, ser implementados, mas você pode usar muitos outros. Consulte DocumentsProvider
para ver detalhes.
Definir uma raiz
Sua implementação de queryRoots()
precisa retornar um Cursor
que aponte para todos os diretórios raiz do seu provedor de documentos, usando as 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. Você pode ter mais de uma, por exemplo, se houver 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 seu provedor de documentos se conectar a um conjunto dinâmico de raízes, por exemplo, um dispositivo USB que pode estar desconectado ou uma conta da qual o usuário pode sair, será possível atualizar a IU do documento para ficar em sincronia com essas mudanças por meio do 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
A implementação de queryChildDocuments()
precisa retornar um Cursor
que aponte para todos os arquivos no diretório especificado usando as 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 código do documento especificado por COLUMN_DOCUMENT_ID
.
Então, 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 direto 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 as colunas definidas em DocumentsContract.Document
.
O método queryDocument()
retorna as mesmas informações 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 disponibilizar 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 a seguir 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 que o dobro do tamanho especificado pelo parâmetro sizeHint
.
Abrir um documento
Você precisa implementar openDocument()
para retornar um ParcelFileDescriptor
que represente o arquivo especificado. Outros apps podem usar o ParcelFileDescriptor
retornado para transmitir 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 transferir arquivos ou processar estruturas de dados complexas, implemente os métodos createReliablePipe()
ou createReliableSocketPair()
.
Esses métodos permitem criar um par de objetos ParcelFileDescriptor
, sendo 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
É possível oferecer uma lista de documentos modificados recentemente na raiz do seu provedor de documentos modificando 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 .
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; }
Para ver o código completo do snippet acima, faça o download da amostra de código StorageProvider (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 a criação de novos documentos no próprio provedor.
Para oferecer compatibilidade com a criação de documentos, sua raiz precisa ter a sinalização FLAG_SUPPORTS_CREATE
.
Os diretórios que permitem que novos arquivos sejam criados precisam ter a sinalização FLAG_DIR_SUPPORTS_CREATE
.
Seu provedor de documentos também precisa implementar o método createDocument()
. Quando um usuário seleciona um diretório dentro do seu provedor de documentos para salvar um novo arquivo, o provedor recebe uma chamada para createDocument()
. Dentro da implementação do método createDocument()
, retorne um novo COLUMN_DOCUMENT_ID
para o arquivo. O app cliente pode usar esse código 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); }
Para ver o código completo do snippet acima, faça o download da amostra de código StorageProvider (em inglês).
Oferecer compatibilidade com recursos de gerenciamento de documentos
Além de abrir, criar e visualizar arquivos, seu 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 oferece a sinalização 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 em um novo diretório pai no provedor de documentos |
FLAG_SUPPORTS_COPY
|
copyDocument()
|
Mover um arquivo de um diretório para outro dentro do 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
Arquivos virtuais, um recurso introduzido no Android 7.0 (API nível 24), permite que 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 disponível para abertura para os arquivos virtuais.
Por exemplo, imagine que o provedor de documentos contenha um formato de arquivo que outros apps não possam 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 dentro do fornecedor de documentos para visualização. Então, o provedor de documentos retorna o arquivo virtual em um formato de arquivo diferente, mas que pode ser aberto, por exemplo, 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, é preciso adicionar 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 do bytecode e não pode ser aberto diretamente.
Ao declarar que um arquivo no seu provedor de documentos é virtual, é altamente recomendável disponibilizá-lo em outro tipo de MIME, como uma imagem ou um PDF. O provedor de documentos declara os tipos MIME alternativos que são compatíveis 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. Em seguida, o método getDocumentStreamTypes()
retorna uma matriz de tipos MIME alternativos 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 visualizável, o app cliente chama o método openTypedAssetFileDescriptor()
, que chama o método openTypedDocument()
do provedor de documentos internamente. O provedor de documentos retorna o arquivo para o 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 que você queira garantir que os usuários estejam conectados antes de começar a compartilhar arquivos.
O que seu app deverá fazer se o usuário não estiver conectado? A solução é retornar zero raiz na sua 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
? Estamos usando ele para criar este URI. O snippet a seguir diz ao sistema para consultar as raízes do seu 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ó estarão disponíveis se o usuário estiver conectado no provedor.
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 (em inglês)
- StorageClient (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 (em inglês)
- Framework de acesso ao armazenamento: como criar um DocumentsProvider (em inglês)
- Arquivos virtuais no Framework de acesso ao armazenamento (em inglês)
Para ver mais informações relacionadas, consulte: