Jeśli tworzysz aplikację udostępniającą usługi przechowywania plików (np. usługę Cloud Save), możesz udostępnić swoje pliki za pomocą Storage Access Framework (SAF), korzystając z niestandardowego dostawcy dokumentów. Na tej stronie opisaliśmy, jak utworzyć niestandardowego dostawcę dokumentów.
Więcej informacji o tym, jak działa Storage Access Framework, znajdziesz w omówieniu Storage Access Framework.
Plik manifestu
Aby wdrożyć niestandardowy dostawca dokumentów, dodaj do pliku manifestu aplikacji ten kod:
- Wartość docelowa interfejsu API na poziomie 19 lub wyższym.
- Element
<provider>
deklarujący dostawcę niestandardowego miejsca na dane. -
Atrybut
android:name
ustawiony na nazwę podklasyDocumentsProvider
, czyli nazwę jej klasy, włącznie z nazwą pakietu:com.example.android.storageprovider.MyCloudProvider
. -
Atrybut
android:authority
, czyli nazwa pakietu (w tym przykładziecom.example.android.storageprovider
) i typ dostawcy treści (documents
). - Atrybut
android:exported
ustawiony na"true"
. Musisz wyeksportować dostawcę, aby inne aplikacje mogły go odczytać. - Atrybut
android:grantUriPermissions
ustawiony na"true"
. To ustawienie umożliwia systemowi przyznawanie innym aplikacjom dostępu do treści u dostawcy. Aby dowiedzieć się, jak inne aplikacje mogą zachować dostęp do treści od Twojego dostawcy, przeczytaj artykuł Utrzymywanie uprawnień. - Uprawnienie
MANAGE_DOCUMENTS
. Domyślnie usługodawca jest dostępny dla wszystkich. Dodanie tego uprawnienia ogranicza dostawcę do systemu. To ograniczenie jest ważne ze względów bezpieczeństwa. - Filtr intencji zawierający działanie
android.content.action.DOCUMENTS_PROVIDER
, dzięki czemu Twój dostawca pojawi się w selektorze, gdy system wyszuka dostawców.
Oto fragmenty z 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 aplikacja obsługiwała ACTION_GET_CONTENT
i obsługowała urządzenia z Androidem 4.3 lub starszym, wyłącz w pliku manifestu filtr intencji ACTION_GET_CONTENT
na urządzeniach z Androidem 4.4 lub nowszym. Dostawca dokumentów i ACTION_GET_CONTENT
powinny wzajemnie się wykluczać. Jeśli obsługujesz oba te programy jednocześnie, aplikacja pojawi się dwukrotnie w interfejsie selektora systemowego, co daje dwa różne sposoby dostępu do zapisanych danych. Jest to mylące dla użytkowników.
Oto zalecany sposób wyłączenia filtra intencji ACTION_GET_CONTENT
na urządzeniach z Androidem w wersji 4.4 lub nowszej:
- W pliku zasobów
bool.xml
w lokalizacjires/values/
dodaj ten wiersz:<bool name="atMostJellyBeanMR2">true</bool>
- W pliku zasobów
bool.xml
w lokalizacjires/values-v19/
dodaj ten wiersz:<bool name="atMostJellyBeanMR2">false</bool>
- Dodaj alias aktywności, aby wyłączyć filtr intencji
ACTION_GET_CONTENT
w wersji 4.4 (poziom interfejsu API 19) i nowszych. 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
Podczas tworzenia niestandardowego dostawcy treści jednym z zadań jest wdrożenie klas umowy. Opisaliśmy je w przewodniku dla programistów
dostawców treści. Klasa kontraktowa to klasa public final
, która zawiera stałe definicje identyfikatorów URI, nazw kolumn, typów MIME i innych metadanych odnoszących się do dostawcy. SAF udostępnia za Ciebie te klasy umów, nie musisz więc pisać własnych:
Poniżej znajdziesz na przykład kolumny, które możesz zwrócić w miejscu kursora, gdy dostawca dokumentu wysyła zapytanie o dokumenty lub znajduje się w katalogu głównym:
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 na poziomie głównym musi zawierać wymagane kolumny. Te kolumny to:
Kursor odpowiadający dokumentom musi zawierać następujące wymagane kolumny:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
Tworzenie podklasy DocumentsProvider
Następnym krokiem podczas tworzenia niestandardowego dostawcy dokumentów jest podklasa klasy abstrakcyjnej DocumentsProvider
. Musisz zaimplementować przynajmniej te metody:
To jedyne metody, które bezwzględnie musisz wdrożyć, ale jest ich znacznie więcej. Aby dowiedzieć się więcej, wejdź na DocumentsProvider
.
Zdefiniuj pierwiastek
Twoja implementacja queryRoots()
musi zwracać parametr Cursor
, który wskazuje wszystkie katalogi główne dostawcy dokumentów, używając kolumn zdefiniowanych w pliku DocumentsContract.Root
.
W tym fragmencie kodu parametr projection
reprezentuje konkretne pola, które rozmówca chce uzyskać. Fragment kodu tworzy nowy kursor i dodaje do niego jeden wiersz – katalog główny, katalog najwyższego poziomu, np. Pobrane lub Obrazy. Większość dostawców ma tylko jeden poziom główny. Jeśli na przykład masz kilka kont użytkowników,
może być ich więcej. W takiej sytuacji
dodaj drugi wiersz do 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 dokumentu łączy się z dynamicznym zestawem katalogów głównych – na przykład z urządzeniem USB, które może zostać odłączone lub z kontem, z którego użytkownik może się wylogować – możesz zaktualizować interfejs dokumentu, aby zapewnić synchronizację z tymi zmianami za pomocą metody ContentResolver.notifyChange()
, jak pokazano w tym fragmencie 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świetl listę dokumentów w usłudze dostawcy
Twoja implementacja queryChildDocuments()
musi zwracać element Cursor
, który wskazuje wszystkie pliki w określonym katalogu, używając kolumn zdefiniowanych w pliku DocumentsContract.Document
.
Ta metoda jest wywoływana, gdy użytkownik wybierze poziom główny w interfejsie selektora.
Metoda pobiera elementy podrzędne identyfikatora dokumentu określonego przez COLUMN_DOCUMENT_ID
.
System wywołuje tę metodę za każdym razem, gdy użytkownik wybierze podkatalog w systemie dostawcy dokumentów.
Ten fragment kodu tworzy nowy kursor z żądanymi kolumnami, a następnie dodaje do kursora informacje o każdym bezpośrednim elemencie podrzędnym w katalogu nadrzędnym. Podrzędnym może być obraz, inny katalog, czyli 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; }
Pobierz informacje o dokumencie
Twoja implementacja queryDocument()
musi zwracać wartość Cursor
, która wskazuje określony plik przy użyciu kolumn zdefiniowanych w polu DocumentsContract.Document
.
Metoda queryDocument()
zwraca te same informacje, które były przekazane w funkcji queryChildDocuments()
, ale dla konkretnego 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ż udostępnić miniatury dokumentu, zastępując metodę DocumentsProvider.openDocumentThumbnail()
i dodając do obsługiwanych plików flagę FLAG_SUPPORTS_THUMBNAIL
.
Ten fragment kodu zawiera przykład 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 dokumentów nie powinien zwracać miniaturek ponad dwukrotnie większych niż określony przez parametr sizeHint
.
Otwieranie dokumentu
Musisz zaimplementować openDocument()
, aby zwrócić ParcelFileDescriptor
reprezentujący określony plik. Inne aplikacje mogą używać zwróconego elementu ParcelFileDescriptor
do strumieniowego przesyłania danych. System wywołuje tę metodę po wybraniu pliku przez użytkownika, a aplikacja kliencka prosi o dostęp do niego, wywołując metodę 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 pliki strumieniowo lub obsługuje złożone struktury danych, rozważ wdrożenie metody createReliablePipe()
lub createReliableSocketPair()
.
Te metody umożliwiają utworzenie pary obiektów ParcelFileDescriptor
, przy której możesz zwrócić jeden, a drugi wysłać za pomocą ParcelFileDescriptor.AutoCloseOutputStream
lub ParcelFileDescriptor.AutoCloseInputStream
.
Obsługuj ostatnie dokumenty i wyszukiwanie
Listę ostatnio zmodyfikowanych dokumentów możesz umieścić w katalogu głównym dostawcy dokumentów, zastępując metodę queryRecentDocuments()
i zwracając FLAG_SUPPORTS_RECENTS
. Poniższy fragment kodu pokazuje przykład implementacji metod 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; }
Pełny kod powyższego fragmentu możesz pobrać z przykładowego kodu StorageProvider.
Pomoc przy tworzeniu dokumentów
Możesz zezwolić aplikacjom klienckim na tworzenie plików u dostawcy dokumentów.
Jeśli aplikacja kliencka wysyła intencję ACTION_CREATE_DOCUMENT
, dostawca dokumentów może zezwolić tej aplikacji klienckiej na tworzenie nowych dokumentów w ramach tego dostawcy.
Aby można było tworzyć dokumenty, katalog główny musi mieć flagę FLAG_SUPPORTS_CREATE
.
Katalogi, które umożliwiają tworzenie w nich nowych plików, muszą mieć flagę FLAG_DIR_SUPPORTS_CREATE
.
Dostawca dokumentu musi też zaimplementować metodę createDocument()
. Gdy użytkownik wybierze katalog u dostawcy dokumentów, aby zapisać nowy plik, dostawca dokumentu otrzyma wywołanie createDocument()
. W ramach implementacji metody createDocument()
zwracasz dla pliku nową wartość COLUMN_DOCUMENT_ID
. Aplikacja kliencka może następnie użyć tego identyfikatora, aby uzyskać nick dla pliku. Ostatecznie może też wywołać openDocument()
, aby zapisać nowy plik.
Poniższy fragment kodu pokazuje, jak utworzyć nowy plik u 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 powyższego fragmentu możesz pobrać z przykładowego kodu StorageProvider.
Obsługa funkcji zarządzania dokumentami
Oprócz otwierania, tworzenia i wyświetlania plików Twój dostawca dokumentów może też zezwolić aplikacjom klienckim na zmienianie nazw, kopiowanie, przenoszenie i usuwanie plików. Aby dodać funkcję zarządzania dokumentami u dostawcy dokumentów, dodaj flagę w kolumnie COLUMN_FLAGS
dokumentu, aby wskazać obsługiwane funkcje. Musisz też wdrożyć odpowiednią metodę klasy DocumentsProvider
.
W tabeli poniżej znajdziesz flagę COLUMN_FLAGS
i metodę DocumentsProvider
, które dostawca dokumentów musi wdrożyć, aby udostępnić określone funkcje.
Cecha | 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 w ramach 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 zapewniać możliwość wyświetlania plików, które nie mają bezpośredniego reprezentacji kodu bajtowego. Aby umożliwić innym aplikacjom wyświetlanie plików wirtualnych, dostawca dokumentu musi utworzyć alternatywną reprezentację plików wirtualnych, które można otwierać.
Załóżmy na przykład, że dostawca dokumentów zawiera plik w formacie, którego inne aplikacje nie mogą bezpośrednio otworzyć. Jest to w zasadzie plik wirtualny.
Gdy aplikacja kliencka wysyła intencję ACTION_VIEW
bez kategorii CATEGORY_OPENABLE
, użytkownicy mogą wybrać do wyświetlenia te pliki wirtualne u dostawcy dokumentów. Dostawca dokumentu zwraca wtedy wirtualny plik w innym, ale otwierającym się formacie, takim jak obraz.
Aplikacja kliencka może wtedy otworzyć wirtualny plik, aby użytkownik mógł go wyświetlić.
Aby zadeklarować, że dokument u dostawcy jest wirtualny, musisz dodać do pliku zwracanego przez metodę queryDocument()
flagę FLAG_VIRTUAL_DOCUMENT
. Ta flaga informuje aplikacje klienckie, że plik nie zawiera bezpośredniego kodu bajtowego i nie można go otworzyć.
Jeśli zadeklarujesz, że plik u dostawcy dokumentu jest wirtualny, zdecydowanie zalecamy udostępnienie go w innym typie MIME, np. obrazie lub pliku PDF. Dostawca dokumentu deklaruje alternatywne typy MIME, które obsługuje przy wyświetlaniu pliku wirtualnego, zastępując metodę getDocumentStreamTypes()
. Gdy aplikacje klienckie wywołują metodę getStreamTypes(android.net.Uri, java.lang.String)
, system wywołuje metodę getDocumentStreamTypes()
dostawcy dokumentu. Metoda getDocumentStreamTypes()
zwraca następnie tablicę alternatywnych typów MIME obsługiwanych przez dostawcę dokumentu w przypadku danego pliku.
Gdy klient ustali, że dostawca dokumentu może wygenerować dokument w formacie pliku, który można wyświetlać, aplikacja klienta wywoła metodę openTypedAssetFileDescriptor()
, która wewnętrznie wywoła metodę openTypedDocument()
dostawcy dokumentu. Dostawca dokumentu zwraca plik do aplikacji klienckiej w żądanym formacie pliku.
Ten fragment kodu przedstawia prostą implementację metod getDocumentStreamTypes()
i 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(); }
Zabezpieczenia
Załóżmy, że Twój dostawca dokumentów korzysta z chronionej hasłem usługi przechowywania danych w chmurze i chcesz sprawdzić, czy użytkownicy są zalogowani, zanim zaczniesz udostępniać pliki.
Co powinna zrobić aplikacja, gdy użytkownik nie jest zalogowany? Rozwiązaniem jest zwrócenie zerowej wartości pierwiastków w implementacji funkcji queryRoots()
. To oznacza, że 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; }
Inny krok to wywołanie getContentResolver().notifyChange()
.
Pamiętasz to miejsce (DocumentsContract
)? Używamy go,
aby utworzyć ten identyfikator URI. Ten fragment kodu informuje system, że przy każdej zmianie stanu logowania użytkownika wysyła zapytanie do elementów głównych dostawcy dokumentów. Jeśli użytkownik nie jest zalogowany, wywołanie queryRoots()
zwraca pusty kursor, jak pokazano powyżej. Dzięki temu dokumenty dostawcy będą dostępne tylko wtedy, gdy 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 na temat tej strony znajdziesz tutaj:
- DevBytes: platforma dostępu do miejsca na dane w Androidzie 4.4: dostawca
- Platforma Storage Access Framework: tworzenie obiektu DocumentsProvider
- Pliki wirtualne w platformie Storage Access Framework
Dodatkowe informacje na ten temat znajdziesz tutaj:
- Tworzenie obiektu DocumentsProvider
- Otwieranie plików za pomocą platformy Storage Access Framework
- Podstawowe informacje o dostawcy treści