Se você estiver desenvolvendo um aplicativo que fornece serviços de armazenamento para arquivos (como um serviço de salvamento na nuvem), é possível disponibilizar seus arquivos pelo 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 como o framework de acesso ao armazenamento funciona, consulte a Visão geral do framework de acesso ao armazenamento.
Manifesto
Para implementar um provedor de documentos personalizado, adicione o seguinte ao arquivo manifesto do app:
- Uma segmentação de API nível 19 ou posterior.
- Um elemento
<provider>
que declara o armazenamento personalizado de nuvem. -
O atributo
android:name
definido para o nome daDocumentsProvider
, 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
). além do 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
foi definido como"true"
. Esta configuração permite que o sistema conceda acesso a outros apps ao conteúdo no seu provedor. Para uma discussão sobre como esses outros aplicativos podem manter o acesso ao conteúdo do seu provedor, consulte Persistir do Google Cloud. - 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 inclui o
android.content.action.DOCUMENTS_PROVIDER
, para que seu provedor aparece no seletor quando o sistema procura 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
A intent ACTION_OPEN_DOCUMENT
só está disponível
em dispositivos com Android 4.4 ou superior.
Se você quiser que seu aplicativo seja compatível com ACTION_GET_CONTENT
para acomodar dispositivos que executam o Android 4.3 e versões anteriores,
desativar o filtro de intent ACTION_GET_CONTENT
em
seu manifesto para dispositivos com Android 4.4 ou superior. Um
provedor de documentos e ACTION_GET_CONTENT
precisam ser considerados
mutuamente exclusivos. Se você oferecer suporte aos dois ao mesmo tempo, seu app
aparece duas vezes na interface do seletor do sistema, oferecendo duas maneiras diferentes de acessar
seus dados armazenados. Isso é confuso para os usuários.
Esta é a forma recomendada de desativar o
Filtro de intent ACTION_GET_CONTENT
para dispositivos
com o Android 4.4 ou superior:
- No arquivo de recursos
bool.xml
emres/values/
, adicione esta linha:<bool name="atMostJellyBeanMR2">true</bool>
- No arquivo de recursos
bool.xml
emres/values-v19/
, adicione esta linha:<bool name="atMostJellyBeanMR2">false</bool>
- Adicione um
atividade
alias para desativar a intent
ACTION_GET_CONTENT
filtro 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 nas
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 colunas, tipos MIME e
outros metadados que pertençam ao provedor. A SAF
fornece essas classes de contrato para que você não precise escrever seu
por conta própria:
Por exemplo, aqui estão as colunas que você pode retornar em um cursor quando seu 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 na criação de um provedor de documentos personalizado é criar uma subclasse para
classe abstrata DocumentsProvider
. No mínimo, você precisa
implemente estes métodos:
Esses são os únicos métodos que você precisa implementar, mas há
há muitas outras que você pode querer. Consulte DocumentsProvider
para mais detalhes.
Definir uma raiz
Sua implementação de queryRoots()
precisa retornar um Cursor
apontando para todos
dos diretórios raiz do provedor de documentos, usando colunas definidas em
DocumentsContract.Root
No snippet a seguir, o parâmetro projection
representa
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 um,
por exemplo, no caso de várias contas de usuário. Nesse caso, basta adicionar um
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; }
Caso seu provedor de documentos se conecte a um conjunto dinâmico de raízes, por exemplo, a uma conexão USB
um dispositivo desconectado ou uma conta da qual o usuário possa sair.
pode atualizar a interface do documento para ficar em sincronia com essas alterações usando o
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 do
queryChildDocuments()
precisa retornar um Cursor
que aponte para todos os arquivos em
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 pelo
COLUMN_DOCUMENT_ID
:
Em seguida, o sistema chama esse método sempre que o usuário seleciona um
em seu provedor de documentos.
Esse snippet cria um novo cursor com as colunas solicitadas e adiciona informações sobre cada filho imediato no diretório pai ao cursor. 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 do
queryDocument()
precisa retornar um Cursor
que aponte para o arquivo especificado,
usando colunas definidas em DocumentsContract.Document
.
O queryDocument()
retorna as mesmas informações que foram passadas
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 para um documento ao
substituindo
DocumentsProvider.openDocumentThumbnail()
e adicionar o
FLAG_SUPPORTS_THUMBNAIL
aos arquivos compatíveis.
O snippet de código a seguir fornece um exemplo de como implementar a
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); }
Atenção:
Um provedor de documentos não deve retornar imagens em miniatura mais que o dobro do tamanho
o tamanho especificado pelo parâmetro sizeHint
.
Abrir um documento
Implemente openDocument()
para retornar um ParcelFileDescriptor
que represente
no 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()
:
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); } }
Caso seu provedor de documentos transmita arquivos ou processe arquivos
estruturas de dados, considere implementar
createReliablePipe()
ou
createReliableSocketPair()
.
Esses métodos permitem criar um par de
Objetos ParcelFileDescriptor
, em que é possível retornar um
e enviar a outra por uma
ParcelFileDescriptor.AutoCloseOutputStream
ou
ParcelFileDescriptor.AutoCloseInputStream
Oferecer compatibilidade com documentos recentes e pesquisa
É possível fornecer uma lista de documentos modificados recentemente na raiz da sua
provedor de documentos substituindo o arquivo
queryRecentDocuments()
e retornando
FLAG_SUPPORTS_RECENTS
,
O snippet de código a seguir mostra um exemplo de como implementar a
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 obter o código completo para o snippet acima fazendo download do StorageProvider (link em inglês) exemplo de código.
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 ACTION_CREATE_DOCUMENT
seu provedor de documentos pode permitir que esse app cliente crie
novos documentos no provedor de documentos.
Para oferecer suporte à criação de documentos, sua raiz precisa ter o
sinalização FLAG_SUPPORTS_CREATE
.
Os diretórios que permitem a criação de novos arquivos precisam ter a
FLAG_DIR_SUPPORTS_CREATE
.
Seu provedor de documentos também precisa implementar a
createDocument()
. Quando um usuário seleciona um diretório em seu
provedor de documentos salvar um novo arquivo, ele receberá uma chamada para
createDocument()
: Dentro da implementação
createDocument()
, você retorna uma nova
COLUMN_DOCUMENT_ID
para o
. 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 obter o código completo para o snippet acima fazendo download do StorageProvider (link em inglês) exemplo de código.
Oferecer compatibilidade com recursos de gerenciamento de documentos
Além de abrir, criar e visualizar arquivos, seu provedor de documentos
também permite que apps clientes renomeiem, copiem, movam e excluam
. Para adicionar a funcionalidade de gerenciamento de documentos ao
seu provedor de documentos, adicione uma sinalização
COLUMN_FLAGS
coluna
para indicar a funcionalidade compatível. Você também precisa implementar
O método correspondente do DocumentsProvider
.
A tabela a seguir mostra
Sinalização COLUMN_FLAGS
e DocumentsProvider
que um documento
provedor 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
Arquivos virtuais, um recurso introduzido no Android 7.0 (API de nível 24), que permite aos provedores de documentos para fornecer acesso de visualização a arquivos representação direta de bytecode. Para permitir que outros apps visualizem arquivos virtuais, faça o seguinte: o provedor de documentos precisar produzir um arquivo alternativo que possa ser aberto representação para os arquivos virtuais.
Por exemplo, imagine que um provedor de documentos contém um arquivo
que outros aplicativos não possam abrir diretamente, essencialmente um arquivo virtual.
Quando um app cliente envia uma intent ACTION_VIEW
sem a categoria CATEGORY_OPENABLE
,
os usuários poderão selecionar esses arquivos virtuais no provedor de documentos
para visualização. 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 o
FLAG_VIRTUAL_DOCUMENT
ao arquivo retornado pelo
queryDocument()
. Essa sinalização alerta os aplicativos clientes de que o arquivo não tem um endereço
representação 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
O tipo MIME, como uma imagem ou um PDF. O provedor de documentos
declara os tipos MIME alternativos que
oferece suporte para a visualização de um arquivo virtual por meio da substituição do
getDocumentStreamTypes()
. Quando os aplicativos clientes chamam o método
getStreamTypes(android.net.Uri, java.lang.String)
método, o sistema chama o método
getDocumentStreamTypes()
do provedor de documentos. A
getDocumentStreamTypes()
retorna uma matriz de tipos MIME alternativos que o
que o provedor de documentos oferece para o arquivo.
Depois que o cliente determinar
de que o provedor possa produzir o documento em um arquivo visualizável
formato, o aplicativo cliente chama o
openTypedAssetFileDescriptor()
, que chama internamente o método
openTypedDocument()
. O provedor de documentos retorna o arquivo para o app cliente em
o formato de arquivo solicitado.
O snippet de código a seguir demonstra uma implementação simples da
getDocumentStreamTypes()
e
openTypedDocument()
métodos.
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 quer ter certeza de 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
raízes zero 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
? Usamos isso para tornar
esse URI. O snippet a seguir pede que o sistema consulte as raízes dos
provedor de documentos sempre que o status de login do usuário mudar. Se o usuário não for
tiver feito login, uma chamada para queryRoots()
retornará um
cursor vazio, como mostrado acima. Isso garante que os documentos de um provedor sejam
disponível se o usuário estiver conectado ao 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 (link 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)
- Framework de acesso ao armazenamento: como criar um DocumentsProvider (em inglês)
- Arquivos virtuais no Framework de acesso ao armazenamento
Para ver mais informações relacionadas, consulte:
- Como criar um DocumentsProvider
- Abrir arquivos com o framework de acesso ao armazenamento
- Fundamentos do provedor de conteúdo