Crea un fornitore di documenti personalizzati

Se stai sviluppando un'app che offre servizi di archiviazione per i file (ad esempio, un servizio di salvataggio sul cloud), puoi rendere i 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 maggiori informazioni sul funzionamento di Storage Access Framework, consulta la panoramica di Storage Access Framework.

Manifest

Per implementare un fornitore di documenti personalizzato, aggiungi quanto segue al file manifest dell'applicazione:

  • Un target del livello API 19 o superiore.
  • Un elemento <provider> che dichiara il tuo fornitore di spazio di archiviazione personalizzato.
  • L'attributo android:name è impostato sul nome della sottoclasse DocumentsProvider, ovvero il nome della classe, incluso il nome del pacchetto:

    com.example.android.storageprovider.MyCloudProvider.

  • L'attributo android:authority, che corrisponde al 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 provider in modo che altre app possano vederlo.
  • Attributo android:grantUriPermissions impostato su "true". Questa impostazione consente al sistema di concedere ad altre app l'accesso ai contenuti del tuo provider. Per una discussione su come queste altre app possono mantenere l'accesso ai contenuti del tuo provider, consulta Rendere persistenti le autorizzazioni.
  • L'autorizzazione MANAGE_DOCUMENTS. Per impostazione predefinita, un provider è disponibile per tutti. L'aggiunta di questa autorizzazione limita il provider al sistema. Questa limitazione è importante per la sicurezza.
  • Un filtro per intent che include l'azione android.content.action.DOCUMENTS_PROVIDER, in modo che il tuo provider venga visualizzato nel selettore quando il sistema cerca i fornitori.

Ecco alcuni estratti di un file 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>

Supporto di dispositivi con Android 4.3 e versioni precedenti

L'intent ACTION_OPEN_DOCUMENT è disponibile solo sui 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 disattivare il filtro per intent ACTION_GET_CONTENT nel file manifest per i dispositivi con Android 4.4 o versioni successive. Un provider di documenti e ACTION_GET_CONTENT devono essere considerati a vicenda. Se li supporti entrambi contemporaneamente, la tua app viene visualizzata due volte nell'interfaccia utente del selettore di sistema, offrendo due modi diversi per accedere ai dati archiviati. Questo crea confusione per gli utenti.

Ecco il modo consigliato per disabilitare il filtro per intent ACTION_GET_CONTENT per i dispositivi con Android 4.4 o versioni successive:

  1. Nel file di risorse bool.xml, in res/values/, aggiungi questa riga:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. Nel file di risorse bool.xml, in res/values-v19/, aggiungi questa riga:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Aggiungi un alias attività per disattivare il filtro per intent ACTION_GET_CONTENT per la versione 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 personalizzato, una delle attività consiste nell'implementare le classi di contratto, come descritto nella guida per gli sviluppatori dei fornitori di contenuti. Una classe di contratto è una classe public final contenente le definizioni costanti di URI, nomi delle colonne, tipi MIME e altri metadati relativi al provider. Il SAF fornisce queste classi di contratto per te, quindi non devi scrivere tu:

Ad esempio, ecco le colonne che potresti restituire in un cursore quando al fornitore di documenti viene eseguita una query per i documenti o la directory principale:

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 directory principale deve includere alcune colonne obbligatorie. Queste colonne sono:

Il cursore dei documenti deve includere le seguenti colonne obbligatorie:

Crea una sottoclasse di DocumentsProvider

Il passaggio successivo per scrivere un provider di documenti personalizzato è una sottoclasse della classe astratta DocumentsProvider. Come minimo, devi implementare i seguenti metodi:

Questi sono gli unici metodi che devi obbligatoriamente implementare, ma ce ne sono molti altri che potresti volerti implementare. Per informazioni dettagliate, visita la pagina DocumentsProvider.

Definisci un root

L'implementazione di queryRoots() deve restituire un Cursor che punta a tutte le directory principali del provider di documenti, utilizzando le colonne definite in DocumentsContract.Root.

Nello snippet seguente, il parametro projection rappresenta i campi specifici che il chiamante vuole recuperare. Lo snippet crea un nuovo cursore a cui aggiunge una riga: una directory principale, una directory di primo livello come Download o Immagini. La maggior parte dei provider ha una sola radice. Potresti averne più di uno, ad esempio nel caso di più account utente. In tal caso, basta aggiungere una 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 provider di documenti si connette a un insieme dinamico di certificati radice, ad esempio a un dispositivo USB che potrebbe essere disconnesso o a un account da cui l'utente può uscire, puoi aggiornare l'interfaccia utente del documento in modo che rimanga sincronizzata con queste modifiche utilizzando il metodo 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

L'implementazione di queryChildDocuments() deve restituire un valore Cursor che rimandi a tutti i file nella directory specificata, utilizzando le colonne definite in DocumentsContract.Document.

Questo metodo viene richiamato 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 quindi chiama questo metodo ogni volta che l'utente seleziona una sottodirectory all'interno del tuo fornitore di documenti.

Questo snippet crea un nuovo cursore con le colonne richieste, quindi aggiunge al cursore informazioni su ogni elemento secondario immediato nella directory principale. Un asset secondario può essere un'immagine, un'altra directory—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;
}

Recuperare informazioni sul documento

L'implementazione di queryDocument() deve restituire un valore Cursor che rimandi al file specificato, utilizzando le colonne definite in DocumentsContract.Document.

Il metodo queryDocument() restituisce le stesse informazioni trasmesse in 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 miniature di un documento eseguendo l'override del metodo DocumentsProvider.openDocumentThumbnail() e aggiungendo il flag FLAG_SUPPORTS_THUMBNAIL ai file supportati. Il seguente snippet di codice fornisce un esempio di come implementare 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 provider di documenti non deve restituire immagini in miniatura più del doppio delle dimensioni specificate dal parametro sizeHint.

Aprire un documento

Devi implementare openDocument() per restituire un valore ParcelFileDescriptor che rappresenti il file specificato. Altre app possono utilizzare il valore ParcelFileDescriptor restituito per trasmettere dati in streaming. Il sistema chiama questo metodo dopo che l'utente ha selezionato un file e l'app client richiede l'accesso chiamando openFileDescriptor(). Ecco alcuni esempi:

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 provider di documenti trasmette in streaming file o gestisce strutture di dati complesse, valuta la possibilità di implementare i metodi createReliablePipe() o createReliableSocketPair(). Questi metodi consentono di creare una coppia di oggetti ParcelFileDescriptor in 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 nella directory principale del tuo fornitore di documenti sostituendo il metodo queryRecentDocuments() e restituendo FLAG_SUPPORTS_RECENTS. Il seguente snippet di codice mostra un esempio di come implementare i metodi 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 l'esempio di codice StorageProvider.

Creazione di documenti di supporto

Puoi consentire alle app client di creare file all'interno del tuo fornitore di documenti. Se un'app client invia un intent ACTION_CREATE_DOCUMENT, il fornitore di documenti può consentire all'app client di creare nuovi documenti all'interno del fornitore.

Per supportare la creazione di documenti, la directory radice deve avere il flag FLAG_SUPPORTS_CREATE. Le directory che consentono di creare nuovi file al loro interno devono avere il flag FLAG_DIR_SUPPORTS_CREATE.

Il tuo provider di documenti deve anche implementare il metodo createDocument(). Quando un utente seleziona una directory all'interno del tuo provider di documenti per salvare un nuovo file, il fornitore di documenti riceve una chiamata a createDocument(). All'interno dell'implementazione del metodo createDocument(), restituisci un nuovo COLUMN_DOCUMENT_ID per il file. L'app client può quindi utilizzare quell'ID per ottenere un handle per il file e, infine, chiamare openDocument() per scrivere nel nuovo file.

Il seguente snippet di codice mostra come creare un nuovo file all'interno di 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 l'esempio di codice StorageProvider.

Funzionalità di gestione dei documenti di supporto

Oltre ad aprire, creare e visualizzare file, il provider di documenti può anche consentire alle app client di rinominare, copiare, spostare ed eliminare file. Per aggiungere la funzionalità di gestione dei documenti al tuo provider di documenti, aggiungi un flag alla colonna COLUMN_FLAGS del documento per indicare la funzionalità supportata. Devi inoltre implementare il metodo corrispondente della classe DocumentsProvider.

La seguente tabella fornisce il flag COLUMN_FLAGS e il metodo DocumentsProvider che un provider di documenti deve implementare per esporre funzionalità specifiche.

Feature Segnala Metodo
Eliminare un file FLAG_SUPPORTS_DELETE deleteDocument()
Rinominare un file FLAG_SUPPORTS_RENAME renameDocument()
Copia un file in una nuova directory principale all'interno del provider di documenti FLAG_SUPPORTS_COPY copyDocument()
Spostare un file da una directory a un'altra all'interno del fornitore di documenti FLAG_SUPPORTS_MOVE moveDocument()
Rimuovere un file dalla directory principale FLAG_SUPPORTS_REMOVE removeDocument()

Supporto di file virtuali e formati di file alternativi

I file virtuali, una funzionalità introdotta in Android 7.0 (livello API 24), consentono ai provider di documenti di fornire accesso in visualizzazione ai file che non hanno una rappresentazione in bytecode diretta. Per consentire ad altre app di visualizzare i file virtuali, il provider di documenti deve produrre una rappresentazione file apribile alternativa per i file virtuali.

Ad esempio, immagina un fornitore di documenti che contenga un formato file che le altre app non possono aprire direttamente, ovvero un file virtuale. Quando un'app client invia un intent ACTION_VIEW senza la categoria CATEGORY_OPENABLE, gli utenti possono selezionare questi file virtuali all'interno del fornitore di documenti per la visualizzazione. Il fornitore dei 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 per mostrarlo all'utente.

Per dichiarare che un documento nel provider è virtuale, devi aggiungere il flag FLAG_VIRTUAL_DOCUMENT al file restituito dal metodo queryDocument(). Questo flag avvisa le app client che il file non ha una rappresentazione in bytecode diretta e non può essere aperto direttamente.

Se dichiari che un file nel tuo provider di documenti è virtuale, ti consigliamo vivamente di renderlo disponibile in un altro tipo MIME, ad esempio un'immagine o un PDF. Il provider di documenti dichiara i tipi MIME alternativi che supporta per la visualizzazione di un file virtuale eseguendo l'override del metodo getDocumentStreamTypes(). Quando le app client chiamano il metodo getStreamTypes(android.net.Uri, java.lang.String), il sistema chiama il metodo getDocumentStreamTypes() del provider di documenti. Il metodo getDocumentStreamTypes() restituisce quindi un array di tipi MIME alternativi supportati dal provider di documenti per il file.

Dopo che il client ha determinato che il provider di documenti può produrre il documento in un formato file visibile, l'app client chiama il metodo openTypedAssetFileDescriptor(), che chiama internamente il metodo openTypedDocument() del fornitore di documenti. Il provider di documenti restituisce il file all'app client nel formato file richiesto.

Lo snippet di codice riportato di seguito illustra una semplice implementazione dei metodi getDocumentStreamTypes() e openTypedDocument().

Kotlin

var SUPPORTED_MIME_TYPES : Array<String> = arrayOf("image/png", "image/jpg")
override fun openTypedDocument(
        documentId: String?,
        mimeTypeFilter: String,
        opts: Bundle?,
        signal: CancellationSignal?
): AssetFileDescriptor? {
    return try {
        // Determine which supported MIME type the client app requested.
        when(mimeTypeFilter) {
            "image/jpg" -> openJpgDocument(documentId)
            "image/png", "image/*", "*/*" -> openPngDocument(documentId)
            else -> throw IllegalArgumentException("Invalid mimeTypeFilter $mimeTypeFilter")
        }
    } catch (ex: Exception) {
        Log.e(TAG, ex.message)
        null
    }
}

override fun getDocumentStreamTypes(documentId: String, mimeTypeFilter: String): Array<String> {
    return when (mimeTypeFilter) {
        "*/*", "image/*" -> {
            // Return all supported MIME types if the client app
            // passes in '*/*' or 'image/*'.
            SUPPORTED_MIME_TYPES
        }
        else -> {
            // Filter the list of supported mime types to find a match.
            SUPPORTED_MIME_TYPES.filter { it == mimeTypeFilter }.toTypedArray()
        }
    }
}

Java


public static String[] SUPPORTED_MIME_TYPES = {"image/png", "image/jpg"};

@Override
public AssetFileDescriptor openTypedDocument(String documentId,
    String mimeTypeFilter,
    Bundle opts,
    CancellationSignal signal) {

    try {

        // Determine which supported MIME type the client app requested.
        if ("image/png".equals(mimeTypeFilter) ||
            "image/*".equals(mimeTypeFilter) ||
            "*/*".equals(mimeTypeFilter)) {

            // Return the file in the specified format.
            return openPngDocument(documentId);

        } else if ("image/jpg".equals(mimeTypeFilter)) {
            return openJpgDocument(documentId);
        } else {
            throw new IllegalArgumentException("Invalid mimeTypeFilter " + mimeTypeFilter);
        }

    } catch (Exception ex) {
        Log.e(TAG, ex.getMessage());
    } finally {
        return null;
    }
}

@Override
public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) {

    // Return all supported MIME tyupes if the client app
    // passes in '*/*' or 'image/*'.
    if ("*/*".equals(mimeTypeFilter) ||
        "image/*".equals(mimeTypeFilter)) {
        return SUPPORTED_MIME_TYPES;
    }

    ArrayList requestedMimeTypes = new ArrayList&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 che tu voglia assicurarti che gli utenti abbiano eseguito l'accesso prima di iniziare a condividere i loro file. Cosa dovrebbe fare la tua app se l'utente non ha eseguito l'accesso? La soluzione è restituire zero radice nell'implementazione di queryRoots(). Ciò significa che 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(). Ricordi DocumentsContract? Lo stiamo utilizzando per creare questo URI. Lo snippet seguente indica al sistema di eseguire una query sui certificati radice del fornitore di documenti ogni volta che lo stato di accesso dell'utente cambia. Se l'utente non ha eseguito l'accesso, una chiamata a queryRoots() restituisce un cursore vuoto, come mostrato sopra. Ciò garantisce che i documenti di un provider siano disponibili solo 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 codice di esempio relativo a questa pagina, consulta:

Per i video correlati a questa pagina, consulta:

Per ulteriori informazioni correlate, consulta: