Se stai sviluppando un'app che fornisce servizi di archiviazione per i file (ad esempio un servizio di salvataggio sul cloud), puoi rendere i tuoi file disponibili tramite Storage Access Framework (SAF) scrivendo un provider di documenti personalizzato. In questa pagina viene descritto come creare un fornitore di documenti personalizzato.
Per saperne di più su come funziona Storage Access Framework, vedi Panoramica di Storage Access Framework.
Manifest
Per implementare un fornitore di documenti personalizzati, aggiungi quanto segue al manifest:
- Un target di livello API 19 o superiore.
- Un elemento
<provider>
che dichiara il tuo spazio di archiviazione personalizzato o il provider di servizi di terze parti. -
L'attributo
android:name
impostato sul nome del tuoDocumentsProvider
sottoclasse, ossia il nome della classe, incluso il nome del pacchetto:com.example.android.storageprovider.MyCloudProvider
. -
Attributo
android:authority
, che è il nome del pacchetto (in questo esempio,com.example.android.storageprovider
) più il tipo di fornitore di contenuti (documents
). - Attributo
android:exported
impostato su"true"
. Devi esportare il tuo provider in modo che sia visibile ad altre app. - Attributo
android:grantUriPermissions
impostato su"true"
. Questa impostazione consente al sistema di concedere l'accesso ad altre app ai contenuti nel tuo provider. Per una discussione su come queste altre app possono mantenere l'accesso ai contenuti del tuo provider, consulta Persistenza autorizzazioni. - L'autorizzazione
MANAGE_DOCUMENTS
. Per impostazione predefinita è disponibile un fornitore a tutti. Se aggiungi questa autorizzazione, il tuo provider sarà limitato al sistema. Questa restrizione è importante per la sicurezza. - Un filtro per intent che include
android.content.action.DOCUMENTS_PROVIDER
, in modo che il tuo fornitore appare nel selettore quando il sistema cerca i fornitori.
Ecco alcuni estratti di un manifest di esempio che include un provider:
<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>
Sono supportati i dispositivi con Android 4.3 e versioni precedenti
La
ACTION_OPEN_DOCUMENT
intent è disponibile solo
su dispositivi con Android 4.4 e versioni successive.
Se vuoi che la tua applicazione supporti ACTION_GET_CONTENT
per i dispositivi con Android 4.3 e versioni precedenti, devi
disattiva il filtro per intent ACTION_GET_CONTENT
in
il file manifest per i dispositivi con Android 4.4 o versioni successive. R
il fornitore di documenti e ACTION_GET_CONTENT
devono essere presi in considerazione
che si escludono a vicenda. Se li supporti entrambi contemporaneamente, la tua app
appare due volte nell'interfaccia utente del selettore di sistema, offrendo due modi diversi per accedere
dati archiviati. Questo può confondere gli utenti.
Ecco il metodo consigliato per disattivare
ACTION_GET_CONTENT
filtro per intent per i dispositivi
con Android 4.4 o versioni successive:
- Nel file delle risorse
bool.xml
inres/values/
, aggiungi questa riga:<bool name="atMostJellyBeanMR2">true</bool>
- Nel file delle risorse
bool.xml
inres/values-v19/
, aggiungi questa riga:<bool name="atMostJellyBeanMR2">false</bool>
- Aggiungi un
attività
alias per disattivare l'intent
ACTION_GET_CONTENT
applica un filtro per le versioni 4.4 (livello API 19) e successive. Ad esempio:<!-- 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>
Contratti
In genere, quando scrivi un fornitore di contenuti personalizzati, una delle attività è
di implementare le classi contrattuali, come descritto
Guida per gli sviluppatori dei fornitori di contenuti. Una classe contratto è un corso public final
che contiene definizioni costanti per URI, nomi di colonna, tipi MIME e
e altri metadati relativi al provider. La SAF
che ti offre queste classi di contratto, quindi non devi scrivere
proprio:
Ad esempio, ecco le colonne che potresti restituire in un cursore quando viene eseguita una query sul fornitore di documenti per trovare i documenti o il root:
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,};
Il cursore per la radice deve includere alcune colonne obbligatorie. Le colonne sono:
Il cursore dei documenti deve includere le seguenti colonne obbligatorie:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
Crea una sottoclasse di DocumentsProvider
Il passaggio successivo nella scrittura di un fornitore di documenti personalizzato è la sottoclasse
classe astratta DocumentsProvider
. Come minimo, devi
implementare i seguenti metodi:
Questi sono gli unici metodi che devi assolutamente implementare, ma
ci sono molti altri che potresti voler fare. Vedi DocumentsProvider
per maggiori dettagli.
Definisci una radice
La tua implementazione di queryRoots()
deve restituire un Cursor
che rimandi a tutti
le directory radice del fornitore di documenti, utilizzando le colonne definite
DocumentsContract.Root
.
Nel seguente snippet, il parametro projection
rappresenta
campi specifici che il chiamante vuole recuperare. Lo snippet crea un nuovo cursore
e vi aggiunge una riga, una radice, una directory di primo livello,
Download o immagini. La maggior parte dei fornitori ha una sola radice. Potresti averne più di uno,
ad esempio nel caso di più account utente. In questo caso, devi solo aggiungere
seconda riga al cursore.
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 il tuo fornitore di documenti si connette a un set dinamico di root, ad esempio a un
dispositivo che potrebbe essere disconnesso o da un account da cui l'utente può disconnettersi.
puoi aggiornare l'interfaccia utente del documento per mantenerla in sintonia con le modifiche utilizzando
ContentResolver.notifyChange()
, come mostrato nel seguente snippet di codice.
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);
Elenca documenti nel provider
La tua implementazione
queryChildDocuments()
deve restituire un Cursor
che rimandi a tutti i file in
alla directory specificata, utilizzando le colonne definite
DocumentsContract.Document
.
Questo metodo viene chiamato quando l'utente sceglie il root nell'interfaccia utente del selettore.
Il metodo recupera gli elementi secondari dell'ID documento specificato da
COLUMN_DOCUMENT_ID
.
Il sistema chiama questo metodo ogni volta che l'utente seleziona un
nella sottodirectory del fornitore dei documenti.
Questo snippet crea un nuovo cursore con le colonne richieste, poi aggiunge informazioni su ogni elemento figlio immediato nella directory padre al cursore. Un file secondario può essere un'immagine, un'altra directory e qualsiasi file:
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; }
Recupera informazioni sul documento
La tua implementazione
queryDocument()
deve restituire un Cursor
che punta al file specificato,
utilizzando le colonne definite in DocumentsContract.Document
.
queryDocument()
restituisce le stesse informazioni trasmesse nel
queryChildDocuments()
,
ma per un file specifico:
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; }
Il tuo fornitore di documenti può anche fornire le miniature di un documento
eseguendo l'override
DocumentsProvider.openDocumentThumbnail()
e aggiungere il metodo
FLAG_SUPPORTS_THUMBNAIL
ai file supportati.
Lo snippet di codice riportato di seguito fornisce un esempio di come implementare il parametro
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); }
Attenzione:
Un fornitore di documenti non deve restituire immagini in miniatura più del doppio
la dimensione specificata dal parametro sizeHint
.
Apri un documento
Devi implementare openDocument()
per restituire un ParcelFileDescriptor
che rappresenta
del file specificato. Altre app possono utilizzare l'attributo ParcelFileDescriptor
restituito
per i flussi di dati. Il sistema chiama questo metodo dopo che l'utente ha selezionato un file,
e l'app client richiede l'accesso chiamando
openFileDescriptor()
.
Ad esempio:
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 il tuo fornitore di documenti trasmette in streaming file o gestisce contenuti complicati
strutture dati, valuta la possibilità di implementare
createReliablePipe()
o
createReliableSocketPair()
metodi.
Questi metodi consentono di creare una coppia
ParcelFileDescriptor
oggetti, per cui puoi restituirne uno
e inviare l'altro tramite
ParcelFileDescriptor.AutoCloseOutputStream
o
ParcelFileDescriptor.AutoCloseInputStream
.
Supporta la ricerca e i documenti recenti
Puoi fornire un elenco dei documenti modificati di recente sotto la radice del
del fornitore di documenti eseguendo l'override
queryRecentDocuments()
metodo e di ritorno
FLAG_SUPPORTS_RECENTS
,
Il seguente snippet di codice mostra un esempio di come implementare il parametro
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; }
Puoi ottenere il codice completo dello snippet riportato sopra scaricando il Fornitore di archiviazione di esempio di codice.
Creazione di documenti di assistenza
Puoi consentire alle app client di creare file all'interno del tuo fornitore di documenti.
Se un'app client invia un ACTION_CREATE_DOCUMENT
per intent, il fornitore di documenti può consentire all'app client di creare
nuovi documenti all'interno del fornitore.
Per supportare la creazione di documenti, il root deve disporre del
FLAG_SUPPORTS_CREATE
flag.
Le directory che consentono la creazione di nuovi file al loro interno devono disporre
FLAG_DIR_SUPPORTS_CREATE
flag.
Il tuo fornitore di documenti deve anche implementare
createDocument()
. Quando un utente seleziona una directory all'interno di un
fornitore di documenti per salvare un nuovo file, il fornitore di documenti riceve una chiamata a
createDocument()
. Nell'ambito dell'implementazione
createDocument()
, restituisci un nuovo metodo
COLUMN_DOCUMENT_ID
per
. L'app client può quindi utilizzare questo ID per ottenere un handle per il file
per poi richiamare
openDocument()
per scrivere nel nuovo file.
Il seguente snippet di codice illustra come creare un nuovo file all'interno un fornitore di documenti.
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); }
Puoi ottenere il codice completo dello snippet riportato sopra scaricando il Fornitore di archiviazione di esempio di codice.
Funzionalità di gestione dei documenti di assistenza
Oltre ad aprire, creare e visualizzare file, il tuo fornitore di documenti
può anche consentire alle app client di rinominare, copiare, spostare ed eliminare
. Per aggiungere la funzionalità di gestione dei documenti a
il tuo fornitore di documenti, aggiungi un flag
COLUMN_FLAGS
colonna
per indicare la funzionalità supportata. Devi inoltre implementare
il metodo corrispondente di DocumentsProvider
.
La seguente tabella fornisce le
COLUMN_FLAGS
flag
e DocumentsProvider
che un documento
che il provider deve implementare per esporre funzionalità specifiche.
Funzionalità | Segnala | Metodo |
---|---|---|
Eliminare un file |
FLAG_SUPPORTS_DELETE
|
deleteDocument()
|
Rinominare un file |
FLAG_SUPPORTS_RENAME
|
renameDocument()
|
Copia un file in una nuova directory padre all'interno del provider di documenti |
FLAG_SUPPORTS_COPY
|
copyDocument()
|
Sposta un file da una directory a un'altra all'interno del provider di documenti |
FLAG_SUPPORTS_MOVE
|
moveDocument()
|
Rimuovere un file dalla directory principale |
FLAG_SUPPORTS_REMOVE
|
removeDocument()
|
Supporta file virtuali e formati di file alternativi
File virtuali, una funzionalità introdotta in Android 7.0 (livello API 24), consente ai fornitori di documenti per fornire l'accesso in visualizzazione ai file che non hanno un una rappresentazione diretta in bytecode. Per consentire ad altre app di visualizzare i file virtuali: il tuo fornitore di documenti deve produrre un file apribile alternativo per i file virtuali.
Ad esempio, immagina che un fornitore di documenti contenga un file
un file virtuale che le altre app non possono aprire direttamente.
Quando un'app client invia un intent ACTION_VIEW
senza la categoria CATEGORY_OPENABLE
,
gli utenti possono selezionare i file virtuali
all'interno del fornitore di documenti
per la visualizzazione. Il fornitore di documenti restituisce quindi il file virtuale
in un formato file diverso, ma apribile, come un'immagine.
L'app client può quindi aprire il file virtuale in modo che l'utente possa visualizzarlo.
Per dichiarare che un documento nel provider è virtuale, devi aggiungere il metodo
FLAG_VIRTUAL_DOCUMENT
al file restituito
queryDocument()
. Questo flag avvisa le app client che il file non ha un indirizzo
e non può essere aperta direttamente.
Se dichiari che un file nel tuo fornitore di documenti è virtuale,
ti consigliamo vivamente di renderlo disponibile in un altro
Tipo MIME, ad esempio un'immagine o un PDF. Il fornitore dei documenti
dichiara i tipi MIME alternativi che
supporta la visualizzazione di un file virtuale mediante l'override
getDocumentStreamTypes()
. Quando le app client chiamano
getStreamTypes(android.net.Uri, java.lang.String)
, il sistema chiama
getDocumentStreamTypes()
del fornitore dei documenti. La
getDocumentStreamTypes()
restituisce un array di tipi MIME alternativi
supportato dal fornitore di documenti per il file.
Dopo che il cliente ha determinato
che il fornitore del documento possa produrre il documento in un file visualizzabile
specifico, l'app client chiama
openTypedAssetFileDescriptor()
che chiama internamente il metodo
openTypedDocument()
. Il fornitore di documenti restituisce il file all'app client in
il formato file richiesto.
Il seguente snippet di codice illustra una semplice implementazione
getDocumentStreamTypes()
e
openTypedDocument()
di machine learning.
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(); }
Sicurezza
Supponiamo che il tuo fornitore di documenti sia un servizio di archiviazione sul cloud protetto da password
e vuoi assicurarti che gli utenti abbiano eseguito l'accesso prima di iniziare a condividere i loro file.
Che cosa deve fare la tua app se l'utente non ha eseguito l'accesso? La soluzione è restituire
nella tua implementazione di queryRoots()
. Vale a dire, un cursore principale vuoto:
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; }
L'altro passaggio consiste nel chiamare getContentResolver().notifyChange()
.
Ti ricordi di DocumentsContract
? Lo utilizziamo per creare
questo URI. Lo snippet che segue indica al sistema di eseguire una query sulle radici dei tuoi
fornitore di documenti ogni volta che lo stato di accesso dell'utente cambia. Se l'utente non è
ha eseguito l'accesso, una chiamata al numero queryRoots()
restituisce
un cursore vuoto, come mostrato sopra. In questo modo i documenti di un provider vengono
se l'utente ha eseguito l'accesso al provider.
Kotlin
private fun onLoginButtonClick() { loginOrLogout() getContentResolver().notifyChange( DocumentsContract.buildRootsUri(AUTHORITY), null ) }
Java
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }
Per un esempio di codice correlato a questa pagina, consulta:
Per i video correlati a questa pagina, consulta:
- DevBytes: Framework di accesso allo spazio di archiviazione per Android 4.4: provider
- Framework di accesso allo spazio di archiviazione: creazione di un DocumentsProvider
- File virtuali in Storage Access Framework
Per ulteriori informazioni correlate, consulta:
- Creazione di un DocumentsProvider
- Aprire Files usando Storage Access Framework
- Nozioni di base per i fornitori di contenuti