Crea un fornitore di documenti personalizzati

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 tuo DocumentsProvider 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:

  1. Nel file delle risorse bool.xml in res/values/, aggiungi questa riga:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. Nel file delle risorse bool.xml in res/values-v19/, aggiungi questa riga:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 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:

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&lt;&gt;();

    // Iterate over the list of supported mime types to find a match.
    for (int i=0; i &lt; 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:

Per ulteriori informazioni correlate, consulta: