Jeśli tworzysz aplikację, która udostępnia usługi przechowywania plików (np. usłudze Cloud Save możesz udostępniać pliki w Storage Access Framework (SAF) przez wpisanie niestandardowego dostawcy dokumentów. Na tej stronie dowiesz się, jak utworzyć niestandardowego dostawcę dokumentów.
Więcej informacji o tym, jak działa Storage Access Framework, znajdziesz w dokumentacji Omówienie platformy Storage Access Framework.
Plik manifestu
Aby wdrożyć dostawcę dokumentów niestandardowych, dodaj do swojej aplikacji ten kod plik manifestu:
- Cel API na poziomie 19 lub wyższym.
- Element
<provider>
deklarujący Twoją niestandardową pamięć masową dostawcy usług. -
Atrybut
android:name
został ustawiony na nazwę Twojego PodklasaDocumentsProvider
, czyli nazwa jego klasy, w tym nazwa pakietu:com.example.android.storageprovider.MyCloudProvider
. -
Atrybut
android:authority
, czyli nazwa pakietu (w tym przykładziecom.example.android.storageprovider
). oraz typ dostawcy treści, (documents
). - Atrybut
android:exported
ma wartość"true"
. Dostawca musisz wyeksportować, aby były widoczne dla innych aplikacji. - Ustawiono atrybut
android:grantUriPermissions
na"true"
To ustawienie pozwala systemowi na udzielanie dostępu innym aplikacjom do treści w Twojej usłudze. Podczas rozmowy o tym, jak inne aplikacje mogą zachować dostęp do treści od dostawcy, zobacz Utrzymywanie . - Uprawnienie
MANAGE_DOCUMENTS
. Domyślnie dostępny jest usługodawca. do wszystkich. Dodanie tego uprawnienia ogranicza dostawcę do systemu. To ograniczenie ma duże znaczenie ze względu na bezpieczeństwo. - Filtr intencji, który zawiera parametr
android.content.action.DOCUMENTS_PROVIDER
, dzięki czemu dostawca pojawia się w selektorze, gdy system wyszukuje dostawców.
Oto fragmenty przykładowego pliku manifestu zawierającego dostawcę:
<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>
Obsługa urządzeń z Androidem 4.3 lub starszym
Intencja ACTION_OPEN_DOCUMENT
jest dostępna tylko
na urządzeniach z Androidem 4.4 lub nowszym.
Jeśli chcesz, aby Twoja aplikacja obsługiwała ACTION_GET_CONTENT
aby działała na urządzeniach z Androidem 4.3 lub starszym,
wyłącz filtr intencji ACTION_GET_CONTENT
w
pliku manifestu dla urządzeń z Androidem 4.4 lub nowszym. O
dostawcy dokumentu i ACTION_GET_CONTENT
należy wziąć pod uwagę
wzajemnie się wykluczają. Jeśli obie są obsługiwane jednocześnie, aplikacja
pojawia się dwukrotnie w interfejsie selektora systemowego, oferując 2 różne sposoby
przechowywane dane. Jest to mylące dla użytkowników.
Zalecany sposób wyłączania
ACTION_GET_CONTENT
filtr intencji dla urządzeń
z Androidem 4.4 lub nowszym:
- W pliku zasobów
bool.xml
w katalogures/values/
dodaj ten wiersz:<bool name="atMostJellyBeanMR2">true</bool>
- W pliku zasobów
bool.xml
w katalogures/values-v19/
dodaj ten wiersz:<bool name="atMostJellyBeanMR2">false</bool>
- Dodaj
aktywność
alias, aby wyłączyć intencję
ACTION_GET_CONTENT
. filtr wersji 4.4 (poziom interfejsu API 19) i nowszych. Na przykład:<!-- 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>
Umowy
Zwykle podczas pisania niestandardowego dostawcy treści jednym z zadań jest
wdrażania klas umów, jak opisano w
Przewodnik dla programistów dotyczący dostawców treści. Klasa umowy to klasa public final
który zawiera stałe definicje identyfikatorów URI, nazw kolumn, typów MIME oraz
inne metadane odnoszące się do dostawcy. SAF
udostępnia te klasy kontraktów, dzięki czemu nie musisz pisać
własny:
Na przykład oto kolumny, które możesz zwrócić po najechaniu kursorem, Twój dostawca dokumentów otrzymuje zapytanie o dokumenty lub katalog główny:
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,};
Kursor katalogu głównego musi zawierać pewne wymagane kolumny. Te kolumny to:
Kursor dokumentów musi zawierać te wymagane kolumny:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
Tworzenie podklasy klasy DocumentsProvider
Następnym krokiem podczas tworzenia niestandardowego dostawcy dokumentu jest podklasyfikacja
klasa abstrakcyjna DocumentsProvider
. Wymagane jest
zastosuj te metody:
To jedyne metody, które musisz wdrożyć, ale pamiętaj,
jest o wiele więcej. Zobacz DocumentsProvider
.
Zdefiniuj pierwiastek
Implementacja funkcji queryRoots()
musi zwrócić element Cursor
wskazujący wszystkie
katalogu głównego dostawcy dokumentu, przy użyciu kolumn zdefiniowanych w
DocumentsContract.Root
W tym fragmencie kodu parametr projection
reprezentuje wartość
do określonych pól, do których dostęp chce zwrócić. Fragment kodu tworzy nowy kursor
i dodaje do niego jeden wiersz – jeden katalog główny, katalog najwyższego poziomu,
Pobrane pliki lub Obrazy. Większość dostawców ma tylko jeden poziom główny. Możesz mieć ich więcej,
na przykład w przypadku wielu kont użytkowników. W takim przypadku wystarczy dodać
na drugi wiersz kursora.
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; }
Jeśli dostawca dokumentów łączy się z dynamicznym zbiorem pierwiastków – na przykład przez USB
urządzenie, które może być odłączone, lub konto, z którego użytkownik może się wylogować –
może zaktualizować interfejs dokumentu, aby śledzić
zmiany za pomocą
ContentResolver.notifyChange()
zgodnie z tym fragmentem kodu.
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);
Wyświetlenie listy dokumentów u dostawcy
Twoja implementacja
queryChildDocuments()
musi zwracać wartość Cursor
wskazującą wszystkie pliki w
wskazanego katalogu, z użyciem kolumn zdefiniowanych w argumencie
DocumentsContract.Document
Ta metoda jest wywoływana, gdy użytkownik wybierze poziom główny w interfejsie selektora.
Ta metoda pobiera elementy podrzędne identyfikatora dokumentu określonego przez funkcję
COLUMN_DOCUMENT_ID
Następnie system wywołuje tę metodę za każdym razem, gdy użytkownik wybierze
w podkatalogu Twojego dostawcy dokumentów.
Ten fragment kodu tworzy nowy kursor z żądanymi kolumnami, a następnie dodaje informacje o każdym najbliższym elemencie podrzędnym w katalogu nadrzędnym. Elementem podrzędnym może być obraz, inny katalog – dowolny plik:
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; }
Uzyskaj informacje o dokumencie
Twoja implementacja
queryDocument()
musi zwrócić Cursor
wskazujący określony plik,
za pomocą kolumn zdefiniowanych w DocumentsContract.Document
.
queryDocument()
zwraca te same informacje, które zostały przekazane
queryChildDocuments()
,
ale w konkretnym pliku:
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; }
Dostawca dokumentu może też dostarczyć miniatury dokumentu poprzez:
zastępując
DocumentsProvider.openDocumentThumbnail()
i dodanie funkcji
FLAG_SUPPORTS_THUMBNAIL
do obsługiwanych plików.
Poniższy fragment kodu zawiera przykładowy sposób implementacji
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); }
Uwaga:
Dostawca dokumentu nie powinien zwracać obrazów miniatur więcej niż podwójnej liczby
rozmiar określony przez parametr sizeHint
.
Otwieranie dokumentu
Musisz zaimplementować funkcję openDocument()
, aby zwrócić element ParcelFileDescriptor
reprezentujący wartość
określonego pliku. Inne aplikacje mogą używać zwróconego ParcelFileDescriptor
aby przesyłać strumieniowo dane. System wywołuje tę metodę, gdy użytkownik wybierze plik.
a aplikacja kliencka prosi o dostęp przez wywołanie
openFileDescriptor()
Na przykład:
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); } }
Jeśli dostawca dokumentów przesyła strumieniowo pliki lub obsługuje skomplikowane
struktury danych, rozważ wdrożenie
createReliablePipe()
lub
createReliableSocketPair()
metody.
Metody te pozwalają utworzyć parę
ParcelFileDescriptor
obiektów, do których można zwrócić 1 obiekt
i wysyłać drugie za pomocą
ParcelFileDescriptor.AutoCloseOutputStream
lub
ParcelFileDescriptor.AutoCloseInputStream
Obsługa ostatnich dokumentów i wyszukiwania
Listę ostatnio zmodyfikowanych dokumentów można umieścić w katalogu głównym
dostawcy dokumentu, zastępując
Metoda queryRecentDocuments()
i pobieranie
FLAG_SUPPORTS_RECENTS
,
Ten fragment kodu pokazuje przykładową implementację tagu
queryRecentDocuments()
metod.
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; }
Pełny kod dla powyższego fragmentu znajdziesz, pobierając Dostawca miejsca na dane przykładowego kodu.
Pomoc w tworzeniu dokumentów
Możesz zezwolić aplikacjom klienckim na tworzenie plików u dostawcy dokumentów.
Jeśli aplikacja kliencka wysyła ACTION_CREATE_DOCUMENT
intencja, dostawca dokumentu może umożliwić tej aplikacji klienckiej utworzenie
nowe dokumenty u dostawcy dokumentu.
Aby można było tworzyć dokumenty, w katalogu głównym musi znajdować się
flaga FLAG_SUPPORTS_CREATE
.
Katalogi, które pozwalają na tworzenie nowych plików, muszą mieć
FLAG_DIR_SUPPORTS_CREATE
flaga.
Dostawca dokumentu musi również zaimplementować
Metoda createDocument()
. Gdy użytkownik wybierze katalog w Twojej
dostawcy dokumentów w celu zapisania nowego pliku, otrzyma on wywołanie
createDocument()
W ramach implementacji
createDocument()
, zwracasz nową
COLUMN_DOCUMENT_ID
dla
. Aplikacja kliencka może następnie użyć tego identyfikatora, aby uzyskać nick dla pliku
i w końcu
openDocument()
, aby zapisać zmiany w nowym pliku.
Fragment kodu poniżej pokazuje, jak utworzyć nowy plik w od dostawcy dokumentu.
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); }
Pełny kod dla powyższego fragmentu znajdziesz, pobierając Dostawca miejsca na dane przykładowego kodu.
Obsługa funkcji zarządzania dokumentami
Oprócz otwierania, tworzenia i wyświetlania plików dostawca dokumentu
mogą też zezwalać aplikacjom klienckim na zmienianie nazw, kopiowanie, przenoszenie i usuwanie
. Aby dodać funkcję zarządzania dokumentami do
dostawcy dokumentu, dodaj flagę do pola
COLUMN_FLAGS
kolumna
aby wskazać
obsługiwaną funkcję. Musisz też zaimplementować
odpowiednią metodę funkcji DocumentsProvider
zajęcia.
Tabela poniżej zawiera
Flaga COLUMN_FLAGS
i DocumentsProvider
, które potwierdzają, że dokumenty
który dostawca musi wdrożyć, aby udostępnić określone funkcje.
Funkcja | Zgłoś | Metoda |
---|---|---|
Usuwanie pliku |
FLAG_SUPPORTS_DELETE
|
deleteDocument()
|
Zmienianie nazwy pliku |
FLAG_SUPPORTS_RENAME
|
renameDocument()
|
Skopiuj plik do nowego katalogu nadrzędnego u dostawcy dokumentu |
FLAG_SUPPORTS_COPY
|
copyDocument()
|
Przenoszenie pliku z jednego katalogu do innego u dostawcy dokumentu |
FLAG_SUPPORTS_MOVE
|
moveDocument()
|
Usuwanie pliku z katalogu nadrzędnego |
FLAG_SUPPORTS_REMOVE
|
removeDocument()
|
Obsługa plików wirtualnych i alternatywnych formatów plików
Pliki wirtualne, funkcja wprowadzona w Androidzie 7.0 (poziom interfejsu API 24) pozwala dostawcom dokumentów aby umożliwiać wyświetlanie plików, które nie mają bezpośrednią reprezentację kodu bajtowego. Aby umożliwić innym aplikacjom wyświetlanie plików wirtualnych: dostawca dokumentu musi utworzyć alternatywny plik, który można otworzyć dla plików wirtualnych.
Wyobraź sobie na przykład, że dostawca dokumentu zawiera plik
format, którego inne aplikacje nie mogą bezpośrednio otworzyć, czyli plik wirtualny.
Gdy aplikacja kliencka wysyła intencję ACTION_VIEW
bez kategorii CATEGORY_OPENABLE
,
użytkownicy mogą wybrać te pliki wirtualne u dostawcy dokumentu
do oglądania. Następnie dostawca dokumentu zwraca plik wirtualny.
w innym, lecz otwartym formacie
takim jak zdjęcie.
Aplikacja kliencka może następnie otworzyć plik wirtualny, który użytkownik może wyświetlić.
Aby zadeklarować, że dokument u dostawcy jest wirtualny, musisz dodać atrybut
FLAG_VIRTUAL_DOCUMENT
do pliku zwróconego przez
queryDocument()
. Ta flaga powiadamia aplikacje klienckie, że plik nie ma bezpośredniego
zawiera kod bajtowy i nie można go otworzyć bezpośrednio.
Jeśli zadeklarujesz, że plik u dostawcy dokumentu jest wirtualny,
zdecydowanie zalecamy udostępnienie go w innym
Typ MIME, np. obraz lub plik PDF. Dostawca dokumentu
deklaruje alternatywne typy MIME,
obsługuje wyświetlanie pliku wirtualnego przez zastąpienie
getDocumentStreamTypes()
. Gdy aplikacje klienckie wywołują metodę
getStreamTypes(android.net.Uri, java.lang.String)
system wywołuje metodę
getDocumentStreamTypes()
u dostawcy dokumentu.
getDocumentStreamTypes()
zwraca tablicę alternatywnych typów MIME, które
obsługiwanych przez dostawcę dokumentów dla tego pliku.
Gdy klient ustali,
że dostawca dokumentu może wygenerować dokument w pliku, który można wyświetlić.
, aplikacja kliencka wywołuje metodę
openTypedAssetFileDescriptor()
która wywołuje wewnętrznie funkcję dostawcy dokumentu
openTypedDocument()
. Dostawca dokumentu zwraca plik do aplikacji klienckiej w
w żądanym formacie pliku.
Fragment kodu poniżej pokazuje prostą implementację
getDocumentStreamTypes()
oraz
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(); }
Bezpieczeństwo
Załóżmy, że dostawcą dokumentów jest usługa przechowywania w chmurze chronionej hasłem.
i chcesz się upewnić, że użytkownicy są zalogowani, zanim zaczniesz udostępniać ich pliki.
Co powinna zrobić aplikacja, jeśli użytkownik nie jest zalogowany? Rozwiązaniem jest zwrócenie
żadnych pierwiastków w implementacji queryRoots()
. Czyli pusty kursor główny:
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; }
Następnie musisz wywołać funkcję getContentResolver().notifyChange()
.
Pamiętasz to miejsce (DocumentsContract
)? Wykorzystujemy je,
ten identyfikator URI. Ten fragment kodu informuje system, że ma wysłać zapytanie do elementów głównych Twojej
dostawcy dokumentu przy każdej zmianie stanu logowania użytkownika. Jeśli użytkownik nie jest
jest zalogowany, wywołanie queryRoots()
zwraca
pusty kursor, jak pokazano powyżej. Dzięki temu dokumenty dostawcy są
dostępne, jeśli użytkownik jest zalogowany u dostawcy.
Kotlin
private fun onLoginButtonClick() { loginOrLogout() getContentResolver().notifyChange( DocumentsContract.buildRootsUri(AUTHORITY), null ) }
Java
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }
Przykładowy kod związany z tą stroną znajdziesz tutaj:
Filmy powiązane z tą stroną znajdziesz tutaj:
- DevBytes: platforma dostępu do pamięci masowej na Androidzie 4.4: dostawca
- Platforma dostępu do pamięci masowej: tworzenie komponentu DocumentsProvider
- Pliki wirtualne w ramach platformy Storage Access Framework
Dodatkowe informacje znajdziesz tutaj:
- Tworzenie dostawcy DocumentsProvider
- Otwieranie plików za pomocą platformy Storage Access Framework
- Podstawowe informacje o dostawcach treści