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 abgeleitetenDocumentsProvider
-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 Beispielcom.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:
- Fügen Sie in der Ressourcendatei
bool.xml
unterres/values/
diese Zeile ein:<bool name="atMostJellyBeanMR2">true</bool>
- Fügen Sie in der Ressourcendatei
bool.xml
unterres/values-v19/
diese Zeile ein:<bool name="atMostJellyBeanMR2">false</bool>
- 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:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
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<>(); // Iterate over the list of supported mime types to find a match. for (int i=0; i < SUPPORTED_MIME_TYPES.length; i++) { if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) { requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]); } } return (String[])requestedMimeTypes.toArray(); }
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:
- DevBytes: Android 4.4 Storage Access Framework: Provider
- Storage Access Framework: Building a DocumentsProvider
- Virtuelle Dateien im Storage Access Framework
Weitere Informationen finden Sie hier:
- DocumentsProvider erstellen
- Dateien mit dem Storage Access Framework öffnen
- Grundlagen zu Contentanbietern