Создание собственного поставщика документов

Если вы разрабатываете приложение, которое предоставляет услуги хранения файлов (например, сервис сохранения в облаке), вы можете сделать свои файлы доступными через Storage Access Framework (SAF), написав собственный поставщик документов. This page describes how to create a custom document provider.

Подробную информацию о том, как работает Storage Access Framework, можно найти в обзоре Storage Access Framework.

Манифест

Чтобы реализовать собственный поставщик документов, добавьте в манифест приложения следующее:

  • Целевой уровень API не ниже 19.
  • Элемент <provider>, который объявляет ваш собственный поставщик хранилища.
  • Атрибут android:name, в котором указано название подкласса DocumentsProvider (название класса, включая название пакета):

    com.example.android.storageprovider.MyCloudProvider.

  • Атрибут android:authority, который состоит из названия пакета (в этом примере – com.example.android.storageprovider) и типа поставщика контента (documents).
  • Для атрибута android:exported задано значение "true". Чтобы другие приложения могли видеть поставщика, его нужно экспортировать.
  • Атрибут android:grantUriPermissions со значением "true". Этот параметр позволяет системе предоставлять другим приложениям доступ к контенту вашего поставщика. О том, как другие приложения могут сохранять доступ к контенту вашего поставщика, читайте в разделе Сохранение разрешений.
  • Разрешение MANAGE_DOCUMENTS. По умолчанию поставщик доступен всем. Добавление этого разрешения ограничивает доступ к вашему поставщику. Это ограничение важно для безопасности.
  • Фильтр интентов, который включает действие android.content.action.DOCUMENTS_PROVIDER, чтобы ваш поставщик появлялся в окне выбора, когда система ищет поставщиков.

Ниже приведены фрагменты примера манифеста, в котором указан поставщик.

<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>

Поддержка устройств с Android 4.3 и более ранними версиями

Намерение ACTION_OPEN_DOCUMENT доступно только на устройствах с Android 4.4 и более поздними версиями. Если вы хотите, чтобы ваше приложение поддерживало ACTION_GET_CONTENT для устройств с Android 4.3 и более ранними версиями, отключите фильтр интентов ACTION_GET_CONTENT в манифесте для устройств с Android 4.4 и более поздними версиями. Поставщик документов и ACTION_GET_CONTENT должны быть взаимоисключающими. Если вы поддерживаете оба варианта одновременно, ваше приложение дважды появится в интерфейсе системного инструмента выбора, предлагая два разных способа доступа к сохраненным данным. Это может запутать пользователей.

Ниже описан рекомендуемый способ отключения фильтра интентов ACTION_GET_CONTENT для устройств с Android 4.4 или более поздней версии.

  1. Добавьте в файл ресурсов bool.xml в каталоге res/values/ следующую строку:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. Добавьте в файл ресурсов bool.xml в каталоге res/values-v19/ следующую строку:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Добавьте псевдоним действия, чтобы отключить фильтр интентов ACTION_GET_CONTENT для версий 4.4 (уровень API 19) и выше. Пример:
    <!-- 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>

Соглашения

Обычно при написании собственного поставщика контента одной из задач является реализация классов контрактов, как описано в руководстве для разработчиков Поставщики контента. Класс контракта – это класс public final, который содержит определения констант для URI, названий столбцов, MIME-типов и других метаданных, относящихся к поставщику. SAF предоставляет следующие классы контрактов, поэтому вам не нужно писать собственные:

Например, ниже приведены столбцы, которые могут быть возвращены в курсоре, когда поставщик документов запрашивает документы или корневой каталог:

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,};

Курсор для корневого элемента должен включать определенные обязательные столбцы. Эти столбцы перечислены далее.

Курсор для документов должен содержать следующие обязательные столбцы:

Создание подкласса DocumentsProvider

Следующий шаг при написании пользовательского поставщика документов – создание подкласса абстрактного класса DocumentsProvider. Как минимум, необходимо реализовать следующие методы:

Это единственные методы, которые вы обязаны реализовать, но есть и другие, которые могут вам пригодиться. Подробную информацию можно найти в статье DocumentsProvider.

Определить корень

Реализация queryRoots() должна возвращать Cursor, указывающий на все корневые каталоги поставщика документов, используя столбцы, определенные в DocumentsContract.Root.

В следующем фрагменте параметр projection представляет определенные поля, которые вызывающий абонент хочет получить обратно. Фрагмент создает новый курсор и добавляет в него одну строку – один корень, каталог верхнего уровня, например "Скачанные файлы" или "Изображения". У большинства поставщиков только один корневой сертификат. У вас может быть несколько аккаунтов, например, если у вас несколько аккаунтов пользователей. В этом случае просто добавьте вторую строку к курсору.

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;
}

Если ваш поставщик документов подключается к динамическому набору корней (например, к USB-устройству, которое может быть отключено, или к учетной записи, из которой пользователь может выйти), вы можете обновить пользовательский интерфейс документа, чтобы синхронизировать его с этими изменениями, используя метод ContentResolver.notifyChange(), как показано в следующем фрагменте кода.

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);

Просмотр документов в поставщике

Ваша реализация queryChildDocuments() должна возвращать Cursor, указывающий на все файлы в указанном каталоге, используя столбцы, определенные в DocumentsContract.Document.

Этот метод вызывается, когда пользователь выбирает ваш корневой каталог в интерфейсе выбора. Метод извлекает дочерние элементы идентификатора документа, указанного в COLUMN_DOCUMENT_ID. Система вызывает этот метод каждый раз, когда пользователь выбирает подкаталог в поставщике документов.

Этот фрагмент кода создает новый курсор с запрошенными столбцами, а затем добавляет в него информацию о каждом дочернем элементе в родительском каталоге. Дочерним элементом может быть изображение, другой каталог – любой файл:

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;
}

Получить информацию о документе

Ваша реализация queryDocument() должна возвращать Cursor, указывающий на заданный файл, с использованием столбцов, определенных в DocumentsContract.Document.

Метод queryDocument() возвращает ту же информацию, которая была передана в queryChildDocuments(), но для определенного файла:

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;
}

Поставщик документов также может предоставлять уменьшенные изображения документов, переопределив метод DocumentsProvider.openDocumentThumbnail() и добавив флаг FLAG_SUPPORTS_THUMBNAIL к поддерживаемым файлам. В приведенном ниже фрагменте кода показано, как реализовать 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);
}

Внимание! Поставщик документов не должен возвращать значки, размер которых более чем в два раза превышает значение, указанное в параметре sizeHint.

Как открыть документ

Вы должны реализовать openDocument(), чтобы возвращать ParcelFileDescriptor, представляющий указанный файл. Другие приложения могут использовать возвращенный ParcelFileDescriptor для потоковой передачи данных. Система вызывает этот метод после того, как пользователь выбирает файл, а клиентское приложение запрашивает доступ к нему, вызывая openFileDescriptor(). Пример:

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

Если поставщик документов передает файлы в потоковом режиме или обрабатывает сложные структуры данных, рекомендуем реализовать методы createReliablePipe() или createReliableSocketPair(). Эти методы позволяют создать пару объектов ParcelFileDescriptor, один из которых можно вернуть, а другой – отправить через ParcelFileDescriptor.AutoCloseOutputStream или ParcelFileDescriptor.AutoCloseInputStream.

Поддержка недавних документов и поиска

Вы можете предоставить список недавно измененных документов в корневом каталоге поставщика документов, переопределив метод queryRecentDocuments() и возвратив FLAG_SUPPORTS_RECENTS. В следующем фрагменте кода показан пример реализации методов 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;
}

Полный код для приведенного выше фрагмента можно получить, скачав пример кода StorageProvider.

Создание документов поддержки

Вы можете разрешить клиентским приложениям создавать файлы в поставщике документов. Если клиентское приложение отправляет намерение ACTION_CREATE_DOCUMENT, поставщик документов может разрешить ему создавать новые документы в своем хранилище.

Чтобы поддерживать создание документов, у корневого каталога должен быть флаг FLAG_SUPPORTS_CREATE. Каталоги, в которых разрешено создавать новые файлы, должны иметь флаг FLAG_DIR_SUPPORTS_CREATE.

Поставщик документов также должен реализовать метод createDocument(). Когда пользователь выбирает каталог в поставщике документов для сохранения нового файла, поставщик документов получает вызов createDocument(). Внутри реализации метода createDocument() вы возвращаете новый COLUMN_DOCUMENT_ID для файла. Затем клиентское приложение может использовать этот идентификатор, чтобы получить дескриптор файла и вызвать метод openDocument() для записи в новый файл.

В следующем фрагменте кода показано, как создать файл в поставщике документов.

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

Полный код для приведенного выше фрагмента можно получить, скачав пример кода StorageProvider.

Поддержка функций управления документами

Помимо открытия, создания и просмотра файлов, поставщик документов также может разрешить клиентским приложениям переименовывать, копировать, перемещать и удалять файлы. Чтобы добавить в поставщик документов функции управления документами, добавьте флаг в столбец COLUMN_FLAGS документа, чтобы указать поддерживаемую функцию. Вам также нужно реализовать соответствующий метод класса DocumentsProvider.

В таблице ниже указаны флаг COLUMN_FLAGS и метод DocumentsProvider, которые поставщик документов должен реализовать, чтобы предоставить доступ к определенным функциям.

Функция Флаг Метод
Как удалить файл FLAG_SUPPORTS_DELETE deleteDocument()
Переименовать файл FLAG_SUPPORTS_RENAME renameDocument()
Копирование файла в новый родительский каталог в поставщике документов FLAG_SUPPORTS_COPY copyDocument()
Переместить файл из одного каталога в другой в поставщике документов. FLAG_SUPPORTS_MOVE moveDocument()
Как удалить файл из родительского каталога FLAG_SUPPORTS_REMOVE removeDocument()

Поддержка виртуальных файлов и альтернативных форматов файлов

Виртуальные файлы – функция, представленная в Android 7.0 (уровень API 24). Она позволяет поставщикам документов предоставлять доступ к просмотру файлов, у которых нет прямого представления в виде байт-кода. Чтобы другие приложения могли просматривать виртуальные файлы, поставщик документов должен создать альтернативное представление файла, которое можно открыть.

Например, поставщик документов может содержать файл, формат которого другие приложения не могут открыть напрямую, по сути, виртуальный файл. Если клиентское приложение отправляет намерение ACTION_VIEW без категории CATEGORY_OPENABLE, пользователи могут выбирать виртуальные файлы в поставщике документов для просмотра. Затем поставщик документов возвращает виртуальный файл в другом формате, который можно открыть, например в виде изображения. После этого клиентское приложение может открыть виртуальный файл для просмотра.

Чтобы объявить, что документ в поставщике является виртуальным, необходимо добавить флаг FLAG_VIRTUAL_DOCUMENT в файл, возвращаемый методом queryDocument(). Этот флаг сообщает клиентским приложениям, что у файла нет прямого представления в виде байт-кода и его нельзя открыть напрямую.

Если вы объявляете, что файл в вашем поставщике документов является виртуальным, настоятельно рекомендуется сделать его доступным в другом MIME-типе, например в виде изображения или PDF-файла. Поставщик документов объявляет альтернативные MIME-типы, которые он поддерживает для просмотра виртуального файла, переопределяя метод getDocumentStreamTypes(). Когда клиентские приложения вызывают метод getStreamTypes(android.net.Uri, java.lang.String), система вызывает метод getDocumentStreamTypes() поставщика документов. Затем метод getDocumentStreamTypes() возвращает массив альтернативных MIME-типов, которые поставщик документов поддерживает для файла.

После того как клиент определит, что поставщик документов может создать документ в формате файла, доступном для просмотра, клиентское приложение вызывает метод openTypedAssetFileDescriptor(), который внутренне вызывает метод openTypedDocument() поставщика документов. Поставщик документов возвращает файл клиентскому приложению в запрошенном формате.

В приведенном ниже фрагменте кода показана простая реализация методов getDocumentStreamTypes() и 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();
}

Безопасность

Предположим, что ваш поставщик документов – это защищенное паролем облачное хранилище, и вы хотите убедиться, что пользователи вошли в систему, прежде чем разрешить им делиться файлами. Что должно делать приложение, если пользователь не вошел в аккаунт? Решение состоит в том, чтобы возвращать ноль корней в вашей реализации queryRoots(). То есть пустой корневой курсор:

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;
}

Второй шаг – позвонить в getContentResolver().notifyChange(). Помните DocumentsContract? Мы используем его для создания этого URI. Следующий фрагмент кода указывает системе запрашивать корневые каталоги поставщика документов при каждом изменении статуса входа пользователя. Если пользователь не вошел в систему, вызов queryRoots() возвращает пустой курсор, как показано выше. Это гарантирует, что документы поставщика будут доступны только в том случае, если пользователь вошел в систему поставщика.

Kotlin

private fun onLoginButtonClick() {
    loginOrLogout()
    getContentResolver().notifyChange(
        DocumentsContract.buildRootsUri(AUTHORITY),
        null
    )
}

Java

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}

Образцы кода, связанные с этой страницей, можно найти в следующих источниках:

Видео по теме:

Дополнительную информацию можно найти в следующих статьях: