Benutzerdefinierten Dokumentanbieter erstellen

Wenn Sie eine Anwendung entwickeln, die Speicherdienste für Dateien bereitstellt (z. B. einen Cloud-Speicherdienst), können Sie Ihre Dateien über das Storage Access Framework (SAF) verfügbar machen, indem Sie einen benutzerdefinierten Dokumentanbieter schreiben. Auf dieser Seite wird beschrieben, wie Sie einen benutzerdefinierten Dokumentanbieter erstellen.

Weitere Informationen zur Funktionsweise des Storage Access Framework finden Sie in der Übersicht zum Storage Access Framework.

Manifest

Fügen Sie dem Manifest Ihrer Anwendung Folgendes hinzu, um einen benutzerdefinierten Dokumentanbieter zu implementieren:

  • Ziel ist API-Level 19 oder höher.
  • Ein <provider>-Element, das Ihren benutzerdefinierten Speicheranbieter deklariert.
  • Das Attribut android:name, das auf den Namen Ihrer abgeleiteten DocumentsProvider-Klasse festgelegt ist, also den Klassennamen, einschließlich Paketname:

    com.example.android.storageprovider.MyCloudProvider.

  • Das Attribut android:authority, das sich aus dem Paketnamen (in diesem Beispiel com.example.android.storageprovider) und dem Typ des Inhaltsanbieters (documents) zusammensetzt.
  • Das Attribut android:exported ist auf "true" festgelegt. Du musst deinen Anbieter exportieren, damit er für andere Apps sichtbar ist.
  • Das Attribut android:grantUriPermissions ist auf "true" festgelegt. Mit dieser Einstellung kann das System anderen Apps Zugriff auf Inhalte bei Ihrem Anbieter gewähren. Informationen dazu, wie diese anderen Anwendungen ihren Zugriff auf Inhalte Ihres Anbieters dauerhaft beibehalten können, finden Sie unter Berechtigungen beibehalten.
  • Die Berechtigung MANAGE_DOCUMENTS. Standardmäßig ist ein Anbieter für alle verfügbar. Wenn Sie diese Berechtigung hinzufügen, wird Ihr Anbieter auf das System beschränkt. Diese Einschränkung ist aus Sicherheitsgründen wichtig.
  • Einen Intent-Filter, der die Aktion android.content.action.DOCUMENTS_PROVIDER enthält, damit Ihr Anbieter in der Auswahl erscheint, wenn das System nach Anbietern sucht.

Hier sind Auszüge aus einem Beispielmanifest, das einen Anbieter enthält:

<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>

Unterstützt Geräte mit Android 4.3 und niedriger

Der Intent ACTION_OPEN_DOCUMENT ist nur auf Geräten mit Android 4.4 und höher verfügbar. Wenn deine App ACTION_GET_CONTENT unterstützen soll, damit Geräte mit Android 4.3 und niedriger ausgeführt werden können, solltest du den ACTION_GET_CONTENT-Intent-Filter in deinem Manifest für Geräte mit Android 4.4 oder höher deaktivieren. Ein Dokumentanbieter und ACTION_GET_CONTENT sollten sich gegenseitig ausschließen. Wenn Sie beide gleichzeitig unterstützen, wird Ihre App zweimal in der Benutzeroberfläche der Systemauswahl angezeigt, was zwei verschiedene Möglichkeiten für den Zugriff auf Ihre gespeicherten Daten bietet. Das ist verwirrend für Nutzer.

So deaktivieren Sie den Intent-Filter ACTION_GET_CONTENT für Geräte mit Android 4.4 oder höher:

  1. Fügen Sie in der Ressourcendatei bool.xml unter res/values/ diese Zeile ein:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. Fügen Sie in der Ressourcendatei bool.xml unter res/values-v19/ diese Zeile ein:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Fügen Sie einen Aktivitätsalias hinzu, um den Intent-Filter ACTION_GET_CONTENT für Version 4.4 (API-Level 19) und höher zu deaktivieren. Beispiel:
    <!-- 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>
    

Verträge

Wenn Sie einen benutzerdefinierten Contentanbieter schreiben, besteht eine der Aufgaben normalerweise darin, Vertragsklassen zu implementieren, wie im Entwicklerleitfaden für Contentanbieter beschrieben. Eine Vertragsklasse ist eine public final-Klasse, die konstante Definitionen für die URIs, Spaltennamen, MIME-Typen und andere Metadaten enthält, die zum Anbieter gehören. Die SAF stellt diese Vertragsklassen für Sie bereit, sodass Sie nicht Ihre eigene schreiben müssen:

Hier sind beispielsweise die Spalten, die Sie in einem Cursor zurückgeben können, wenn bei Ihrem Dokumentanbieter Dokumente oder der Stamm abgefragt wird:

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,};

Der Cursor für den Stamm muss bestimmte erforderliche Spalten enthalten. Diese Spalten sind:

Der Cursor für Dokumente muss die folgenden erforderlichen Spalten enthalten:

Unterklasse von DocumentsProvider erstellen

Der nächste Schritt beim Schreiben eines benutzerdefinierten Dokumentanbieters besteht darin, eine abgeleitete Klasse der abstrakten Klasse DocumentsProvider zu erstellen. Sie müssen mindestens die folgenden Methoden implementieren:

Dies sind die einzigen Methoden, die Sie unbedingt implementieren müssen. Es gibt jedoch noch viele weitere Methoden, die Sie möglicherweise anwenden möchten. Weitere Informationen finden Sie unter DocumentsProvider.

Stamm definieren

Ihre Implementierung von queryRoots() muss einen Cursor zurückgeben, der auf alle Stammverzeichnisse Ihres Dokumentanbieters verweist, wobei die in DocumentsContract.Root definierten Spalten verwendet werden.

Im folgenden Snippet stellt der Parameter projection die spezifischen Felder dar, die der Aufrufer zurückgeben möchte. Das Snippet erstellt einen neuen Cursor und fügt ihm eine Zeile hinzu: ein Stammverzeichnis und ein Verzeichnis der obersten Ebene wie Downloads oder Bilder. Die meisten Anbieter haben nur eine Root-Ebene. Wenn Sie mehrere Nutzerkonten haben, können Sie mehrere haben. Fügen Sie in diesem Fall einfach eine zweite Zeile am Cursor hinzu.

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;
}

Wenn Ihr Dokumentanbieter eine Verbindung zu einer dynamischen Stammgruppe hergestellt hat, z. B. mit einem nicht verbundenen USB-Gerät oder einem Konto, von dem sich der Nutzer abmelden kann, können Sie die Dokument-UI aktualisieren, um mit diesen Änderungen synchron zu bleiben. Dazu verwenden Sie die Methode ContentResolver.notifyChange(), wie im folgenden Code-Snippet gezeigt.

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);

Dokumente im Anbieter auflisten

Ihre Implementierung von queryChildDocuments() muss ein Cursor zurückgeben, das auf alle Dateien im angegebenen Verzeichnis verweist. Dabei werden die in DocumentsContract.Document definierten Spalten verwendet.

Diese Methode wird aufgerufen, wenn der Nutzer in der Auswahl-UI den Stamm auswählt. Die Methode ruft die untergeordneten Elemente der durch COLUMN_DOCUMENT_ID angegebenen Dokument-ID ab. Das System ruft diese Methode dann jedes Mal auf, wenn der Nutzer ein Unterverzeichnis innerhalb Ihres Dokumentanbieters auswählt.

Dieses Snippet erstellt einen neuen Cursor mit den angeforderten Spalten und fügt dann am Cursor Informationen zu jedem unmittelbar untergeordneten Element im übergeordneten Verzeichnis hinzu. Ein untergeordnetes Element kann ein Bild oder ein anderes Verzeichnis sein – eine beliebige Datei:

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;
}

Dokumentinformationen abrufen

Ihre Implementierung von queryDocument() muss einen Cursor zurückgeben, der mithilfe von in DocumentsContract.Document definierten Spalten auf die angegebene Datei verweist.

Die Methode queryDocument() gibt dieselben Informationen zurück, die in queryChildDocuments() übergeben wurden, jedoch für eine bestimmte Datei:

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;
}

Ihr Dokumentanbieter kann auch Miniaturansichten für ein Dokument bereitstellen. Dazu überschreibt er die Methode DocumentsProvider.openDocumentThumbnail() und fügt den unterstützten Dateien das Flag FLAG_SUPPORTS_THUMBNAIL hinzu. Das folgende Code-Snippet zeigt ein Beispiel für die Implementierung von 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);
}

Achtung: Ein Dokumentanbieter sollte Miniaturansichten nicht mehr als doppelt so groß wie der Parameter sizeHint zurückgeben.

Dokumente öffnen

Sie müssen openDocument() so implementieren, dass ein ParcelFileDescriptor zurückgegeben wird, das die angegebene Datei darstellt. Andere Anwendungen können die zurückgegebene ParcelFileDescriptor zum Streamen von Daten verwenden. Das System ruft diese Methode auf, nachdem der Nutzer eine Datei ausgewählt hat und die Client-App durch Aufrufen von openFileDescriptor() Zugriff darauf anfordert. Beispiele:

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);
    }
}

Wenn Ihr Dokumentanbieter Dateien streamt oder komplizierte Datenstrukturen verarbeitet, sollten Sie die Methoden createReliablePipe() oder createReliableSocketPair() implementieren. Mit diesen Methoden können Sie ein Paar von ParcelFileDescriptor-Objekten erstellen, von denen Sie eines zurückgeben und das andere über eine ParcelFileDescriptor.AutoCloseOutputStream oder ParcelFileDescriptor.AutoCloseInputStream senden können.

Letzte Dokumente und Suche unterstützen

Sie können eine Liste der zuletzt geänderten Dokumente im Stammverzeichnis Ihres Dokumentanbieters bereitstellen, indem Sie die Methode queryRecentDocuments() überschreiben und FLAG_SUPPORTS_RECENTS zurückgeben. Das folgende Code-Snippet zeigt ein Beispiel für die Implementierung der Methoden 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;
}

Sie können den vollständigen Code für das obige Snippet abrufen, indem Sie das Codebeispiel StorageProvider herunterladen.

Erstellung von Supportdokumenten

Sie können Client-Apps erlauben, Dateien innerhalb Ihres Dokumentanbieters zu erstellen. Wenn eine Client-App einen ACTION_CREATE_DOCUMENT-Intent sendet, kann Ihr Dokumentanbieter dieser Client-App erlauben, neue Dokumente innerhalb des Dokumentanbieters zu erstellen.

Damit das Erstellen von Dokumenten unterstützt wird, muss Ihr Stamm das Flag FLAG_SUPPORTS_CREATE haben. Verzeichnisse, in denen neue Dateien erstellt werden können, müssen das Flag FLAG_DIR_SUPPORTS_CREATE haben.

Außerdem muss Ihr Dokumentanbieter die Methode createDocument() implementieren. Wenn ein Nutzer ein Verzeichnis bei Ihrem Dokumentanbieter zum Speichern einer neuen Datei auswählt, erhält der Dokumentanbieter einen Aufruf an createDocument(). Innerhalb der Implementierung der Methode createDocument() geben Sie eine neue COLUMN_DOCUMENT_ID für die Datei zurück. Die Client-App kann diese ID dann verwenden, um einen Handle für die Datei zu erhalten und schließlich openDocument() aufzurufen, um in die neue Datei zu schreiben.

Das folgende Code-Snippet zeigt, wie Sie eine neue Datei bei einem Dokumentanbieter erstellen.

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);
}

Sie können den vollständigen Code für das obige Snippet abrufen, indem Sie das Codebeispiel StorageProvider herunterladen.

Funktionen zur Dokumentverwaltung unterstützen

Zusätzlich zum Öffnen, Erstellen und Anzeigen von Dateien kann Ihr Dokumentanbieter Client-Apps die Möglichkeit geben, Dateien umzubenennen, zu kopieren, zu verschieben und zu löschen. Wenn Sie Ihrem Dokumentanbieter Dokumentverwaltungsfunktionen hinzufügen möchten, fügen Sie der Spalte COLUMN_FLAGS des Dokuments ein Flag hinzu, um die unterstützte Funktion anzugeben. Außerdem müssen Sie die entsprechende Methode der DocumentsProvider-Klasse implementieren.

Die folgende Tabelle enthält das Flag COLUMN_FLAGS und die Methode DocumentsProvider, die ein Dokumentanbieter implementieren muss, um bestimmte Features verfügbar zu machen.

Funktion Melden Methode
Dateien löschen FLAG_SUPPORTS_DELETE deleteDocument()
Dateien umbenennen FLAG_SUPPORTS_RENAME renameDocument()
Datei in ein neues übergeordnetes Verzeichnis innerhalb des Dokumentanbieters kopieren FLAG_SUPPORTS_COPY copyDocument()
Dateien innerhalb des Dokumentanbieters von einem Verzeichnis in ein anderes verschieben FLAG_SUPPORTS_MOVE moveDocument()
Datei aus dem übergeordneten Verzeichnis entfernen FLAG_SUPPORTS_REMOVE removeDocument()

Virtuelle Dateien und alternative Dateiformate unterstützen

Mit Virtuellen Dateien, einer in Android 7.0 (API-Ebene 24) eingeführten Funktion, können Dokumentanbieter Lesezugriff auf Dateien gewähren, die keine direkte Bytecodedarstellung haben. Damit andere Anwendungen virtuelle Dateien ansehen können, muss Ihr Dokumentanbieter eine alternative öffnebare Dateidarstellung für die virtuellen Dateien erstellen.

Angenommen, ein Dokumentanbieter enthält ein Dateiformat, das von anderen Apps nicht direkt geöffnet werden kann. Es handelt sich dabei im Wesentlichen um eine virtuelle Datei. Wenn eine Client-App einen ACTION_VIEW-Intent ohne die Kategorie CATEGORY_OPENABLE sendet, können Nutzer diese virtuellen Dateien beim Dokumentanbieter zum Anzeigen auswählen. Der Dokumentanbieter gibt die virtuelle Datei dann in einem anderen, aber öffnebaren Dateiformat wie einem Bild zurück. Die Client-App kann dann die virtuelle Datei öffnen, die der Nutzer anzeigen kann.

Wenn Sie deklarieren möchten, dass ein Dokument beim Anbieter virtuell ist, müssen Sie der Datei, die von der Methode queryDocument() zurückgegeben wird, das Flag FLAG_VIRTUAL_DOCUMENT hinzufügen. Dieses Flag benachrichtigt Clientanwendungen, dass die Datei keine direkte Bytecodedarstellung hat und nicht direkt geöffnet werden kann.

Wenn Sie angeben, dass eine Datei bei Ihrem Dokumentanbieter virtuell ist, wird dringend empfohlen, sie in einem anderen MIME-Typ verfügbar zu machen, z. B. als Bild oder PDF. Der Dokumentanbieter deklariert die alternativen MIME-Typen, die er für die Anzeige einer virtuellen Datei unterstützt, indem er die Methode getDocumentStreamTypes() überschreibt. Wenn Client-Apps die Methode getStreamTypes(android.net.Uri, java.lang.String) aufrufen, ruft das System die Methode getDocumentStreamTypes() des Dokumentanbieters auf. Die Methode getDocumentStreamTypes() gibt dann ein Array alternativer MIME-Typen zurück, die der Dokumentanbieter für die Datei unterstützt.

Nachdem der Client festgestellt hat, dass der Dokumentanbieter das Dokument in einem sichtbaren Dateiformat erstellen kann, ruft die Client-App die Methode openTypedAssetFileDescriptor() auf, die intern die Methode openTypedDocument() des Dokumentanbieters aufruft. Der Dokumentanbieter gibt die Datei im angeforderten Dateiformat an die Client-App zurück.

Das folgende Code-Snippet zeigt eine einfache Implementierung der Methoden getDocumentStreamTypes() und 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();
}

Sicherheit

Angenommen, Ihr Dokumentanbieter ist ein passwortgeschützter Cloud-Speicherdienst und Sie möchten sicherstellen, dass Nutzer angemeldet sind, bevor Sie mit der Freigabe ihrer Dateien beginnen. Was sollte Ihre App tun, wenn der Nutzer nicht angemeldet ist? Die Lösung besteht darin, in der Implementierung von queryRoots() Null-Root-Zertifikate zurückzugeben. Das heißt, ein leerer Stamm-Cursor:

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;
}

Der andere Schritt besteht darin, getContentResolver().notifyChange() aufzurufen. Erinnern Sie sich an den DocumentsContract? Wir verwenden ihn, um diesen URI zu erstellen. Mit dem folgenden Snippet wird das System angewiesen, die Stammdaten Ihres Dokumentanbieters abzufragen, wenn sich der Anmeldestatus des Nutzers ändert. Wenn der Nutzer nicht angemeldet ist, wird bei einem Aufruf von queryRoots() ein leerer Cursor zurückgegeben (siehe oben). Dadurch wird sichergestellt, dass die Dokumente eines Anbieters nur verfügbar sind, wenn der Nutzer beim Anbieter angemeldet ist.

Kotlin

private fun onLoginButtonClick() {
    loginOrLogout()
    getContentResolver().notifyChange(
        DocumentsContract.buildRootsUri(AUTHORITY),
        null
    )
}

Java

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}

Beispielcode für diese Seite finden Sie hier:

Videos zu dieser Seite finden Sie hier:

Weitere Informationen finden Sie hier: