Tworzenie niestandardowego dostawcy dokumentów

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ę podklasy DocumentsProvider, czyli nazwę jej klasy, włącznie z nazwą pakietu:

    com.example.android.storageprovider.MyCloudProvider.

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

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

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

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:

Dodatkowe informacje na ten temat znajdziesz tutaj: