Tworzenie niestandardowego dostawcy dokumentów

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 Podklasa DocumentsProvider, czyli nazwa jego klasy, w tym nazwa pakietu:

    com.example.android.storageprovider.MyCloudProvider.

  • Atrybut android:authority, czyli nazwa pakietu (w tym przykładzie com.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:

  1. W pliku zasobów bool.xml w katalogu res/values/ dodaj ten wiersz:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. W pliku zasobów bool.xml w katalogu res/values-v19/ dodaj ten wiersz:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 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:

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&lt;&gt;();

    // Iterate over the list of supported mime types to find a match.
    for (int i=0; i &lt; SUPPORTED_MIME_TYPES.length; i++) {
        if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) {
            requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]);
        }
    }
    return (String[])requestedMimeTypes.toArray();
}

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:

Dodatkowe informacje znajdziesz tutaj: